From d27418bdf2ae071d897d1bfb9090c3c09098bed6 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:04:30 +0100 Subject: [PATCH 1/7] Remove remindme functionality --- cogs/__init__.py | 3 - cogs/remind_me.py | 357 ---------------------------------------------- 2 files changed, 360 deletions(-) delete mode 100644 cogs/remind_me.py diff --git a/cogs/__init__.py b/cogs/__init__.py index 0c74de61..45035e36 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -37,7 +37,6 @@ from .make_applicant import MakeApplicantContextCommandsCog, MakeApplicantSlashCommandCog from .make_member import MakeMemberCommandCog, MemberCountCommandCog from .ping import PingCommandCog -from .remind_me import ClearRemindersBacklogTaskCog, RemindMeCommandCog from .send_get_roles_reminders import SendGetRolesRemindersTaskCog from .send_introduction_reminders import SendIntroductionRemindersTaskCog from .source import SourceCommandCog @@ -78,7 +77,6 @@ "ManualModerationCog", "MemberCountCommandCog", "PingCommandCog", - "RemindMeCommandCog", "SendGetRolesRemindersTaskCog", "SendIntroductionRemindersTaskCog", "SourceCommandCog", @@ -119,7 +117,6 @@ def setup(bot: "TeXBot") -> None: ManualModerationCog, MemberCountCommandCog, PingCommandCog, - RemindMeCommandCog, SendGetRolesRemindersTaskCog, SendIntroductionRemindersTaskCog, SourceCommandCog, diff --git a/cogs/remind_me.py b/cogs/remind_me.py deleted file mode 100644 index bd3457a2..00000000 --- a/cogs/remind_me.py +++ /dev/null @@ -1,357 +0,0 @@ -"""Contains cog classes for any remind_me interactions.""" - -import datetime -import itertools -import logging -import re -from typing import TYPE_CHECKING, override - -import discord -import parsedatetime -from discord.ext import tasks -from django.core.exceptions import ValidationError -from django.utils import timezone - -from db.core.models import DiscordMember, DiscordReminder -from utils import TeXBotBaseCog - -if TYPE_CHECKING: - import time - from collections.abc import Iterator, Sequence - from collections.abc import Set as AbstractSet - from logging import Logger - from typing import Final - - from utils import TeXBot, TeXBotApplicationContext, TeXBotAutocompleteContext - -__all__: "Sequence[str]" = ("ClearRemindersBacklogTaskCog", "RemindMeCommandCog") - - -logger: "Final[Logger]" = logging.getLogger("TeX-Bot") - - -class RemindMeCommandCog(TeXBotBaseCog): - """Cog class that defines the "/remind-me" command and its call-back method.""" - - @staticmethod - async def autocomplete_get_delays( # noqa: PLR0912, PLR0915 - ctx: "TeXBotAutocompleteContext", - ) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]": - """ - Autocomplete callable that generates the common delay input values. - - The delay entered by a member in the "remind_me" slash-command must be within this set - of common delay input values. - """ - if not ctx.value: - return { - "in 5 minutes", - "1 hours time", - "1min", - "30 secs", - "2 days time", - "22/9/2040", - "5h", - } - - SECONDS_CHOICES: Final[frozenset[str]] = frozenset({"s", "sec", "second"}) - MINUTES_CHOICES: Final[frozenset[str]] = frozenset({"m", "min", "minute"}) - HOURS_CHOICES: Final[frozenset[str]] = frozenset({"h", "hr", "hour"}) - DAYS_CHOICES: Final[frozenset[str]] = frozenset({"d", "dy", "day"}) - WEEKS_CHOICES: Final[frozenset[str]] = frozenset({"w", "wk", "week"}) - YEARS_CHOICES: Final[frozenset[str]] = frozenset({"y", "yr", "year"}) - TIME_CHOICES: Final[frozenset[str]] = ( - SECONDS_CHOICES - | MINUTES_CHOICES - | HOURS_CHOICES - | DAYS_CHOICES - | WEEKS_CHOICES - | YEARS_CHOICES - ) - - delay_choices: set[str] = set() - - if re.fullmatch(r"\Ain? ?\Z", ctx.value): - FORMATTED_TIME_NUMS: Final[Iterator[tuple[int, str, str]]] = itertools.product( - range(1, 150), {"", " "}, {"", "s"} - ) - time_num: int - joiner: str - has_s: str - for time_num, joiner, has_s in FORMATTED_TIME_NUMS: - delay_choices.update( - f"{time_num}{joiner}{time_choice}{has_s}" - for time_choice in TIME_CHOICES - if not (len(time_choice) <= 1 and has_s) - ) - - return {f"in {delay_choice}" for delay_choice in delay_choices} - - match: re.Match[str] | None - if match := re.fullmatch(r"\Ain (?P\d{0,3})\Z", ctx.value): - for joiner, has_s in itertools.product({"", " "}, {"", "s"}): - delay_choices.update( - f"""{match.group("partial_date")}{joiner}{time_choice}{has_s}""" - for time_choice in TIME_CHOICES - if not (len(time_choice) <= 1 and has_s) - ) - - return {f"in {delay_choice}" for delay_choice in delay_choices} - - current_year: int = discord.utils.utcnow().year - - if re.fullmatch(r"\A\d{1,3}\Z", ctx.value): - for joiner, has_s in itertools.product({"", " "}, {"", "s"}): - delay_choices.update( - f"{joiner}{time_choice}{has_s}" - for time_choice in TIME_CHOICES - if not (len(time_choice) <= 1 and has_s) - ) - - if 1 <= int(ctx.value) <= 31: - FORMATTED_DAY_DATE_CHOICES: Final[Iterator[tuple[int, int, str]]] = ( - itertools.product( - range(1, 12), - range(current_year, current_year + 40), - ("/", " / ", "-", " - ", ".", " . "), - ) - ) - month: int - year: int - for month, year, joiner in FORMATTED_DAY_DATE_CHOICES: - delay_choices.add(f"{joiner}{month}{joiner}{year}") - if month < 10: - delay_choices.add(f"{joiner}0{month}{joiner}{year}") - - elif match := re.fullmatch(r"\A\d{1,3}(?P ?[A-Za-z]*)\Z", ctx.value): - FORMATTED_TIME_CHOICES: Final[Iterator[tuple[str, str, str]]] = itertools.product( - {"", " "}, TIME_CHOICES, {"", "s"} - ) - time_choice: str - for joiner, time_choice, has_s in FORMATTED_TIME_CHOICES: - if has_s and len(time_choice) <= 1: - continue - - formatted_time_choice: str = joiner + time_choice + has_s - - slice_size: int - for slice_size in range(1, len(formatted_time_choice) + 1): - if ( - match.group("ctx_time_choice").casefold() - == formatted_time_choice[:slice_size] - ): - delay_choices.add(formatted_time_choice[slice_size:]) - - elif match := re.fullmatch(r"\A(?P\d{1,2}) ?[/\-.] ?\Z", ctx.value): - if 1 <= int(match.group("date")) <= 31: - FORMATTED_DAY_AND_JOINER_DATE_CHOICES: Final[ - Iterator[tuple[int, int, str]] - ] = itertools.product( - range(1, 12), - range(current_year, current_year + 40), - ("/", " / ", "-", " - ", ".", " . "), - ) - for month, year, joiner in FORMATTED_DAY_AND_JOINER_DATE_CHOICES: - delay_choices.add(f"{month}{joiner}{year}") - if month < 10: - delay_choices.add(f"0{month}{joiner}{year}") - - elif match := re.fullmatch( - r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2})\Z", ctx.value - ): - if 1 <= int(match.group("date")) <= 31 and 1 <= int(match.group("month")) <= 12: - for year in range(current_year, current_year + 40): - for joiner in ("/", " / ", "-", " - ", ".", " . "): - delay_choices.add(f"{joiner}{year}") - - elif match := re.fullmatch( - r"\A(?P\d{1,2}) ?[/\-.] ?(?P\d{1,2}) ?[/\-.] ?\Z", ctx.value - ): - if 1 <= int(match.group("date")) <= 31 and 1 <= int(match.group("month")) <= 12: - for year in range(current_year, current_year + 40): - delay_choices.add(f"{year}") - - elif match := re.fullmatch( - ( - r"\A(?P\d{1,2}) ?[/\-.] ?" - r"(?P\d{1,2}) ?[/\-.] ?" - r"(?P\d{1,3})\Z" - ), - ctx.value, - ): - if 1 <= int(match.group("date")) <= 31 and 1 <= int(match.group("month")) <= 12: - for year in range(current_year, current_year + 40): - delay_choices.add(f"{year}"[len(match.group("partial_year")) :]) - - return {f"{ctx.value}{delay_choice}".casefold() for delay_choice in delay_choices} - - @discord.slash_command( - name="remind-me", - description="Responds with the given message after the specified time.", - ) - @discord.option( - name="delay", - input_type=str, - description="The amount of time to wait before reminding you.", - required=True, - autocomplete=discord.utils.basic_autocomplete(autocomplete_get_delays), - ) - @discord.option( - name="message", - input_type=str, - description="The message you want to be reminded with.", - required=False, - ) - async def remind_me( - self, ctx: "TeXBotApplicationContext", delay: str, message: str - ) -> None: - """ - Definition & callback response of the "remind_me" command. - - The "remind_me" command responds with the given message after the specified time. - """ - parsed_time: tuple[time.struct_time, int] = parsedatetime.Calendar().parseDT( - delay, tzinfo=timezone.get_current_timezone() - ) - - if parsed_time[1] == 0: - await self.command_send_error( - ctx, - message=f"The value provided in the {'delay'!r} argument was not a time/date.", - ) - return - - if message: - message = re.sub(r"<@[&#]?\d+>", "@...", message.strip()) - - try: - reminder: DiscordReminder = await DiscordReminder.objects.acreate( # type: ignore[misc] - discord_member=( - await DiscordMember.objects.aget_or_create(discord_id=ctx.user.id) - )[0], - message=message or "", - channel_id=ctx.channel_id, - send_datetime=parsed_time[0], - channel_type=ctx.channel.type, - ) - except ValidationError as create_discord_reminder_error: - ERROR_IS_ALREADY_EXISTS: Final[bool] = bool( - "__all__" in create_discord_reminder_error.message_dict - and any( - "already exists" in error - for error in create_discord_reminder_error.message_dict["__all__"] - ) - ) - if not ERROR_IS_ALREADY_EXISTS: - await self.command_send_error(ctx, message="An unrecoverable error occurred.") - logger.critical( - "Error when creating DiscordReminder object: %s", - create_discord_reminder_error, - ) - await self.bot.close() - - await self.command_send_error( - ctx, message="You already have a reminder with that message in this channel!" - ) - return - - await ctx.respond("Reminder set!", ephemeral=True) - - await discord.utils.sleep_until(reminder.send_datetime) - - user_mention: str | None = None - if ctx.guild: - user_mention = ctx.user.mention - - await ctx.send_followup(reminder.get_formatted_message(user_mention)) - - await reminder.adelete() - - -class ClearRemindersBacklogTaskCog(TeXBotBaseCog): - """Cog class that defines the clear_reminders_backlog task.""" - - @override - def __init__(self, bot: "TeXBot") -> None: - """Start all task managers when this cog is initialised.""" - _ = self.clear_reminders_backlog.start() - - super().__init__(bot) - - @override - def cog_unload(self) -> None: - """ - Unload-hook that ends all running tasks whenever the tasks cog is unloaded. - - This may be run dynamically or when the bot closes. - """ - self.clear_reminders_backlog.cancel() - - @tasks.loop(minutes=15) - async def clear_reminders_backlog(self) -> None: - """Recurring task to send any late Discord reminders still stored in the database.""" - TEXTABLE_CHANNEL_TYPES: Final[frozenset[discord.ChannelType]] = frozenset( - { - discord.ChannelType.text, - discord.ChannelType.group, - discord.ChannelType.public_thread, - discord.ChannelType.private_thread, - } - ) - - reminder: DiscordReminder - async for reminder in DiscordReminder.objects.select_related("discord_member").all(): - time_since_reminder_needed_to_be_sent: datetime.timedelta = ( - discord.utils.utcnow() - reminder.send_datetime - ) - if time_since_reminder_needed_to_be_sent > datetime.timedelta(minutes=15): - user: discord.User | None = await self.bot.get_or_fetch_user( - int(reminder.discord_member.discord_id) - ) - - if not user: - logger.warning( - "Failed to send reminder to user with ID: %s " - "because the user no longer exists.", - reminder.discord_member.discord_id, - ) - await reminder.adelete() - continue - - if user.bot: - logger.warning( - "Failed to send reminder to user with ID: %s " - "because the user is a bot.", - reminder.discord_member.discord_id, - ) - await reminder.adelete() - continue - - channel: discord.PartialMessageable = self.bot.get_partial_messageable( - reminder.channel_id, type=reminder.channel_type - ) - - user_mention: str | None = None - if channel.type in TEXTABLE_CHANNEL_TYPES: - user_mention = user.mention - - elif channel.type != discord.ChannelType.private: - logger.critical( - ValueError( - "Reminder's channel_id must refer to a valid text channel/DM." - ) - ) - await self.bot.close() - - await channel.send( - "**Sorry it's a bit late! " - "(I'm just catching up with some reminders I missed!)**\n\n" - f"{reminder.get_formatted_message(user_mention)}" - ) - - await reminder.adelete() - - @clear_reminders_backlog.before_loop - async def before_tasks(self) -> None: - """Pre-execution hook, preventing any tasks from executing before the bot is ready.""" - await self.bot.wait_until_ready() From bb9a008e3bf03dcd6f29cbe3ae8a82226f254dba Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:06:21 +0100 Subject: [PATCH 2/7] Fix errors --- cogs/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/__init__.py b/cogs/__init__.py index 45035e36..cd4cde44 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -96,7 +96,6 @@ def setup(bot: "TeXBot") -> None: AnnualRolesResetCommandCog, AnnualYearChannelsIncrementCommandCog, ArchiveCommandsCog, - ClearRemindersBacklogTaskCog, CommandErrorCog, CommitteeActionsTrackingSlashCommandsCog, CommitteeActionsTrackingContextCommandCog, From 57c850343984e2001ac82c6fd85d6e988e32673f Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:19:16 +0100 Subject: [PATCH 3/7] Mark as deprecated --- db/core/models/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/core/models/__init__.py b/db/core/models/__init__.py index d9be7b40..5112d46d 100644 --- a/db/core/models/__init__.py +++ b/db/core/models/__init__.py @@ -2,6 +2,7 @@ import hashlib import re +import warnings from typing import TYPE_CHECKING, override import discord @@ -256,6 +257,7 @@ def _get_proxy_field_names(cls) -> "AbstractSet[str]": return {*super()._get_proxy_field_names(), "group_member_id"} +@warnings.deprecated() class DiscordReminder(AsyncBaseModel): """Represents a reminder that a Discord member has requested to be sent to them.""" From 6691d298232c47949747af694ffe94edee852943 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:20:07 +0100 Subject: [PATCH 4/7] Remove delete-all command for reminders --- cogs/delete_all.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/cogs/delete_all.py b/cogs/delete_all.py index 7b7a3ad8..7ff2410d 100644 --- a/cogs/delete_all.py +++ b/cogs/delete_all.py @@ -50,20 +50,6 @@ async def _delete_all( f"All {delete_model_instances_name_plural} deleted successfully.", ephemeral=True ) - @delete_all.command( - name="reminders", description="Deletes all Reminders from the backend database." - ) - @CommandChecks.check_interaction_user_has_committee_role - @CommandChecks.check_interaction_user_in_main_guild - async def delete_all_reminders(self, ctx: "TeXBotApplicationContext") -> None: - """ - Definition & callback response of the "delete_all_reminders" command. - - The "delete_all_reminders" command uses the _delete_all() function - to delete all `DiscordReminder` instance objects stored in the database. - """ - await self._delete_all(ctx, delete_model=DiscordReminder) - @delete_all.command( name="group-made-members", description="Deletes all Group Made Members from the backend database.", From 2f429fdf6b3b55aef086fbaad4ab8e5e80a78233 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:21:47 +0100 Subject: [PATCH 5/7] Remove import --- cogs/delete_all.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cogs/delete_all.py b/cogs/delete_all.py index 7ff2410d..60124a12 100644 --- a/cogs/delete_all.py +++ b/cogs/delete_all.py @@ -7,7 +7,6 @@ from db.core.models import ( AssignedCommitteeAction, DiscordMemberStrikes, - DiscordReminder, GroupMadeMember, SentGetRolesReminderMember, SentOneOffIntroductionReminderMember, From 2d4fc2b90957815e5f78b0087ca92bfe3a303995 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:30:03 +0100 Subject: [PATCH 6/7] Add error message --- db/core/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/core/models/__init__.py b/db/core/models/__init__.py index 5112d46d..d0a222e0 100644 --- a/db/core/models/__init__.py +++ b/db/core/models/__init__.py @@ -257,7 +257,7 @@ def _get_proxy_field_names(cls) -> "AbstractSet[str]": return {*super()._get_proxy_field_names(), "group_member_id"} -@warnings.deprecated() +@warnings.deprecated("DiscordReminder is deprecated and may be removed in a future release.") class DiscordReminder(AsyncBaseModel): """Represents a reminder that a Discord member has requested to be sent to them.""" From fc8fec7a10c45c30ddbdc68253a0983be310480b Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:34:54 +0100 Subject: [PATCH 7/7] Add note about command --- utils/tex_bot_base_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/tex_bot_base_cog.py b/utils/tex_bot_base_cog.py index e18c06e9..f78749f8 100644 --- a/utils/tex_bot_base_cog.py +++ b/utils/tex_bot_base_cog.py @@ -35,6 +35,7 @@ class TeXBotBaseCog(Cog): ERROR_ACTIVITIES: "Final[Mapping[str, str]]" = { # noqa: RUF012 "archive": "archive the selected category", + # NOTE: The "/delete-all reminders" command has been removed but it is left here in case it comes up somehow during error handling "delete_all_reminders": ( "delete all `DiscordReminder` objects from the backend database" ),