aboutsummaryrefslogtreecommitdiffstats
path: root/bot/seasons/christmas/adventofcode.py
diff options
context:
space:
mode:
Diffstat (limited to 'bot/seasons/christmas/adventofcode.py')
-rw-r--r--bot/seasons/christmas/adventofcode.py109
1 files changed, 83 insertions, 26 deletions
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py
index 08b07e83..f2ec83df 100644
--- a/bot/seasons/christmas/adventofcode.py
+++ b/bot/seasons/christmas/adventofcode.py
@@ -13,7 +13,9 @@ from bs4 import BeautifulSoup
from discord.ext import commands
from pytz import timezone
-from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Tokens
+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__)
@@ -23,6 +25,8 @@ AOC_SESSION_COOKIE = {"session": Tokens.aoc_session_cookie}
EST = timezone("EST")
COUNTDOWN_STEP = 60 * 5
+AOC_WHITELIST = WHITELISTED_CHANNELS + (Channels.advent_of_code,)
+
def is_in_advent() -> bool:
"""Utility function to check if we are between December 1st and December 25th."""
@@ -45,7 +49,7 @@ def time_left_to_aoc_midnight() -> Tuple[datetime, timedelta]:
return tomorrow, tomorrow - datetime.now(EST)
-async def countdown_status(bot: commands.Bot):
+async def countdown_status(bot: commands.Bot) -> None:
"""Set the playing status of the bot to the minutes & hours left until the next day's challenge."""
while is_in_advent():
_, time_left = time_left_to_aoc_midnight()
@@ -72,7 +76,7 @@ async def countdown_status(bot: commands.Bot):
await asyncio.sleep(delay)
-async def day_countdown(bot: commands.Bot):
+async def day_countdown(bot: commands.Bot) -> None:
"""
Calculate the number of seconds left until the next day of Advent.
@@ -82,17 +86,42 @@ async def day_countdown(bot: commands.Bot):
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.
@@ -119,13 +148,14 @@ 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)
- async def adventofcode_group(self, ctx: commands.Context):
+ @override_in_channel(AOC_WHITELIST)
+ async def adventofcode_group(self, ctx: commands.Context) -> None:
"""All of the Advent of Code commands."""
await ctx.send_help(ctx.command)
@@ -134,7 +164,8 @@ class AdventOfCode(commands.Cog):
aliases=("sub", "notifications", "notify", "notifs"),
brief="Notifications for new days"
)
- async def aoc_subscribe(self, ctx: commands.Context):
+ @override_in_channel(AOC_WHITELIST)
+ async def aoc_subscribe(self, ctx: commands.Context) -> None:
"""Assign the role for notifications about new days being ready."""
role = ctx.guild.get_role(AocConfig.role_id)
unsubscribe_command = f"{ctx.prefix}{ctx.command.root_parent} unsubscribe"
@@ -148,7 +179,8 @@ class AdventOfCode(commands.Cog):
f"If you don't want them any more, run `{unsubscribe_command}` instead.")
@adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days")
- async def aoc_unsubscribe(self, ctx: commands.Context):
+ @override_in_channel(AOC_WHITELIST)
+ async def aoc_unsubscribe(self, ctx: commands.Context) -> None:
"""Remove the role for notifications about new days being ready."""
role = ctx.guild.get_role(AocConfig.role_id)
@@ -159,14 +191,26 @@ class AdventOfCode(commands.Cog):
await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.")
@adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day")
- async def aoc_countdown(self, ctx: commands.Context):
+ @override_in_channel(AOC_WHITELIST)
+ async def aoc_countdown(self, ctx: commands.Context) -> None:
"""Return time left until next day."""
if not is_in_advent():
datetime_now = datetime.now(EST)
- december_first = datetime(datetime_now.year + 1, 12, 1, tzinfo=EST)
- delta = december_first - datetime_now
+
+ # Calculate the delta to this & next year's December 1st to see which one is closest and not in the past
+ this_year = datetime(datetime_now.year, 12, 1, tzinfo=EST)
+ next_year = datetime(datetime_now.year + 1, 12, 1, tzinfo=EST)
+ deltas = (dec_first - datetime_now for dec_first in (this_year, next_year))
+ delta = min(delta for delta in deltas if delta >= timedelta()) # timedelta() gives 0 duration delta
+
+ # Add a finer timedelta if there's less than a day left
+ if delta.days == 0:
+ delta_str = f"approximately {delta.seconds // 3600} hours"
+ else:
+ delta_str = f"{delta.days} days"
+
await ctx.send(f"The Advent of Code event is not currently running. "
- f"The next event will start in {delta.days} days.")
+ f"The next event will start in {delta_str}.")
return
tomorrow, time_left = time_left_to_aoc_midnight()
@@ -176,12 +220,14 @@ class AdventOfCode(commands.Cog):
await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.")
@adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code")
- async def about_aoc(self, ctx: commands.Context):
+ @override_in_channel(AOC_WHITELIST)
+ async def about_aoc(self, ctx: commands.Context) -> None:
"""Respond with an explanation of all things Advent of Code."""
await ctx.send("", embed=self.cached_about_aoc)
- @adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join PyDis' private AoC leaderboard")
- async def join_leaderboard(self, ctx: commands.Context):
+ @adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)")
+ @override_in_channel(AOC_WHITELIST)
+ async def join_leaderboard(self, ctx: commands.Context) -> None:
"""DM the user the information for joining the PyDis AoC private leaderboard."""
author = ctx.message.author
log.info(f"{author.name} ({author.id}) has requested the PyDis AoC leaderboard code")
@@ -195,13 +241,16 @@ class AdventOfCode(commands.Cog):
except discord.errors.Forbidden:
log.debug(f"{author.name} ({author.id}) has disabled DMs from server members")
await ctx.send(f":x: {author.mention}, please (temporarily) enable DMs to receive the join code")
+ else:
+ await ctx.message.add_reaction(Emojis.envelope)
@adventofcode_group.command(
name="leaderboard",
aliases=("board", "lb"),
brief="Get a snapshot of the PyDis private AoC leaderboard",
)
- async def aoc_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10):
+ @override_in_channel(AOC_WHITELIST)
+ async def aoc_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10) -> None:
"""
Pull the top number_of_people_to_display members from the PyDis leaderboard and post an embed.
@@ -242,7 +291,8 @@ class AdventOfCode(commands.Cog):
aliases=("dailystats", "ds"),
brief="Get daily statistics for the PyDis private leaderboard"
)
- async def private_leaderboard_daily_stats(self, ctx: commands.Context):
+ @override_in_channel(AOC_WHITELIST)
+ async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:
"""
Respond with a table of the daily completion statistics for the PyDis private leaderboard.
@@ -285,7 +335,8 @@ class AdventOfCode(commands.Cog):
aliases=("globalboard", "gb"),
brief="Get a snapshot of the global AoC leaderboard",
)
- async def global_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10):
+ @override_in_channel(AOC_WHITELIST)
+ async def global_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10) -> None:
"""
Pull the top number_of_people_to_display members from the global AoC leaderboard and post an embed.
@@ -317,7 +368,7 @@ class AdventOfCode(commands.Cog):
embed=aoc_embed,
)
- async def _check_leaderboard_cache(self, ctx, global_board: bool = False):
+ async def _check_leaderboard_cache(self, ctx: commands.Context, global_board: bool = False) -> None:
"""
Check age of current leaderboard & pull a new one if the board is too old.
@@ -357,7 +408,7 @@ class AdventOfCode(commands.Cog):
)
async def _check_n_entries(self, ctx: commands.Context, number_of_people_to_display: int) -> int:
- """Check for n > max_entries and n <= 0"""
+ """Check for n > max_entries and n <= 0."""
max_entries = AocConfig.leaderboard_max_displayed_members
author = ctx.message.author
if not 0 <= number_of_people_to_display <= max_entries:
@@ -388,13 +439,19 @@ class AdventOfCode(commands.Cog):
return about_embed
- async def _boardgetter(self, global_board: bool):
+ async def _boardgetter(self, global_board: bool) -> None:
"""Invoke the proper leaderboard getter based on the global_board boolean."""
if global_board:
self.cached_global_leaderboard = await AocGlobalLeaderboard.from_url()
else:
self.cached_private_leaderboard = await AocPrivateLeaderboard.from_url()
+ 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()
+
class AocMember:
"""Object representing the Advent of Code user."""