aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2021-12-28 22:55:47 +0000
committerGravatar GitHub <[email protected]>2021-12-28 22:55:47 +0000
commitbab6bb5072e81645b65142fd07b118bb7762a5e2 (patch)
treeae9a6109d974c8821aa2e785f8ab900b136102f4
parentMerge pull request #993 from python-discord/remove-matplotlib (diff)
parentInform invoker after successfully blocking a user from AoC comp. role (diff)
Merge pull request #991 from python-discord/aoc-completer-role
Give AoC completer role to members with 50 stars
-rw-r--r--bot/constants.py1
-rw-r--r--bot/exts/events/advent_of_code/_cog.py73
-rw-r--r--bot/utils/members.py47
3 files changed, 119 insertions, 2 deletions
diff --git a/bot/constants.py b/bot/constants.py
index 854fbe55..01f825a0 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -291,6 +291,7 @@ class Roles(NamedTuple):
helpers = int(environ.get("ROLE_HELPERS", 267630620367257601))
core_developers = 587606783669829632
everyone = int(environ.get("BOT_GUILD", 267624335836053506))
+ aoc_completionist = int(environ.get("AOC_COMPLETIONIST_ROLE_ID", 916691790181056532))
class Tokens(NamedTuple):
diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py
index 52254ea1..30bcaae6 100644
--- a/bot/exts/events/advent_of_code/_cog.py
+++ b/bot/exts/events/advent_of_code/_cog.py
@@ -7,12 +7,15 @@ from typing import Optional
import arrow
import discord
from async_rediscache import RedisCache
-from discord.ext import commands
+from discord.ext import commands, tasks
from bot.bot import Bot
-from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS
+from bot.constants import (
+ AdventOfCode as AocConfig, Channels, Client, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS
+)
from bot.exts.events.advent_of_code import _helpers
from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView
+from bot.utils import members
from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role
from bot.utils.extensions import invoke_help_command
@@ -31,8 +34,13 @@ class AdventOfCode(commands.Cog):
"""Advent of Code festivities! Ho Ho Ho!"""
# Redis Cache for linking Discord IDs to Advent of Code usernames
+ # RedisCache[member_id: aoc_username_string]
account_links = RedisCache()
+ # A dict with keys of member_ids to block from getting the role
+ # RedisCache[member_id: None]
+ completionist_block_list = RedisCache()
+
def __init__(self, bot: Bot):
self.bot = bot
@@ -52,6 +60,52 @@ class AdventOfCode(commands.Cog):
self.status_task.set_name("AoC Status Countdown")
self.status_task.add_done_callback(_helpers.background_task_callback)
+ self.completionist_task.start()
+
+ @tasks.loop(minutes=10.0)
+ async def completionist_task(self) -> None:
+ """
+ Give members who have completed all 50 AoC stars the completionist role.
+
+ Runs on a schedule, as defined in the task.loop decorator.
+ """
+ await self.bot.wait_until_guild_available()
+ guild = self.bot.get_guild(Client.guild)
+ completionist_role = guild.get_role(Roles.aoc_completionist)
+ if completionist_role is None:
+ log.warning("Could not find the AoC completionist role; cancelling completionist task.")
+ self.completer_task.cancel()
+ return
+
+ aoc_name_to_member_id = {
+ aoc_name: member_id
+ for member_id, aoc_name in await self.account_links.items()
+ }
+
+ try:
+ leaderboard = await _helpers.fetch_leaderboard()
+ except _helpers.FetchingLeaderboardFailedError:
+ await self.bot.send_log("Unable to fetch AoC leaderboard during role sync.")
+ return
+
+ placement_leaderboard = json.loads(leaderboard["placement_leaderboard"])
+
+ for member_aoc_info in placement_leaderboard.values():
+ if not member_aoc_info["stars"] == 50:
+ # Only give the role to people who have completed all 50 stars
+ continue
+
+ member_id = aoc_name_to_member_id.get(member_aoc_info["name"], None)
+ if not member_id:
+ continue
+
+ member = await members.get_or_fetch_member(guild, member_id)
+ if member is None or completionist_role in member.roles:
+ continue
+
+ if not await self.completionist_block_list.contains(member_id):
+ await members.handle_role_change(member, member.add_roles, completionist_role)
+
@commands.group(name="adventofcode", aliases=("aoc",))
@whitelist_override(channels=AOC_WHITELIST)
async def adventofcode_group(self, ctx: commands.Context) -> None:
@@ -59,6 +113,20 @@ class AdventOfCode(commands.Cog):
if not ctx.invoked_subcommand:
await invoke_help_command(ctx)
+ @with_role(Roles.admins)
+ @adventofcode_group.command(
+ name="block",
+ brief="Block a user from getting the completionist role.",
+ )
+ async def block_from_role(self, ctx: commands.Context, member: discord.Member) -> None:
+ """Block the given member from receiving the AoC completionist role, removing it from them if needed."""
+ completionist_role = ctx.guild.get_role(Roles.aoc_completionist)
+ if completionist_role in member.roles:
+ await member.remove_roles(completionist_role)
+
+ await self.completionist_block_list.set(member.id, "sentinel")
+ await ctx.send(f":+1: Blocked {member.mention} from getting the AoC completionist role.")
+
@commands.guild_only()
@adventofcode_group.command(
name="subscribe",
@@ -408,6 +476,7 @@ class AdventOfCode(commands.Cog):
log.debug("Unloading the cog and canceling the background task.")
self.notification_task.cancel()
self.status_task.cancel()
+ self.completionist_task.cancel()
def _build_about_embed(self) -> discord.Embed:
"""Build and return the informational "About AoC" embed from the resources file."""
diff --git a/bot/utils/members.py b/bot/utils/members.py
new file mode 100644
index 00000000..9c8c8dd8
--- /dev/null
+++ b/bot/utils/members.py
@@ -0,0 +1,47 @@
+import logging
+import typing as t
+
+import discord
+
+log = logging.getLogger(__name__)
+
+
+async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> t.Optional[discord.Member]:
+ """
+ Attempt to get a member from cache; on failure fetch from the API.
+
+ Return `None` to indicate the member could not be found.
+ """
+ if member := guild.get_member(member_id):
+ log.trace("%s retrieved from cache.", member)
+ else:
+ try:
+ member = await guild.fetch_member(member_id)
+ except discord.errors.NotFound:
+ log.trace("Failed to fetch %d from API.", member_id)
+ return None
+ log.trace("%s fetched from API.", member)
+ return member
+
+
+async def handle_role_change(
+ member: discord.Member,
+ coro: t.Callable[..., t.Coroutine],
+ role: discord.Role
+) -> None:
+ """
+ Change `member`'s cooldown role via awaiting `coro` and handle errors.
+
+ `coro` is intended to be `discord.Member.add_roles` or `discord.Member.remove_roles`.
+ """
+ try:
+ await coro(role)
+ except discord.NotFound:
+ log.debug(f"Failed to change role for {member} ({member.id}): member not found")
+ except discord.Forbidden:
+ log.debug(
+ f"Forbidden to change role for {member} ({member.id}); "
+ f"possibly due to role hierarchy"
+ )
+ except discord.HTTPException as e:
+ log.error(f"Failed to change role for {member} ({member.id}): {e.status} {e.code}")