From 00a421f193041b8fda30d2123cefb24219ec6b67 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:59:18 +0100 Subject: Add context manager to safely unlock role mentionability Currently, our regualar roles are not mentionable by default. This means that features that rely on roles to keep track of users that want to receive announcements, like the AoC Day Countdown, don't actually ping the users subscribed to it. The solution is obviously that the bot should unlock prior to making the announcement. However, this is complicated by the fact that there needs to be a sufficient delay. both between unlocking and sending the message and between sending the message and locking the role again. If not, Discord's not done synchronizing across all servers and some users won't receive a ping. To make this easier, I have implemented a context manager that takes an instance of `discord.Role` and an optional `delay` (default: 5s) that yields a context in which the role is unlocked. This context manager also makes sure that the role is locked even if an exception occured within the unlocked context. --- bot/utils/__init__.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'bot') diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 0aa50af6..25fd4b96 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -1,4 +1,5 @@ import asyncio +import contextlib import re import string from typing import List @@ -127,3 +128,25 @@ def replace_many( return replacement.lower() return regex.sub(_repl, sentence) + + +@contextlib.asynccontextmanager +async def unlocked_role(role: discord.Role, delay: int = 5) -> None: + """ + Create a context in which `role` is unlocked, relocking it automatically after use. + + A configurable `delay` is added before yielding the context and directly after exiting the + context to allow the role settings change to properly propagate at Discord's end. This + prevents things like role mentions from failing because of synchronization issues. + + Usage: + >>> async with unlocked_role(role, delay=5): + ... await ctx.send(f"Hey {role.mention}, free pings for everyone!") + """ + await role.edit(mentionable=True) + await asyncio.sleep(delay) + try: + yield + finally: + await asyncio.sleep(delay) + await role.edit(mentionable=False) -- cgit v1.2.3 From 529102d75682550202b230a32ca59a0a6c7dd58b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:10:59 +0100 Subject: Make AoC channel ID configurable for testing environments I have made the `#advent-of-code` channel id configurable using the same environment variable technique used for other settings. This makes it easier to test features that rely on this channel in a test environment. --- bot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index c09d8369..ce650b80 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -23,7 +23,7 @@ class AdventOfCode: class Channels(NamedTuple): admins = 365960823622991872 - advent_of_code = 517745814039166986 + advent_of_code = int(environ.get("AOC_CHANNEL_ID", 517745814039166986)) announcements = int(environ.get("CHANNEL_ANNOUNCEMENTS", 354619224620138496)) big_brother_logs = 468507907357409333 bot = 267659945086812160 -- cgit v1.2.3 From 62fe183ce6d499f6b2129b91a35fd9d6f2632d3f Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:13:11 +0100 Subject: Unlock AoC role to make announcements actually ping the users The Advent of Code cog has a subscription system that allows members to subscribe to a daily notification indicating the puzzle for that day has become available. However, we introduces a more stringent role mentionability policy that meant that the mentions did not actually ping the members subscribed. To solve this, I've made sure that the bot unlocks the role before making the announcement using the `unlock_role` context manager. This also means the role is automatically unlocked after the message is sent. In addition, I noticed that Seasonalbot was consistently announcing the puzzle about 0.5 seconds early. I've correct this by adding a second to the sleep delay. In addition, the bot now verifies that the puzzle is available using a small HEAD request. While this does send a request to the AoC server, it prevents multiple users from sending unnecessary requests by following our link before the puzzle is actually available. --- bot/seasons/christmas/adventofcode.py | 41 +++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) (limited to 'bot') diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py index 71da8d94..f2ec83df 100644 --- a/bot/seasons/christmas/adventofcode.py +++ b/bot/seasons/christmas/adventofcode.py @@ -15,6 +15,7 @@ from pytz import timezone from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Tokens, WHITELISTED_CHANNELS from bot.decorators import override_in_channel +from bot.utils import unlocked_role log = logging.getLogger(__name__) @@ -85,17 +86,42 @@ async def day_countdown(bot: commands.Bot) -> None: while is_in_advent(): tomorrow, time_left = time_left_to_aoc_midnight() - await asyncio.sleep(time_left.seconds) + # Correct `time_left.seconds` for the sleep we have after unlocking the role (-5) and adding + # a second (+1) as the bot is consistently ~0.5 seconds early in announcing the puzzles. + await asyncio.sleep(time_left.seconds - 4) - channel = bot.get_channel(Channels.seasonalbot_chat) + channel = bot.get_channel(Channels.advent_of_code) if not channel: log.error("Could not find the AoC channel to send notification in") break - await channel.send(f"<@&{AocConfig.role_id}> Good morning! Day {tomorrow.day} is ready to be attempted. " - f"View it online now at https://adventofcode.com/{AocConfig.year}/day/{tomorrow.day}" - f" (this link could take a few minutes to start working). Good luck!") + aoc_role = channel.guild.get_role(AocConfig.role_id) + if not aoc_role: + log.error("Could not find the AoC role to announce the daily puzzle") + break + + async with unlocked_role(aoc_role, delay=5): + puzzle_url = f"https://adventofcode.com/{AocConfig.year}/day/{tomorrow.day}" + + # Check if the puzzle is already available to prevent our members from spamming + # the puzzle page before it's available by making a small HEAD request. + for retry in range(1, 5): + log.debug(f"Checking if the puzzle is already available (attempt {retry}/4)") + async with bot.http_session.head(puzzle_url, raise_for_status=False) as resp: + if resp.status == 200: + log.debug("Puzzle is available; let's send an announcement message.") + break + log.debug(f"The puzzle is not yet available (status={resp.status})") + await asyncio.sleep(10) + else: + log.error("The puzzle does does not appear to be available at this time, canceling announcement") + break + + await channel.send( + f"{aoc_role.mention} Good morning! Day {tomorrow.day} is ready to be attempted. " + f"View it online now at {puzzle_url}. Good luck!" + ) # Wait a couple minutes so that if our sleep didn't sleep enough # time we don't end up announcing twice. @@ -122,10 +148,10 @@ class AdventOfCode(commands.Cog): self.status_task = None countdown_coro = day_countdown(self.bot) - self.countdown_task = asyncio.ensure_future(self.bot.loop.create_task(countdown_coro)) + self.countdown_task = self.bot.loop.create_task(countdown_coro) status_coro = countdown_status(self.bot) - self.status_task = asyncio.ensure_future(self.bot.loop.create_task(status_coro)) + self.status_task = self.bot.loop.create_task(status_coro) @commands.group(name="adventofcode", aliases=("aoc",), invoke_without_command=True) @override_in_channel(AOC_WHITELIST) @@ -422,6 +448,7 @@ class AdventOfCode(commands.Cog): def cog_unload(self) -> None: """Cancel season-related tasks on cog unload.""" + log.debug("Unloading the cog and canceling the background task.") self.countdown_task.cancel() self.status_task.cancel() -- cgit v1.2.3