From 3f3be73d7835ead2d8e9805478ae9bf44b66064b Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Tue, 24 Mar 2026 00:27:36 +0000 Subject: [PATCH 1/5] Start working on auto report --- cogs/__init__.py | 3 ++ cogs/moderation.py | 132 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 cogs/moderation.py diff --git a/cogs/__init__.py b/cogs/__init__.py index 0c74de61e..51afaf0a2 100644 --- a/cogs/__init__.py +++ b/cogs/__init__.py @@ -36,6 +36,7 @@ from .kill import KillCommandCog from .make_applicant import MakeApplicantContextCommandsCog, MakeApplicantSlashCommandCog from .make_member import MakeMemberCommandCog, MemberCountCommandCog +from .moderation import ModerationCog from .ping import PingCommandCog from .remind_me import ClearRemindersBacklogTaskCog, RemindMeCommandCog from .send_get_roles_reminders import SendGetRolesRemindersTaskCog @@ -77,6 +78,7 @@ "MakeMemberCommandCog", "ManualModerationCog", "MemberCountCommandCog", + "ModerationCog", "PingCommandCog", "RemindMeCommandCog", "SendGetRolesRemindersTaskCog", @@ -118,6 +120,7 @@ def setup(bot: "TeXBot") -> None: MakeMemberCommandCog, ManualModerationCog, MemberCountCommandCog, + ModerationCog, PingCommandCog, RemindMeCommandCog, SendGetRolesRemindersTaskCog, diff --git a/cogs/moderation.py b/cogs/moderation.py new file mode 100644 index 000000000..c561d67d8 --- /dev/null +++ b/cogs/moderation.py @@ -0,0 +1,132 @@ +"""File to hold cog classes related to moderation actions and tracking.""" + +import logging +from typing import TYPE_CHECKING + +import discord + +from utils import TeXBotBaseCog +from utils.error_capture_decorators import capture_guild_does_not_exist_error + +if TYPE_CHECKING: + from collections.abc import Sequence + from logging import Logger + from typing import Final + + +__all__: "Sequence[str]" = ("ModerationCog",) + + +logger: "Final[Logger]" = logging.getLogger("TeX-Bot") + + +class ModerationCog(TeXBotBaseCog): + + most_recently_deleted_message: discord.Message | None = None + + async def _send_message_to_committee( + self, message: discord.Message, deleter: discord.Member + ) -> None: + discord_channel: discord.TextChannel | None = discord.utils.get( + self.bot.main_guild.text_channels, + name="discord", # TODO: Make this user-configurable # noqa: FIX002 + ) + + if not discord_channel: + logger.error("Could not find the channel to send the message deletion report to!") + return + + embed_content: str = "" + + if message.content: + embed_content += message.content[:600] + if len(message.content) > 600: + embed_content += " _... (truncated to 600 characters)_" + else: + embed_content += "_Deleted message had no content_" + if len(message.attachments) > 0 or len(message.embeds) > 0: + embed_content += " _but did have one or more attachments!_" + + embed_content += f"\n[View Original]({message.jump_url})" + + if message.reference: + embed_content += f"\n[View Message this replied to]({message.reference.jump_url})" + + message_author_avatar_url: str | None = message.author.display_avatar.url + + embed_author: discord.EmbedAuthor = discord.EmbedAuthor( + name=message.author.display_name, icon_url=message_author_avatar_url + ) + + embed_image: str | None = None + if len(message.attachments) == 1: + attachment_type: str | None = message.attachments[0].content_type + if attachment_type and "image" in attachment_type: + embed_image = message.attachments[0].url + + await discord_channel.send( + content=( + f"{deleter.mention} deleted a message from {message.author.mention} " + f"in { + message.channel.mention + if isinstance( + message.channel, + ( + discord.TextChannel, + discord.VoiceChannel, + discord.StageChannel, + discord.Thread, + ), + ) + else message.channel + }:" + ), + embed=discord.Embed( + author=embed_author, + description=embed_content, + colour=message.author.colour, + image=embed_image, + ), + ) + + @TeXBotBaseCog.listener() + @capture_guild_does_not_exist_error + async def on_message_delete(self, message: discord.Message) -> None: + """Listen for message deletions.""" + self.most_recently_deleted_message = message + + @TeXBotBaseCog.listener() + @capture_guild_does_not_exist_error + async def on_audit_log_entry(self, entry: discord.AuditLogEntry) -> None: + """Listen for audit log entries.""" + if entry.action != discord.AuditLogAction.message_delete or not self.most_recently_deleted_message: + return + + deleter: discord.Member | discord.User | None = entry.user + author: discord.User | discord.Member | None = entry.target + channel: discord.TextChannel | None = entry.extra.channel + + if ( + not isinstance(channel, discord.TextChannel) + or not isinstance(author, discord.Member) + or not isinstance(deleter, discord.Member) + ): + return + + if author != self.most_recently_deleted_message.author: + return + + author_name = author.name + author_id = author.id + channel_name = channel.name + deleter_name = deleter.name + deleter_id = deleter.id + + logger.error( + "Message by %s (ID: %s) in #%s was deleted by %s (ID: %s).", + author_name, + author_id, + channel_name, + deleter_name, + deleter_id, + ) From 732d8372a45870af4e49d65572baa01b25ff6e0e Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:36:23 +0000 Subject: [PATCH 2/5] Implement automatic reporting message deletion to committee --- cogs/moderation.py | 23 +++++++---------------- main.py | 4 +++- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/cogs/moderation.py b/cogs/moderation.py index c561d67d8..30ec8020f 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -21,6 +21,7 @@ class ModerationCog(TeXBotBaseCog): + """Cog to track moderation actions and report them to the committee.""" most_recently_deleted_message: discord.Message | None = None @@ -99,12 +100,15 @@ async def on_message_delete(self, message: discord.Message) -> None: @capture_guild_does_not_exist_error async def on_audit_log_entry(self, entry: discord.AuditLogEntry) -> None: """Listen for audit log entries.""" - if entry.action != discord.AuditLogAction.message_delete or not self.most_recently_deleted_message: + if ( + entry.action != discord.AuditLogAction.message_delete + or not self.most_recently_deleted_message + ): return deleter: discord.Member | discord.User | None = entry.user author: discord.User | discord.Member | None = entry.target - channel: discord.TextChannel | None = entry.extra.channel + channel: discord.TextChannel | None = entry.extra.channel # type: ignore[union-attr] if ( not isinstance(channel, discord.TextChannel) @@ -116,17 +120,4 @@ async def on_audit_log_entry(self, entry: discord.AuditLogEntry) -> None: if author != self.most_recently_deleted_message.author: return - author_name = author.name - author_id = author.id - channel_name = channel.name - deleter_name = deleter.name - deleter_id = deleter.id - - logger.error( - "Message by %s (ID: %s) in #%s was deleted by %s (ID: %s).", - author_name, - author_id, - channel_name, - deleter_name, - deleter_id, - ) + await self._send_message_to_committee(self.most_recently_deleted_message, deleter) diff --git a/main.py b/main.py index 548f1e3cd..f6c865bab 100755 --- a/main.py +++ b/main.py @@ -26,7 +26,9 @@ config.run_setup() bot: TeXBot = TeXBot( - intents=discord.Intents.default() | discord.Intents.members + intents=discord.Intents.default() + | discord.Intents.members + | discord.Intents.message_content ) # NOTE: See https://github.com/CSSUoB/TeX-Bot-Py-V2/issues/261 bot.load_extension("cogs") From d0e0daaa91674f08b1117a484e735530a7550e70 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:40:24 +0000 Subject: [PATCH 3/5] Add extra checks --- cogs/moderation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cogs/moderation.py b/cogs/moderation.py index 30ec8020f..1fa536250 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -100,6 +100,8 @@ async def on_message_delete(self, message: discord.Message) -> None: @capture_guild_does_not_exist_error async def on_audit_log_entry(self, entry: discord.AuditLogEntry) -> None: """Listen for audit log entries.""" + committee_role: discord.Role = await self.bot.committee_role + if ( entry.action != discord.AuditLogAction.message_delete or not self.most_recently_deleted_message @@ -117,7 +119,11 @@ async def on_audit_log_entry(self, entry: discord.AuditLogEntry) -> None: ): return - if author != self.most_recently_deleted_message.author: + if ( + author != self.most_recently_deleted_message.author + or channel != self.most_recently_deleted_message.channel + or committee_role in author.roles + ): return await self._send_message_to_committee(self.most_recently_deleted_message, deleter) From b7b8a6e6e30613e9700b580571da9fba232f4f1c Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:55:48 +0000 Subject: [PATCH 4/5] Fixes --- cogs/moderation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cogs/moderation.py b/cogs/moderation.py index 1fa536250..03fe903f2 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -91,9 +91,11 @@ async def _send_message_to_committee( ) @TeXBotBaseCog.listener() - @capture_guild_does_not_exist_error async def on_message_delete(self, message: discord.Message) -> None: """Listen for message deletions.""" + if message.guild is None or message.author.bot or message.guild != self.bot.main_guild: + return + self.most_recently_deleted_message = message @TeXBotBaseCog.listener() @@ -126,4 +128,6 @@ async def on_audit_log_entry(self, entry: discord.AuditLogEntry) -> None: ): return + self.most_recently_deleted_message = None + await self._send_message_to_committee(self.most_recently_deleted_message, deleter) From 48bb5c2fc785afb57c2e429f7d1e38db54ab7f84 Mon Sep 17 00:00:00 2001 From: Matty Widdop <18513864+MattyTheHacker@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:01:14 +0000 Subject: [PATCH 5/5] Fix --- cogs/moderation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/moderation.py b/cogs/moderation.py index 03fe903f2..a79a702f3 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -128,6 +128,6 @@ async def on_audit_log_entry(self, entry: discord.AuditLogEntry) -> None: ): return - self.most_recently_deleted_message = None - await self._send_message_to_committee(self.most_recently_deleted_message, deleter) + + self.most_recently_deleted_message = None