aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/CODEOWNERS10
-rw-r--r--bot/__init__.py5
-rw-r--r--bot/exts/events/advent_of_code/_cog.py83
-rw-r--r--bot/exts/events/advent_of_code/_helpers.py3
-rw-r--r--bot/exts/utilities/bookmark.py2
-rw-r--r--bot/monkey_patches.py24
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)