Refine log view layout
This commit is contained in:
parent
684eca4e14
commit
92746ac51f
@ -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}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user