diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 7e43beb..560f22a 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -89,7 +89,9 @@ class LoggingCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.session: Optional[aiohttp.ClientSession] = None # Session for webhooks - self.last_audit_log_ids: dict[int, Optional[int]] = {} # Store last ID per guild + self.last_audit_log_ids: dict[int, Optional[int]] = ( + {} + ) # Store last ID per guild # Start the audit log poller task if the bot is ready, otherwise wait if bot.is_ready(): asyncio.create_task(self.initialize_cog()) # Use async init helper @@ -129,6 +131,12 @@ class LoggingCog(commands.Cog): ui.Separator(spacing=discord.SeparatorSpacing.small) ) + self.content_container = ui.Container() + self.container.add_item(self.content_container) + self.container.add_item( + ui.Separator(spacing=discord.SeparatorSpacing.small) + ) + timestamp = discord.utils.format_dt(datetime.datetime.utcnow(), style="f") parts = [timestamp, footer or f"Bot ID: {bot.user.id}"] if author: @@ -138,7 +146,7 @@ class LoggingCog(commands.Cog): self.container.add_item(self.footer_display) def add_field(self, name: str, value: str, inline: bool = False) -> None: - self.container.add_item(ui.TextDisplay(f"**{name}:** {value}")) + self.content_container.add_item(ui.TextDisplay(f"**{name}:** {value}")) def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" @@ -160,7 +168,9 @@ class LoggingCog(commands.Cog): """Fetch the latest audit log ID for each guild the bot is in.""" log.info("Initializing last audit log IDs for guilds...") for guild in self.bot.guilds: - if guild.id not in self.last_audit_log_ids: # Only initialize if not already set + if ( + guild.id not in self.last_audit_log_ids + ): # Only initialize if not already set try: if guild.me.guild_permissions.view_audit_log: async for entry in guild.audit_logs(limit=1): @@ -173,20 +183,26 @@ class LoggingCog(commands.Cog): log.warning( f"Missing 'View Audit Log' permission in guild {guild.id}. Cannot initialize audit log ID." ) - self.last_audit_log_ids[guild.id] = None # Mark as unable to fetch + self.last_audit_log_ids[guild.id] = ( + None # Mark as unable to fetch + ) except discord.Forbidden: log.warning( f"Forbidden error fetching initial audit log ID for guild {guild.id}." ) self.last_audit_log_ids[guild.id] = None except discord.HTTPException as e: - log.error(f"HTTP error fetching initial audit log ID for guild {guild.id}: {e}") + log.error( + f"HTTP error fetching initial audit log ID for guild {guild.id}: {e}" + ) self.last_audit_log_ids[guild.id] = None except Exception as e: log.exception( f"Unexpected error fetching initial audit log ID for guild {guild.id}: {e}" ) - self.last_audit_log_ids[guild.id] = None # Mark as unable on other errors + self.last_audit_log_ids[guild.id] = ( + None # Mark as unable on other errors + ) log.info("Finished initializing audit log IDs.") async def start_audit_log_poller_when_ready(self): @@ -230,7 +246,9 @@ class LoggingCog(commands.Cog): ) # log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy except ValueError as e: - log.exception(f"ValueError sending log via webhook for guild {guild.id}. Error: {e}") + log.exception( + f"ValueError sending log via webhook for guild {guild.id}. Error: {e}" + ) # Consider notifying an admin or disabling logging for this guild temporarily # await settings_manager.set_logging_webhook(guild.id, None) # Example: Auto-disable on invalid URL except (discord.Forbidden, discord.NotFound): @@ -242,9 +260,13 @@ class LoggingCog(commands.Cog): except discord.HTTPException as e: log.error(f"HTTP error sending log via webhook for guild {guild.id}: {e}") except aiohttp.ClientError as e: - log.error(f"aiohttp client error sending log via webhook for guild {guild.id}: {e}") + log.error( + f"aiohttp client error sending log via webhook for guild {guild.id}: {e}" + ) except Exception as e: - log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}") + log.exception( + f"Unexpected error sending log via webhook for guild {guild.id}: {e}" + ) def _create_log_embed( self, @@ -364,7 +386,9 @@ class LoggingCog(commands.Cog): allowed_mentions=AllowedMentions.none(), ) except Exception as e: - log.exception(f"Error fetching existing webhook during setup for guild {guild.id}") + log.exception( + f"Error fetching existing webhook during setup for guild {guild.id}" + ) await ctx.send( f"⚠️ An error occurred while checking the existing webhook. Proceeding to create a new one for {channel.mention}.", allowed_mentions=AllowedMentions.none(), @@ -378,7 +402,9 @@ class LoggingCog(commands.Cog): try: avatar_bytes = await self.bot.user.display_avatar.read() except Exception: - log.warning(f"Could not read bot avatar for webhook creation in guild {guild.id}.") + log.warning( + f"Could not read bot avatar for webhook creation in guild {guild.id}." + ) new_webhook = await channel.create_webhook( name=webhook_name, @@ -389,7 +415,9 @@ class LoggingCog(commands.Cog): f"Created logging webhook '{webhook_name}' in channel {channel.id} for guild {guild.id}" ) except discord.HTTPException as e: - log.error(f"Failed to create webhook in {channel.mention} for guild {guild.id}: {e}") + log.error( + f"Failed to create webhook in {channel.mention} for guild {guild.id}: {e}" + ) await ctx.send( f"❌ Failed to create webhook. Error: {e}. This could be due to hitting the channel webhook limit (15).", allowed_mentions=AllowedMentions.none(), @@ -426,7 +454,9 @@ class LoggingCog(commands.Cog): allowed_mentions=AllowedMentions.none(), ) except Exception as e: - log.error(f"Failed to send test message via new webhook for guild {guild.id}: {e}") + log.error( + f"Failed to send test message via new webhook for guild {guild.id}: {e}" + ) await ctx.send( "⚠️ Could not send a test message via the new webhook, but the URL has been saved.", allowed_mentions=AllowedMentions.none(), @@ -442,7 +472,9 @@ class LoggingCog(commands.Cog): # Attempt to delete the created webhook to avoid orphans try: await new_webhook.delete(reason="Failed to save URL to settings") - log.info(f"Deleted orphaned webhook '{new_webhook.name}' for guild {guild.id}") + log.info( + f"Deleted orphaned webhook '{new_webhook.name}' for guild {guild.id}" + ) except Exception as del_e: log.error( f"Failed to delete orphaned webhook '{new_webhook.name}' for guild {guild.id}: {del_e}" @@ -484,7 +516,9 @@ class LoggingCog(commands.Cog): new_status = enabled_status # Save the new status - success = await settings_manager.set_log_event_enabled(guild_id, event_key, new_status) + success = await settings_manager.set_log_event_enabled( + guild_id, event_key, new_status + ) if success: status_str = "ENABLED" if new_status else "DISABLED" @@ -518,11 +552,15 @@ class LoggingCog(commands.Cog): # Paginate if too long for one embed description description = "" for line in lines: - if len(description) + len(line) + 1 > 4000: # Embed description limit (approx) + if ( + len(description) + len(line) + 1 > 4000 + ): # Embed description limit (approx) embed.description = description await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none()) description = line + "\n" # Start new description - embed = discord.Embed(color=discord.Color.blue()) # New embed for continuation + embed = discord.Embed( + color=discord.Color.blue() + ) # New embed for continuation else: description += line + "\n" @@ -533,7 +571,9 @@ class LoggingCog(commands.Cog): @log_group.command(name="list_keys") async def log_list_keys(self, ctx: commands.Context): """Lists all valid event keys for use with the 'log toggle' command.""" - embed = discord.Embed(title="Available Logging Event Keys", color=discord.Color.purple()) + embed = discord.Embed( + title="Available Logging Event Keys", color=discord.Color.purple() + ) keys_text = "\n".join(f"`{key}`" for key in ALL_EVENT_KEYS) # Paginate if needed @@ -577,7 +617,9 @@ class LoggingCog(commands.Cog): footer=f"Thread ID: {thread.id} | Parent ID: {thread.parent_id}", ) if thread.owner: # Sometimes owner isn't cached immediately - embed.set_author(name=str(thread.owner), icon_url=thread.owner.display_avatar.url) + embed.set_author( + name=str(thread.owner), icon_url=thread.owner.display_avatar.url + ) await self._send_log_embed(guild, embed) @commands.Cog.listener() @@ -611,7 +653,9 @@ class LoggingCog(commands.Cog): if before.locked != after.locked: changes.append(f"**Locked:** `{before.locked}` → `{after.locked}`") if before.slowmode_delay != after.slowmode_delay: - changes.append(f"**Slowmode:** `{before.slowmode_delay}s` → `{after.slowmode_delay}s`") + changes.append( + f"**Slowmode:** `{before.slowmode_delay}s` → `{after.slowmode_delay}s`" + ) if before.auto_archive_duration != after.auto_archive_duration: changes.append( f"**Auto-Archive:** `{before.auto_archive_duration} mins` → `{after.auto_archive_duration} mins`" @@ -690,7 +734,9 @@ class LoggingCog(commands.Cog): # Initialization is now handled by initialize_cog called from __init__ or start_audit_log_poller_when_ready # Ensure the poller is running if it wasn't started earlier if self.bot.is_ready() and not self.poll_audit_log.is_running(): - log.warning("Poll audit log task was not running after on_ready, attempting to start.") + log.warning( + "Poll audit log task was not running after on_ready, attempting to start." + ) await self.initialize_cog() # Re-initialize just in case @commands.Cog.listener() @@ -707,10 +753,14 @@ class LoggingCog(commands.Cog): ) break else: - log.warning(f"Missing 'View Audit Log' permission in new guild {guild.id}.") + log.warning( + f"Missing 'View Audit Log' permission in new guild {guild.id}." + ) self.last_audit_log_ids[guild.id] = None except Exception as e: - log.exception(f"Error fetching initial audit log ID for new guild {guild.id}: {e}") + log.exception( + f"Error fetching initial audit log ID for new guild {guild.id}: {e}" + ) self.last_audit_log_ids[guild.id] = None @commands.Cog.listener() @@ -763,7 +813,9 @@ class LoggingCog(commands.Cog): await self._send_log_embed(member.guild, embed) @commands.Cog.listener() - async def on_member_ban(self, guild: discord.Guild, user: Union[discord.User, discord.Member]): + async def on_member_ban( + self, guild: discord.Guild, user: Union[discord.User, discord.Member] + ): event_key = "member_ban_event" if not await self._check_log_enabled(guild.id, event_key): return @@ -803,7 +855,9 @@ class LoggingCog(commands.Cog): changes = [] # Nickname change if before.nick != after.nick: - changes.append(f"**Nickname:** `{before.nick or 'None'}` → `{after.nick or 'None'}`") + changes.append( + f"**Nickname:** `{before.nick or 'None'}` → `{after.nick or 'None'}`" + ) # Role changes (handled more reliably by audit log for who did it) if before.roles != after.roles: added_roles = [r.mention for r in after.roles if r not in before.roles] @@ -815,7 +869,9 @@ class LoggingCog(commands.Cog): # Timeout change if before.timed_out_until != after.timed_out_until: if after.timed_out_until: - timeout_duration = discord.utils.format_dt(after.timed_out_until, style="R") + timeout_duration = discord.utils.format_dt( + after.timed_out_until, style="R" + ) changes.append(f"**Timed Out Until:** {timeout_duration}") else: changes.append("**Timeout Removed**") @@ -824,7 +880,9 @@ class LoggingCog(commands.Cog): # Add avatar change detection if before.display_avatar != after.display_avatar: - changes.append(f"**Avatar Changed**") # URL is enough, no need to show old/new + changes.append( + f"**Avatar Changed**" + ) # URL is enough, no need to show old/new if changes: embed = self._create_log_embed( @@ -882,7 +940,9 @@ class LoggingCog(commands.Cog): if before.hoist != after.hoist: changes.append(f"**Hoisted:** `{before.hoist}` → `{after.hoist}`") if before.mentionable != after.mentionable: - changes.append(f"**Mentionable:** `{before.mentionable}` → `{after.mentionable}`") + changes.append( + f"**Mentionable:** `{before.mentionable}` → `{after.mentionable}`" + ) if before.permissions != after.permissions: # Comparing permissions can be complex, just note that they changed. # Audit log provides specifics on permission changes. @@ -951,20 +1011,28 @@ class LoggingCog(commands.Cog): if before.name != after.name: changes.append(f"**Name:** `{before.name}` → `{after.name}`") - if isinstance(before, discord.TextChannel) and isinstance(after, discord.TextChannel): + if isinstance(before, discord.TextChannel) and isinstance( + after, discord.TextChannel + ): if before.topic != after.topic: - changes.append(f"**Topic:** `{before.topic or 'None'}` → `{after.topic or 'None'}`") + changes.append( + f"**Topic:** `{before.topic or 'None'}` → `{after.topic or 'None'}`" + ) if before.slowmode_delay != after.slowmode_delay: changes.append( f"**Slowmode:** `{before.slowmode_delay}s` → `{after.slowmode_delay}s`" ) if before.nsfw != after.nsfw: changes.append(f"**NSFW:** `{before.nsfw}` → `{after.nsfw}`") - if isinstance(before, discord.VoiceChannel) and isinstance(after, discord.VoiceChannel): + if isinstance(before, discord.VoiceChannel) and isinstance( + after, discord.VoiceChannel + ): if before.bitrate != after.bitrate: changes.append(f"**Bitrate:** `{before.bitrate}` → `{after.bitrate}`") if before.user_limit != after.user_limit: - changes.append(f"**User Limit:** `{before.user_limit}` → `{after.user_limit}`") + changes.append( + f"**User Limit:** `{before.user_limit}` → `{after.user_limit}`" + ) # Permission overwrites change if before.overwrites != after.overwrites: # Identify changes without detailing every permission bit @@ -986,13 +1054,17 @@ class LoggingCog(commands.Cog): f"Removed overwrites for: {', '.join([f'<@{t.id}>' if isinstance(t, discord.Member) else f'<@&{t.id}>' for t in removed_targets])}" ) # Check if any *values* changed for targets present both before and after - if any(before.overwrites[t] != after.overwrites[t] for t in updated_targets): + if any( + before.overwrites[t] != after.overwrites[t] for t in updated_targets + ): overwrite_changes.append( f"Modified overwrites for: {', '.join([f'<@{t.id}>' if isinstance(t, discord.Member) else f'<@&{t.id}>' for t in updated_targets if before.overwrites[t] != after.overwrites[t]])}" ) if overwrite_changes: - changes.append(f"**Permission Overwrites:**\n - " + "\n - ".join(overwrite_changes)) + changes.append( + f"**Permission Overwrites:**\n - " + "\n - ".join(overwrite_changes) + ) else: changes.append( "**Permission Overwrites Updated** (No specific target changes detected by event)" @@ -1032,21 +1104,27 @@ class LoggingCog(commands.Cog): if not await self._check_log_enabled(guild.id, event_key): return - emoji, color = EVENT_STYLES.get("message_edit", ("", discord.Color.light_grey())) + emoji, color = EVENT_STYLES.get( + "message_edit", ("", discord.Color.light_grey()) + ) embed = self._create_log_embed( title=f"{emoji} Message Edited", description=f"Message edited in {after.channel.mention}", color=color, author=after.author, ) - diff = "\n".join(difflib.ndiff(before.content.splitlines(), after.content.splitlines())) + diff = "\n".join( + difflib.ndiff(before.content.splitlines(), after.content.splitlines()) + ) if len(diff) > 1000: diff = diff[:997] + "..." embed.add_field(name="Jump", value=f"[Link]({after.jump_url})") embed.add_field( name="Changes", value=( - f"```diff\n{diff}\n```" if diff.strip() else "`(only embeds/attachments changed)`" + f"```diff\n{diff}\n```" + if diff.strip() + else "`(only embeds/attachments changed)`" ), inline=False, ) @@ -1072,7 +1150,9 @@ class LoggingCog(commands.Cog): if not await self._check_log_enabled(guild.id, event_key): return - emoji, color = EVENT_STYLES.get("message_delete", ("", discord.Color.dark_grey())) + emoji, color = EVENT_STYLES.get( + "message_delete", ("", discord.Color.dark_grey()) + ) embed = self._create_log_embed( title=f"{emoji} Message Deleted", description=f"Message deleted in {message.channel.mention}", @@ -1083,7 +1163,8 @@ class LoggingCog(commands.Cog): if message.content: embed.add_field( name="Content", - value=message.content[:1020] + ("..." if len(message.content) > 1020 else "") + value=message.content[:1020] + + ("..." if len(message.content) > 1020 else "") or "`Empty Message`", inline=False, ) @@ -1143,7 +1224,9 @@ class LoggingCog(commands.Cog): await self._send_log_embed(guild, embed) @commands.Cog.listener() - async def on_reaction_clear(self, message: discord.Message, _: list[discord.Reaction]): + async def on_reaction_clear( + self, message: discord.Message, _: list[discord.Reaction] + ): guild = message.guild if not guild: return # Should not happen in guilds but safety check @@ -1363,7 +1446,9 @@ class LoggingCog(commands.Cog): if invite.max_age: # Use invite.created_at if available, otherwise fall back to current time created_time = ( - invite.created_at if invite.created_at is not None else discord.utils.utcnow() + invite.created_at + if invite.created_at is not None + else discord.utils.utcnow() ) expires_at = created_time + datetime.timedelta(seconds=invite.max_age) desc += f"\nExpires: {discord.utils.format_dt(expires_at, style='R')}" @@ -1421,7 +1506,9 @@ class LoggingCog(commands.Cog): # await self._send_log_embed(ctx.guild, embed) @commands.Cog.listener() - async def on_command_error(self, ctx: commands.Context, error: commands.CommandError): + async def on_command_error( + self, ctx: commands.Context, error: commands.CommandError + ): # Log only significant errors, ignore things like CommandNotFound or CheckFailure if desired ignored = ( commands.CommandNotFound, @@ -1449,10 +1536,14 @@ class LoggingCog(commands.Cog): # Get traceback if available (might need error handling specific to your bot's setup) import traceback - tb = "".join(traceback.format_exception(type(error), error, error.__traceback__)) + tb = "".join( + traceback.format_exception(type(error), error, error.__traceback__) + ) embed.add_field( name="Error Details", - value=(f"```py\n{tb[:1000]}\n...```" if len(tb) > 1000 else f"```py\n{tb}```"), + value=( + f"```py\n{tb[:1000]}\n...```" if len(tb) > 1000 else f"```py\n{tb}```" + ), inline=False, ) @@ -1516,7 +1607,9 @@ class LoggingCog(commands.Cog): ) break except Exception as e: - log.exception(f"Error re-initializing audit log ID for guild {guild.id}: {e}") + log.exception( + f"Error re-initializing audit log ID for guild {guild.id}: {e}" + ) continue # Skip this cycle if re-init fails last_id = self.last_audit_log_ids.get(guild_id) @@ -1583,10 +1676,14 @@ class LoggingCog(commands.Cog): ) # Consider adding backoff logic here if errors persist except Exception as e: - log.exception(f"Unexpected error in poll_audit_log for guild {guild.id}: {e}") + log.exception( + f"Unexpected error in poll_audit_log for guild {guild.id}: {e}" + ) # Don't update last_audit_log_id on unexpected error, retry next time - async def _process_audit_log_entry(self, guild: discord.Guild, entry: discord.AuditLogEntry): + async def _process_audit_log_entry( + self, guild: discord.Guild, entry: discord.AuditLogEntry + ): """Processes a single relevant audit log entry and sends an embed.""" user = entry.user # Moderator/Actor target = entry.target # User/Channel/Role/Message affected @@ -1604,7 +1701,9 @@ class LoggingCog(commands.Cog): if not await self._check_log_enabled(guild.id, audit_event_key): return title = "🛡️ Audit Log: Member Banned" - action_desc = f"{self._user_display(user)} banned {self._user_display(target)}" + action_desc = ( + f"{self._user_display(user)} banned {self._user_display(target)}" + ) color = discord.Color.red() # self._add_id_footer(embed, target, id_name="Target ID") # Footer set later elif entry.action == discord.AuditLogAction.unban: @@ -1612,7 +1711,9 @@ class LoggingCog(commands.Cog): if not await self._check_log_enabled(guild.id, audit_event_key): return title = "🛡️ Audit Log: Member Unbanned" - action_desc = f"{self._user_display(user)} unbanned {self._user_display(target)}" + action_desc = ( + f"{self._user_display(user)} unbanned {self._user_display(target)}" + ) color = discord.Color.blurple() # self._add_id_footer(embed, target, id_name="Target ID") # Footer set later elif entry.action == discord.AuditLogAction.kick: @@ -1620,7 +1721,9 @@ class LoggingCog(commands.Cog): if not await self._check_log_enabled(guild.id, audit_event_key): return title = "🛡️ Audit Log: Member Kicked" - action_desc = f"{self._user_display(user)} kicked {self._user_display(target)}" + action_desc = ( + f"{self._user_display(user)} kicked {self._user_display(target)}" + ) color = discord.Color.brand_red() # self._add_id_footer(embed, target, id_name="Target ID") # Footer set later elif entry.action == discord.AuditLogAction.member_prune: @@ -1630,9 +1733,7 @@ class LoggingCog(commands.Cog): title = "🛡️ Audit Log: Member Prune" days = entry.extra.get("delete_member_days") count = entry.extra.get("members_removed") - action_desc = ( - f"{self._user_display(user)} pruned {count} members inactive for {days} days." - ) + action_desc = f"{self._user_display(user)} pruned {count} members inactive for {days} days." color = discord.Color.dark_red() # No specific target ID here @@ -1666,7 +1767,9 @@ class LoggingCog(commands.Cog): return title = "🛡️ Audit Log: Member Timeout Update" if after_timed_out: - timeout_duration = discord.utils.format_dt(after_timed_out, style="R") + timeout_duration = discord.utils.format_dt( + after_timed_out, style="R" + ) action_desc = f"{self._user_display(user)} timed out {self._user_display(target)} ({target.id}) until {timeout_duration}" color = discord.Color.orange() else: @@ -1724,7 +1827,9 @@ class LoggingCog(commands.Cog): and hasattr(entry.after, "hoist") and entry.before.hoist != entry.after.hoist ): - changes.append(f"Hoisted: `{entry.before.hoist}` → `{entry.after.hoist}`") + changes.append( + f"Hoisted: `{entry.before.hoist}` → `{entry.after.hoist}`" + ) if ( hasattr(entry.before, "mentionable") and hasattr(entry.after, "mentionable") @@ -1760,9 +1865,7 @@ class LoggingCog(commands.Cog): title = "🛡️ Audit Log: Channel Created" channel = target ch_type = str(channel.type).capitalize() - action_desc = ( - f"{user.mention} created {ch_type} channel {channel.mention} (`{channel.name}`)" - ) + action_desc = f"{user.mention} created {ch_type} channel {channel.mention} (`{channel.name}`)" color = discord.Color.green() # self._add_id_footer(embed, channel, id_name="Channel ID") # Footer set later elif entry.action == discord.AuditLogAction.channel_delete: @@ -1774,9 +1877,7 @@ class LoggingCog(commands.Cog): channel_name = entry.before.name channel_id = entry.target.id ch_type = str(entry.before.type).capitalize() - action_desc = ( - f"{user.mention} deleted {ch_type} channel `{channel_name}` ({channel_id})" - ) + action_desc = f"{user.mention} deleted {ch_type} channel `{channel_name}` ({channel_id})" color = discord.Color.red() # self._add_id_footer(embed, obj_id=channel_id, id_name="Channel ID") # Footer set later elif entry.action == discord.AuditLogAction.channel_update: @@ -1819,7 +1920,9 @@ class LoggingCog(commands.Cog): and hasattr(entry.after, "bitrate") and entry.before.bitrate != entry.after.bitrate ): - changes.append(f"Bitrate: `{entry.before.bitrate}` → `{entry.after.bitrate}`") + changes.append( + f"Bitrate: `{entry.before.bitrate}` → `{entry.after.bitrate}`" + ) # Process detailed changes from entry.changes detailed_changes = [] @@ -1832,21 +1935,33 @@ class LoggingCog(commands.Cog): before_val = change.before after_val = change.after if attr == "name": - detailed_changes.append(f"Name: `{before_val}` → `{after_val}`") + detailed_changes.append( + f"Name: `{before_val}` → `{after_val}`" + ) elif attr == "topic": detailed_changes.append( f"Topic: `{before_val or 'None'}` → `{after_val or 'None'}`" ) elif attr == "nsfw": - detailed_changes.append(f"NSFW: `{before_val}` → `{after_val}`") + detailed_changes.append( + f"NSFW: `{before_val}` → `{after_val}`" + ) elif attr == "slowmode_delay": - detailed_changes.append(f"Slowmode: `{before_val}s` → `{after_val}s`") + detailed_changes.append( + f"Slowmode: `{before_val}s` → `{after_val}s`" + ) elif attr == "bitrate": - detailed_changes.append(f"Bitrate: `{before_val}` → `{after_val}`") + detailed_changes.append( + f"Bitrate: `{before_val}` → `{after_val}`" + ) elif attr == "user_limit": - detailed_changes.append(f"User Limit: `{before_val}` → `{after_val}`") + detailed_changes.append( + f"User Limit: `{before_val}` → `{after_val}`" + ) elif attr == "position": - detailed_changes.append(f"Position: `{before_val}` → `{after_val}`") + detailed_changes.append( + f"Position: `{before_val}` → `{after_val}`" + ) elif attr == "category": detailed_changes.append( f"Category: {getattr(before_val, 'mention', 'None')} → {getattr(after_val, 'mention', 'None')}" @@ -1867,7 +1982,9 @@ class LoggingCog(commands.Cog): ) # Determine if added, removed, or updated (before/after values are PermissionOverwrite objects) if before_val is None and after_val is not None: - detailed_changes.append(f"Added overwrite for {target_mention}") + detailed_changes.append( + f"Added overwrite for {target_mention}" + ) elif before_val is not None and after_val is None: detailed_changes.append( f"Removed overwrite for {target_mention}" @@ -1888,7 +2005,9 @@ class LoggingCog(commands.Cog): else: # Handle AuditLogChanges as a non-iterable object # We can access the before and after attributes directly - if hasattr(entry.changes, "before") and hasattr(entry.changes, "after"): + if hasattr(entry.changes, "before") and hasattr( + entry.changes, "after" + ): before = entry.changes.before after = entry.changes.after @@ -1898,7 +2017,9 @@ class LoggingCog(commands.Cog): and hasattr(after, "name") and before.name != after.name ): - detailed_changes.append(f"Name: `{before.name}` → `{after.name}`") + detailed_changes.append( + f"Name: `{before.name}` → `{after.name}`" + ) if ( hasattr(before, "topic") and hasattr(after, "topic") @@ -1912,7 +2033,9 @@ class LoggingCog(commands.Cog): and hasattr(after, "nsfw") and before.nsfw != after.nsfw ): - detailed_changes.append(f"NSFW: `{before.nsfw}` → `{after.nsfw}`") + detailed_changes.append( + f"NSFW: `{before.nsfw}` → `{after.nsfw}`" + ) if ( hasattr(before, "slowmode_delay") and hasattr(after, "slowmode_delay") @@ -1983,15 +2106,21 @@ class LoggingCog(commands.Cog): channel_display = "" if hasattr(channel_target, "mention"): channel_display = channel_target.mention - elif isinstance(channel_target, discord.Object) and hasattr(channel_target, "id"): + elif isinstance(channel_target, discord.Object) and hasattr( + channel_target, "id" + ): # If it's an Object, it might be a deleted channel or not fully loaded. # Using <#id> is a safe way to reference it. channel_display = f"<#{channel_target.id}>" else: # Fallback if it's not an object with 'mention' or an 'Object' with 'id' - channel_display = f"an unknown channel (ID: {getattr(channel_target, 'id', 'N/A')})" + channel_display = ( + f"an unknown channel (ID: {getattr(channel_target, 'id', 'N/A')})" + ) - action_desc = f"{user.mention} bulk deleted {count} messages in {channel_display}" + action_desc = ( + f"{user.mention} bulk deleted {count} messages in {channel_display}" + ) color = discord.Color.dark_grey() # self._add_id_footer(embed, channel_target, id_name="Channel ID") # Footer set later @@ -2045,7 +2174,9 @@ class LoggingCog(commands.Cog): if invite.max_age: # Use invite.created_at if available, otherwise fall back to current time created_time = ( - invite.created_at if invite.created_at is not None else discord.utils.utcnow() + invite.created_at + if invite.created_at is not None + else discord.utils.utcnow() ) expires_at = created_time + datetime.timedelta(seconds=invite.max_age) desc += f"\nExpires: {discord.utils.format_dt(expires_at, style='R')}" @@ -2063,9 +2194,7 @@ class LoggingCog(commands.Cog): invite_code = entry.before.code channel_id = entry.before.channel_id channel_mention = f"<#{channel_id}>" if channel_id else "Unknown Channel" - action_desc = ( - f"{user.mention} deleted invite `{invite_code}` for channel {channel_mention}" - ) + action_desc = f"{user.mention} deleted invite `{invite_code}` for channel {channel_mention}" color = discord.Color.dark_red() # Cannot get invite ID after deletion easily, use code in footer later @@ -2120,7 +2249,8 @@ class LoggingCog(commands.Cog): if ( hasattr(entry.before, "explicit_content_filter") and hasattr(entry.after, "explicit_content_filter") - and entry.before.explicit_content_filter != entry.after.explicit_content_filter + and entry.before.explicit_content_filter + != entry.after.explicit_content_filter ): changes.append( f"Explicit Content Filter: `{entry.before.explicit_content_filter}` → `{entry.after.explicit_content_filter}`" @@ -2151,9 +2281,7 @@ class LoggingCog(commands.Cog): # Generic fallback log title = f"🛡️ Audit Log: {str(entry.action).replace('_', ' ').title()}" # Determine the generic audit key based on the action category if possible - generic_audit_key = ( - f"audit_{str(entry.action).split('.')[0]}" # e.g., audit_member, audit_channel - ) + generic_audit_key = f"audit_{str(entry.action).split('.')[0]}" # e.g., audit_member, audit_channel if generic_audit_key in ALL_EVENT_KEYS: if not await self._check_log_enabled(guild.id, generic_audit_key): return @@ -2171,7 +2299,9 @@ class LoggingCog(commands.Cog): # self._add_id_footer(embed, target, id_name="Target ID") # Footer set later color = discord.Color.light_grey() - if not action_desc: # If no description was generated (e.g., skipped update), skip logging + if ( + not action_desc + ): # If no description was generated (e.g., skipped update), skip logging # log.debug(f"Skipping audit log entry {entry.id} (action: {entry.action}) as no action description was generated.") return @@ -2183,22 +2313,24 @@ class LoggingCog(commands.Cog): author=user, # The moderator/actor is the author of the log entry ) if reason: - embed.add_field(name="Reason", value=reason[:1024], inline=False) # Limit reason length + embed.add_field( + name="Reason", value=reason[:1024], inline=False + ) # Limit reason length # Add relevant IDs to footer (target ID if available, otherwise just mod/entry ID) target_id_str = "" if target: target_id_str = f" | Target ID: {target.id}" elif entry.action == discord.AuditLogAction.role_delete: - target_id_str = ( - f" | Role ID: {entry.target.id}" # Get ID from target even if object deleted - ) + target_id_str = f" | Role ID: {entry.target.id}" # Get ID from target even if object deleted elif entry.action == discord.AuditLogAction.channel_delete: target_id_str = f" | Channel ID: {entry.target.id}" elif entry.action == discord.AuditLogAction.emoji_delete: target_id_str = f" | Emoji ID: {entry.target.id}" elif entry.action == discord.AuditLogAction.invite_delete: - target_id_str = f" | Invite Code: {entry.before.code}" # Use code for deleted invites + target_id_str = ( + f" | Invite Code: {entry.before.code}" # Use code for deleted invites + ) embed.set_footer( text=f"Audit Log Entry ID: {entry.id} | Moderator ID: {user.id}{target_id_str}"