From 5315e1102d1d2a2e536aa2ef8000ece15910b0fc Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:42:07 -0700 Subject: [PATCH 01/12] Rewrite Message Edit Logging --- botlogging/logger.py | 18 ++-- configuration/config.default.json | 1 + configuration/config.meta.json | 6 +- modules/moderation/events.py | 135 ++++++++++++++++++++++++------ 4 files changed, 127 insertions(+), 33 deletions(-) diff --git a/botlogging/logger.py b/botlogging/logger.py index 6331cc47..41eee000 100644 --- a/botlogging/logger.py +++ b/botlogging/logger.py @@ -199,6 +199,7 @@ async def send_log( console_only: bool = False, embed: discord.Embed = None, exception: Exception = None, + embed_as_is: bool = False, ) -> None: """A comprehensive logging system This will log a message, embed, and/or exception to the console and discord @@ -238,17 +239,18 @@ async def send_log( if console_only or not self.send: return - # Ensure message is never too long - if len(message) > 4000: - message = f"{message[:4000]}..." - # Get the appropriate target to send to on discord log_channel = await self.get_discord_target(channel) - if embed: - embed = log_level.embed(message).modify_embed(embed) - else: - embed = log_level.embed(message) + if not embed_as_is: + # Ensure message is never too long + if len(message) > 4000: + message = f"{message[:4000]}..." + + if embed: + embed = log_level.embed(message).modify_embed(embed) + else: + embed = log_level.embed(message) try: await log_channel.send(embed=embed) diff --git a/configuration/config.default.json b/configuration/config.default.json index 91cd2ba6..82af4605 100644 --- a/configuration/config.default.json +++ b/configuration/config.default.json @@ -23,6 +23,7 @@ "core_guild_id": "", "core_logging_channel": "", "core_member_events_channel": "", + "core_message_events_channel": "", "core_nickname_filter": false, "core_private_channels": [], "duck_allow_manipulation": true, diff --git a/configuration/config.meta.json b/configuration/config.meta.json index c543e9a6..7f4fa682 100644 --- a/configuration/config.meta.json +++ b/configuration/config.meta.json @@ -81,7 +81,7 @@ }, "core_guild_events_channel": { "datatype": "discord.TextChannel", - "description": "The channel to log guild events to. This includes message events" + "description": "The channel to log guild events to." }, "core_guild_id": { "datatype": "discord.Guild", @@ -95,6 +95,10 @@ "datatype": "discord.TextChannel", "description": "The channel to log member events to." }, + "core_message_events_channel": { + "datatype": "discord.TextChannel", + "description": "The channel to log message events to." + }, "core_nickname_filter": { "datatype": "bool", "description": "Whether to run the nickname filter or not" diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 6d58df65..227b8ecc 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -32,48 +32,135 @@ class EventLogger(cogs.BaseCog): For the explicit purpose of logging, not taking further action """ + CONFIG_MAP: dict[str, str] = { + "guild": "core_guild_events_channel", + "member": "core_member_events_channel", + "message": "core_message_events_channel", + } + + async def send_event_log( + self: Self, + guild: discord.Guild, + log_location: str, + string_message: str, + embed_message: discord.Embed, + channel_location: discord.abc.GuildChannel = None, + ) -> None: + context = LogContext(guild=guild, channel=channel_location) + message_header = f"Events for {guild.name} ({guild.id}): " + log_channel = self.CONFIG_MAP[log_location] + log_channel_id = configuration.get_config_entry(guild.id, log_channel) + await self.bot.logger.send_log( + message=message_header + string_message, + level=LogLevel.INFO, + context=context, + channel=log_channel_id, + embed=embed_message, + embed_as_is=True, + ) + + # MESSAGE EVENTS + @commands.Cog.listener() - async def on_message_edit( - self: Self, before: discord.Message, after: discord.Message + async def on_raw_message_edit( + self: Self, payload: discord.RawMessageUpdateEvent ) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_message_edit + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_raw_message_edit Args: before (discord.Message): The previous version of the message after (discord.Message): The current version of the message """ - # this seems to spam, not sure why - if before.content == after.content: + before = payload.cached_message + after = payload.message + + # Ignore message edit events for not content changes + if before and before.content == after.content: return - guild = getattr(before.channel, "guild", None) + guild = getattr(after.channel, "guild", None) + + # Ignore all message edit events in DMs + if not guild: + return # Ignore ephemeral slash command messages - if not guild and before.type == discord.MessageType.chat_input_command: + if after.type == discord.MessageType.chat_input_command: return - attrs = ["content", "embeds"] - diff = auxiliary.get_object_diff(before, after, attrs) - embed = discord.Embed() - embed = auxiliary.add_diff_fields(embed, diff) - embed.add_field(name="Author", value=before.author) - embed.add_field(name="Channel", value=getattr(before.channel, "name", "DM")) + embed = discord.Embed( + title="Message Edited", + description=f"[Jump to Message]({after.jump_url})", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), + ) + + embed.set_author( + name=str(after.author), + icon_url=after.author.display_avatar.url, + ) + embed.add_field( - name="Server", - value=guild, + name="Author", + value=( + f"**User:** {after.author.mention}\n" + f"**Name:** {after.author}\n" + f"**ID:** {after.author.id}" + ), + inline=True, ) - embed.set_footer(text=f"Author ID: {before.author.id}") - log_channel = configuration.get_config_entry( - before.author.id, "core_guild_events_channel" + embed.add_field( + name="Channel", + value=( + f"**Channel:** {after.channel.mention}\n" + f"**Name:** #{after.channel.name}\n" + f"**ID:** {after.channel.id}" + ), + inline=True, ) - await self.bot.logger.send_log( - message=f"Message edit detected on message with ID {before.id}", - level=LogLevel.INFO, - context=LogContext(guild=before.channel.guild, channel=before.channel), - channel=log_channel, - embed=embed, + embed.add_field( + name="Timestamps", + value=( + f"**Sent:** " + f"()\n" + f"**Edited:** " + f"()" + ), + inline=False, + ) + if before: + old_content = before.clean_content + embed.add_field( + name="Original Content", + value=before.content[:1024] if before.content else "*No content*", + inline=False, + ) + else: + old_content = "**Unknown. Perhaps this message was too old?**" + embed.add_field( + name="Original Content", + value=old_content, + inline=False, + ) + + embed.add_field( + name="New Content", + value=after.content[:1024] if after.content else "*No content*", + inline=False, + ) + + embed.set_footer(text=f"Message ID: {after.id}") + + console_message = f"Message edit: ID: {after.id} in channel ({after.channel.name} ({after.channel.id})). Old: {old_content}, new {after.clean_content}" + + await self.send_event_log( + guild=after.guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=after.channel, ) @commands.Cog.listener() From 13a4c5357bbcd1d67126c60f6fe3aa7e04f87de3 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:44:00 -0700 Subject: [PATCH 02/12] Update message edit docstring --- modules/moderation/events.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 227b8ecc..9b24f187 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -68,8 +68,7 @@ async def on_raw_message_edit( """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_raw_message_edit Args: - before (discord.Message): The previous version of the message - after (discord.Message): The current version of the message + payload (discord.RawMessageUpdateEvent): The raw payload object for the message edit events """ before = payload.cached_message after = payload.message From 6e60142936b36ec453fe191bd6afca79befaea16 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:47:01 -0700 Subject: [PATCH 03/12] Sort the events.py file a bit --- modules/moderation/events.py | 129 +++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 9b24f187..f05aa5df 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -33,6 +33,7 @@ class EventLogger(cogs.BaseCog): """ CONFIG_MAP: dict[str, str] = { + "bot": "core_logging_channel", "guild": "core_guild_events_channel", "member": "core_member_events_channel", "message": "core_message_events_channel", @@ -374,6 +375,8 @@ async def on_reaction_clear( embed=embed, ) + # Guild Events + @commands.Cog.listener() async def on_guild_channel_delete( self: Self, channel: discord.abc.GuildChannel @@ -553,66 +556,6 @@ async def on_webhooks_update(self: Self, channel: discord.abc.GuildChannel) -> N embed=embed, ) - @commands.Cog.listener() - async def on_member_update( - self: Self, before: discord.Member, after: discord.Member - ) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_update - - Args: - before (discord.Member): The updated member's old info - after (discord.Member): Teh updated member's new info - """ - changed_role = set(before.roles) ^ set(after.roles) - if changed_role: - if len(before.roles) < len(after.roles): - embed = discord.Embed() - embed.add_field(name="Roles added", value=next(iter(changed_role))) - embed.add_field(name="Server", value=before.guild.name) - else: - embed = discord.Embed() - embed.add_field(name="Roles lost", value=next(iter(changed_role))) - embed.add_field(name="Server", value=before.guild.name) - - log_channel = configuration.get_config_entry( - before.guild.id, "core_member_events_channel" - ) - - await self.bot.logger.send_log( - message=( - f"Member with ID {before.id} has changed status in guild with ID" - f" {before.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=before.guild), - channel=log_channel, - embed=embed, - ) - - @commands.Cog.listener() - async def on_member_remove(self: Self, member: discord.Member) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_remove - - Args: - member (discord.Member): The member who left - """ - embed = discord.Embed() - embed.add_field(name="Member", value=member) - embed.add_field(name="Server", value=member.guild.name) - log_channel = configuration.get_config_entry( - member.guild.id, "core_member_events_channel" - ) - - await self.bot.logger.send_log( - message=( - f"Member with ID {member.id} has left guild with ID {member.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=member.guild), - channel=log_channel, - embed=embed, - ) - @commands.Cog.listener() async def on_guild_remove(self: Self, guild: discord.Guild) -> None: """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_remove @@ -808,6 +751,68 @@ async def on_guild_emojis_update( embed=embed, ) + # Member Events + + @commands.Cog.listener() + async def on_member_update( + self: Self, before: discord.Member, after: discord.Member + ) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_update + + Args: + before (discord.Member): The updated member's old info + after (discord.Member): Teh updated member's new info + """ + changed_role = set(before.roles) ^ set(after.roles) + if changed_role: + if len(before.roles) < len(after.roles): + embed = discord.Embed() + embed.add_field(name="Roles added", value=next(iter(changed_role))) + embed.add_field(name="Server", value=before.guild.name) + else: + embed = discord.Embed() + embed.add_field(name="Roles lost", value=next(iter(changed_role))) + embed.add_field(name="Server", value=before.guild.name) + + log_channel = configuration.get_config_entry( + before.guild.id, "core_member_events_channel" + ) + + await self.bot.logger.send_log( + message=( + f"Member with ID {before.id} has changed status in guild with ID" + f" {before.guild.id}" + ), + level=LogLevel.INFO, + context=LogContext(guild=before.guild), + channel=log_channel, + embed=embed, + ) + + @commands.Cog.listener() + async def on_member_remove(self: Self, member: discord.Member) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_remove + + Args: + member (discord.Member): The member who left + """ + embed = discord.Embed() + embed.add_field(name="Member", value=member) + embed.add_field(name="Server", value=member.guild.name) + log_channel = configuration.get_config_entry( + member.guild.id, "core_member_events_channel" + ) + + await self.bot.logger.send_log( + message=( + f"Member with ID {member.id} has left guild with ID {member.guild.id}" + ), + level=LogLevel.INFO, + context=LogContext(guild=member.guild), + channel=log_channel, + embed=embed, + ) + @commands.Cog.listener() async def on_member_ban( self: Self, guild: discord.Guild, user: discord.User | discord.Member @@ -885,6 +890,8 @@ async def on_member_join(self: Self, member: discord.Member) -> None: embed=embed, ) + # Bot Events + @commands.Cog.listener() async def on_command(self: Self, ctx: commands.Context) -> None: """ @@ -915,6 +922,8 @@ async def on_command(self: Self, ctx: commands.Context) -> None: embed=embed, ) + # CONSOLE ONLY STUFF + @commands.Cog.listener() async def on_error(self: Self, event_method: str) -> None: """Catches non-command errors and sends them to the error logger for processing. From ee59a2a84caa70b81e5a44995f09002a087e2645 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:08:55 -0700 Subject: [PATCH 04/12] Delete message events --- modules/moderation/events.py | 154 +++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 61 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index f05aa5df..3a7435bc 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -153,7 +153,7 @@ async def on_raw_message_edit( embed.set_footer(text=f"Message ID: {after.id}") - console_message = f"Message edit: ID: {after.id} in channel ({after.channel.name} ({after.channel.id})). Old: {old_content}, new {after.clean_content}" + console_message = f"Message edit: ID: {after.id} in channel: {after.channel.name} ({after.channel.id}). Old: {old_content}, new {after.clean_content}" await self.send_event_log( guild=after.guild, @@ -170,38 +170,70 @@ async def on_message_delete(self: Self, message: discord.Message) -> None: Args: message (discord.Message): The deleted message """ - guild = getattr(message.channel, "guild", None) + guild = message.guild + channel = message.channel # Ignore ephemeral slash command messages - if not guild and message.type == discord.MessageType.chat_input_command: + if message.type == discord.MessageType.chat_input_command: return - embed = discord.Embed() - embed.add_field(name="Content", value=message.content[:1024] or "None") - if len(message.content) > 1024: - embed.add_field(name="\a", value=message.content[1025:2048]) - if len(message.content) > 2048: - embed.add_field(name="\a", value=message.content[2049:3072]) - if len(message.content) > 3072: - embed.add_field(name="\a", value=message.content[3073:4096]) - embed.add_field(name="Author", value=message.author) + embed = discord.Embed( + title="Message Deleted", + description=f"[Jump to Message]({message.jump_url})", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), + ) + + embed.set_author( + name=str(message.author), + icon_url=message.author.display_avatar.url, + ) + + embed.add_field( + name="Author", + value=( + f"**User:** {message.author.mention}\n" + f"**Name:** {message.author}\n" + f"**ID:** {message.author.id}" + ), + inline=True, + ) + embed.add_field( name="Channel", - value=getattr(message.channel, "name", "DM"), + value=( + f"**Channel:** {channel.mention}\n" + f"**Name:** #{channel.name}\n" + f"**ID:** {channel.id}" + ), + inline=True, ) - embed.add_field(name="Server", value=getattr(guild, "name", "None")) - embed.set_footer(text=f"Author ID: {message.author.id}") - log_channel = configuration.get_config_entry( - message.author.id, "core_guild_events_channel" + embed.add_field( + name="Timestamps", + value=( + f"**Sent:** " + f"()\n" + ), + inline=False, ) - await self.bot.logger.send_log( - message=f"Message with ID {message.id} deleted", - level=LogLevel.INFO, - context=LogContext(guild=message.channel.guild, channel=message.channel), - channel=log_channel, - embed=embed, + embed.add_field( + name="Message Content", + value=message.content[:1024] if message.content else "*No content*", + inline=False, + ) + + embed.set_footer(text=f"Message ID: {message.id}") + + console_message = f"Message delete: ID: {message.id} in channel: {channel.name} ({channel.id}). Content: {message.clean_content}" + + await self.send_event_log( + guild=guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=channel, ) @commands.Cog.listener() @@ -556,44 +588,6 @@ async def on_webhooks_update(self: Self, channel: discord.abc.GuildChannel) -> N embed=embed, ) - @commands.Cog.listener() - async def on_guild_remove(self: Self, guild: discord.Guild) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_remove - - Args: - guild (discord.Guild): The guild that got removed - """ - embed = discord.Embed() - embed.add_field(name="Server", value=guild.name) - await self.bot.logger.send_log( - message=f"Left guild with ID {guild.id}", - level=LogLevel.INFO, - context=LogContext(guild=guild), - embed=embed, - ) - - @commands.Cog.listener() - async def on_guild_join(self: Self, guild: discord.Guild) -> None: - """Configures a new guild upon joining. - - Args: - guild (discord.Guild): the guild that was joined - """ - embed = discord.Embed() - embed.add_field(name="Server", value=guild.name) - - log_channel = configuration.get_config_entry( - guild.id, "core_guild_events_channel" - ) - - await self.bot.logger.send_log( - message=f"Joined guild with ID {guild.id}", - level=LogLevel.INFO, - context=LogContext(guild=guild), - channel=log_channel, - embed=embed, - ) - @commands.Cog.listener() async def on_guild_update( self: Self, before: discord.Guild, after: discord.Guild @@ -964,3 +958,41 @@ async def on_disconnect(self: Self) -> None: level=LogLevel.INFO, console_only=True, ) + + @commands.Cog.listener() + async def on_guild_remove(self: Self, guild: discord.Guild) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_remove + + Args: + guild (discord.Guild): The guild that got removed + """ + embed = discord.Embed() + embed.add_field(name="Server", value=guild.name) + await self.bot.logger.send_log( + message=f"Left guild with ID {guild.id}", + level=LogLevel.INFO, + context=LogContext(guild=guild), + embed=embed, + ) + + @commands.Cog.listener() + async def on_guild_join(self: Self, guild: discord.Guild) -> None: + """Configures a new guild upon joining. + + Args: + guild (discord.Guild): the guild that was joined + """ + embed = discord.Embed() + embed.add_field(name="Server", value=guild.name) + + log_channel = configuration.get_config_entry( + guild.id, "core_guild_events_channel" + ) + + await self.bot.logger.send_log( + message=f"Joined guild with ID {guild.id}", + level=LogLevel.INFO, + context=LogContext(guild=guild), + channel=log_channel, + embed=embed, + ) From 07695905f0f6b1d72e1ca8c1ae9ca28a5b623e9a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:39:41 -0700 Subject: [PATCH 05/12] bulk message delete rewritten --- modules/moderation/events.py | 73 ++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 3a7435bc..0f10929d 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -246,31 +246,64 @@ async def on_bulk_message_delete( Args: messages (list[discord.Message]): The messages that have been deleted """ - guild = getattr(messages[0].channel, "guild", None) - - unique_channels = set() - unique_servers = set() - for message in messages: - unique_channels.add(message.channel.name) - unique_servers.add( - f"{message.channel.guild.name} ({message.channel.guild.id})" - ) + channel = messages[0].channel + guild = channel.guild + + # Don't log stuff not in a guild + if not guild: + return embed = discord.Embed() - embed.add_field(name="Channels", value=",".join(unique_channels)) - embed.add_field(name="Servers", value=",".join(unique_servers)) - log_channel = configuration.get_config_entry( - guild.id, "core_guild_events_channel" + embed = discord.Embed( + title="Bulk Message Delete", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), ) - await self.bot.logger.send_log( - message=f"{len(messages)} messages bulk deleted!", - level=LogLevel.INFO, - context=LogContext( - guild=messages[0].channel.guild, channel=messages[0].channel + + embed.add_field( + name="Channel", + value=( + f"**Channel:** {channel.mention}\n" + f"**Name:** #{channel.name}\n" + f"**ID:** {channel.id}" ), - channel=log_channel, - embed=embed, + inline=True, + ) + + description_prefix = f"{len(messages)} messages were deleted:\n" + + max_embed_length = 4096 + content_limit = 100 + + while True: + lines: list[str] = [] + + for message in messages: + clean_content = message.clean_content + + if len(clean_content) > content_limit: + clean_content = f"{clean_content[:content_limit]}..." + + lines.append(f"{message.id}, {message.author.name}: {clean_content}") + + description = description_prefix + "\n".join(lines) + + if len(description) <= max_embed_length or content_limit <= 0: + break + + content_limit -= 1 + + embed.description = description + + console_message = f"Bulk message delete: Channel: {channel.name} ({channel.id}). Amount: {len(messages)}" + + await self.send_event_log( + guild=guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=channel, ) @commands.Cog.listener() From 3887b8b1ebd0f559d55bda57d52e8e4ac2569ea0 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:12:47 -0700 Subject: [PATCH 06/12] Reaction add and remove done --- modules/moderation/events.py | 187 ++++++++++++++++++++++++++--------- 1 file changed, 142 insertions(+), 45 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 0f10929d..fbd27d28 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -47,6 +47,21 @@ async def send_event_log( embed_message: discord.Embed, channel_location: discord.abc.GuildChannel = None, ) -> None: + """This sends a log to discord and the console for the event + + Args: + guild (discord.Guild): The guild the event happened in + log_location (str): The location to log, string in CONFIG_MAP + string_message (str): The string message to send to the console + embed_message (discord.Embed): The embed to send to the configured log channel + channel_location (discord.abc.GuildChannel, optional): + The channel the event happened in, if applicable. Defaults to None. + """ + + # Do nothing if events is disabled in current guild + if not self.extension_enabled(guild): + return + context = LogContext(guild=guild, channel=channel_location) message_header = f"Events for {guild.name} ({guild.id}): " log_channel = self.CONFIG_MAP[log_location] @@ -316,43 +331,84 @@ async def on_reaction_add( reaction (discord.Reaction): The current state of the reaction user (discord.Member | discord.User): The user who added the reaction """ - guild = getattr(reaction.message.channel, "guild", None) + message = reaction.message + channel = message.channel + guild = getattr(channel, "guild", None) - if isinstance(reaction.message.channel, discord.DMChannel): + if isinstance(channel, discord.DMChannel): await self.bot.logger.send_log( message=( f"PM from `{user}`: added {reaction.emoji} reaction to message" - f" {reaction.message.content} in DMs" + f" {message.content} in DMs" ), level=LogLevel.INFO, ) return - embed = discord.Embed() - embed.add_field(name="Emoji", value=reaction.emoji) - embed.add_field(name="User", value=user) - embed.add_field(name="Message", value=reaction.message.content or "None") - embed.add_field(name="Message Author", value=reaction.message.author) - embed.add_field( - name="Channel", value=getattr(reaction.message.channel, "name", "DM") + if not guild: + return + + embed = discord.Embed( + title="Reaction Added", + description=f"[Jump to Message]({message.jump_url})", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), ) - embed.add_field(name="Server", value=guild.name) - log_channel = configuration.get_config_entry( - guild.id, "core_guild_events_channel" + embed.set_author( + name=str(user), + icon_url=user.display_avatar.url, ) - await self.bot.logger.send_log( - message=( - f"Reaction added to message with ID {reaction.message.id} by user with" - f" ID {user.id}" + # Do a better job at handling custom emotes + if isinstance(reaction.emoji, (discord.Emoji, discord.PartialEmoji)): + emoji_value = ( + f"**Emoji:** {reaction.emoji}\n" + f"**Name:** {reaction.emoji.name}\n" + f"**ID:** {reaction.emoji.id}" + ) + else: + emoji_value = reaction.emoji + + embed.add_field(name="Emoji", value=emoji_value) + + embed.add_field( + name="User", + value=( + f"**User:** {user.mention}\n" + f"**Name:** {user.name}\n" + f"**ID:** {user.id}" ), - level=LogLevel.INFO, - context=LogContext( - guild=reaction.message.channel.guild, channel=reaction.message.channel + inline=True, + ) + + embed.add_field( + name="Channel", + value=( + f"**Channel:** {channel.mention}\n" + f"**Name:** #{channel.name}\n" + f"**ID:** {channel.id}" ), - channel=log_channel, - embed=embed, + inline=True, + ) + + embed.add_field( + name="Message Info", + value=( + f"**Message Content:** {message.clean_content[:50]}\n" + f"**Message Author:** {message.author.name} ({message.author.mention})\n" + f"**Message ID:** {message.id}" + ), + ) + + console_message = f"Reaction {reaction.emoji} added to message with ID: {message.id} by user {user.name} ({user.id})" + + await self.send_event_log( + guild=guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=channel, ) @commands.Cog.listener() @@ -365,43 +421,84 @@ async def on_reaction_remove( reaction (discord.Reaction): The current state of the reaction user (discord.Member | discord.User): The user whose reaction was removed """ - guild = getattr(reaction.message.channel, "guild", None) + message = reaction.message + channel = message.channel + guild = getattr(channel, "guild", None) - if isinstance(reaction.message.channel, discord.DMChannel): + if isinstance(channel, discord.DMChannel): await self.bot.logger.send_log( message=( - f"PM from `{user}`: removed {reaction.emoji} reaction to message" - f" {reaction.message.content} in DMs" + f"PM from `{user}`: added {reaction.emoji} reaction to message" + f" {message.content} in DMs" ), level=LogLevel.INFO, ) return - embed = discord.Embed() - embed.add_field(name="Emoji", value=reaction.emoji) - embed.add_field(name="User", value=user) - embed.add_field(name="Message", value=reaction.message.content or "None") - embed.add_field(name="Message Author", value=reaction.message.author) - embed.add_field( - name="Channel", value=getattr(reaction.message.channel, "name", "DM") + if not guild: + return + + embed = discord.Embed( + title="Reaction Removed", + description=f"[Jump to Message]({message.jump_url})", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), ) - embed.add_field(name="Server", value=guild.name) - log_channel = configuration.get_config_entry( - guild.id, "core_guild_events_channel" + embed.set_author( + name=str(user), + icon_url=user.display_avatar.url, ) - await self.bot.logger.send_log( - message=( - f"Reaction removed from message with ID {reaction.message.id} by user" - f" with ID {user.id}" + # Do a better job at handling custom emotes + if isinstance(reaction.emoji, (discord.Emoji, discord.PartialEmoji)): + emoji_value = ( + f"**Emoji:** {reaction.emoji}\n" + f"**Name:** {reaction.emoji.name}\n" + f"**ID:** {reaction.emoji.id}" + ) + else: + emoji_value = reaction.emoji + + embed.add_field(name="Emoji", value=emoji_value) + + embed.add_field( + name="User", + value=( + f"**User:** {user.mention}\n" + f"**Name:** {user.name}\n" + f"**ID:** {user.id}" ), - level=LogLevel.INFO, - context=LogContext( - guild=reaction.message.channel.guild, channel=reaction.message.channel + inline=True, + ) + + embed.add_field( + name="Channel", + value=( + f"**Channel:** {channel.mention}\n" + f"**Name:** #{channel.name}\n" + f"**ID:** {channel.id}" ), - channel=log_channel, - embed=embed, + inline=True, + ) + + embed.add_field( + name="Message Info", + value=( + f"**Message Content:** {message.clean_content[:50]}\n" + f"**Message Author:** {message.author.name} ({message.author.mention})\n" + f"**Message ID:** {message.id}" + ), + ) + + console_message = f"Reaction {reaction.emoji} removed from message with ID: {message.id} by user {user.name} ({user.id})" + + await self.send_event_log( + guild=guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=channel, ) @commands.Cog.listener() From 99263d112f183e9c0dbaa108659eb9e51f29fa61 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:50:47 -0700 Subject: [PATCH 07/12] Reaction clear done --- modules/moderation/events.py | 61 ++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index fbd27d28..1a07eced 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -89,6 +89,10 @@ async def on_raw_message_edit( before = payload.cached_message after = payload.message + # If for some reason there is no after message, log nothing + if not after: + return + # Ignore message edit events for not content changes if before and before.content == after.content: return @@ -513,28 +517,53 @@ async def on_reaction_clear( reactions (list[discord.Reaction]): The reactions that were removed """ guild = getattr(message.channel, "guild", None) + channel = message.channel + + # Don't log messages without a guild + if not guild: + return - unique_emojis = set() + emoji_str = "" + total_emoji = 0 for reaction in reactions: - unique_emojis.add(reaction.emoji) + emoji_str += f"`{reaction.emoji}`: {reaction.count}\n" + total_emoji += reaction.count - embed = discord.Embed() - embed.add_field(name="Emojis", value=",".join(unique_emojis)) - embed.add_field(name="Message", value=message.content or "None") - embed.add_field(name="Message Author", value=message.author) - embed.add_field(name="Channel", value=getattr(message.channel, "name", "DM")) - embed.add_field(name="Server", value=guild.name) + embed = discord.Embed( + title="Reactions Cleared", + description=f"[Jump to Message]({message.jump_url})", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), + ) + embed.add_field(name="Emojis", value=emoji_str) - log_channel = configuration.get_config_entry( - guild.id, "core_guild_events_channel" + embed.add_field( + name="Channel", + value=( + f"**Channel:** {channel.mention}\n" + f"**Name:** #{channel.name}\n" + f"**ID:** {channel.id}" + ), + inline=True, ) - await self.bot.logger.send_log( - message=f"{len(reactions)} cleared from message with ID {message.id}", - level=LogLevel.INFO, - context=LogContext(guild=message.channel.guild, channel=message.channel), - channel=log_channel, - embed=embed, + embed.add_field( + name="Message Info", + value=( + f"**Message Content:** {message.clean_content[:50]}\n" + f"**Message Author:** {message.author.name} ({message.author.mention})\n" + f"**Message ID:** {message.id}" + ), + ) + + console_message = f"{total_emoji} reactions cleared from message with ID: {message.id} in channel {channel.name} ({channel.id})" + + await self.send_event_log( + guild=guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=channel, ) # Guild Events From 6e7370ad168dff5a64b215f0ae8f831cee4ea93a Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:54:33 -0700 Subject: [PATCH 08/12] Work more on better events --- modules/moderation/events.py | 257 ++++++++++++++++++++--------------- 1 file changed, 150 insertions(+), 107 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 1a07eced..a2235c79 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -78,23 +78,16 @@ async def send_event_log( # MESSAGE EVENTS @commands.Cog.listener() - async def on_raw_message_edit( - self: Self, payload: discord.RawMessageUpdateEvent + async def on_message_edit( + self: Self, before: discord.Message, after: discord.Message ) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_raw_message_edit + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_message_edit Args: payload (discord.RawMessageUpdateEvent): The raw payload object for the message edit events """ - before = payload.cached_message - after = payload.message - - # If for some reason there is no after message, log nothing - if not after: - return - - # Ignore message edit events for not content changes - if before and before.content == after.content: + # If for some reason there is no message object, log nothing + if not after or not before: return guild = getattr(after.channel, "guild", None) @@ -107,80 +100,136 @@ async def on_raw_message_edit( if after.type == discord.MessageType.chat_input_command: return - embed = discord.Embed( - title="Message Edited", - description=f"[Jump to Message]({after.jump_url})", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), - ) + # Message edits for content edit: + if before.content != after.content: + embed = discord.Embed( + title="Message Edited", + description=f"[Jump to Message]({after.jump_url})", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), + ) - embed.set_author( - name=str(after.author), - icon_url=after.author.display_avatar.url, - ) + embed.set_author( + name=str(after.author), + icon_url=after.author.display_avatar.url, + ) - embed.add_field( - name="Author", - value=( - f"**User:** {after.author.mention}\n" - f"**Name:** {after.author}\n" - f"**ID:** {after.author.id}" - ), - inline=True, - ) + embed.add_field( + name="Author", + value=( + f"**User:** {after.author.mention}\n" + f"**Name:** {after.author}\n" + f"**ID:** {after.author.id}" + ), + inline=True, + ) - embed.add_field( - name="Channel", - value=( - f"**Channel:** {after.channel.mention}\n" - f"**Name:** #{after.channel.name}\n" - f"**ID:** {after.channel.id}" - ), - inline=True, - ) + embed.add_field( + name="Channel", + value=( + f"**Channel:** {after.channel.mention}\n" + f"**Name:** #{after.channel.name}\n" + f"**ID:** {after.channel.id}" + ), + inline=True, + ) - embed.add_field( - name="Timestamps", - value=( - f"**Sent:** " - f"()\n" - f"**Edited:** " - f"()" - ), - inline=False, - ) - if before: - old_content = before.clean_content embed.add_field( - name="Original Content", - value=before.content[:1024] if before.content else "*No content*", + name="Timestamps", + value=( + f"**Sent:** " + f"()\n" + f"**Edited:** " + f"()" + ), inline=False, ) - else: - old_content = "**Unknown. Perhaps this message was too old?**" + if before: + old_content = before.clean_content + embed.add_field( + name="Original Content", + value=before.content[:1024] if before.content else "*No content*", + inline=False, + ) + else: + old_content = "**Unknown. Perhaps this message was too old?**" + embed.add_field( + name="Original Content", + value=old_content, + inline=False, + ) + embed.add_field( - name="Original Content", - value=old_content, + name="New Content", + value=after.content[:1024] if after.content else "*No content*", inline=False, ) - embed.add_field( - name="New Content", - value=after.content[:1024] if after.content else "*No content*", - inline=False, - ) + embed.set_footer(text=f"Message ID: {after.id}") - embed.set_footer(text=f"Message ID: {after.id}") + console_message = f"Message edit: ID: {after.id} in channel: {after.channel.name} ({after.channel.id}). Old: {old_content}, new {after.clean_content}" - console_message = f"Message edit: ID: {after.id} in channel: {after.channel.name} ({after.channel.id}). Old: {old_content}, new {after.clean_content}" + await self.send_event_log( + guild=after.guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=after.channel, + ) - await self.send_event_log( - guild=after.guild, - log_location="message", - string_message=console_message, - embed_message=embed, - channel_location=after.channel, - ) + # Message edits for pin update: + if before.pinned != after.pinned: + + title = "Message pinned" if after.pinned else "Message unpinned" + embed = discord.Embed( + title=title, + description=f"[Jump to Message]({after.jump_url})", + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), + ) + + embed.set_author( + name=str(after.author), + icon_url=after.author.display_avatar.url, + ) + + embed.add_field( + name="Message Author", + value=( + f"**User:** {after.author.mention}\n" + f"**Name:** {after.author.name}\n" + f"**ID:** {after.author.id}" + ), + inline=True, + ) + + embed.add_field( + name="Channel", + value=( + f"**Channel:** {after.channel.mention}\n" + f"**Name:** #{after.channel.name}\n" + f"**ID:** {after.channel.id}" + ), + inline=True, + ) + + embed.add_field( + name="Content", + value=after.content[:1024] if after.content else "*No content*", + inline=False, + ) + + embed.set_footer(text=f"Message ID: {after.id}") + + console_message = f"Message pins changed: ID: {after.id} in channel: {after.channel.name} ({after.channel.id}). Pinned status: {after.pinned}" + + await self.send_event_log( + guild=after.guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=after.channel, + ) @commands.Cog.listener() async def on_message_delete(self: Self, message: discord.Message) -> None: @@ -568,6 +617,7 @@ async def on_reaction_clear( # Guild Events + # Useful @commands.Cog.listener() async def on_guild_channel_delete( self: Self, channel: discord.abc.GuildChannel @@ -597,6 +647,7 @@ async def on_guild_channel_delete( embed=embed, ) + # Useful @commands.Cog.listener() async def on_guild_channel_create( self: Self, channel: discord.abc.GuildChannel @@ -624,6 +675,7 @@ async def on_guild_channel_create( embed=embed, ) + # Useful @commands.Cog.listener() async def on_guild_channel_update( self: Self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel @@ -664,41 +716,7 @@ async def on_guild_channel_update( embed=embed, ) - @commands.Cog.listener() - async def on_guild_channel_pins_update( - self: Self, - channel: discord.abc.GuildChannel | discord.Thread, - _last_pin: datetime.datetime | None, - ) -> None: - """ - See: - https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_channel_pins_update - - Args: - channel (discord.abc.GuildChannel | discord.Thread): The guild channel - that had its pins updated. - _last_pin (datetime.datetime | None): The latest message that was pinned as an - aware datetime in UTC. Could be None. - """ - embed = discord.Embed() - embed.add_field(name="Channel Name", value=channel.name) - embed.add_field(name="Server", value=channel.guild) - - log_channel = configuration.get_config_entry( - channel.guild.id, "core_guild_events_channel" - ) - - await self.bot.logger.send_log( - message=( - f"Channel pins updated in channel with ID {channel.id} in guild with ID" - f" {channel.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=channel.guild, channel=channel), - channel=log_channel, - embed=embed, - ) - + # Useless @commands.Cog.listener() async def on_guild_integrations_update(self: Self, guild: discord.Guild) -> None: """ @@ -721,6 +739,7 @@ async def on_guild_integrations_update(self: Self, guild: discord.Guild) -> None embed=embed, ) + # Useless @commands.Cog.listener() async def on_webhooks_update(self: Self, channel: discord.abc.GuildChannel) -> None: """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_webhooks_update @@ -747,6 +766,7 @@ async def on_webhooks_update(self: Self, channel: discord.abc.GuildChannel) -> N embed=embed, ) + # Useful @commands.Cog.listener() async def on_guild_update( self: Self, before: discord.Guild, after: discord.Guild @@ -800,6 +820,7 @@ async def on_guild_update( embed=embed, ) + # Useful @commands.Cog.listener() async def on_guild_role_create(self: Self, role: discord.Role) -> None: """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_role_create @@ -823,6 +844,7 @@ async def on_guild_role_create(self: Self, role: discord.Role) -> None: embed=embed, ) + # Useful @commands.Cog.listener() async def on_guild_role_delete(self: Self, role: discord.Role) -> None: """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_role_delete @@ -845,6 +867,7 @@ async def on_guild_role_delete(self: Self, role: discord.Role) -> None: embed=embed, ) + # Useful @commands.Cog.listener() async def on_guild_role_update( self: Self, before: discord.Role, after: discord.Role @@ -877,6 +900,7 @@ async def on_guild_role_update( embed=embed, ) + # Useful @commands.Cog.listener() async def on_guild_emojis_update( self: Self, @@ -906,6 +930,8 @@ async def on_guild_emojis_update( # Member Events + # Useful + # This will have a lot of potential things to log in it, such as roles and nickname @commands.Cog.listener() async def on_member_update( self: Self, before: discord.Member, after: discord.Member @@ -942,6 +968,7 @@ async def on_member_update( embed=embed, ) + # Useful. Should probably be turned into raw @commands.Cog.listener() async def on_member_remove(self: Self, member: discord.Member) -> None: """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_remove @@ -966,6 +993,7 @@ async def on_member_remove(self: Self, member: discord.Member) -> None: embed=embed, ) + # Maybe leave this to modlog and member remove? @commands.Cog.listener() async def on_member_ban( self: Self, guild: discord.Guild, user: discord.User | discord.Member @@ -993,6 +1021,7 @@ async def on_member_ban( embed=embed, ) + # Maybe leave this to modlog? @commands.Cog.listener() async def on_member_unban( self: Self, guild: discord.Guild, user: discord.User @@ -1019,6 +1048,7 @@ async def on_member_unban( embed=embed, ) + # Useful @commands.Cog.listener() async def on_member_join(self: Self, member: discord.Member) -> None: """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_join @@ -1155,3 +1185,16 @@ async def on_guild_join(self: Self, guild: discord.Guild) -> None: channel=log_channel, embed=embed, ) + + + +# Should probably log: +""" +Server deafen/mute (Member?) +Polls creation (Message) +Thread creation/delete (guild) - MAYBE +Automod stuff (guild) +Soundboard & stickers (guild) +Integrations (guild) +user_update (modifications to global name/username?) (member) +""" From 793900993223ccbd29da4ef938750e1b3e6a606e Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:25:38 -0700 Subject: [PATCH 09/12] Standardize functions --- modules/moderation/events.py | 393 +++++++++++++---------------------- 1 file changed, 146 insertions(+), 247 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index a2235c79..40c11753 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -27,6 +27,88 @@ async def setup(bot: bot.TechSupportBot) -> None: await bot.add_cog(EventLogger(bot=bot)) +class EventEmbed(discord.Embed): + """This subclass of embed contains several functions to create consistent fields for displaying various types of data in the event logs""" + + def __init__(self, *, title, description) -> None: + super().__init__( + title=title, + description=description, + colour=discord.Colour.orange(), + timestamp=discord.utils.utcnow(), + ) + + def setEventAuthor(self, author: discord.Member) -> None: + self.set_author( + name=str(author.display_name), + icon_url=author.display_avatar.url, + ) + + def addMemberField(self: Self, title: str, member: discord.Member) -> None: + self.add_field( + name=title, + value=( + f"**User:** {member.mention}\n" + f"**Name:** {member.name}\n" + f"**ID:** {member.id}" + ), + inline=True, + ) + + def addMessageContentField( + self: Self, title: str, message: discord.Message + ) -> None: + if not message.clean_content: + content = "*Non content*" + elif len(message.clean_content) > 1024: + content = message.clean_content[:1021] + "..." + else: + content = message.clean_content + self.add_field( + name=title, + value=content, + inline=True, + ) + + def addMessageInfoField(self: Self, title: str, message: discord.Message) -> None: + self.add_field( + name=title, + value=( + f"**Message Content:** {message.clean_content[:50]}\n" + f"**Message Author:** {message.author.name} ({message.author.mention})\n" + f"**Message ID:** {message.id}" + ), + ) + + def addChannelField( + self: Self, title: str, channel: discord.abc.GuildChannel + ) -> None: + self.add_field( + name=title, + value=( + f"**Channel:** {channel.mention}\n" + f"**Name:** #{channel.name}\n" + f"**ID:** {channel.id}" + ), + inline=True, + ) + + def addEmojiField( + self: Self, title: str, emoji: discord.Emoji | discord.PartialEmoji | str + ) -> None: + # This is to better display custom emotes + if isinstance(emoji, (discord.Emoji, discord.PartialEmoji)): + emoji_value = ( + f"**Emoji:** {emoji}\n" + f"**Name:** {emoji.name}\n" + f"**ID:** {emoji.id}" + ) + else: + emoji_value = f"**Emoji:** {emoji}" + + self.add_field(name=title, value=emoji_value) + + class EventLogger(cogs.BaseCog): """This is the cog that holds all of the discord event listeners For the explicit purpose of logging, not taking further action @@ -102,38 +184,20 @@ async def on_message_edit( # Message edits for content edit: if before.content != after.content: - embed = discord.Embed( + embed = EventEmbed( title="Message Edited", description=f"[Jump to Message]({after.jump_url})", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), ) - embed.set_author( - name=str(after.author), - icon_url=after.author.display_avatar.url, - ) + embed.setEventAuthor(after.author) + embed.addMemberField("Message Author", after.author) + embed.addChannelField("Channel", after.channel) - embed.add_field( - name="Author", - value=( - f"**User:** {after.author.mention}\n" - f"**Name:** {after.author}\n" - f"**ID:** {after.author.id}" - ), - inline=True, - ) - - embed.add_field( - name="Channel", - value=( - f"**Channel:** {after.channel.mention}\n" - f"**Name:** #{after.channel.name}\n" - f"**ID:** {after.channel.id}" - ), - inline=True, - ) + old_content = before.clean_content + embed.addMessageContentField("Old Content", before) + embed.addMessageContentField("New Content", after) + # Custom field for this event embed.add_field( name="Timestamps", value=( @@ -144,26 +208,6 @@ async def on_message_edit( ), inline=False, ) - if before: - old_content = before.clean_content - embed.add_field( - name="Original Content", - value=before.content[:1024] if before.content else "*No content*", - inline=False, - ) - else: - old_content = "**Unknown. Perhaps this message was too old?**" - embed.add_field( - name="Original Content", - value=old_content, - inline=False, - ) - - embed.add_field( - name="New Content", - value=after.content[:1024] if after.content else "*No content*", - inline=False, - ) embed.set_footer(text=f"Message ID: {after.id}") @@ -181,43 +225,15 @@ async def on_message_edit( if before.pinned != after.pinned: title = "Message pinned" if after.pinned else "Message unpinned" - embed = discord.Embed( + embed = EventEmbed( title=title, description=f"[Jump to Message]({after.jump_url})", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), - ) - - embed.set_author( - name=str(after.author), - icon_url=after.author.display_avatar.url, - ) - - embed.add_field( - name="Message Author", - value=( - f"**User:** {after.author.mention}\n" - f"**Name:** {after.author.name}\n" - f"**ID:** {after.author.id}" - ), - inline=True, - ) - - embed.add_field( - name="Channel", - value=( - f"**Channel:** {after.channel.mention}\n" - f"**Name:** #{after.channel.name}\n" - f"**ID:** {after.channel.id}" - ), - inline=True, ) - embed.add_field( - name="Content", - value=after.content[:1024] if after.content else "*No content*", - inline=False, - ) + embed.setEventAuthor(after.author) + embed.addMemberField("Message Author", after.author) + embed.addChannelField("Channel", after.channel) + embed.addMessageContentField("Content", after) embed.set_footer(text=f"Message ID: {after.id}") @@ -245,37 +261,15 @@ async def on_message_delete(self: Self, message: discord.Message) -> None: if message.type == discord.MessageType.chat_input_command: return - embed = discord.Embed( + embed = EventEmbed( title="Message Deleted", description=f"[Jump to Message]({message.jump_url})", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), ) - embed.set_author( - name=str(message.author), - icon_url=message.author.display_avatar.url, - ) - - embed.add_field( - name="Author", - value=( - f"**User:** {message.author.mention}\n" - f"**Name:** {message.author}\n" - f"**ID:** {message.author.id}" - ), - inline=True, - ) - - embed.add_field( - name="Channel", - value=( - f"**Channel:** {channel.mention}\n" - f"**Name:** #{channel.name}\n" - f"**ID:** {channel.id}" - ), - inline=True, - ) + embed.setEventAuthor(message.author) + embed.addMemberField("Message Author", message.author) + embed.addChannelField("Channel", message.channel) + embed.addMessageContentField("Content", message) embed.add_field( name="Timestamps", @@ -286,12 +280,6 @@ async def on_message_delete(self: Self, message: discord.Message) -> None: inline=False, ) - embed.add_field( - name="Message Content", - value=message.content[:1024] if message.content else "*No content*", - inline=False, - ) - embed.set_footer(text=f"Message ID: {message.id}") console_message = f"Message delete: ID: {message.id} in channel: {channel.name} ({channel.id}). Content: {message.clean_content}" @@ -321,23 +309,11 @@ async def on_bulk_message_delete( if not guild: return - embed = discord.Embed() - - embed = discord.Embed( + embed = EventEmbed( title="Bulk Message Delete", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), - ) - - embed.add_field( - name="Channel", - value=( - f"**Channel:** {channel.mention}\n" - f"**Name:** #{channel.name}\n" - f"**ID:** {channel.id}" - ), - inline=True, + description="", ) + embed.addChannelField("Channel", channel) description_prefix = f"{len(messages)} messages were deleted:\n" @@ -401,58 +377,16 @@ async def on_reaction_add( if not guild: return - embed = discord.Embed( + embed = EventEmbed( title="Reaction Added", description=f"[Jump to Message]({message.jump_url})", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), - ) - - embed.set_author( - name=str(user), - icon_url=user.display_avatar.url, - ) - - # Do a better job at handling custom emotes - if isinstance(reaction.emoji, (discord.Emoji, discord.PartialEmoji)): - emoji_value = ( - f"**Emoji:** {reaction.emoji}\n" - f"**Name:** {reaction.emoji.name}\n" - f"**ID:** {reaction.emoji.id}" - ) - else: - emoji_value = reaction.emoji - - embed.add_field(name="Emoji", value=emoji_value) - - embed.add_field( - name="User", - value=( - f"**User:** {user.mention}\n" - f"**Name:** {user.name}\n" - f"**ID:** {user.id}" - ), - inline=True, - ) - - embed.add_field( - name="Channel", - value=( - f"**Channel:** {channel.mention}\n" - f"**Name:** #{channel.name}\n" - f"**ID:** {channel.id}" - ), - inline=True, ) - embed.add_field( - name="Message Info", - value=( - f"**Message Content:** {message.clean_content[:50]}\n" - f"**Message Author:** {message.author.name} ({message.author.mention})\n" - f"**Message ID:** {message.id}" - ), - ) + embed.setEventAuthor(user) + embed.addEmojiField("Emoji", reaction.emoji) + embed.addMemberField("Message Author", user) + embed.addChannelField("Channel", message.channel) + embed.addMessageInfoField("Message Info", message) console_message = f"Reaction {reaction.emoji} added to message with ID: {message.id} by user {user.name} ({user.id})" @@ -491,58 +425,16 @@ async def on_reaction_remove( if not guild: return - embed = discord.Embed( + embed = EventEmbed( title="Reaction Removed", description=f"[Jump to Message]({message.jump_url})", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), - ) - - embed.set_author( - name=str(user), - icon_url=user.display_avatar.url, ) - # Do a better job at handling custom emotes - if isinstance(reaction.emoji, (discord.Emoji, discord.PartialEmoji)): - emoji_value = ( - f"**Emoji:** {reaction.emoji}\n" - f"**Name:** {reaction.emoji.name}\n" - f"**ID:** {reaction.emoji.id}" - ) - else: - emoji_value = reaction.emoji - - embed.add_field(name="Emoji", value=emoji_value) - - embed.add_field( - name="User", - value=( - f"**User:** {user.mention}\n" - f"**Name:** {user.name}\n" - f"**ID:** {user.id}" - ), - inline=True, - ) - - embed.add_field( - name="Channel", - value=( - f"**Channel:** {channel.mention}\n" - f"**Name:** #{channel.name}\n" - f"**ID:** {channel.id}" - ), - inline=True, - ) - - embed.add_field( - name="Message Info", - value=( - f"**Message Content:** {message.clean_content[:50]}\n" - f"**Message Author:** {message.author.name} ({message.author.mention})\n" - f"**Message ID:** {message.id}" - ), - ) + embed.setEventAuthor(user) + embed.addEmojiField("Emoji", reaction.emoji) + embed.addMemberField("Message Author", user) + embed.addChannelField("Channel", message.channel) + embed.addMessageInfoField("Message Info", message) console_message = f"Reaction {reaction.emoji} removed from message with ID: {message.id} by user {user.name} ({user.id})" @@ -578,32 +470,15 @@ async def on_reaction_clear( emoji_str += f"`{reaction.emoji}`: {reaction.count}\n" total_emoji += reaction.count - embed = discord.Embed( + embed = EventEmbed( title="Reactions Cleared", description=f"[Jump to Message]({message.jump_url})", - colour=discord.Colour.orange(), - timestamp=discord.utils.utcnow(), ) + embed.add_field(name="Emojis", value=emoji_str) - embed.add_field( - name="Channel", - value=( - f"**Channel:** {channel.mention}\n" - f"**Name:** #{channel.name}\n" - f"**ID:** {channel.id}" - ), - inline=True, - ) - - embed.add_field( - name="Message Info", - value=( - f"**Message Content:** {message.clean_content[:50]}\n" - f"**Message Author:** {message.author.name} ({message.author.mention})\n" - f"**Message ID:** {message.id}" - ), - ) + embed.addChannelField("Channel", message.channel) + embed.addMessageInfoField("Message Info", message) console_message = f"{total_emoji} reactions cleared from message with ID: {message.id} in channel {channel.name} ({channel.id})" @@ -1187,14 +1062,38 @@ async def on_guild_join(self: Self, guild: discord.Guild) -> None: ) - # Should probably log: """ -Server deafen/mute (Member?) Polls creation (Message) + discord.on_poll_vote_add + discord.on_poll_vote_remove + +Server deafen/mute (Member?) + discord.on_voice_state_update +user_update (modifications to global name/username?) (member) (maybe?) + discord.on_user_update + Thread creation/delete (guild) - MAYBE + discord.on_thread_create + discord.on_thread_update + discord.on_thread_delete Automod stuff (guild) + discord.on_automod_rule_create + discord.on_automod_rule_update + discord.on_automod_rule_delete Soundboard & stickers (guild) + discord.on_soundboard_sound_create + discord.on_soundboard_sound_delete + discord.on_soundboard_sound_update + discord.on_guild_stickers_update Integrations (guild) -user_update (modifications to global name/username?) (member) + discord.on_integration_create + discord.on_integration_update +Invites (Guild) + discord.on_invite_create + discord.on_invite_delete +Scheduled Events (guild) + discord.on_scheduled_event_create + discord.on_scheduled_event_delete + discord.on_scheduled_event_update """ From 08380604731aed39070ef07b27a2b0fcba6521aa Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:40:37 -0700 Subject: [PATCH 10/12] Poll vote add/remove --- modules/moderation/events.py | 96 ++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 40c11753..35e5d1f7 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -59,7 +59,7 @@ def addMessageContentField( self: Self, title: str, message: discord.Message ) -> None: if not message.clean_content: - content = "*Non content*" + content = "*No content*" elif len(message.clean_content) > 1024: content = message.clean_content[:1021] + "..." else: @@ -76,7 +76,9 @@ def addMessageInfoField(self: Self, title: str, message: discord.Message) -> Non value=( f"**Message Content:** {message.clean_content[:50]}\n" f"**Message Author:** {message.author.name} ({message.author.mention})\n" - f"**Message ID:** {message.id}" + f"**Message ID:** {message.id}\n" + f"**Sent:** " + f"()" ), ) @@ -108,6 +110,24 @@ def addEmojiField( self.add_field(name=title, value=emoji_value) + def addPollField(self: Self, title: str, poll: discord.Poll) -> None: + self.add_field( + name=title, + value=( + f"**Question:** {poll.question}\n" + f"**Duration:** {poll.duration}\n" + f"**Answers:** {', '.join([answer.text for answer in poll.answers])}" + ), + inline=True, + ) + + def addPollAnswerField(self: Self, title: str, answer: discord.PollAnswer) -> None: + self.add_field( + name=title, + value=(f"**Answer:** {answer.text}\n**ID:** {answer.id}"), + inline=True, + ) + class EventLogger(cogs.BaseCog): """This is the cog that holds all of the discord event listeners @@ -272,10 +292,10 @@ async def on_message_delete(self: Self, message: discord.Message) -> None: embed.addMessageContentField("Content", message) embed.add_field( - name="Timestamps", + name="Message Sent", value=( - f"**Sent:** " - f"()\n" + f" " + f"()" ), inline=False, ) @@ -490,6 +510,72 @@ async def on_reaction_clear( channel_location=channel, ) + @commands.Cog.listener() + async def on_poll_vote_add( + self: Self, user: discord.Member, answer: discord.PollAnswer + ) -> None: + if not user.guild: + return + + guild = user.guild + message = answer.poll.message + channel = message.channel + + embed = EventEmbed( + title="Poll Answered", + description=f"[Jump to Message]({message.jump_url})", + ) + + embed.setEventAuthor(user) + embed.addPollField("Poll", answer.poll) + embed.addPollAnswerField("Answer", answer) + embed.addChannelField("Channel", channel) + embed.addMemberField("Member", user) + embed.addMessageInfoField("Message", message) + + console_message = f"User {user.name} ({user.id}) voted {answer.text} to poll message {message.id} in channel {channel.name} ({channel.id})" + + await self.send_event_log( + guild=guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=channel, + ) + + @commands.Cog.listener() + async def on_poll_vote_remove( + self: Self, user: discord.Member, answer: discord.PollAnswer + ) -> None: + if not user.guild: + return + + guild = user.guild + message = answer.poll.message + channel = message.channel + + embed = EventEmbed( + title="Poll Answer Removed", + description=f"[Jump to Message]({message.jump_url})", + ) + + embed.setEventAuthor(user) + embed.addPollField("Poll", answer.poll) + embed.addPollAnswerField("Answer", answer) + embed.addChannelField("Channel", channel) + embed.addMemberField("Member", user) + embed.addMessageInfoField("Message", message) + + console_message = f"User {user.name} ({user.id}) removed vote {answer.text} from a poll message {message.id} in channel {channel.name} ({channel.id})" + + await self.send_event_log( + guild=guild, + log_location="message", + string_message=console_message, + embed_message=embed, + channel_location=channel, + ) + # Guild Events # Useful From 29f3876de766904fe995f5023092a09be91c2c7d Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:56:22 -0700 Subject: [PATCH 11/12] Do all the member events --- modules/moderation/events.py | 414 +++++++++++++++++++++-------------- 1 file changed, 250 insertions(+), 164 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 35e5d1f7..0fc62864 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -13,6 +13,7 @@ import configuration from botlogging import LogContext, LogLevel from core import auxiliary, cogs +from modules.moderation import logger if TYPE_CHECKING: import bot @@ -177,7 +178,7 @@ async def send_event_log( embed_as_is=True, ) - # MESSAGE EVENTS + # Message events @commands.Cog.listener() async def on_message_edit( @@ -205,7 +206,7 @@ async def on_message_edit( # Message edits for content edit: if before.content != after.content: embed = EventEmbed( - title="Message Edited", + title="Message edited", description=f"[Jump to Message]({after.jump_url})", ) @@ -282,7 +283,7 @@ async def on_message_delete(self: Self, message: discord.Message) -> None: return embed = EventEmbed( - title="Message Deleted", + title="Message deleted", description=f"[Jump to Message]({message.jump_url})", ) @@ -330,7 +331,7 @@ async def on_bulk_message_delete( return embed = EventEmbed( - title="Bulk Message Delete", + title="Bulk message delete", description="", ) embed.addChannelField("Channel", channel) @@ -398,7 +399,7 @@ async def on_reaction_add( return embed = EventEmbed( - title="Reaction Added", + title="Reaction added", description=f"[Jump to Message]({message.jump_url})", ) @@ -446,7 +447,7 @@ async def on_reaction_remove( return embed = EventEmbed( - title="Reaction Removed", + title="Reaction removed", description=f"[Jump to Message]({message.jump_url})", ) @@ -491,7 +492,7 @@ async def on_reaction_clear( total_emoji += reaction.count embed = EventEmbed( - title="Reactions Cleared", + title="Reactions cleared", description=f"[Jump to Message]({message.jump_url})", ) @@ -522,7 +523,7 @@ async def on_poll_vote_add( channel = message.channel embed = EventEmbed( - title="Poll Answered", + title="Poll answered", description=f"[Jump to Message]({message.jump_url})", ) @@ -555,7 +556,7 @@ async def on_poll_vote_remove( channel = message.channel embed = EventEmbed( - title="Poll Answer Removed", + title="Poll answer removed", description=f"[Jump to Message]({message.jump_url})", ) @@ -576,7 +577,246 @@ async def on_poll_vote_remove( channel_location=channel, ) - # Guild Events + # Member events + + @commands.Cog.listener() + async def on_member_join(self: Self, member: discord.Member) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_join + + Args: + member (discord.Member): The member who joined + """ + embed = EventEmbed( + title="Member joined", + description="", + ) + + embed.setEventAuthor(member) + embed.addMemberField("New Member", member) + + if member.flags.did_rejoin: + embed.set_footer(text="This user has joined this server before") + + console_message = f"Member joined: {member.name} ({member.id})" + + await self.send_event_log( + guild=member.guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + @commands.Cog.listener() + async def on_raw_member_remove( + self: Self, payload: discord.RawMemberRemoveEvent + ) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_remove + + Args: + member (discord.Member): The member who left + """ + member = payload.user + embed = EventEmbed( + title="Member left", + description="", + ) + + embed.setEventAuthor(member) + embed.addMemberField("Member", member) + + if isinstance(member, discord.Member): + embed.add_field( + name="Joined at", + value=( + f" " + f"()" + ), + ) + embed.add_field( + name="Roles", + value=", ".join(logger.generate_role_list(member)), + ) + + # If member object, show roles and date joined? + + console_message = f"Member left: {member.name} ({member.id})" + + await self.send_event_log( + guild=member.guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + @commands.Cog.listener() + async def on_voice_state_update( + self: Self, + member: discord.Member, + before: discord.VoiceState, + after: discord.VoiceState, + ) -> None: + # We need to handle server deafen and server mute + if before.mute != after.mute: + embed = EventEmbed( + title=f"Member Server {'un' if before.mute else ''}muted", + description="", + ) + + embed.setEventAuthor(member) + embed.addMemberField("Member", member) + + if after.channel: + embed.addChannelField("Current Channel", after.channel) + + console_message = f"{embed.title}: {member.name} ({member.id})" + + await self.send_event_log( + guild=member.guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + if before.deaf != after.deaf: + embed = EventEmbed( + title=f"Member Server {'un' if before.deaf else ''}deafened", + description="", + ) + + embed.setEventAuthor(member) + embed.addMemberField("Member", member) + + if after.channel: + embed.addChannelField("Current Channel", after.channel) + + console_message = f"{embed.title}: {member.name} ({member.id})" + + await self.send_event_log( + guild=member.guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + @commands.Cog.listener() + async def on_user_update( + self: Self, before: discord.User, after: discord.User + ) -> None: + # We want to track name and global name changes + if before.name != after.name: + embed = EventEmbed( + title="Member username changed", + description="", + ) + + embed.setEventAuthor(after) + embed.addMemberField("Member", after) + embed.add_field( + name="name:", value=f"**Old:** {before.name}\n**New:** {after.name}" + ) + + console_message = f"Member changed their name: {after.name} ({after.id})" + + for guild in after.mutual_guilds: + await self.send_event_log( + guild=guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + if before.global_name != after.global_name: + embed = EventEmbed( + title="Member global name changed", + description="", + ) + + embed.setEventAuthor(after) + embed.addMemberField("Member", after) + embed.add_field( + name="global_name:", + value=f"**Old:** {before.global_name}\n**New:** {after.global_name}", + ) + + console_message = ( + f"Member changed their global_name: {after.name} ({after.id})" + ) + + for guild in after.mutual_guilds: + await self.send_event_log( + guild=guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + @commands.Cog.listener() + async def on_member_update( + self: Self, before: discord.Member, after: discord.Member + ) -> None: + """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_update + + Args: + before (discord.Member): The updated member's old info + after (discord.Member): Teh updated member's new info + """ + # We want to track role and nickname changes + + if before.nick != after.nick: + embed = EventEmbed( + title="Member nickname changed", + description="", + ) + + embed.setEventAuthor(after) + embed.addMemberField("Member", after) + embed.add_field( + name="nick:", + value=f"**Old:** {before.nick}\n**New:** {after.nick}", + ) + + console_message = f"Member changed their nick: {after.name} ({after.id})" + + await self.send_event_log( + guild=after.guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + roles_lost = set(before.roles) - set(after.roles) + roles_gained = set(after.roles) - set(before.roles) + changed_role = set(before.roles) ^ set(after.roles) + if changed_role: + embed = EventEmbed( + title="Member roles updated", + description="", + ) + embed.setEventAuthor(after) + embed.addMemberField("Member", after) + + if roles_gained: + embed.add_field( + name="Roles added", + value=", ".join([role.mention for role in roles_gained]), + ) + + if roles_lost: + embed.add_field( + name="Roles removed", + value=", ".join([role.mention for role in roles_lost]), + ) + + console_message = f"Member roles updated: {after.name} ({after.id}). Roles changed {', '.join(role.name for role in changed_role)}" + + await self.send_event_log( + guild=after.guild, + log_location="member", + string_message=console_message, + embed_message=embed, + ) + + # Guild events # Useful @commands.Cog.listener() @@ -889,151 +1129,6 @@ async def on_guild_emojis_update( embed=embed, ) - # Member Events - - # Useful - # This will have a lot of potential things to log in it, such as roles and nickname - @commands.Cog.listener() - async def on_member_update( - self: Self, before: discord.Member, after: discord.Member - ) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_update - - Args: - before (discord.Member): The updated member's old info - after (discord.Member): Teh updated member's new info - """ - changed_role = set(before.roles) ^ set(after.roles) - if changed_role: - if len(before.roles) < len(after.roles): - embed = discord.Embed() - embed.add_field(name="Roles added", value=next(iter(changed_role))) - embed.add_field(name="Server", value=before.guild.name) - else: - embed = discord.Embed() - embed.add_field(name="Roles lost", value=next(iter(changed_role))) - embed.add_field(name="Server", value=before.guild.name) - - log_channel = configuration.get_config_entry( - before.guild.id, "core_member_events_channel" - ) - - await self.bot.logger.send_log( - message=( - f"Member with ID {before.id} has changed status in guild with ID" - f" {before.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=before.guild), - channel=log_channel, - embed=embed, - ) - - # Useful. Should probably be turned into raw - @commands.Cog.listener() - async def on_member_remove(self: Self, member: discord.Member) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_remove - - Args: - member (discord.Member): The member who left - """ - embed = discord.Embed() - embed.add_field(name="Member", value=member) - embed.add_field(name="Server", value=member.guild.name) - log_channel = configuration.get_config_entry( - member.guild.id, "core_member_events_channel" - ) - - await self.bot.logger.send_log( - message=( - f"Member with ID {member.id} has left guild with ID {member.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=member.guild), - channel=log_channel, - embed=embed, - ) - - # Maybe leave this to modlog and member remove? - @commands.Cog.listener() - async def on_member_ban( - self: Self, guild: discord.Guild, user: discord.User | discord.Member - ) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_ban - - Args: - guild (discord.Guild): The guild the user got banned from - user (discord.User | discord.Member): The user that got banned. Can be either User - or Member depending if the user was in the guild or not at the time of removal. - """ - embed = discord.Embed() - embed.add_field(name="User", value=user) - embed.add_field(name="Server", value=guild.name) - - log_channel = configuration.get_config_entry( - guild.id, "core_member_events_channel" - ) - - await self.bot.logger.send_log( - message=f"User with ID {user.id} banned from guild with ID {guild.id}", - level=LogLevel.INFO, - context=LogContext(guild=guild), - channel=log_channel, - embed=embed, - ) - - # Maybe leave this to modlog? - @commands.Cog.listener() - async def on_member_unban( - self: Self, guild: discord.Guild, user: discord.User - ) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_unban - - Args: - guild (discord.Guild): The guild the user got unbanned from - user (discord.User): The user that got unbanned - """ - embed = discord.Embed() - embed.add_field(name="User", value=user) - embed.add_field(name="Server", value=guild.name) - - log_channel = configuration.get_config_entry( - guild.id, "core_member_events_channel" - ) - - await self.bot.logger.send_log( - message=f"User with ID {user.id} unbanned from guild with ID {guild.id}", - level=LogLevel.INFO, - context=LogContext(guild=guild), - channel=log_channel, - embed=embed, - ) - - # Useful - @commands.Cog.listener() - async def on_member_join(self: Self, member: discord.Member) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_join - - Args: - member (discord.Member): The member who joined - """ - embed = discord.Embed() - embed.add_field(name="Member", value=member) - embed.add_field(name="Server", value=member.guild.name) - log_channel = configuration.get_config_entry( - member.guild.id, "core_member_events_channel" - ) - - await self.bot.logger.send_log( - message=( - f"Member with ID {member.id} has joined guild with ID {member.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=member.guild), - channel=log_channel, - embed=embed, - ) - # Bot Events @commands.Cog.listener() @@ -1150,15 +1245,6 @@ async def on_guild_join(self: Self, guild: discord.Guild) -> None: # Should probably log: """ -Polls creation (Message) - discord.on_poll_vote_add - discord.on_poll_vote_remove - -Server deafen/mute (Member?) - discord.on_voice_state_update -user_update (modifications to global name/username?) (member) (maybe?) - discord.on_user_update - Thread creation/delete (guild) - MAYBE discord.on_thread_create discord.on_thread_update From 5e1136751d0903d5ba3b2cdad4b7e31e0e705592 Mon Sep 17 00:00:00 2001 From: ajax146 <31014239+ajax146@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:55:29 -0700 Subject: [PATCH 12/12] AddMoreEvents --- modules/moderation/events.py | 374 ++++++++++++++++++++++++----------- 1 file changed, 254 insertions(+), 120 deletions(-) diff --git a/modules/moderation/events.py b/modules/moderation/events.py index 0fc62864..494e2e01 100644 --- a/modules/moderation/events.py +++ b/modules/moderation/events.py @@ -5,7 +5,7 @@ import datetime import sys from collections.abc import Sequence -from typing import TYPE_CHECKING, Self +from typing import TYPE_CHECKING, Any, Self import discord from discord.ext import commands @@ -91,6 +91,7 @@ def addChannelField( value=( f"**Channel:** {channel.mention}\n" f"**Name:** #{channel.name}\n" + f"**Type:** {channel.type.name}\n" f"**ID:** {channel.id}" ), inline=True, @@ -129,6 +130,51 @@ def addPollAnswerField(self: Self, title: str, answer: discord.PollAnswer) -> No inline=True, ) + def addPropertyChangeFields( + self: Self, properties: list[str], before: Any, after: Any + ) -> bool: + changes = [] + + for attr in properties: + old_value = getattr(before, attr, None) + new_value = getattr(after, attr, None) + + # If both are lists, sort them before comparing + if isinstance(old_value, list) and isinstance(new_value, list): + old_compare = sorted(old_value, key=str) + new_compare = sorted(new_value, key=str) + else: + old_compare = old_value + new_compare = new_value + + if old_compare != new_compare: + changes.append((attr, old_value, new_value)) + + if changes: + for attr, old_value, new_value in changes: + # Make the property name prettier + field_name = attr.replace("_", " ").title() + + # Special formatting for categories + if attr == "category": + old_value = old_value.mention if old_value else "None" + new_value = new_value.mention if new_value else "None" + + # Better formatting for booleans + elif isinstance(old_value, bool): + old_value = "Yes" if old_value else "No" + new_value = "Yes" if new_value else "No" + + self.add_field( + name=field_name, + value=f"**Old:** {old_value}\n**New:** {new_value}", + inline=True, + ) + + return True + + return False + class EventLogger(cogs.BaseCog): """This is the cog that holds all of the discord event listeners @@ -818,62 +864,59 @@ async def on_member_update( # Guild events - # Useful @commands.Cog.listener() - async def on_guild_channel_delete( + async def on_guild_channel_create( self: Self, channel: discord.abc.GuildChannel ) -> None: """ - See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_channel_delete + See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_channel_create Args: - channel (discord.abc.GuildChannel): The channel that got deleted + channel (discord.abc.GuildChannel): The channel that got created """ - embed = discord.Embed() - embed.add_field(name="Channel Name", value=channel.name) - embed.add_field(name="Server", value=channel.guild.name) - log_channel = configuration.get_config_entry( - channel.guild.id, "core_guild_events_channel" + embed = EventEmbed( + title="Channel created", + description=f"", ) - await self.bot.logger.send_log( - message=( - f"Channel with ID {channel.id} deleted in guild with ID" - f" {channel.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=channel.guild, channel=channel), - channel=log_channel, - embed=embed, + embed.addChannelField("Channel", channel) + + console_message = f"Channel {channel.name} ({channel.id}) was created" + + await self.send_event_log( + guild=channel.guild, + log_location="guild", + string_message=console_message, + embed_message=embed, + channel_location=channel, ) - # Useful @commands.Cog.listener() - async def on_guild_channel_create( + async def on_guild_channel_delete( self: Self, channel: discord.abc.GuildChannel ) -> None: """ - See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_channel_create + See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_channel_delete Args: - channel (discord.abc.GuildChannel): The channel that got created + channel (discord.abc.GuildChannel): The channel that got deleted """ - embed = discord.Embed() - embed.add_field(name="Channel Name", value=channel.name) - embed.add_field(name="Server", value=channel.guild.name) - log_channel = configuration.get_config_entry( - channel.guild.id, "core_guild_events_channel" + embed = EventEmbed( + title="Channel deleted", + description=f"", ) - await self.bot.logger.send_log( - message=( - f"Channel with ID {channel.id} created in guild with ID" - f" {channel.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=channel.guild, channel=channel), - channel=log_channel, - embed=embed, + + embed.addChannelField("Channel", channel) + + console_message = f"Channel {channel.name} ({channel.id}) was deleted" + + await self.send_event_log( + guild=channel.guild, + log_location="guild", + string_message=console_message, + embed_message=embed, + channel_location=channel, ) # Useful @@ -888,84 +931,143 @@ async def on_guild_channel_update( before (discord.abc.GuildChannel): The updated guild channel's old info after (discord.abc.GuildChannel): The updated guild channel's new info """ - attrs = [ - "category", - "changed_roles", - "name", - "overwrites", - "permissions_synced", - "position", - ] - diff = auxiliary.get_object_diff(before, after, attrs) - embed = discord.Embed() - embed = auxiliary.add_diff_fields(embed, diff) - embed.add_field(name="Channel Name", value=before.name) - embed.add_field(name="Server", value=before.guild.name) + # This is hell. Thanks claude + if before.overwrites != after.overwrites: + embed = EventEmbed( + title="Channel permissions updated", + description="", + ) - log_channel = configuration.get_config_entry( - before.guild.id, "core_guild_events_channel" - ) - await self.bot.logger.send_log( - message=( - f"Channel with ID {before.id} modified in guild with ID" - f" {before.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=before.guild, channel=before), - channel=log_channel, - embed=embed, - ) + embed.addChannelField("Channel", after) - # Useless - @commands.Cog.listener() - async def on_guild_integrations_update(self: Self, guild: discord.Guild) -> None: - """ - See: - https://discordpy.readthedocs.io/en/latest/api.html#discord.on_guild_integrations_update + console_changes: list[str] = [] - Args: - guild (discord.Guild): The guild that had its integrations updated. - """ - embed = discord.Embed() - embed.add_field(name="Server", value=guild) - log_channel = configuration.get_config_entry( - guild.id, "core_guild_events_channel" - ) - await self.bot.logger.send_log( - message=f"Integrations updated in guild with ID {guild.id}", - level=LogLevel.INFO, - context=LogContext(guild=guild), - channel=log_channel, - embed=embed, - ) + all_targets = set(before.overwrites) | set(after.overwrites) - # Useless - @commands.Cog.listener() - async def on_webhooks_update(self: Self, channel: discord.abc.GuildChannel) -> None: - """See: https://discordpy.readthedocs.io/en/latest/api.html#discord.on_webhooks_update + for target in all_targets: + before_overwrite = before.overwrites.get( + target, discord.PermissionOverwrite() + ) + after_overwrite = after.overwrites.get( + target, discord.PermissionOverwrite() + ) - Args: - channel (discord.abc.GuildChannel): The channel that had its webhooks updated. - """ - embed = discord.Embed() - embed.add_field(name="Channel", value=channel.name) - embed.add_field(name="Server", value=channel.guild) + before_perms = dict(before_overwrite) + after_perms = dict(after_overwrite) - log_channel = configuration.get_config_entry( - channel.guild.id, "core_guild_events_channel" - ) + added: list[str] = [] + removed: list[str] = [] + changed: list[str] = [] - await self.bot.logger.send_log( - message=( - f"Webooks updated for channel with ID {channel.id} in guild with ID" - f" {channel.guild.id}" - ), - level=LogLevel.INFO, - context=LogContext(guild=channel.guild, channel=channel), - channel=log_channel, - embed=embed, - ) + all_permissions = set(before_perms) | set(after_perms) + + for permission in sorted(all_permissions): + old = before_perms.get(permission) + new = after_perms.get(permission) + + if old == new: + continue + + if old is None: + added.append( + f"✅ `{permission.replace('_', ' ').title()}` → {new}" + ) + elif new is None: + removed.append( + f"❌ `{permission.replace('_', ' ').title()}` (was {old})" + ) + else: + old_emoji = "✅" if old else "❌" + new_emoji = "✅" if new else "❌" + + changed.append( + f"➖ `{permission.replace('_', ' ').title()}` " + f"{old_emoji} → {new_emoji}" + ) + + if not (added or removed or changed): + continue + + value_parts = [] + + if isinstance(target, discord.Role): + target_name = f"Role:" + value_parts.append(f"{target.mention}") + elif isinstance(target, discord.Member): + target_name = f"Member:" + value_parts.append(f"{target.mention}") + else: + target_name = f"Unknown:" + value_parts.append(f"{target.id}") + + if added: + value_parts.append("**Added**\n" + "\n".join(added)) + + if removed: + value_parts.append("**Removed**\n" + "\n".join(removed)) + + if changed: + value_parts.append("**Changed**\n" + "\n".join(changed)) + + value = "\n\n".join(value_parts) + + # Discord field value limit + if len(value) > 1024: + value = value[:1021] + "..." + + embed.add_field( + name=target_name, + value=value, + inline=False, + ) + + console_changes.append(target_name) + + if not console_changes: + return + + console_message = ( + f"Permission overwrites updated for channel " + f"{after.name} ({after.id})" + ) + + await self.send_event_log( + guild=after.guild, + log_location="guild", + string_message=console_message, + embed_message=embed, + channel_location=after, + ) + + properties_to_track = [ + "category", + "name", + "permissions_synced", + "position", + "topic", + "slowmode_delay", + "bitrate", + "user_limit", + "nsfw", + "rtc_region", + "type", + ] + embed = EventEmbed(title="Channel properties updated", description="") + embed.addChannelField("Channel", after) + + if embed.addPropertyChangeFields(properties_to_track, before, after): + console_message = ( + f"Channel properties updated for channel " f"{after.name} ({after.id})" + ) + + await self.send_event_log( + guild=after.guild, + log_location="guild", + string_message=console_message, + embed_message=embed, + channel_location=after, + ) # Useful @commands.Cog.listener() @@ -1006,20 +1108,46 @@ async def on_guild_update( ], ) - embed = discord.Embed() - embed = auxiliary.add_diff_fields(embed, diff) - embed.add_field(name="Server", value=before.name) + properties_to_track = [ + "afk_channel", + "afk_timeout", + "banner", + "bitrate_limit", + "categories", + "description", + "default_notifications", + "dms_paused_until", + "discovery_splash", + "emoji_limit", + "explicit_content_filter", + "features", + "filesize_limit", + "icon", + "invites_paused_until", + "mfa_level", + "name", + "nsfw_level", + "owner", + "preferred_locale", + "premium_tier", + "public_updates_channel", + "rules_channel", + "safety_alerts_channel", + "splash", + "system_channel", + "verification_level", + ] + embed = EventEmbed(title="Guild properties updated", description="") - log_channel = configuration.get_config_entry( - before.guild.id, "core_guild_events_channel" - ) - await self.bot.logger.send_log( - message=f"Guild with ID {before.id} updated", - level=LogLevel.INFO, - context=LogContext(guild=before), - channel=log_channel, - embed=embed, - ) + if embed.addPropertyChangeFields(properties_to_track, before, after): + console_message = f"Guild properties updated." + + await self.send_event_log( + guild=after, + log_location="guild", + string_message=console_message, + embed_message=embed, + ) # Useful @commands.Cog.listener() @@ -1080,6 +1208,12 @@ async def on_guild_role_update( after (discord.Role): The updated role's updated info. """ attrs = ["color", "mentionable", "name", "permissions", "position", "tags"] + # Tags cannot change, so doesn't matter + # Probably want to do better with color changes, with 2nd/3rd color + # Probably want to do display_icon changes + # Probably want to do hoist changes + + # We probably want properties (everything but permissions) and permissions as 2 different logs diff = auxiliary.get_object_diff(before, after, attrs) embed = discord.Embed()