diff options
| author | 2020-02-06 21:40:40 +0000 | |
|---|---|---|
| committer | 2020-02-06 21:40:40 +0000 | |
| commit | 9e758d49a905b5e0c7ee8e722c331157888b7ba9 (patch) | |
| tree | 64e95c43444cd9b65bd8f939d9da4dc7736089f8 /bot/seasons/christmas/adventofcode.py | |
| parent | Post results and boards to initial channel (diff) | |
| parent | Update CODEOWNERS (diff) | |
Merge branch 'master' into battleships
Diffstat (limited to '')
| -rw-r--r-- | bot/seasons/christmas/adventofcode.py | 109 | 
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."""  |