Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Adds automatic reporting of moderator-deleted messages to committee channels by correlating message deletion events with audit log entries, and enables the required Discord intent to access message content.
Changes:
- Enable
Intents.message_contenton bot startup to allow access to message content for deletion reporting. - Add a new
ModerationCogthat listens for message deletions and attempts to attribute them to a deleter via audit logs, then forwards details to the committee channel. - Register the new cog in
cogs/__init__.pyso it’s loaded on startup.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| main.py | Enables message_content intent needed for deletion content reporting. |
| cogs/moderation.py | New cog implementing deletion tracking + committee reporting logic. |
| cogs/init.py | Adds ModerationCog import, export, and startup registration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @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 | ||
|
|
There was a problem hiding this comment.
most_recently_deleted_message only tracks a single deletion and is never cleared/expired. If a user deletes their own message (no audit-log entry), this cached message can later be incorrectly paired with a moderator message_delete audit-log entry for the same author/channel, causing the bot to report the wrong content (and miss the real deleted message). Consider storing a short-lived queue keyed by (channel_id, author_id) with deletion timestamps (and clearing on successful match), rather than a single global reference.
| class ModerationCog(TeXBotBaseCog): | ||
| """Cog to track moderation actions and report them to the committee.""" | ||
|
|
||
| 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, | ||
| ), | ||
| ) | ||
|
|
There was a problem hiding this comment.
_send_message_to_committee is effectively duplicated from StrikeContextCommandsCog._send_message_to_committee (same embed construction, truncation rules, attachment handling, etc.). Having two copies makes future changes easy to miss and risks inconsistent committee reporting behavior; consider extracting this into a shared helper (e.g. a util function or a method on TeXBotBaseCog) and reusing it from both cogs.
| class ModerationCog(TeXBotBaseCog): | |
| """Cog to track moderation actions and report them to the committee.""" | |
| 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, | |
| ), | |
| ) | |
| async def _send_message_deletion_to_committee( | |
| bot: discord.Client, message: discord.Message, deleter: discord.Member | |
| ) -> None: | |
| discord_channel: discord.TextChannel | None = discord.utils.get( | |
| 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, | |
| ), | |
| ) | |
| class ModerationCog(TeXBotBaseCog): | |
| """Cog to track moderation actions and report them to the committee.""" | |
| most_recently_deleted_message: discord.Message | None = None | |
| async def _send_message_to_committee( | |
| self, message: discord.Message, deleter: discord.Member | |
| ) -> None: | |
| await _send_message_deletion_to_committee(self.bot, message, deleter) |
| intents=discord.Intents.default() | discord.Intents.members | ||
| intents=discord.Intents.default() | ||
| | discord.Intents.members | ||
| | discord.Intents.message_content |
There was a problem hiding this comment.
This is the biggest issue here. We have purposefully limited the privileges of TeX-Bot to not have access to read members messages and this is a departure from this. Making this change requires a vote of approval from CSS committee.
Send a quoted message to committee channels when a message is deleted.
Under the following conditions:
This feature is designed to make sure that messages deleted as part of moderation action are retained; in the event that the committee member forgets to do it manually.