diff options
| -rw-r--r-- | .github/CODEOWNERS | 10 | ||||
| -rw-r--r-- | bot/__init__.py | 5 | ||||
| -rw-r--r-- | bot/exts/events/advent_of_code/_cog.py | 83 | ||||
| -rw-r--r-- | bot/exts/events/advent_of_code/_helpers.py | 3 | ||||
| -rw-r--r-- | bot/exts/utilities/bookmark.py | 2 | ||||
| -rw-r--r-- | bot/monkey_patches.py | 24 | 
6 files changed, 118 insertions, 9 deletions
| diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cffe15d5..d164ad04 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,10 +4,6 @@ bot/exts/events/hacktoberfest/**                    @ks129  bot/exts/holidays/halloween/**                      @ks129  # CI & Docker -.github/workflows/**                    @Akarys42 @SebastiaanZ @Den4200 -Dockerfile                              @Akarys42 @Den4200 -docker-compose.yml                      @Akarys42 @Den4200 - -# Tools -poetry.lock                             @Akarys42 -pyproject.toml                          @Akarys42 +.github/workflows/**                    @SebastiaanZ @Den4200 +Dockerfile                              @Den4200 +docker-compose.yml                      @Den4200 diff --git a/bot/__init__.py b/bot/__init__.py index ae53a5a5..3136c863 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -43,6 +43,11 @@ if os.name == "nt":  monkey_patches.patch_typing() +# This patches any convertors that use PartialMessage, but not the PartialMessageConverter itself +# as library objects are made by this mapping. +# https://github.com/Rapptz/discord.py/blob/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f/discord/ext/commands/converter.py#L984-L1004 +commands.converter.PartialMessageConverter = monkey_patches.FixedPartialMessageConverter +  # Monkey-patch discord.py decorators to use the both the Command and Group subclasses which supports root aliases.  # Must be patched before any cogs are added.  commands.command = partial(commands.command, cls=monkey_patches.Command) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 3ad24b3b..52254ea1 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -6,6 +6,7 @@ from typing import Optional  import arrow  import discord +from async_rediscache import RedisCache  from discord.ext import commands  from bot.bot import Bot @@ -29,6 +30,9 @@ AOC_WHITELIST = AOC_WHITELIST_RESTRICTED + (Channels.advent_of_code,)  class AdventOfCode(commands.Cog):      """Advent of Code festivities! Ho Ho Ho!""" +    # Redis Cache for linking Discord IDs to Advent of Code usernames +    account_links = RedisCache() +      def __init__(self, bot: Bot):          self.bot = bot @@ -175,6 +179,79 @@ class AdventOfCode(commands.Cog):          else:              await ctx.message.add_reaction(Emojis.envelope) +    @in_month(Month.NOVEMBER, Month.DECEMBER) +    @adventofcode_group.command( +        name="link", +        aliases=("connect",), +        brief="Tie your Discord account with your Advent of Code name." +    ) +    @whitelist_override(channels=AOC_WHITELIST) +    async def aoc_link_account(self, ctx: commands.Context, *, aoc_name: str = None) -> None: +        """ +        Link your Discord Account to your Advent of Code name. + +        Stored in a Redis Cache with the format of `Discord ID: Advent of Code Name` +        """ +        cache_items = await self.account_links.items() +        cache_aoc_names = [value for _, value in cache_items] + +        if aoc_name: +            # Let's check the current values in the cache to make sure it isn't already tied to a different account +            if aoc_name == await self.account_links.get(ctx.author.id): +                await ctx.reply(f"{aoc_name} is already tied to your account.") +                return +            elif aoc_name in cache_aoc_names: +                log.info( +                    f"{ctx.author} ({ctx.author.id}) tried to connect their account to {aoc_name}," +                    " but it's already connected to another user." +                ) +                await ctx.reply( +                    f"{aoc_name} is already tied to another account." +                    " Please contact an admin if you believe this is an error." +                ) +                return + +            # Update an existing link +            if old_aoc_name := await self.account_links.get(ctx.author.id): +                log.info(f"Changing link for {ctx.author} ({ctx.author.id}) from {old_aoc_name} to {aoc_name}.") +                await self.account_links.set(ctx.author.id, aoc_name) +                await ctx.reply(f"Your linked account has been changed to {aoc_name}.") +            else: +                # Create a new link +                log.info(f"Linking {ctx.author} ({ctx.author.id}) to account {aoc_name}.") +                await self.account_links.set(ctx.author.id, aoc_name) +                await ctx.reply(f"You have linked your Discord ID to {aoc_name}.") +        else: +            # User has not supplied a name, let's check if they're in the cache or not +            if cache_name := await self.account_links.get(ctx.author.id): +                await ctx.reply(f"You have already linked an Advent of Code account: {cache_name}.") +            else: +                await ctx.reply( +                    "You have not linked an Advent of Code account." +                    " Please re-run the command with one specified." +                ) + +    @in_month(Month.NOVEMBER, Month.DECEMBER) +    @adventofcode_group.command( +        name="unlink", +        aliases=("disconnect",), +        brief="Tie your Discord account with your Advent of Code name." +    ) +    @whitelist_override(channels=AOC_WHITELIST) +    async def aoc_unlink_account(self, ctx: commands.Context) -> None: +        """ +        Unlink your Discord ID with your Advent of Code leaderboard name. + +        Deletes the entry that was Stored in the Redis cache. +        """ +        if aoc_cache_name := await self.account_links.get(ctx.author.id): +            log.info(f"Unlinking {ctx.author} ({ctx.author.id}) from Advent of Code account {aoc_cache_name}") +            await self.account_links.delete(ctx.author.id) +            await ctx.reply(f"We have removed the link between your Discord ID and {aoc_cache_name}.") +        else: +            log.info(f"Attempted to unlink {ctx.author} ({ctx.author.id}), but no link was found.") +            await ctx.reply("You don't have an Advent of Code account linked.") +      @in_month(Month.DECEMBER)      @adventofcode_group.command(          name="dayandstar", @@ -234,6 +311,10 @@ class AdventOfCode(commands.Cog):          if aoc_name and aoc_name.startswith('"') and aoc_name.endswith('"'):              aoc_name = aoc_name[1:-1] +        # Check if an advent of code account is linked in the Redis Cache if aoc_name is not given +        if (aoc_cache_name := await self.account_links.get(ctx.author.id)) and aoc_name is None: +            aoc_name = aoc_cache_name +          async with ctx.typing():              try:                  leaderboard = await _helpers.fetch_leaderboard(self_placement_name=aoc_name) @@ -244,7 +325,7 @@ class AdventOfCode(commands.Cog):          number_of_participants = leaderboard["number_of_participants"]          top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) -        self_placement_header = "(and your personal stats compared to the top 10)" if aoc_name else "" +        self_placement_header = " (and your personal stats compared to the top 10)" if aoc_name else ""          header = f"Here's our current top {top_count}{self_placement_header}! {Emojis.christmas_tree * 3}"          table = "```\n" \                  f"{leaderboard['placement_leaderboard'] if aoc_name else leaderboard['top_leaderboard']}" \ diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 35258544..807cc275 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -216,6 +216,9 @@ def _format_leaderboard(leaderboard: dict[str, dict], self_placement_name: str =      if self_placement_name and not self_placement_exists:          raise commands.BadArgument(              "Sorry, your profile does not exist in this leaderboard." +            "\n\n" +            "To join our leaderboard, run the command `.aoc join`." +            " If you've joined recently, please wait up to 30 minutes for our leaderboard to refresh."          )      return "\n".join(leaderboard_lines) diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py index a11c366b..b50205a0 100644 --- a/bot/exts/utilities/bookmark.py +++ b/bot/exts/utilities/bookmark.py @@ -102,7 +102,7 @@ class Bookmark(commands.Cog):                      "You must either provide a valid message to bookmark, or reply to one."                      "\n\nThe lookup strategy for a message is as follows (in order):"                      "\n1. Lookup by '{channel ID}-{message ID}' (retrieved by shift-clicking on 'Copy ID')" -                    "\n2. Lookup by message ID (the message **must** have been sent after the bot last started)" +                    "\n2. Lookup by message ID (the message **must** be in the context channel)"                      "\n3. Lookup by message URL"                  )              target_message = ctx.message.reference.resolved diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index fa6627d1..19965c19 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -1,10 +1,12 @@  import logging +import re  from datetime import datetime, timedelta  from discord import Forbidden, http  from discord.ext import commands  log = logging.getLogger(__name__) +MESSAGE_ID_RE = re.compile(r'(?P<message_id>[0-9]{15,20})$')  class Command(commands.Command): @@ -65,3 +67,25 @@ def patch_typing() -> None:              pass      http.HTTPClient.send_typing = honeybadger_type + + +class FixedPartialMessageConverter(commands.PartialMessageConverter): +    """ +    Make the Message converter infer channelID from the given context if only a messageID is given. + +    Discord.py's Message converter is supposed to infer channelID based +    on ctx.channel if only a messageID is given. A refactor commit, linked below, +    a few weeks before d.py's archival broke this defined behaviour of the converter. +    Currently, if only a messageID is given to the converter, it will only find that message +    if it's in the bot's cache. + +    https://github.com/Rapptz/discord.py/commit/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f +    """ + +    @staticmethod +    def _get_id_matches(ctx: commands.Context, argument: str) -> tuple[int, int, int]: +        """Inserts ctx.channel.id before calling super method if argument is just a messageID.""" +        match = MESSAGE_ID_RE.match(argument) +        if match: +            argument = f"{ctx.channel.id}-{match.group('message_id')}" +        return commands.PartialMessageConverter._get_id_matches(ctx, argument) | 
