diff --git a/bot/constants.py b/bot/constants.py index b07573d..352c67c 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -84,6 +84,7 @@ def _parse_aoc_leaderboard_env() -> dict[str, AdventOfCodeLeaderboard]: class _Channels(EnvConfig, env_prefix="CHANNEL_"): advent_of_code: int = 897932085766004786 advent_of_code_commands: int = 897932607545823342 + advent_of_code_spoilers: int = 1047673173447020564 bot_commands: int = 267659945086812160 devlog: int = 622895325144940554 code_jam_planning: int = 490217981872177157 diff --git a/bot/exts/advent_of_code/_caches.py b/bot/exts/advent_of_code/_caches.py index bfa9b3c..99dee8e 100644 --- a/bot/exts/advent_of_code/_caches.py +++ b/bot/exts/advent_of_code/_caches.py @@ -5,6 +5,7 @@ class AoCSettingOption(Enum): COMPLETIONIST_ENABLED = "completionist_enabled" + AUTO_POSTING_ENABLED = "auto_posting_enabled" # How many people are in each leaderboard diff --git a/bot/exts/advent_of_code/_cog.py b/bot/exts/advent_of_code/_cog.py index 1cfe20b..0431a2f 100644 --- a/bot/exts/advent_of_code/_cog.py +++ b/bot/exts/advent_of_code/_cog.py @@ -1,6 +1,7 @@ import json -from datetime import UTC, datetime, timedelta +from datetime import UTC, datetime, time, timedelta from pathlib import Path +from zoneinfo import ZoneInfo import arrow import discord @@ -79,6 +80,9 @@ async def cog_load(self) -> None: if await _caches.aoc_settings.get(_caches.AoCSettingOption.COMPLETIONIST_ENABLED.value): self.completionist_task.start() + if await _caches.aoc_settings.get(_caches.AoCSettingOption.AUTO_POSTING_ENABLED.value): + self.daily_posting_task.start() + @tasks.loop(minutes=10.0) async def completionist_task(self) -> None: """ @@ -131,6 +135,30 @@ async def completionist_task(self) -> None: log.debug(f"Giving completionist role to {member.name} ({member.mention}).") await members.handle_role_change(member, member.add_roles, completionist_role) + @tasks.loop(time=time(0, 0, 0, tzinfo=ZoneInfo(_helpers.EST))) + async def daily_posting_task(self) -> None: + """ + Create a spoilers post for the new puzzle posted at this time. + + Runs every day at midnight EST during the event. + """ + today = arrow.now(_helpers.EST).date() + + if not _helpers.is_in_advent(): + log.info("AoC daily posting is on, but it's not the AoC period. Skipping.") + return + + guild = self.bot.get_guild(Bot.guild) + aoc_spoilers_channel: discord.ForumChannel = guild.get_channel(Channels.advent_of_code_spoilers) + if not aoc_spoilers_channel: + log.info("Couldn't find the AoC spoilers channel. Skipping.") + return + + await aoc_spoilers_channel.create_thread( + name=f"AoC {today.year} | Day {today.day}", + content=f"Spoilers discussion for day {today.day} — <{_helpers.aoc_puzzle_link(today.day)}>", + ) + @commands.group(name="adventofcode", aliases=("aoc",)) async def adventofcode_group(self, ctx: commands.Context) -> None: """All of the Advent of Code commands.""" @@ -140,7 +168,7 @@ async def adventofcode_group(self, ctx: commands.Context) -> None: @with_role(Roles.admins, Roles.events_lead, fail_silently=True) @adventofcode_group.command( name="completionist_toggle", - aliases=("ct", "toggle"), + aliases=("ct",), brief="Toggle whether or not the completionist role is issued to new users.", ) async def completionist_toggle(self, ctx: commands.Context) -> None: @@ -157,6 +185,38 @@ async def completionist_toggle(self, ctx: commands.Context) -> None: await _caches.aoc_settings.set(_caches.AoCSettingOption.COMPLETIONIST_ENABLED.value, new_state) await ctx.send(f":+1: Completionist role issuing is now {state_string}.") + @with_role(Roles.admins, Roles.events_lead, fail_silently=True) + @adventofcode_group.command( + name="posting_toggle", + aliases=("pt",), + ) + async def daily_posting_toggle(self, ctx: commands.Context) -> None: + """Toggle whether a daily post is to be created every midnight EST throughout the event.""" + current_state = await _caches.aoc_settings.get(_caches.AoCSettingOption.AUTO_POSTING_ENABLED.value) + new_state = not current_state + if new_state: + self.daily_posting_task.start() + state_string = "on" + else: + self.daily_posting_task.cancel() + state_string = "off" + + await _caches.aoc_settings.set(_caches.AoCSettingOption.AUTO_POSTING_ENABLED.value, new_state) + await ctx.send(f":+1: Daily posting is now {state_string}.") + + @with_role(Roles.admins, Roles.events_lead, fail_silently=True) + @adventofcode_group.command() + async def status(self, ctx: commands.Context) -> None: + """Show status of the AoC cog.""" + completionist_state = await _caches.aoc_settings.get(_caches.AoCSettingOption.COMPLETIONIST_ENABLED.value) + daily_posting_state = await _caches.aoc_settings.get(_caches.AoCSettingOption.AUTO_POSTING_ENABLED.value) + + embed = discord.Embed(title="AoC Cog Status", colour=discord.Colour.green()) + embed.add_field(name="Completionist role", value="On" if completionist_state else "Off") + embed.add_field(name="Daily posting", value="On" if daily_posting_state else "Off") + + await ctx.reply(embed=embed) + @with_role(Roles.admins, fail_silently=True) @adventofcode_group.command( name="block", diff --git a/bot/exts/advent_of_code/_helpers.py b/bot/exts/advent_of_code/_helpers.py index f75245a..b76cb00 100644 --- a/bot/exts/advent_of_code/_helpers.py +++ b/bot/exts/advent_of_code/_helpers.py @@ -555,6 +555,11 @@ async def countdown_status(bot: SirRobin) -> None: await asyncio.sleep(delay) +def aoc_puzzle_link(day: int) -> str: + """Get a link to the AoC puzzle page for the given day.""" + return f"https://adventofcode.com/{AdventOfCode.year}/day/{day}" + + async def new_puzzle_notification(bot: SirRobin) -> None: """Announce the release of a new Advent of Code puzzle when published.""" log.info("The Advent of Code has started or will start soon, waking up notification task.") @@ -584,7 +589,7 @@ async def new_puzzle_notification(bot: SirRobin) -> None: log.trace(f"The puzzle notification task will sleep for {sleep_seconds} seconds") await asyncio.sleep(sleep_seconds) - puzzle_url = f"https://adventofcode.com/{AdventOfCode.year}/day/{tomorrow.day}" + puzzle_url = aoc_puzzle_link(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.