Refine log view layout

This commit is contained in:
Codex 2025-06-06 06:44:44 +00:00 committed by Slipstream
parent 684eca4e14
commit 92746ac51f
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD

View File

@ -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}"