From dfcab5eb6dd07c6485b09a12fa7a7ff0674de1f1 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 16:59:09 +0000 Subject: [PATCH 01/25] Improve logging display --- cogs/logging_cog.py | 38 ++++++++++++++++------------ cogs/mod_log_cog.py | 50 ++++++++++++++++++++++++++----------- cogs/real_moderation_cog.py | 37 ++++++++++++++++----------- cogs/role_management_cog.py | 6 +++-- 4 files changed, 84 insertions(+), 47 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 66c9a6c..a5e4390 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -54,6 +54,12 @@ class LoggingCog(commands.Cog): else: asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start + def _user_display(self, user: Union[discord.Member, discord.User]) -> str: + """Return display name, username and ID string for a user.""" + display = user.display_name if isinstance(user, discord.Member) else user.name + username = f"{user.name}#{user.discriminator}" + return f"{display} ({username}) [ID: {user.id}]" + async def initialize_cog(self): """Asynchronous initialization tasks.""" log.info("Initializing LoggingCog...") @@ -418,7 +424,7 @@ class LoggingCog(commands.Cog): user = await self.bot.fetch_user(member.id) # Get user object embed = self._create_log_embed( title="โž• Member Joined Thread", - description=f"{user.mention} joined thread {thread.mention}.", + description=f"{self._user_display(user)} joined thread {thread.mention}.", color=discord.Color.dark_green(), author=user, footer=f"Thread ID: {thread.id} | User ID: {user.id}" @@ -435,7 +441,7 @@ class LoggingCog(commands.Cog): user = await self.bot.fetch_user(member.id) # Get user object embed = self._create_log_embed( title="โž– Member Left Thread", - description=f"{user.mention} left thread {thread.mention}.", + description=f"{self._user_display(user)} left thread {thread.mention}.", color=discord.Color.dark_orange(), author=user, footer=f"Thread ID: {thread.id} | User ID: {user.id}" @@ -509,7 +515,7 @@ class LoggingCog(commands.Cog): embed = self._create_log_embed( title="๐Ÿ“ฅ Member Joined", - description=f"{member.mention} ({member.id}) joined the server.", + description=f"{self._user_display(member)} joined the server.", color=discord.Color.green(), author=member # Footer already includes User ID via _create_log_embed @@ -527,7 +533,7 @@ class LoggingCog(commands.Cog): # We log it as a generic "left" event here. embed = self._create_log_embed( title="๐Ÿ“ค Member Left", - description=f"{member.mention} left the server.", + description=f"{self._user_display(member)} left the server.", color=discord.Color.orange(), author=member ) @@ -542,7 +548,7 @@ class LoggingCog(commands.Cog): # Note: Ban reason isn't available directly in this event. Audit log might have it. embed = self._create_log_embed( title="๐Ÿ”จ Member Banned (Event)", # Clarify this is the event, audit log has more details - description=f"{user.mention} was banned.\n*Audit log may contain moderator and reason.*", + description=f"{self._user_display(user)} was banned.\n*Audit log may contain moderator and reason.*", color=discord.Color.red(), author=user # User who was banned ) @@ -556,7 +562,7 @@ class LoggingCog(commands.Cog): embed = self._create_log_embed( title="๐Ÿ”“ Member Unbanned", - description=f"{user.mention} was unbanned.", + description=f"{self._user_display(user)} was unbanned.", color=discord.Color.blurple(), author=user # User who was unbanned ) @@ -841,7 +847,7 @@ class LoggingCog(commands.Cog): embed = self._create_log_embed( title="๐Ÿ‘ Reaction Added", - description=f"{user.mention} added {reaction.emoji} to a message by {reaction.message.author.mention} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})", + description=f"{self._user_display(user)} added {reaction.emoji} to a message by {self._user_display(reaction.message.author)} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})", color=discord.Color.gold(), author=user ) @@ -860,7 +866,7 @@ class LoggingCog(commands.Cog): embed = self._create_log_embed( title="๐Ÿ‘Ž Reaction Removed", - description=f"{user.mention} removed {reaction.emoji} from a message by {reaction.message.author.mention} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})", + description=f"{self._user_display(user)} removed {reaction.emoji} from a message by {self._user_display(reaction.message.author)} in {reaction.message.channel.mention} [Jump to Message]({reaction.message.jump_url})", color=discord.Color.dark_gold(), author=user ) @@ -959,7 +965,7 @@ class LoggingCog(commands.Cog): embed = self._create_log_embed( title=action, - description=f"{member.mention}\n{details}", + description=f"{self._user_display(member)}\n{details}", color=color, author=member ) @@ -1264,21 +1270,21 @@ class LoggingCog(commands.Cog): audit_event_key = "audit_ban" if not await self._check_log_enabled(guild.id, audit_event_key): return title = "๐Ÿ›ก๏ธ Audit Log: Member Banned" - action_desc = f"{user.mention} banned {target.mention}" + 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: audit_event_key = "audit_unban" if not await self._check_log_enabled(guild.id, audit_event_key): return title = "๐Ÿ›ก๏ธ Audit Log: Member Unbanned" - action_desc = f"{user.mention} unbanned {target.mention}" + 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: audit_event_key = "audit_kick" if not await self._check_log_enabled(guild.id, audit_event_key): return title = "๐Ÿ›ก๏ธ Audit Log: Member Kicked" - action_desc = f"{user.mention} kicked {target.mention}" + 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: @@ -1287,7 +1293,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"{user.mention} 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 @@ -1301,7 +1307,7 @@ class LoggingCog(commands.Cog): added = [r.mention for r in after_roles if r not in before_roles] removed = [r.mention for r in before_roles if r not in after_roles] if added or removed: # Only log if roles actually changed - action_desc = f"{user.mention} updated roles for {target.mention} ({target.id}):" + action_desc = f"{self._user_display(user)} updated roles for {self._user_display(target)} ({target.id}):" if added: action_desc += f"\n**Added:** {', '.join(added)}" if removed: action_desc += f"\n**Removed:** {', '.join(removed)}" color = discord.Color.blue() @@ -1317,10 +1323,10 @@ class LoggingCog(commands.Cog): title = "๐Ÿ›ก๏ธ Audit Log: Member Timeout Update" if after_timed_out: timeout_duration = discord.utils.format_dt(after_timed_out, style='R') - action_desc = f"{user.mention} timed out {target.mention} ({target.id}) until {timeout_duration}" + action_desc = f"{self._user_display(user)} timed out {self._user_display(target)} ({target.id}) until {timeout_duration}" color = discord.Color.orange() else: - action_desc = f"{user.mention} removed timeout from {target.mention} ({target.id})" + action_desc = f"{self._user_display(user)} removed timeout from {self._user_display(target)} ({target.id})" color = discord.Color.green() # self._add_id_footer(embed, target, id_name="Target ID") # Footer set later else: diff --git a/cogs/mod_log_cog.py b/cogs/mod_log_cog.py index 95d35e9..f518967 100644 --- a/cogs/mod_log_cog.py +++ b/cogs/mod_log_cog.py @@ -32,6 +32,34 @@ class ModLogCog(commands.Cog): # Add command group to the bot's tree self.bot.tree.add_command(self.modlog_group) + def _format_user(self, user: Union[Member, User, Object], guild: Optional[discord.Guild] = None) -> str: + """Return a string with display name, username and ID for a user-like object.""" + if isinstance(user, Object): + return f"Unknown User (ID: {user.id})" + if isinstance(user, Member): + display = user.display_name + elif guild and isinstance(user, User): + member = guild.get_member(user.id) + display = member.display_name if member else user.name + else: + display = user.name + username = f"{user.name}#{user.discriminator}" if isinstance(user, (Member, User)) else "Unknown" + return f"{display} ({username}) [ID: {user.id}]" + + async def _fetch_user_display(self, user_id: int, guild: discord.Guild) -> str: + """Fetch and format a user by ID for display.""" + member = guild.get_member(user_id) + if member: + return self._format_user(member, guild) + user = self.bot.get_user(user_id) + if user: + return self._format_user(user, guild) + try: + user = await self.bot.fetch_user(user_id) + return self._format_user(user, guild) + except discord.HTTPException: + return f"Unknown User (ID: {user_id})" + def register_commands(self): """Register all commands for this cog""" @@ -251,23 +279,12 @@ class ModLogCog(commands.Cog): timestamp=discord.utils.utcnow() ) - # Handle target display - check if it's a Discord Object or User/Member - if isinstance(target, discord.Object): - # For Object, we only have the ID - target_display = f"<@{target.id}> ({target.id})" - else: - # For User/Member, we can use mention - target_display = f"{target.mention} ({target.id})" + target_display = self._format_user(target, guild) - # Determine moderator display based on source if source == "AI_API": moderator_display = f"AI System (ID: {moderator_id_override or 'Unknown'})" - elif isinstance(moderator, discord.Object): - # For Object, we only have the ID - moderator_display = f"<@{moderator.id}> ({moderator.id})" else: - # For User/Member, we can use mention - moderator_display = f"{moderator.mention} ({moderator.id})" + moderator_display = self._format_user(moderator, guild) embed.add_field(name="User", value=target_display, inline=True) @@ -359,9 +376,14 @@ class ModLogCog(commands.Cog): timestamp_str = record['timestamp'].strftime('%Y-%m-%d %H:%M:%S') reason_str = record['reason'] or "N/A" duration_str = f" ({record['duration_seconds']}s)" if record['duration_seconds'] else "" + target_disp = await self._fetch_user_display(record['target_user_id'], interaction.guild) + if record['moderator_id'] == 0: + mod_disp = "AI System" + else: + mod_disp = await self._fetch_user_display(record['moderator_id'], interaction.guild) response_lines.append( f"`Case #{record['case_id']}` [{timestamp_str}] **{record['action_type']}** " - f"Target: <@{record['target_user_id']}> Mod: <@{record['moderator_id']}> " + f"Target: {target_disp} Mod: {mod_disp} " f"Reason: {reason_str}{duration_str}" ) diff --git a/cogs/real_moderation_cog.py b/cogs/real_moderation_cog.py index 249c9a4..e6a05b9 100644 --- a/cogs/real_moderation_cog.py +++ b/cogs/real_moderation_cog.py @@ -30,6 +30,12 @@ class ModerationCog(commands.Cog): # Add command group to the bot's tree self.bot.tree.add_command(self.moderate_group) + def _user_display(self, user: Union[discord.Member, discord.User]) -> str: + """Return display name, username and ID string for a user.""" + display = user.display_name if isinstance(user, discord.Member) else user.name + username = f"{user.name}#{user.discriminator}" + return f"{display} ({username}) [ID: {user.id}]" + def register_commands(self): """Register all commands for this cog""" @@ -285,11 +291,12 @@ class ModerationCog(commands.Cog): # ------------------------- # Send confirmation message with DM status + target_text = self._user_display(member) if send_dm: dm_status = "โœ… DM notification sent" if dm_sent else "โŒ Could not send DM notification (user may have DMs disabled)" - await interaction.response.send_message(f"๐Ÿ”จ **Banned {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}") + await interaction.response.send_message(f"๐Ÿ”จ **Banned {target_text}**! Reason: {reason or 'No reason provided'}\n{dm_status}") else: - await interaction.response.send_message(f"๐Ÿ”จ **Banned {member.mention}**! Reason: {reason or 'No reason provided'}\nโš ๏ธ DM notification was disabled") + await interaction.response.send_message(f"๐Ÿ”จ **Banned {target_text}**! Reason: {reason or 'No reason provided'}\nโš ๏ธ DM notification was disabled") except discord.Forbidden: await interaction.response.send_message("โŒ I don't have permission to ban this member.", ephemeral=True) except discord.HTTPException as e: @@ -349,7 +356,7 @@ class ModerationCog(commands.Cog): # ------------------------- # Send confirmation message - await interaction.response.send_message(f"๐Ÿ”“ **Unbanned {banned_user}**! Reason: {reason or 'No reason provided'}") + await interaction.response.send_message(f"๐Ÿ”“ **Unbanned {self._user_display(banned_user)}**! Reason: {reason or 'No reason provided'}") except discord.Forbidden: await interaction.response.send_message("โŒ I don't have permission to unban this user.", ephemeral=True) except discord.HTTPException as e: @@ -429,7 +436,7 @@ class ModerationCog(commands.Cog): # Send confirmation message with DM status dm_status = "โœ… DM notification sent" if dm_sent else "โŒ Could not send DM notification (user may have DMs disabled)" - await interaction.response.send_message(f"๐Ÿ‘ข **Kicked {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}") + await interaction.response.send_message(f"๐Ÿ‘ข **Kicked {self._user_display(member)}**! Reason: {reason or 'No reason provided'}\n{dm_status}") except discord.Forbidden: await interaction.response.send_message("โŒ I don't have permission to kick this member.", ephemeral=True) except discord.HTTPException as e: @@ -541,7 +548,7 @@ class ModerationCog(commands.Cog): # Send confirmation message with DM status dm_status = "โœ… DM notification sent" if dm_sent else "โŒ Could not send DM notification (user may have DMs disabled)" - await safe_followup(f"โฐ **Timed out {member.mention}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}") + await safe_followup(f"โฐ **Timed out {self._user_display(member)}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}") except discord.Forbidden: await safe_followup("โŒ I don't have permission to timeout this member.", ephemeral=True) except discord.HTTPException as e: @@ -606,7 +613,7 @@ class ModerationCog(commands.Cog): # Send confirmation message with DM status dm_status = "โœ… DM notification sent" if dm_sent else "โŒ Could not send DM notification (user may have DMs disabled)" - await interaction.response.send_message(f"โฐ **Removed timeout from {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}") + await interaction.response.send_message(f"โฐ **Removed timeout from {self._user_display(member)}**! Reason: {reason or 'No reason provided'}\n{dm_status}") except discord.Forbidden: await interaction.response.send_message("โŒ I don't have permission to remove the timeout from this member.", ephemeral=True) except discord.HTTPException as e: @@ -645,7 +652,7 @@ class ModerationCog(commands.Cog): logger.info(f"{len(deleted)} messages from user {user} (ID: {user.id}) were purged from channel {interaction.channel.name} (ID: {interaction.channel.id}) in {interaction.guild.name} (ID: {interaction.guild.id}) by {interaction.user} (ID: {interaction.user.id}).") # Send confirmation message - await interaction.followup.send(f"๐Ÿงน **Purged {len(deleted)} messages** from {user.mention}!", ephemeral=True) + await interaction.followup.send(f"๐Ÿงน **Purged {len(deleted)} messages** from {self._user_display(user)}!", ephemeral=True) else: # Delete messages from anyone deleted = await interaction.channel.purge(limit=amount) @@ -699,7 +706,7 @@ class ModerationCog(commands.Cog): # ------------------------- # Send warning message in the channel - await interaction.response.send_message(f"โš ๏ธ **{member.mention} has been warned**! Reason: {reason}") + await interaction.response.send_message(f"โš ๏ธ **{self._user_display(member)} has been warned**! Reason: {reason}") # Try to DM the user about the warning try: @@ -788,7 +795,7 @@ class ModerationCog(commands.Cog): infractions = await mod_log_db.get_user_mod_logs(self.bot.pg_pool, interaction.guild.id, member.id) if not infractions: - await interaction.response.send_message(f"No infractions found for {member.mention}.", ephemeral=True) + await interaction.response.send_message(f"No infractions found for {self._user_display(member)}.", ephemeral=True) return embed = discord.Embed( @@ -901,11 +908,11 @@ class ModerationCog(commands.Cog): reason=f"Cleared {deleted_count} infractions. Reason: {reason or 'Not specified'}", duration=None ) - await interaction_confirm.response.edit_message(content=f"โœ… Successfully cleared {deleted_count} infractions for {member.mention}. Reason: {reason or 'Not specified'}", view=None) + await interaction_confirm.response.edit_message(content=f"โœ… Successfully cleared {deleted_count} infractions for {self._user_display(member)}. Reason: {reason or 'Not specified'}", view=None) elif deleted_count == 0: - await interaction_confirm.response.edit_message(content=f"โ„น๏ธ No infractions found for {member.mention} to clear.", view=None) + await interaction_confirm.response.edit_message(content=f"โ„น๏ธ No infractions found for {self._user_display(member)} to clear.", view=None) else: # Should not happen if 0 is returned for no logs - await interaction_confirm.response.edit_message(content=f"โŒ Failed to clear infractions for {member.mention}. An error occurred.", view=None) + await interaction_confirm.response.edit_message(content=f"โŒ Failed to clear infractions for {self._user_display(member)}. An error occurred.", view=None) async def cancel_callback(interaction_cancel: discord.Interaction): if interaction_cancel.user.id != interaction.user.id: @@ -919,7 +926,7 @@ class ModerationCog(commands.Cog): view.add_item(cancel_button) await interaction.response.send_message( - f"โš ๏ธ Are you sure you want to clear **ALL** infractions for {member.mention}?\n" + f"โš ๏ธ Are you sure you want to clear **ALL** infractions for {self._user_display(member)}?\n" f"This action is irreversible. Reason: {reason or 'Not specified'}", view=view, ephemeral=True @@ -1044,7 +1051,7 @@ class ModerationCog(commands.Cog): # Send confirmation message with DM status dm_status = "โœ… DM notification sent" if dm_sent else "โŒ Could not send DM notification (user may have DMs disabled)" - await ctx.reply(f"โฐ **Timed out {member.mention}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}") + await ctx.reply(f"โฐ **Timed out {self._user_display(member)}** for {duration}! Reason: {reason or 'No reason provided'}\n{dm_status}") except discord.Forbidden: await ctx.reply("โŒ I don't have permission to timeout this member.") except discord.HTTPException as e: @@ -1124,7 +1131,7 @@ class ModerationCog(commands.Cog): # Send confirmation message with DM status dm_status = "โœ… DM notification sent" if dm_sent else "โŒ Could not send DM notification (user may have DMs disabled)" - await ctx.reply(f"โฐ **Removed timeout from {member.mention}**! Reason: {reason or 'No reason provided'}\n{dm_status}") + await ctx.reply(f"โฐ **Removed timeout from {self._user_display(member)}**! Reason: {reason or 'No reason provided'}\n{dm_status}") except discord.Forbidden: await ctx.reply("โŒ I don't have permission to remove the timeout from this member.") except discord.HTTPException as e: diff --git a/cogs/role_management_cog.py b/cogs/role_management_cog.py index be63fef..f678ef3 100644 --- a/cogs/role_management_cog.py +++ b/cogs/role_management_cog.py @@ -439,9 +439,10 @@ class RoleManagementCog(commands.Cog): # Attempt to DM the user try: + role_info = f"{role.name} (ID: {role.id})" dm_embed = discord.Embed( title="Role Added", - description=f"The role {role.mention} was added to you in **{interaction.guild.name}**.", + description=f"The role {role_info} was added to you in **{interaction.guild.name}**.", color=role.color ) dm_embed.add_field(name="Added by", value=interaction.user.mention, inline=True) @@ -506,9 +507,10 @@ class RoleManagementCog(commands.Cog): # Attempt to DM the user try: + role_info = f"{role.name} (ID: {role.id})" dm_embed = discord.Embed( title="Role Removed", - description=f"The role {role.mention} was removed from you in **{interaction.guild.name}**.", + description=f"The role {role_info} was removed from you in **{interaction.guild.name}**.", color=role.color ) dm_embed.add_field(name="Removed by", value=interaction.user.mention, inline=True) From dc01bb62df751fa40b6e95a305ab05dc63500dbb Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 17:46:25 +0000 Subject: [PATCH 02/25] refactor logging views --- cogs/logging_cog.py | 60 +++++++++++------- cogs/mod_log_cog.py | 148 ++++++++++++++++++++------------------------ 2 files changed, 104 insertions(+), 104 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index a5e4390..3ece6e4 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -1,5 +1,6 @@ import discord from discord.ext import commands, tasks +from discord import ui import datetime import asyncio import aiohttp # Added for webhook sending @@ -54,6 +55,30 @@ class LoggingCog(commands.Cog): else: asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start + class LogView(ui.LayoutView): + """Simple view for log messages.""" + + def __init__(self, bot: commands.Bot, title: str, description: str, + color: discord.Color, author: Optional[discord.abc.User], + footer: Optional[str]): + super().__init__(timeout=None) + container = ui.Container(accent_colour=color) + self.add_item(container) + + header = ui.Section(accessory=(ui.Thumbnail(media=author.display_avatar.url) + if author else None)) + header.add_item(ui.TextDisplay(f"**{title}**")) + if description: + header.add_item(ui.TextDisplay(description)) + container.add_item(header) + + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + + footer_text = footer or f"Bot ID: {bot.user.id}" + ( + f" | User ID: {author.id}" if author else "") + self.footer_display = ui.TextDisplay(footer_text) + container.add_item(self.footer_display) + def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" display = user.display_name if isinstance(user, discord.Member) else user.name @@ -109,8 +134,8 @@ class LoggingCog(commands.Cog): await self.session.close() log.info("aiohttp ClientSession closed for LoggingCog.") - async def _send_log_embed(self, guild: discord.Guild, embed: discord.Embed): - """Sends the log embed via the configured webhook for the guild.""" + async def _send_log_embed(self, guild: discord.Guild, embed: ui.LayoutView): + """Sends the log view via the configured webhook for the guild.""" if not self.session or self.session.closed: log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.") return @@ -124,9 +149,9 @@ class LoggingCog(commands.Cog): try: webhook = discord.Webhook.from_url(webhook_url, session=self.session) await webhook.send( - embed=embed, - username=f"{self.bot.user.name} Logs", # Optional: Customize webhook appearance - avatar_url=self.bot.user.display_avatar.url # Optional: Use bot's avatar + view=embed, + username=f"{self.bot.user.name} Logs", + avatar_url=self.bot.user.display_avatar.url, ) # log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy except ValueError: @@ -145,26 +170,17 @@ class LoggingCog(commands.Cog): log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}") - def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> discord.Embed: - """Creates a standardized log embed.""" - embed = discord.Embed(title=title, description=description, color=color, timestamp=datetime.datetime.now(datetime.timezone.utc)) - if author: - embed.set_author(name=str(author), icon_url=author.display_avatar.url) - if footer: - embed.set_footer(text=footer) - else: - # Add User ID to footer if author is present and footer isn't custom - user_id_str = f" | User ID: {author.id}" if author else "" - embed.set_footer(text=f"Bot ID: {self.bot.user.id}{user_id_str}") - return embed + def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> ui.LayoutView: + """Creates a standardized log view.""" + return self.LogView(self.bot, title, description, color, author, footer) - def _add_id_footer(self, embed: discord.Embed, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"): - """Adds an ID to the embed footer if possible.""" + def _add_id_footer(self, embed: ui.LayoutView, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"): + """Adds an ID to the footer text if possible.""" target_id = obj_id or (obj.id if obj else None) - if target_id: - existing_footer = embed.footer.text or "" + if target_id and hasattr(embed, "footer_display"): + existing_footer = embed.footer_display.content or "" separator = " | " if existing_footer else "" - embed.set_footer(text=f"{existing_footer}{separator}{id_name}: {target_id}") + embed.footer_display.content = f"{existing_footer}{separator}{id_name}: {target_id}" async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool: """Checks if logging is enabled for a specific event key in a guild.""" diff --git a/cogs/mod_log_cog.py b/cogs/mod_log_cog.py index f518967..4f8c359 100644 --- a/cogs/mod_log_cog.py +++ b/cogs/mod_log_cog.py @@ -1,6 +1,6 @@ import discord from discord.ext import commands -from discord import app_commands, Interaction, Embed, Color, User, Member, Object +from discord import app_commands, Interaction, Embed, Color, User, Member, Object, ui import asyncpg import logging from typing import Optional, Union, Dict, Any @@ -32,6 +32,21 @@ class ModLogCog(commands.Cog): # Add command group to the bot's tree self.bot.tree.add_command(self.modlog_group) + class LogView(ui.LayoutView): + """View used for moderation log messages.""" + + def __init__(self, bot: commands.Bot, title: str, color: discord.Color, lines: list[str], footer: str): + super().__init__(timeout=None) + container = ui.Container(accent_colour=color) + self.add_item(container) + container.add_item(ui.TextDisplay(f"**{title}**")) + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + for line in lines: + container.add_item(ui.TextDisplay(line)) + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + self.footer_display = ui.TextDisplay(footer) + container.add_item(self.footer_display) + def _format_user(self, user: Union[Member, User, Object], guild: Optional[discord.Guild] = None) -> str: """Return a string with display name, username and ID for a user-like object.""" if isinstance(user, Object): @@ -220,8 +235,8 @@ class ModLogCog(commands.Cog): # Optionally update DB to remove channel ID? Or just leave it. return - # 3. Format and send embed - embed = self._format_log_embed( + # 3. Format and send view + view = self._format_log_embed( case_id=case_id, moderator=moderator, # Pass the object for display formatting target=target, @@ -233,7 +248,7 @@ class ModLogCog(commands.Cog): ai_details=ai_details, moderator_id_override=moderator_id_override # Pass override for formatting ) - log_message = await log_channel.send(embed=embed) + log_message = await log_channel.send(view=view) # 4. Update DB with message details await mod_log_db.update_mod_log_message_details(self.pool, case_id, log_message.id, log_channel.id) @@ -241,6 +256,7 @@ class ModLogCog(commands.Cog): except Exception as e: log.exception(f"Error during Discord mod log message sending/updating for case {case_id} in guild {guild_id}: {e}") + def _format_log_embed( self, case_id: int, @@ -252,9 +268,9 @@ class ModLogCog(commands.Cog): guild: discord.Guild, source: str = "BOT", ai_details: Optional[Dict[str, Any]] = None, - moderator_id_override: Optional[int] = None - ) -> Embed: - """Helper function to create the standard log embed.""" + moderator_id_override: Optional[int] = None, + ) -> ui.LayoutView: + """Helper function to create the standard log view.""" color_map = { "BAN": Color.red(), "UNBAN": Color.green(), @@ -265,87 +281,56 @@ class ModLogCog(commands.Cog): "AI_ALERT": Color.purple(), "AI_DELETE_REQUESTED": Color.dark_grey(), } - # Use a distinct color for AI actions - if source == "AI_API": - embed_color = Color.blurple() - else: - embed_color = color_map.get(action_type.upper(), Color.greyple()) + embed_color = Color.blurple() if source == "AI_API" else color_map.get(action_type.upper(), Color.greyple()) action_title_prefix = "๐Ÿค– AI Moderation Action" if source == "AI_API" else action_type.replace("_", " ").title() action_title = f"{action_title_prefix} | Case #{case_id}" - - embed = Embed( - title=action_title, - color=embed_color, - timestamp=discord.utils.utcnow() - ) - target_display = self._format_user(target, guild) - - if source == "AI_API": - moderator_display = f"AI System (ID: {moderator_id_override or 'Unknown'})" - else: - moderator_display = self._format_user(moderator, guild) - - - embed.add_field(name="User", value=target_display, inline=True) - embed.add_field(name="Moderator", value=moderator_display, inline=True) - - # Add AI-specific details if available + moderator_display = ( + f"AI System (ID: {moderator_id_override or 'Unknown'})" if source == "AI_API" else self._format_user(moderator, guild) + ) + lines = [f"**User:** {target_display}", f"**Moderator:** {moderator_display}"] if ai_details: - if 'rule_violated' in ai_details: - embed.add_field(name="Rule Violated", value=ai_details['rule_violated'], inline=True) - if 'reasoning' in ai_details: - # Use AI reasoning as the main reason field if bot reason is empty - reason_to_display = reason or ai_details['reasoning'] - embed.add_field(name="Reason / AI Reasoning", value=reason_to_display or "No reason provided.", inline=False) - # Optionally add bot reason separately if both exist and differ - if reason and reason != ai_details['reasoning']: - embed.add_field(name="Original Bot Reason", value=reason, inline=False) + if "rule_violated" in ai_details: + lines.append(f"**Rule Violated:** {ai_details['rule_violated']}") + if "reasoning" in ai_details: + reason_to_display = reason or ai_details["reasoning"] + lines.append(f"**Reason / AI Reasoning:** {reason_to_display or 'No reason provided.'}") + if reason and reason != ai_details["reasoning"]: + lines.append(f"**Original Bot Reason:** {reason}") else: - embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False) - - # Add full message content if available - if 'message_content' in ai_details: - # Truncate if too long (Discord has a 1024 character limit for embed fields) - message_content = ai_details['message_content'] + lines.append(f"**Reason:** {reason or 'No reason provided.'}") + if "message_content" in ai_details: + message_content = ai_details["message_content"] if len(message_content) > 1000: message_content = message_content[:997] + "..." - embed.add_field(name="Message Content", value=message_content, inline=False) + lines.append(f"**Message Content:** {message_content}") else: - embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False) - + lines.append(f"**Reason:** {reason or 'No reason provided.'}") if duration: - # Format duration nicely (e.g., "1 day", "2 hours 30 minutes") - # This is a simple version, could be made more robust total_seconds = int(duration.total_seconds()) days, remainder = divmod(total_seconds, 86400) hours, remainder = divmod(remainder, 3600) minutes, seconds = divmod(remainder, 60) duration_str = "" - if days > 0: duration_str += f"{days}d " - if hours > 0: duration_str += f"{hours}h " - if minutes > 0: duration_str += f"{minutes}m " - if seconds > 0 or not duration_str: duration_str += f"{seconds}s" + if days > 0: + duration_str += f"{days}d " + if hours > 0: + duration_str += f"{hours}h " + if minutes > 0: + duration_str += f"{minutes}m " + if seconds > 0 or not duration_str: + duration_str += f"{seconds}s" duration_str = duration_str.strip() - - embed.add_field(name="Duration", value=duration_str, inline=True) - # Add expiration timestamp if applicable (e.g., for timeouts) + lines.append(f"**Duration:** {duration_str}") if action_type.upper() == "TIMEOUT": - expires_at = discord.utils.utcnow() + duration - embed.add_field(name="Expires", value=f"", inline=True) - - - if source == "AI_API": - ai_model = ai_details.get("ai_model") if ai_details else None - embed.set_footer( - text=f"AI Moderation Action โ€ข {guild.name} ({guild.id})" + (f" โ€ข Model: {ai_model}" if ai_model else ""), - icon_url="https://cdn-icons-png.flaticon.com/512/4712/4712035.png" - ) - else: - embed.set_footer(text=f"Guild: {guild.name} ({guild.id})") - - return embed - + expires_at = discord.utils.utcnow() + duration + lines.append(f"**Expires:** ") + footer = ( + f"AI Moderation Action โ€ข {guild.name} ({guild.id})" + (f" โ€ข Model: {ai_details.get('ai_model')}" if ai_details and ai_details.get('ai_model') else "") + if source == "AI_API" + else f"Guild: {guild.name} ({guild.id})" + ) + return self.LogView(self.bot, action_title, embed_color, lines, footer) # --- View Command Callback --- @app_commands.checks.has_permissions(moderate_members=True) # Adjust permissions as needed async def modlog_view_callback(self, interaction: Interaction, user: Optional[discord.User] = None): @@ -430,7 +415,7 @@ class ModLogCog(commands.Cog): duration = datetime.timedelta(seconds=record['duration_seconds']) if record['duration_seconds'] else None - embed = self._format_log_embed( + view = self._format_log_embed( case_id, moderator or Object(id=record['moderator_id']), # Fallback to Object if user not found target or Object(id=record['target_user_id']), # Fallback to Object if user not found @@ -443,9 +428,10 @@ class ModLogCog(commands.Cog): # Add log message link if available if record['log_message_id'] and record['log_channel_id']: link = f"https://discord.com/channels/{record['guild_id']}/{record['log_channel_id']}/{record['log_message_id']}" - embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False) + # Append jump link as extra line + view.footer_display.content += f" | [Jump to Log]({link})" - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(view=view, ephemeral=True) @app_commands.checks.has_permissions(manage_guild=True) # Higher permission for editing reasons @@ -477,7 +463,7 @@ class ModLogCog(commands.Cog): log_channel = interaction.guild.get_channel(original_record['log_channel_id']) if log_channel and isinstance(log_channel, discord.TextChannel): log_message = await log_channel.fetch_message(original_record['log_message_id']) - if log_message and log_message.author == self.bot.user and log_message.embeds: + if log_message and log_message.author == self.bot.user: # Re-fetch users/duration to reconstruct embed accurately # Special handling for AI moderator (ID 0) to avoid Discord API 404 error if original_record['moderator_id'] == 0: @@ -498,7 +484,7 @@ class ModLogCog(commands.Cog): duration = datetime.timedelta(seconds=original_record['duration_seconds']) if original_record['duration_seconds'] else None - new_embed = self._format_log_embed( + new_view = self._format_log_embed( case_id, moderator or Object(id=original_record['moderator_id']), target or Object(id=original_record['target_user_id']), @@ -507,13 +493,11 @@ class ModLogCog(commands.Cog): duration, interaction.guild ) - # Add log message link again link = f"https://discord.com/channels/{original_record['guild_id']}/{original_record['log_channel_id']}/{original_record['log_message_id']}" - new_embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False) - new_embed.add_field(name="Updated Reason By", value=f"{interaction.user.mention}", inline=False) # Indicate update + new_view.footer_display.content += f" | [Jump to Log]({link}) | Updated By: {interaction.user.mention}" - await log_message.edit(embed=new_embed) - log.info(f"Successfully updated log message embed for case {case_id}") + await log_message.edit(view=new_view) + log.info(f"Successfully updated log message view for case {case_id}") except discord.NotFound: log.warning(f"Original log message or channel not found for case {case_id} when updating reason.") except discord.Forbidden: From 1e6b1ce91dc1fb61ed6aa4b61103ad58503b46a7 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 17:57:25 +0000 Subject: [PATCH 03/25] fix: implement LogView compatibility --- cogs/logging_cog.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 3ece6e4..18ce177 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -56,28 +56,50 @@ class LoggingCog(commands.Cog): asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start class LogView(ui.LayoutView): - """Simple view for log messages.""" + """Simple view for log messages with helper methods.""" def __init__(self, bot: commands.Bot, title: str, description: str, color: discord.Color, author: Optional[discord.abc.User], footer: Optional[str]): super().__init__(timeout=None) - container = ui.Container(accent_colour=color) - self.add_item(container) - header = ui.Section(accessory=(ui.Thumbnail(media=author.display_avatar.url) - if author else None)) - header.add_item(ui.TextDisplay(f"**{title}**")) + self.container = ui.Container(accent_colour=color) + self.add_item(self.container) + + self.header = ui.Section(accessory=(ui.Thumbnail(media=author.display_avatar.url) + if author else None)) + self.header.add_item(ui.TextDisplay(f"**{title}**")) if description: - header.add_item(ui.TextDisplay(description)) - container.add_item(header) + self.header.add_item(ui.TextDisplay(description)) + self.container.add_item(self.header) - container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + self.fields_section = ui.Section() + self.container.add_item(self.fields_section) + + self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) footer_text = footer or f"Bot ID: {bot.user.id}" + ( f" | User ID: {author.id}" if author else "") self.footer_display = ui.TextDisplay(footer_text) - container.add_item(self.footer_display) + self.container.add_item(self.footer_display) + + # --- Compatibility helpers --- + def add_field(self, name: str, value: str, inline: bool = False): + """Mimic Embed.add_field by appending a bolded name/value line.""" + self.fields_section.add_item(ui.TextDisplay(f"**{name}:** {value}")) + + def set_footer(self, text: str): + """Mimic Embed.set_footer by replacing the footer text display.""" + self.footer_display.content = text + + def set_author(self, name: str, icon_url: Optional[str] = None): + """Mimic Embed.set_author by adjusting the header section.""" + self.header.clear_items() + if icon_url: + self.header.accessory = ui.Thumbnail(media=icon_url) + else: + self.header.accessory = None + self.header.add_item(ui.TextDisplay(name)) def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" From fdb9d4b7cbd894244e7aeb7aba1821e740acb2ef Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 18:29:34 +0000 Subject: [PATCH 04/25] Applying previous commit. --- cogs/logging_cog.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 18ce177..78e444d 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -62,7 +62,6 @@ class LoggingCog(commands.Cog): color: discord.Color, author: Optional[discord.abc.User], footer: Optional[str]): super().__init__(timeout=None) - self.container = ui.Container(accent_colour=color) self.add_item(self.container) @@ -73,7 +72,7 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - self.fields_section = ui.Section() + self.fields_section = ui.Section(accessory=None) self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) @@ -100,7 +99,6 @@ class LoggingCog(commands.Cog): else: self.header.accessory = None self.header.add_item(ui.TextDisplay(name)) - def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" display = user.display_name if isinstance(user, discord.Member) else user.name From 2a57175e47d24910e2f8c70a25fd99d4df6d2358 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 18:31:39 +0000 Subject: [PATCH 05/25] fix logging view field section --- cogs/logging_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 78e444d..5b019da 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -72,6 +72,7 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) + # Section to hold log fields with no accessory self.fields_section = ui.Section(accessory=None) self.container.add_item(self.fields_section) From 9f039a0b65dc3ba2c9d27066a06baf1a64c86301 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 18:36:19 +0000 Subject: [PATCH 06/25] Applying previous commit. --- cogs/logging_cog.py | 77 ++++++++++++++++------- cogs/mod_log_cog.py | 148 ++++++++++++++++++++------------------------ 2 files changed, 121 insertions(+), 104 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index a5e4390..3c41b20 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -1,5 +1,6 @@ import discord from discord.ext import commands, tasks +from discord import ui import datetime import asyncio import aiohttp # Added for webhook sending @@ -54,6 +55,47 @@ class LoggingCog(commands.Cog): else: asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start + class LogView(ui.LayoutView): + """Simple view for log messages.""" + + def __init__(self, bot: commands.Bot, title: str, description: str, + color: discord.Color, author: Optional[discord.abc.User], + footer: Optional[str]): + super().__init__(timeout=None) + self.container = ui.Container(accent_colour=color) + self.add_item(self.container) + + self.header = ui.Section(accessory=(ui.Thumbnail(media=author.display_avatar.url) + if author else None)) + self.header.add_item(ui.TextDisplay(f"**{title}**")) + if description: + self.header.add_item(ui.TextDisplay(description)) + self.container.add_item(self.header) + + self.fields_container = ui.Container() + self.container.add_item(self.fields_container) + + self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + + footer_text = footer or f"Bot ID: {bot.user.id}" + ( + f" | User ID: {author.id}" if author else "") + self.footer_display = ui.TextDisplay(footer_text) + self.container.add_item(self.footer_display) + + def add_field(self, *, name: str, value: str, inline: bool = False) -> None: + """Mimic discord.Embed.add_field for compatibility.""" + self.fields_container.add_item(ui.TextDisplay(f"**{name}:** {value}")) + + def set_author(self, *, name: str, icon_url: Optional[str] | None = None) -> None: + """Set the author line similarly to discord.Embed.set_author.""" + if icon_url: + self.header.accessory = ui.Thumbnail(media=icon_url) + self.header.add_item(ui.TextDisplay(name)) + + def set_footer(self, *, text: str) -> None: + """Set footer text similarly to discord.Embed.set_footer.""" + self.footer_display.content = text + def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" display = user.display_name if isinstance(user, discord.Member) else user.name @@ -109,8 +151,8 @@ class LoggingCog(commands.Cog): await self.session.close() log.info("aiohttp ClientSession closed for LoggingCog.") - async def _send_log_embed(self, guild: discord.Guild, embed: discord.Embed): - """Sends the log embed via the configured webhook for the guild.""" + async def _send_log_embed(self, guild: discord.Guild, embed: ui.LayoutView): + """Sends the log view via the configured webhook for the guild.""" if not self.session or self.session.closed: log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.") return @@ -124,9 +166,9 @@ class LoggingCog(commands.Cog): try: webhook = discord.Webhook.from_url(webhook_url, session=self.session) await webhook.send( - embed=embed, - username=f"{self.bot.user.name} Logs", # Optional: Customize webhook appearance - avatar_url=self.bot.user.display_avatar.url # Optional: Use bot's avatar + view=embed, + username=f"{self.bot.user.name} Logs", + avatar_url=self.bot.user.display_avatar.url, ) # log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy except ValueError: @@ -145,26 +187,17 @@ class LoggingCog(commands.Cog): log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}") - def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> discord.Embed: - """Creates a standardized log embed.""" - embed = discord.Embed(title=title, description=description, color=color, timestamp=datetime.datetime.now(datetime.timezone.utc)) - if author: - embed.set_author(name=str(author), icon_url=author.display_avatar.url) - if footer: - embed.set_footer(text=footer) - else: - # Add User ID to footer if author is present and footer isn't custom - user_id_str = f" | User ID: {author.id}" if author else "" - embed.set_footer(text=f"Bot ID: {self.bot.user.id}{user_id_str}") - return embed + def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> ui.LayoutView: + """Creates a standardized log view.""" + return self.LogView(self.bot, title, description, color, author, footer) - def _add_id_footer(self, embed: discord.Embed, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"): - """Adds an ID to the embed footer if possible.""" + def _add_id_footer(self, embed: ui.LayoutView, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"): + """Adds an ID to the footer text if possible.""" target_id = obj_id or (obj.id if obj else None) - if target_id: - existing_footer = embed.footer.text or "" + if target_id and hasattr(embed, "footer_display"): + existing_footer = embed.footer_display.content or "" separator = " | " if existing_footer else "" - embed.set_footer(text=f"{existing_footer}{separator}{id_name}: {target_id}") + embed.footer_display.content = f"{existing_footer}{separator}{id_name}: {target_id}" async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool: """Checks if logging is enabled for a specific event key in a guild.""" diff --git a/cogs/mod_log_cog.py b/cogs/mod_log_cog.py index f518967..4f8c359 100644 --- a/cogs/mod_log_cog.py +++ b/cogs/mod_log_cog.py @@ -1,6 +1,6 @@ import discord from discord.ext import commands -from discord import app_commands, Interaction, Embed, Color, User, Member, Object +from discord import app_commands, Interaction, Embed, Color, User, Member, Object, ui import asyncpg import logging from typing import Optional, Union, Dict, Any @@ -32,6 +32,21 @@ class ModLogCog(commands.Cog): # Add command group to the bot's tree self.bot.tree.add_command(self.modlog_group) + class LogView(ui.LayoutView): + """View used for moderation log messages.""" + + def __init__(self, bot: commands.Bot, title: str, color: discord.Color, lines: list[str], footer: str): + super().__init__(timeout=None) + container = ui.Container(accent_colour=color) + self.add_item(container) + container.add_item(ui.TextDisplay(f"**{title}**")) + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + for line in lines: + container.add_item(ui.TextDisplay(line)) + container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + self.footer_display = ui.TextDisplay(footer) + container.add_item(self.footer_display) + def _format_user(self, user: Union[Member, User, Object], guild: Optional[discord.Guild] = None) -> str: """Return a string with display name, username and ID for a user-like object.""" if isinstance(user, Object): @@ -220,8 +235,8 @@ class ModLogCog(commands.Cog): # Optionally update DB to remove channel ID? Or just leave it. return - # 3. Format and send embed - embed = self._format_log_embed( + # 3. Format and send view + view = self._format_log_embed( case_id=case_id, moderator=moderator, # Pass the object for display formatting target=target, @@ -233,7 +248,7 @@ class ModLogCog(commands.Cog): ai_details=ai_details, moderator_id_override=moderator_id_override # Pass override for formatting ) - log_message = await log_channel.send(embed=embed) + log_message = await log_channel.send(view=view) # 4. Update DB with message details await mod_log_db.update_mod_log_message_details(self.pool, case_id, log_message.id, log_channel.id) @@ -241,6 +256,7 @@ class ModLogCog(commands.Cog): except Exception as e: log.exception(f"Error during Discord mod log message sending/updating for case {case_id} in guild {guild_id}: {e}") + def _format_log_embed( self, case_id: int, @@ -252,9 +268,9 @@ class ModLogCog(commands.Cog): guild: discord.Guild, source: str = "BOT", ai_details: Optional[Dict[str, Any]] = None, - moderator_id_override: Optional[int] = None - ) -> Embed: - """Helper function to create the standard log embed.""" + moderator_id_override: Optional[int] = None, + ) -> ui.LayoutView: + """Helper function to create the standard log view.""" color_map = { "BAN": Color.red(), "UNBAN": Color.green(), @@ -265,87 +281,56 @@ class ModLogCog(commands.Cog): "AI_ALERT": Color.purple(), "AI_DELETE_REQUESTED": Color.dark_grey(), } - # Use a distinct color for AI actions - if source == "AI_API": - embed_color = Color.blurple() - else: - embed_color = color_map.get(action_type.upper(), Color.greyple()) + embed_color = Color.blurple() if source == "AI_API" else color_map.get(action_type.upper(), Color.greyple()) action_title_prefix = "๐Ÿค– AI Moderation Action" if source == "AI_API" else action_type.replace("_", " ").title() action_title = f"{action_title_prefix} | Case #{case_id}" - - embed = Embed( - title=action_title, - color=embed_color, - timestamp=discord.utils.utcnow() - ) - target_display = self._format_user(target, guild) - - if source == "AI_API": - moderator_display = f"AI System (ID: {moderator_id_override or 'Unknown'})" - else: - moderator_display = self._format_user(moderator, guild) - - - embed.add_field(name="User", value=target_display, inline=True) - embed.add_field(name="Moderator", value=moderator_display, inline=True) - - # Add AI-specific details if available + moderator_display = ( + f"AI System (ID: {moderator_id_override or 'Unknown'})" if source == "AI_API" else self._format_user(moderator, guild) + ) + lines = [f"**User:** {target_display}", f"**Moderator:** {moderator_display}"] if ai_details: - if 'rule_violated' in ai_details: - embed.add_field(name="Rule Violated", value=ai_details['rule_violated'], inline=True) - if 'reasoning' in ai_details: - # Use AI reasoning as the main reason field if bot reason is empty - reason_to_display = reason or ai_details['reasoning'] - embed.add_field(name="Reason / AI Reasoning", value=reason_to_display or "No reason provided.", inline=False) - # Optionally add bot reason separately if both exist and differ - if reason and reason != ai_details['reasoning']: - embed.add_field(name="Original Bot Reason", value=reason, inline=False) + if "rule_violated" in ai_details: + lines.append(f"**Rule Violated:** {ai_details['rule_violated']}") + if "reasoning" in ai_details: + reason_to_display = reason or ai_details["reasoning"] + lines.append(f"**Reason / AI Reasoning:** {reason_to_display or 'No reason provided.'}") + if reason and reason != ai_details["reasoning"]: + lines.append(f"**Original Bot Reason:** {reason}") else: - embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False) - - # Add full message content if available - if 'message_content' in ai_details: - # Truncate if too long (Discord has a 1024 character limit for embed fields) - message_content = ai_details['message_content'] + lines.append(f"**Reason:** {reason or 'No reason provided.'}") + if "message_content" in ai_details: + message_content = ai_details["message_content"] if len(message_content) > 1000: message_content = message_content[:997] + "..." - embed.add_field(name="Message Content", value=message_content, inline=False) + lines.append(f"**Message Content:** {message_content}") else: - embed.add_field(name="Reason", value=reason or "No reason provided.", inline=False) - + lines.append(f"**Reason:** {reason or 'No reason provided.'}") if duration: - # Format duration nicely (e.g., "1 day", "2 hours 30 minutes") - # This is a simple version, could be made more robust total_seconds = int(duration.total_seconds()) days, remainder = divmod(total_seconds, 86400) hours, remainder = divmod(remainder, 3600) minutes, seconds = divmod(remainder, 60) duration_str = "" - if days > 0: duration_str += f"{days}d " - if hours > 0: duration_str += f"{hours}h " - if minutes > 0: duration_str += f"{minutes}m " - if seconds > 0 or not duration_str: duration_str += f"{seconds}s" + if days > 0: + duration_str += f"{days}d " + if hours > 0: + duration_str += f"{hours}h " + if minutes > 0: + duration_str += f"{minutes}m " + if seconds > 0 or not duration_str: + duration_str += f"{seconds}s" duration_str = duration_str.strip() - - embed.add_field(name="Duration", value=duration_str, inline=True) - # Add expiration timestamp if applicable (e.g., for timeouts) + lines.append(f"**Duration:** {duration_str}") if action_type.upper() == "TIMEOUT": - expires_at = discord.utils.utcnow() + duration - embed.add_field(name="Expires", value=f"", inline=True) - - - if source == "AI_API": - ai_model = ai_details.get("ai_model") if ai_details else None - embed.set_footer( - text=f"AI Moderation Action โ€ข {guild.name} ({guild.id})" + (f" โ€ข Model: {ai_model}" if ai_model else ""), - icon_url="https://cdn-icons-png.flaticon.com/512/4712/4712035.png" - ) - else: - embed.set_footer(text=f"Guild: {guild.name} ({guild.id})") - - return embed - + expires_at = discord.utils.utcnow() + duration + lines.append(f"**Expires:** ") + footer = ( + f"AI Moderation Action โ€ข {guild.name} ({guild.id})" + (f" โ€ข Model: {ai_details.get('ai_model')}" if ai_details and ai_details.get('ai_model') else "") + if source == "AI_API" + else f"Guild: {guild.name} ({guild.id})" + ) + return self.LogView(self.bot, action_title, embed_color, lines, footer) # --- View Command Callback --- @app_commands.checks.has_permissions(moderate_members=True) # Adjust permissions as needed async def modlog_view_callback(self, interaction: Interaction, user: Optional[discord.User] = None): @@ -430,7 +415,7 @@ class ModLogCog(commands.Cog): duration = datetime.timedelta(seconds=record['duration_seconds']) if record['duration_seconds'] else None - embed = self._format_log_embed( + view = self._format_log_embed( case_id, moderator or Object(id=record['moderator_id']), # Fallback to Object if user not found target or Object(id=record['target_user_id']), # Fallback to Object if user not found @@ -443,9 +428,10 @@ class ModLogCog(commands.Cog): # Add log message link if available if record['log_message_id'] and record['log_channel_id']: link = f"https://discord.com/channels/{record['guild_id']}/{record['log_channel_id']}/{record['log_message_id']}" - embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False) + # Append jump link as extra line + view.footer_display.content += f" | [Jump to Log]({link})" - await interaction.followup.send(embed=embed, ephemeral=True) + await interaction.followup.send(view=view, ephemeral=True) @app_commands.checks.has_permissions(manage_guild=True) # Higher permission for editing reasons @@ -477,7 +463,7 @@ class ModLogCog(commands.Cog): log_channel = interaction.guild.get_channel(original_record['log_channel_id']) if log_channel and isinstance(log_channel, discord.TextChannel): log_message = await log_channel.fetch_message(original_record['log_message_id']) - if log_message and log_message.author == self.bot.user and log_message.embeds: + if log_message and log_message.author == self.bot.user: # Re-fetch users/duration to reconstruct embed accurately # Special handling for AI moderator (ID 0) to avoid Discord API 404 error if original_record['moderator_id'] == 0: @@ -498,7 +484,7 @@ class ModLogCog(commands.Cog): duration = datetime.timedelta(seconds=original_record['duration_seconds']) if original_record['duration_seconds'] else None - new_embed = self._format_log_embed( + new_view = self._format_log_embed( case_id, moderator or Object(id=original_record['moderator_id']), target or Object(id=original_record['target_user_id']), @@ -507,13 +493,11 @@ class ModLogCog(commands.Cog): duration, interaction.guild ) - # Add log message link again link = f"https://discord.com/channels/{original_record['guild_id']}/{original_record['log_channel_id']}/{original_record['log_message_id']}" - new_embed.add_field(name="Log Message", value=f"[Jump to Log]({link})", inline=False) - new_embed.add_field(name="Updated Reason By", value=f"{interaction.user.mention}", inline=False) # Indicate update + new_view.footer_display.content += f" | [Jump to Log]({link}) | Updated By: {interaction.user.mention}" - await log_message.edit(embed=new_embed) - log.info(f"Successfully updated log message embed for case {case_id}") + await log_message.edit(view=new_view) + log.info(f"Successfully updated log message view for case {case_id}") except discord.NotFound: log.warning(f"Original log message or channel not found for case {case_id} when updating reason.") except discord.Forbidden: From 1a0b9094bce15d53116a3e02222ae82071c057ca Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 18:38:09 +0000 Subject: [PATCH 07/25] Fix LogView fields section --- cogs/logging_cog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 3c41b20..8c6450e 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -72,8 +72,8 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - self.fields_container = ui.Container() - self.container.add_item(self.fields_container) + self.fields_section = ui.Section(accessory=None) + self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) @@ -84,7 +84,7 @@ class LoggingCog(commands.Cog): def add_field(self, *, name: str, value: str, inline: bool = False) -> None: """Mimic discord.Embed.add_field for compatibility.""" - self.fields_container.add_item(ui.TextDisplay(f"**{name}:** {value}")) + self.fields_section.add_item(ui.TextDisplay(f"**{name}:** {value}")) def set_author(self, *, name: str, icon_url: Optional[str] | None = None) -> None: """Set the author line similarly to discord.Embed.set_author.""" From 5866fb3a4d0dbd95e22ea33f5dabb568d7f5b06b Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 18:42:15 +0000 Subject: [PATCH 08/25] Fix log view section accessory --- cogs/logging_cog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 5b019da..0cb1559 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -72,8 +72,10 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - # Section to hold log fields with no accessory - self.fields_section = ui.Section(accessory=None) + # Section to hold log fields with no accessory. Using a blank + # TextDisplay avoids ``None`` errors from ``is_dispatchable`` while + # keeping the accessory effectively invisible. + self.fields_section = ui.Section(accessory=ui.TextDisplay("\u200b")) self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) From 6707b20f26504d4110524b3b71eac0466911536e Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 19:18:02 +0000 Subject: [PATCH 09/25] fix log view accessory --- cogs/logging_cog.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 0cb1559..b63d0b8 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -72,10 +72,12 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - # Section to hold log fields with no accessory. Using a blank - # TextDisplay avoids ``None`` errors from ``is_dispatchable`` while - # keeping the accessory effectively invisible. - self.fields_section = ui.Section(accessory=ui.TextDisplay("\u200b")) + # Section to hold log fields with no accessory. The API requires a + # valid component type, so use a disabled button with an invisible + # label as a placeholder accessory. + self.fields_section = ui.Section( + accessory=ui.Button(label="\u200b", disabled=True) + ) self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) From 1080a8e93d0997a2e1655b96ae9eac19f0be3674 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 19:22:24 +0000 Subject: [PATCH 10/25] Applying previous commit. --- cogs/logging_cog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 5b019da..0cb1559 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -72,8 +72,10 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - # Section to hold log fields with no accessory - self.fields_section = ui.Section(accessory=None) + # Section to hold log fields with no accessory. Using a blank + # TextDisplay avoids ``None`` errors from ``is_dispatchable`` while + # keeping the accessory effectively invisible. + self.fields_section = ui.Section(accessory=ui.TextDisplay("\u200b")) self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) From 3d7e9f3f63e16079dfab26b242732898b7e81280 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 19:24:58 +0000 Subject: [PATCH 11/25] fix log view field section accessory --- cogs/logging_cog.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 0cb1559..6cc01b3 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -72,10 +72,14 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - # Section to hold log fields with no accessory. Using a blank - # TextDisplay avoids ``None`` errors from ``is_dispatchable`` while - # keeping the accessory effectively invisible. - self.fields_section = ui.Section(accessory=ui.TextDisplay("\u200b")) + # Section to hold log fields. `discord.py` requires an accessory of + # type ``Button`` or ``Thumbnail`` for a ``Section``. A disabled + # button with a zero-width label acts as an invisible placeholder + # and prevents ``is_dispatchable`` errors without affecting the + # layout visually. + self.fields_section = ui.Section( + accessory=ui.Button(label="\u200b", disabled=True) + ) self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) From 8685bdabda5ed7ce3e4317d9ce3eeff218370b18 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 19:35:22 +0000 Subject: [PATCH 12/25] fix log embed header section --- cogs/logging_cog.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index b63d0b8..e008326 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -65,8 +65,17 @@ class LoggingCog(commands.Cog): self.container = ui.Container(accent_colour=color) self.add_item(self.container) - self.header = ui.Section(accessory=(ui.Thumbnail(media=author.display_avatar.url) - if author else None)) + # Header section with a thumbnail if author is provided. Otherwise + # use a disabled button as a placeholder accessory to satisfy the + # API requirement that a section accessory must be a dispatchable + # component. + self.header = ui.Section( + accessory=( + ui.Thumbnail(media=author.display_avatar.url) + if author + else ui.Button(label="\u200b", disabled=True) + ) + ) self.header.add_item(ui.TextDisplay(f"**{title}**")) if description: self.header.add_item(ui.TextDisplay(description)) @@ -102,7 +111,9 @@ class LoggingCog(commands.Cog): if icon_url: self.header.accessory = ui.Thumbnail(media=icon_url) else: - self.header.accessory = None + # Provide a disabled button as a fallback accessory to keep the + # section dispatchable when no icon is supplied. + self.header.accessory = ui.Button(label="\u200b", disabled=True) self.header.add_item(ui.TextDisplay(name)) def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" From 7b21633913745dfea5e3d139db0ce0b2999452ec Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 14:06:56 -0600 Subject: [PATCH 13/25] fix logging embed header accessory --- cogs/logging_cog.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 5b019da..0cd4775 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -65,15 +65,24 @@ class LoggingCog(commands.Cog): self.container = ui.Container(accent_colour=color) self.add_item(self.container) - self.header = ui.Section(accessory=(ui.Thumbnail(media=author.display_avatar.url) - if author else None)) + self.header = ui.Section( + accessory=( + ui.Thumbnail(media=author.display_avatar.url) + if author + else ui.Button(label="\u200b", disabled=True) + ) + ) self.header.add_item(ui.TextDisplay(f"**{title}**")) if description: self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - # Section to hold log fields with no accessory - self.fields_section = ui.Section(accessory=None) + # Section to hold log fields with no accessory. The API requires a + # valid component type, so use a disabled button with an invisible + # label as a placeholder accessory. + self.fields_section = ui.Section( + accessory=ui.Button(label="\u200b", disabled=True) + ) self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) @@ -98,7 +107,7 @@ class LoggingCog(commands.Cog): if icon_url: self.header.accessory = ui.Thumbnail(media=icon_url) else: - self.header.accessory = None + self.header.accessory = ui.Button(label="\u200b", disabled=True) self.header.add_item(ui.TextDisplay(name)) def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" From c546379194c32ad6f3a99ce5fc7d91d6ac3e4b59 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 20:21:54 +0000 Subject: [PATCH 14/25] Fix webhook test message --- cogs/logging_cog.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index cae01bb..54cf6b9 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -295,8 +295,12 @@ class LoggingCog(commands.Cog): await ctx.send(f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.") # Test send (optional) try: - test_embed = self._create_log_embed("โœ… Logging Setup Complete", f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", color=discord.Color.green()) - await new_webhook.send(embed=test_embed, username=webhook_name, avatar_url=self.bot.user.display_avatar.url) + test_embed = self._create_log_embed( + "โœ… Logging Setup Complete", + f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", + color=discord.Color.green(), + ) + await new_webhook.send(view=test_embed, username=webhook_name, avatar_url=self.bot.user.display_avatar.url) except Exception as 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.") From 26a6c508b029ca6c645c3d8ec99bfbbb24ba8a72 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 14:38:34 -0600 Subject: [PATCH 15/25] Docs: Add usage instructions for Tavily API script - Clarifies error handling for git push operations. - Provides comprehensive documentation for the `tavily.py` script, including basic usage, examples, advanced options, output structure, command combinations, and tips to enable effective web searching. --- AGENTS.md | 62 +++++++++++++++++++++- tavily.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 tavily.py diff --git a/AGENTS.md b/AGENTS.md index ea34e56..8e18594 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,4 +8,64 @@ - You are using python 3.11. - All pip dependencies are installed in the environment. (Except packages related to locally running AI, due to their large size.) - You have the ability to directly push to the git repo over https (url already set). And you should when you finish your task and have tested it. -- If you encounter errors when pushing just leave it to the user to fix them. +- If you encounter any hard to fix errors when pushing just leave it to the user to fix them. Try fixing merge conflicts/not having work from the remote by yourself. +- You have a python script "tavily.py" in the project root, that you can use to search the web. + +# Tavily API Script Usage Instructions + +## Basic Usage +Search for information using simple queries: +```bash +python tavily.py "your search query" +``` + +## Examples +```bash +python tavily.py "latest AI developments 2024" +python tavily.py "how to make chocolate chip cookies" +python tavily.py "current weather in New York" +python tavily.py "best programming practices Python" +``` + +## Advanced Options + +### Search Depth +- **Basic search**: `python tavily.py "query"` (default) +- **Advanced search**: `python tavily.py "query" --depth advanced` + +### Control Results +- **Limit results**: `python tavily.py "query" --max-results 3` +- **Include images**: `python tavily.py "query" --include-images` +- **Skip AI answer**: `python tavily.py "query" --no-answer` + +### Domain Filtering +- **Include specific domains**: `python tavily.py "query" --include-domains reddit.com stackoverflow.com` +- **Exclude domains**: `python tavily.py "query" --exclude-domains wikipedia.org` + +### Output Format +- **Formatted output**: `python tavily.py "query"` (default - human readable) +- **Raw JSON**: `python tavily.py "query" --raw` (for programmatic processing) + +## Output Structure +The default formatted output includes: +- ๐Ÿค– **AI Answer**: Direct answer to your query +- ๐Ÿ” **Search Results**: Titles, URLs, and content snippets +- ๐Ÿ–ผ๏ธ **Images**: Relevant images (when `--include-images` is used) + +## Command Combinations +```bash +# Advanced search with images, limited results +python tavily.py "machine learning tutorials" --depth advanced --include-images --max-results 3 + +# Search specific sites only, raw output +python tavily.py "Python best practices" --include-domains github.com stackoverflow.com --raw + +# Quick search without AI answer +python tavily.py "today's news" --no-answer --max-results 5 +``` + +## Tips +- Always quote your search queries to handle spaces and special characters +- Use `--max-results` to control response length and API usage +- Use `--raw` when you need to parse results programmatically +- Combine options as needed for specific use cases \ No newline at end of file diff --git a/tavily.py b/tavily.py new file mode 100644 index 0000000..3c89167 --- /dev/null +++ b/tavily.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Tavily API Script for AI Agents +Execute with: python tavily.py "your search query" +""" + +import os +import sys +import json +import requests +import argparse +from typing import Dict, List, Optional + +class TavilyAPI: + def __init__(self, api_key: str): + self.api_key = api_key + self.base_url = "https://api.tavily.com" + + def search(self, + query: str, + search_depth: str = "basic", + include_answer: bool = True, + include_images: bool = False, + include_raw_content: bool = False, + max_results: int = 5, + include_domains: Optional[List[str]] = None, + exclude_domains: Optional[List[str]] = None) -> Dict: + """ + Perform a search using Tavily API + + Args: + query: Search query string + search_depth: "basic" or "advanced" + include_answer: Include AI-generated answer + include_images: Include images in results + include_raw_content: Include raw HTML content + max_results: Maximum number of results (1-20) + include_domains: List of domains to include + exclude_domains: List of domains to exclude + + Returns: + Dictionary containing search results + """ + url = f"{self.base_url}/search" + + payload = { + "api_key": self.api_key, + "query": query, + "search_depth": search_depth, + "include_answer": include_answer, + "include_images": include_images, + "include_raw_content": include_raw_content, + "max_results": max_results + } + + if include_domains: + payload["include_domains"] = include_domains + if exclude_domains: + payload["exclude_domains"] = exclude_domains + + try: + response = requests.post(url, json=payload, timeout=30) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + return {"error": f"API request failed: {str(e)}"} + except json.JSONDecodeError: + return {"error": "Invalid JSON response from API"} + +def format_results(results: Dict) -> str: + """Format search results for display""" + if "error" in results: + return f"โŒ Error: {results['error']}" + + output = [] + + # Add answer if available + if results.get("answer"): + output.append("๐Ÿค– AI Answer:") + output.append(f" {results['answer']}") + output.append("") + + # Add search results + if results.get("results"): + output.append("๐Ÿ” Search Results:") + for i, result in enumerate(results["results"], 1): + output.append(f" {i}. {result.get('title', 'No title')}") + output.append(f" URL: {result.get('url', 'No URL')}") + if result.get("content"): + # Truncate content to first 200 chars + content = result["content"][:200] + "..." if len(result["content"]) > 200 else result["content"] + output.append(f" Content: {content}") + output.append("") + + # Add images if available + if results.get("images"): + output.append("๐Ÿ–ผ๏ธ Images:") + for img in results["images"][:3]: # Show first 3 images + output.append(f" {img}") + output.append("") + + return "\n".join(output) + +def main(): + parser = argparse.ArgumentParser(description="Search using Tavily API") + parser.add_argument("query", help="Search query") + parser.add_argument("--depth", choices=["basic", "advanced"], default="basic", + help="Search depth (default: basic)") + parser.add_argument("--max-results", type=int, default=5, + help="Maximum number of results (default: 5)") + parser.add_argument("--include-images", action="store_true", + help="Include images in results") + parser.add_argument("--no-answer", action="store_true", + help="Don't include AI-generated answer") + parser.add_argument("--include-domains", nargs="+", + help="Include only these domains") + parser.add_argument("--exclude-domains", nargs="+", + help="Exclude these domains") + parser.add_argument("--raw", action="store_true", + help="Output raw JSON response") + + args = parser.parse_args() + + # Get API key from environment + api_key = os.getenv("TAVILY_API_KEY") + if not api_key: + print("โŒ Error: TAVILY_API_KEY environment variable not set") + print("Set it with: export TAVILY_API_KEY='your-api-key-here'") + sys.exit(1) + + # Initialize Tavily API + tavily = TavilyAPI(api_key) + + # Perform search + results = tavily.search( + query=args.query, + search_depth=args.depth, + include_answer=not args.no_answer, + include_images=args.include_images, + max_results=args.max_results, + include_domains=args.include_domains, + exclude_domains=args.exclude_domains + ) + + # Output results + if args.raw: + print(json.dumps(results, indent=2)) + else: + print(format_results(results)) + +if __name__ == "__main__": + main() \ No newline at end of file From d38cc6a2a2aa848e0d806a782ca97dedb7b1feaa Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 20:43:34 +0000 Subject: [PATCH 16/25] fix placeholder fields --- cogs/logging_cog.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 54cf6b9..a5081ce 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -77,12 +77,15 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - # Section to hold log fields with no accessory. The API requires a - # valid component type, so use a disabled button with an invisible - # label as a placeholder accessory. + # Section to hold log fields with no accessory. The API requires at + # least one child component, so include an empty text display that + # will be removed when actual fields are added. A disabled button is + # used as the accessory to satisfy Discord's component validation. self.fields_section = ui.Section( accessory=ui.Button(label="\u200b", disabled=True) ) + self._fields_placeholder = ui.TextDisplay("\u200b") + self.fields_section.add_item(self._fields_placeholder) self.container.add_item(self.fields_section) self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) @@ -95,6 +98,11 @@ class LoggingCog(commands.Cog): # --- Compatibility helpers --- def add_field(self, name: str, value: str, inline: bool = False): """Mimic Embed.add_field by appending a bolded name/value line.""" + if ( + hasattr(self, "_fields_placeholder") + and self._fields_placeholder in self.fields_section.children + ): + self.fields_section.remove_item(self._fields_placeholder) self.fields_section.add_item(ui.TextDisplay(f"**{name}:** {value}")) def set_footer(self, text: str): @@ -295,12 +303,16 @@ class LoggingCog(commands.Cog): await ctx.send(f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.") # Test send (optional) try: - test_embed = self._create_log_embed( - "โœ… Logging Setup Complete", - f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", - color=discord.Color.green(), - ) - await new_webhook.send(view=test_embed, username=webhook_name, avatar_url=self.bot.user.display_avatar.url) + test_view = self._create_log_embed( + "โœ… Logging Setup Complete", + f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", + color=discord.Color.green(), + ) + await new_webhook.send( + view=test_view, + username=webhook_name, + avatar_url=self.bot.user.display_avatar.url, + ) except Exception as 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.") From e2806adfa2f4da0e6b3b725cf9c2c600f5bc1f71 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 20:49:50 +0000 Subject: [PATCH 17/25] fix log view sections --- cogs/logging_cog.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index a5081ce..f082d14 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -77,33 +77,35 @@ class LoggingCog(commands.Cog): self.header.add_item(ui.TextDisplay(description)) self.container.add_item(self.header) - # Section to hold log fields with no accessory. The API requires at - # least one child component, so include an empty text display that - # will be removed when actual fields are added. A disabled button is - # used as the accessory to satisfy Discord's component validation. - self.fields_section = ui.Section( - accessory=ui.Button(label="\u200b", disabled=True) - ) - self._fields_placeholder = ui.TextDisplay("\u200b") - self.fields_section.add_item(self._fields_placeholder) - self.container.add_item(self.fields_section) + # Placeholder for future field sections. They are inserted before + # the separator when the first field is added. + self._field_sections: list[ui.Section] = [] - self.container.add_item(ui.Separator(spacing=discord.SeparatorSpacing.small)) + self.separator = ui.Separator(spacing=discord.SeparatorSpacing.small) footer_text = footer or f"Bot ID: {bot.user.id}" + ( f" | User ID: {author.id}" if author else "") self.footer_display = ui.TextDisplay(footer_text) + + self.container.add_item(self.separator) self.container.add_item(self.footer_display) # --- Compatibility helpers --- def add_field(self, name: str, value: str, inline: bool = False): """Mimic Embed.add_field by appending a bolded name/value line.""" - if ( - hasattr(self, "_fields_placeholder") - and self._fields_placeholder in self.fields_section.children - ): - self.fields_section.remove_item(self._fields_placeholder) - self.fields_section.add_item(ui.TextDisplay(f"**{name}:** {value}")) + if not self._field_sections or len(self._field_sections[-1].children) >= 3: + section = ui.Section(accessory=ui.Button(label="\u200b", disabled=True)) + self._insert_field_section(section) + self._field_sections.append(section) + self._field_sections[-1].add_item(ui.TextDisplay(f"**{name}:** {value}")) + + def _insert_field_section(self, section: ui.Section) -> None: + """Insert a field section before the footer separator.""" + self.container.remove_item(self.separator) + self.container.remove_item(self.footer_display) + self.container.add_item(section) + self.container.add_item(self.separator) + self.container.add_item(self.footer_display) def set_footer(self, text: str): """Mimic Embed.set_footer by replacing the footer text display.""" From 7c1b39a9ac3819300cc743866b78ad568502bcff Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 15:08:19 -0600 Subject: [PATCH 18/25] Fix logging error handling to include exception details for invalid webhook URLs --- cogs/logging_cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 54cf6b9..ec7c1c8 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -184,8 +184,8 @@ class LoggingCog(commands.Cog): avatar_url=self.bot.user.display_avatar.url, ) # log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy - except ValueError: - log.error(f"Invalid logging webhook URL configured for guild {guild.id}.") + except ValueError as e: + log.exception(f"Invalid logging webhook URL configured 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): From 30796652253319c4cbec3149bc759a3f9e48a889 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 21:28:47 +0000 Subject: [PATCH 19/25] fix logging webhook --- cogs/logging_cog.py | 120 ++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 77 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 2d70641..13299b2 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -1,6 +1,5 @@ import discord from discord.ext import commands, tasks -from discord import ui import datetime import asyncio import aiohttp # Added for webhook sending @@ -55,70 +54,6 @@ class LoggingCog(commands.Cog): else: asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start - class LogView(ui.LayoutView): - """Simple view for log messages with helper methods.""" - - def __init__(self, bot: commands.Bot, title: str, description: str, - color: discord.Color, author: Optional[discord.abc.User], - footer: Optional[str]): - super().__init__(timeout=None) - self.container = ui.Container(accent_colour=color) - self.add_item(self.container) - - self.header = ui.Section( - accessory=( - ui.Thumbnail(media=author.display_avatar.url) - if author - else ui.Button(label="\u200b", disabled=True) - ) - ) - self.header.add_item(ui.TextDisplay(f"**{title}**")) - if description: - self.header.add_item(ui.TextDisplay(description)) - self.container.add_item(self.header) - - # Placeholder for future field sections. They are inserted before - # the separator when the first field is added. - self._field_sections: list[ui.Section] = [] - - self.separator = ui.Separator(spacing=discord.SeparatorSpacing.small) - - footer_text = footer or f"Bot ID: {bot.user.id}" + ( - f" | User ID: {author.id}" if author else "") - self.footer_display = ui.TextDisplay(footer_text) - - self.container.add_item(self.separator) - self.container.add_item(self.footer_display) - - # --- Compatibility helpers --- - def add_field(self, name: str, value: str, inline: bool = False): - """Mimic Embed.add_field by appending a bolded name/value line.""" - if not self._field_sections or len(self._field_sections[-1].children) >= 3: - section = ui.Section(accessory=ui.Button(label="\u200b", disabled=True)) - self._insert_field_section(section) - self._field_sections.append(section) - self._field_sections[-1].add_item(ui.TextDisplay(f"**{name}:** {value}")) - - def _insert_field_section(self, section: ui.Section) -> None: - """Insert a field section before the footer separator.""" - self.container.remove_item(self.separator) - self.container.remove_item(self.footer_display) - self.container.add_item(section) - self.container.add_item(self.separator) - self.container.add_item(self.footer_display) - - def set_footer(self, text: str): - """Mimic Embed.set_footer by replacing the footer text display.""" - self.footer_display.content = text - - def set_author(self, name: str, icon_url: Optional[str] = None): - """Mimic Embed.set_author by adjusting the header section.""" - self.header.clear_items() - if icon_url: - self.header.accessory = ui.Thumbnail(media=icon_url) - else: - self.header.accessory = ui.Button(label="\u200b", disabled=True) - self.header.add_item(ui.TextDisplay(name)) def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" display = user.display_name if isinstance(user, discord.Member) else user.name @@ -174,8 +109,8 @@ class LoggingCog(commands.Cog): await self.session.close() log.info("aiohttp ClientSession closed for LoggingCog.") - async def _send_log_embed(self, guild: discord.Guild, embed: ui.LayoutView): - """Sends the log view via the configured webhook for the guild.""" + async def _send_log_embed(self, guild: discord.Guild, embed: discord.Embed) -> None: + """Sends the log embed via the configured webhook for the guild.""" if not self.session or self.session.closed: log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.") return @@ -189,7 +124,7 @@ class LoggingCog(commands.Cog): try: webhook = discord.Webhook.from_url(webhook_url, session=self.session) await webhook.send( - view=embed, + embed=embed, username=f"{self.bot.user.name} Logs", avatar_url=self.bot.user.display_avatar.url, ) @@ -210,17 +145,48 @@ class LoggingCog(commands.Cog): log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}") - def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> ui.LayoutView: - """Creates a standardized log view.""" - return self.LogView(self.bot, title, description, color, author, footer) + def _create_log_embed( + self, + title: str, + description: str = "", + color: discord.Color = discord.Color.blue(), + author: Optional[Union[discord.User, discord.Member]] = None, + footer: Optional[str] = None, + ) -> discord.Embed: + """Creates a standardized embed for logging.""" + embed = discord.Embed(title=title, description=description, color=color) + if author: + embed.set_author(name=author.display_name, icon_url=author.display_avatar.url) + if footer: + embed.set_footer(text=footer) + else: + footer_text = f"Bot ID: {self.bot.user.id}" + if author: + footer_text += f" | User ID: {author.id}" + embed.set_footer(text=footer_text) + return embed - def _add_id_footer(self, embed: ui.LayoutView, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"): + def _add_id_footer( + self, + embed: discord.Embed, + obj: Union[ + discord.Member, + discord.User, + discord.Role, + discord.abc.GuildChannel, + discord.Message, + discord.Invite, + None, + ] = None, + obj_id: Optional[int] = None, + id_name: str = "ID", + ) -> None: """Adds an ID to the footer text if possible.""" target_id = obj_id or (obj.id if obj else None) - if target_id and hasattr(embed, "footer_display"): - existing_footer = embed.footer_display.content or "" + if target_id: + existing_footer = embed.footer.text if embed.footer else "" separator = " | " if existing_footer else "" - embed.footer_display.content = f"{existing_footer}{separator}{id_name}: {target_id}" + embed.set_footer(text=f"{existing_footer}{separator}{id_name}: {target_id}") async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool: """Checks if logging is enabled for a specific event key in a guild.""" @@ -305,13 +271,13 @@ class LoggingCog(commands.Cog): await ctx.send(f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.") # Test send (optional) try: - test_view = self._create_log_embed( + test_embed = self._create_log_embed( "โœ… Logging Setup Complete", f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", color=discord.Color.green(), ) await new_webhook.send( - view=test_view, + embed=test_embed, username=webhook_name, avatar_url=self.bot.user.display_avatar.url, ) From 4e035f415135500ad81cdcf2764efb262ea7a1d6 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 15:34:49 -0600 Subject: [PATCH 20/25] Fix script name in documentation from "tavily.py" to "tavilytool.py" --- AGENTS.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8e18594..4c4bb7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,42 +9,42 @@ - All pip dependencies are installed in the environment. (Except packages related to locally running AI, due to their large size.) - You have the ability to directly push to the git repo over https (url already set). And you should when you finish your task and have tested it. - If you encounter any hard to fix errors when pushing just leave it to the user to fix them. Try fixing merge conflicts/not having work from the remote by yourself. -- You have a python script "tavily.py" in the project root, that you can use to search the web. +- You have a python script "tavilytool.py" in the project root, that you can use to search the web. # Tavily API Script Usage Instructions ## Basic Usage Search for information using simple queries: ```bash -python tavily.py "your search query" +python tavilytool.py "your search query" ``` ## Examples ```bash -python tavily.py "latest AI developments 2024" -python tavily.py "how to make chocolate chip cookies" -python tavily.py "current weather in New York" -python tavily.py "best programming practices Python" +python tavilytool.py "latest AI developments 2024" +python tavilytool.py "how to make chocolate chip cookies" +python tavilytool.py "current weather in New York" +python tavilytool.py "best programming practices Python" ``` ## Advanced Options ### Search Depth -- **Basic search**: `python tavily.py "query"` (default) -- **Advanced search**: `python tavily.py "query" --depth advanced` +- **Basic search**: `python tavilytool.py "query"` (default) +- **Advanced search**: `python tavilytool.py "query" --depth advanced` ### Control Results -- **Limit results**: `python tavily.py "query" --max-results 3` -- **Include images**: `python tavily.py "query" --include-images` -- **Skip AI answer**: `python tavily.py "query" --no-answer` +- **Limit results**: `python tavilytool.py "query" --max-results 3` +- **Include images**: `python tavilytool.py "query" --include-images` +- **Skip AI answer**: `python tavilytool.py "query" --no-answer` ### Domain Filtering -- **Include specific domains**: `python tavily.py "query" --include-domains reddit.com stackoverflow.com` -- **Exclude domains**: `python tavily.py "query" --exclude-domains wikipedia.org` +- **Include specific domains**: `python tavilytool.py "query" --include-domains reddit.com stackoverflow.com` +- **Exclude domains**: `python tavilytool.py "query" --exclude-domains wikipedia.org` ### Output Format -- **Formatted output**: `python tavily.py "query"` (default - human readable) -- **Raw JSON**: `python tavily.py "query" --raw` (for programmatic processing) +- **Formatted output**: `python tavilytool.py "query"` (default - human readable) +- **Raw JSON**: `python tavilytool.py "query" --raw` (for programmatic processing) ## Output Structure The default formatted output includes: @@ -55,13 +55,13 @@ The default formatted output includes: ## Command Combinations ```bash # Advanced search with images, limited results -python tavily.py "machine learning tutorials" --depth advanced --include-images --max-results 3 +python tavilytool.py "machine learning tutorials" --depth advanced --include-images --max-results 3 # Search specific sites only, raw output -python tavily.py "Python best practices" --include-domains github.com stackoverflow.com --raw +python tavilytool.py "Python best practices" --include-domains github.com stackoverflow.com --raw # Quick search without AI answer -python tavily.py "today's news" --no-answer --max-results 5 +python tavilytool.py "today's news" --no-answer --max-results 5 ``` ## Tips From bb76d9afcf2f6c0d8007c2c89eeefded7a717a6b Mon Sep 17 00:00:00 2001 From: Slipstream Date: Thu, 5 Jun 2025 21:35:47 +0000 Subject: [PATCH 21/25] Applying previous commit. --- cogs/logging_cog.py | 120 ++++++++++++++++---------------------------- 1 file changed, 43 insertions(+), 77 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 2d70641..13299b2 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -1,6 +1,5 @@ import discord from discord.ext import commands, tasks -from discord import ui import datetime import asyncio import aiohttp # Added for webhook sending @@ -55,70 +54,6 @@ class LoggingCog(commands.Cog): else: asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start - class LogView(ui.LayoutView): - """Simple view for log messages with helper methods.""" - - def __init__(self, bot: commands.Bot, title: str, description: str, - color: discord.Color, author: Optional[discord.abc.User], - footer: Optional[str]): - super().__init__(timeout=None) - self.container = ui.Container(accent_colour=color) - self.add_item(self.container) - - self.header = ui.Section( - accessory=( - ui.Thumbnail(media=author.display_avatar.url) - if author - else ui.Button(label="\u200b", disabled=True) - ) - ) - self.header.add_item(ui.TextDisplay(f"**{title}**")) - if description: - self.header.add_item(ui.TextDisplay(description)) - self.container.add_item(self.header) - - # Placeholder for future field sections. They are inserted before - # the separator when the first field is added. - self._field_sections: list[ui.Section] = [] - - self.separator = ui.Separator(spacing=discord.SeparatorSpacing.small) - - footer_text = footer or f"Bot ID: {bot.user.id}" + ( - f" | User ID: {author.id}" if author else "") - self.footer_display = ui.TextDisplay(footer_text) - - self.container.add_item(self.separator) - self.container.add_item(self.footer_display) - - # --- Compatibility helpers --- - def add_field(self, name: str, value: str, inline: bool = False): - """Mimic Embed.add_field by appending a bolded name/value line.""" - if not self._field_sections or len(self._field_sections[-1].children) >= 3: - section = ui.Section(accessory=ui.Button(label="\u200b", disabled=True)) - self._insert_field_section(section) - self._field_sections.append(section) - self._field_sections[-1].add_item(ui.TextDisplay(f"**{name}:** {value}")) - - def _insert_field_section(self, section: ui.Section) -> None: - """Insert a field section before the footer separator.""" - self.container.remove_item(self.separator) - self.container.remove_item(self.footer_display) - self.container.add_item(section) - self.container.add_item(self.separator) - self.container.add_item(self.footer_display) - - def set_footer(self, text: str): - """Mimic Embed.set_footer by replacing the footer text display.""" - self.footer_display.content = text - - def set_author(self, name: str, icon_url: Optional[str] = None): - """Mimic Embed.set_author by adjusting the header section.""" - self.header.clear_items() - if icon_url: - self.header.accessory = ui.Thumbnail(media=icon_url) - else: - self.header.accessory = ui.Button(label="\u200b", disabled=True) - self.header.add_item(ui.TextDisplay(name)) def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" display = user.display_name if isinstance(user, discord.Member) else user.name @@ -174,8 +109,8 @@ class LoggingCog(commands.Cog): await self.session.close() log.info("aiohttp ClientSession closed for LoggingCog.") - async def _send_log_embed(self, guild: discord.Guild, embed: ui.LayoutView): - """Sends the log view via the configured webhook for the guild.""" + async def _send_log_embed(self, guild: discord.Guild, embed: discord.Embed) -> None: + """Sends the log embed via the configured webhook for the guild.""" if not self.session or self.session.closed: log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.") return @@ -189,7 +124,7 @@ class LoggingCog(commands.Cog): try: webhook = discord.Webhook.from_url(webhook_url, session=self.session) await webhook.send( - view=embed, + embed=embed, username=f"{self.bot.user.name} Logs", avatar_url=self.bot.user.display_avatar.url, ) @@ -210,17 +145,48 @@ class LoggingCog(commands.Cog): log.exception(f"Unexpected error sending log via webhook for guild {guild.id}: {e}") - def _create_log_embed(self, title: str, description: str = "", color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None) -> ui.LayoutView: - """Creates a standardized log view.""" - return self.LogView(self.bot, title, description, color, author, footer) + def _create_log_embed( + self, + title: str, + description: str = "", + color: discord.Color = discord.Color.blue(), + author: Optional[Union[discord.User, discord.Member]] = None, + footer: Optional[str] = None, + ) -> discord.Embed: + """Creates a standardized embed for logging.""" + embed = discord.Embed(title=title, description=description, color=color) + if author: + embed.set_author(name=author.display_name, icon_url=author.display_avatar.url) + if footer: + embed.set_footer(text=footer) + else: + footer_text = f"Bot ID: {self.bot.user.id}" + if author: + footer_text += f" | User ID: {author.id}" + embed.set_footer(text=footer_text) + return embed - def _add_id_footer(self, embed: ui.LayoutView, obj: Union[discord.Member, discord.User, discord.Role, discord.abc.GuildChannel, discord.Message, discord.Invite, None] = None, obj_id: Optional[int] = None, id_name: str = "ID"): + def _add_id_footer( + self, + embed: discord.Embed, + obj: Union[ + discord.Member, + discord.User, + discord.Role, + discord.abc.GuildChannel, + discord.Message, + discord.Invite, + None, + ] = None, + obj_id: Optional[int] = None, + id_name: str = "ID", + ) -> None: """Adds an ID to the footer text if possible.""" target_id = obj_id or (obj.id if obj else None) - if target_id and hasattr(embed, "footer_display"): - existing_footer = embed.footer_display.content or "" + if target_id: + existing_footer = embed.footer.text if embed.footer else "" separator = " | " if existing_footer else "" - embed.footer_display.content = f"{existing_footer}{separator}{id_name}: {target_id}" + embed.set_footer(text=f"{existing_footer}{separator}{id_name}: {target_id}") async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool: """Checks if logging is enabled for a specific event key in a guild.""" @@ -305,13 +271,13 @@ class LoggingCog(commands.Cog): await ctx.send(f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.") # Test send (optional) try: - test_view = self._create_log_embed( + test_embed = self._create_log_embed( "โœ… Logging Setup Complete", f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", color=discord.Color.green(), ) await new_webhook.send( - view=test_view, + embed=test_embed, username=webhook_name, avatar_url=self.bot.user.display_avatar.url, ) From 22baec434d121ac6f5fc86fda5cd9a001ba24c27 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 21:40:58 +0000 Subject: [PATCH 22/25] Restore logging view components --- cogs/logging_cog.py | 113 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 23 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 13299b2..1c83276 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -1,5 +1,6 @@ import discord from discord.ext import commands, tasks +from discord import ui import datetime import asyncio import aiohttp # Added for webhook sending @@ -54,6 +55,78 @@ class LoggingCog(commands.Cog): else: asyncio.create_task(self.start_audit_log_poller_when_ready()) # Keep this for initial start + class LogView(ui.LayoutView): + """Simple view for log messages with helper methods.""" + + def __init__( + self, + bot: commands.Bot, + title: str, + description: str, + color: discord.Color, + author: Optional[discord.abc.User], + footer: Optional[str], + ): + super().__init__(timeout=None) + self.container = ui.Container(accent_colour=color) + self.add_item(self.container) + + self.header = ui.Section( + accessory=( + ui.Thumbnail(media=author.display_avatar.url) + if author + else ui.Button(label="\u200b", disabled=True) + ) + ) + self.header.add_item(ui.TextDisplay(f"**{title}**")) + if description: + self.header.add_item(ui.TextDisplay(description)) + self.container.add_item(self.header) + + # Placeholder for future field sections. They are inserted before + # the separator when the first field is added. + self._field_sections: list[ui.Section] = [] + + self.separator = ui.Separator(spacing=discord.SeparatorSpacing.small) + + footer_text = footer or f"Bot ID: {bot.user.id}" + ( + f" | User ID: {author.id}" if author else "" + ) + self.footer_display = ui.TextDisplay(footer_text) + + self.container.add_item(self.separator) + self.container.add_item(self.footer_display) + + # --- Compatibility helpers --- + def add_field(self, name: str, value: str, inline: bool = False): + """Mimic Embed.add_field by appending a bolded name/value line.""" + if not self._field_sections or len(self._field_sections[-1].children) >= 3: + section = ui.Section(accessory=ui.Button(label="\u200b", disabled=True)) + self._insert_field_section(section) + self._field_sections.append(section) + self._field_sections[-1].add_item(ui.TextDisplay(f"**{name}:** {value}")) + + def _insert_field_section(self, section: ui.Section) -> None: + """Insert a field section before the footer separator.""" + self.container.remove_item(self.separator) + self.container.remove_item(self.footer_display) + self.container.add_item(section) + self.container.add_item(self.separator) + self.container.add_item(self.footer_display) + + def set_footer(self, text: str): + """Mimic Embed.set_footer by replacing the footer text display.""" + self.footer_display.content = text + + def set_author(self, name: str, icon_url: Optional[str] = None): + """Mimic Embed.set_author by adjusting the header section.""" + self.header.clear_items() + if icon_url: + self.header.accessory = ui.Thumbnail(media=icon_url) + else: + self.header.accessory = ui.Button(label="\u200b", disabled=True) + self.header.add_item(ui.TextDisplay(name)) + def _user_display(self, user: Union[discord.Member, discord.User]) -> str: """Return display name, username and ID string for a user.""" display = user.display_name if isinstance(user, discord.Member) else user.name @@ -109,8 +182,8 @@ class LoggingCog(commands.Cog): await self.session.close() log.info("aiohttp ClientSession closed for LoggingCog.") - async def _send_log_embed(self, guild: discord.Guild, embed: discord.Embed) -> None: - """Sends the log embed via the configured webhook for the guild.""" + async def _send_log_embed(self, guild: discord.Guild, embed: ui.LayoutView) -> None: + """Sends the log view via the configured webhook for the guild.""" if not self.session or self.session.closed: log.error(f"aiohttp session not available or closed in LoggingCog for guild {guild.id}. Cannot send log.") return @@ -122,9 +195,13 @@ class LoggingCog(commands.Cog): return try: - webhook = discord.Webhook.from_url(webhook_url, session=self.session) + webhook = discord.Webhook.from_url( + webhook_url, + session=self.session, + client=self.bot, + ) await webhook.send( - embed=embed, + view=embed, username=f"{self.bot.user.name} Logs", avatar_url=self.bot.user.display_avatar.url, ) @@ -152,23 +229,13 @@ class LoggingCog(commands.Cog): color: discord.Color = discord.Color.blue(), author: Optional[Union[discord.User, discord.Member]] = None, footer: Optional[str] = None, - ) -> discord.Embed: - """Creates a standardized embed for logging.""" - embed = discord.Embed(title=title, description=description, color=color) - if author: - embed.set_author(name=author.display_name, icon_url=author.display_avatar.url) - if footer: - embed.set_footer(text=footer) - else: - footer_text = f"Bot ID: {self.bot.user.id}" - if author: - footer_text += f" | User ID: {author.id}" - embed.set_footer(text=footer_text) - return embed + ) -> ui.LayoutView: + """Creates a standardized log view.""" + return self.LogView(self.bot, title, description, color, author, footer) def _add_id_footer( self, - embed: discord.Embed, + embed: ui.LayoutView, obj: Union[ discord.Member, discord.User, @@ -183,10 +250,10 @@ class LoggingCog(commands.Cog): ) -> None: """Adds an ID to the footer text if possible.""" target_id = obj_id or (obj.id if obj else None) - if target_id: - existing_footer = embed.footer.text if embed.footer else "" + if target_id and hasattr(embed, "footer_display"): + existing_footer = embed.footer_display.content or "" separator = " | " if existing_footer else "" - embed.set_footer(text=f"{existing_footer}{separator}{id_name}: {target_id}") + embed.footer_display.content = f"{existing_footer}{separator}{id_name}: {target_id}" async def _check_log_enabled(self, guild_id: int, event_key: str) -> bool: """Checks if logging is enabled for a specific event key in a guild.""" @@ -271,13 +338,13 @@ class LoggingCog(commands.Cog): await ctx.send(f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.") # Test send (optional) try: - test_embed = self._create_log_embed( + test_view = self._create_log_embed( "โœ… Logging Setup Complete", f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", color=discord.Color.green(), ) await new_webhook.send( - embed=test_embed, + view=test_view, username=webhook_name, avatar_url=self.bot.user.display_avatar.url, ) From 68603355c098fc15321bcc3b439b2757fb26814e Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 21:44:03 +0000 Subject: [PATCH 23/25] Add missing ui import --- cogs/logging_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 91f2791..ff24071 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -1,5 +1,6 @@ import discord from discord.ext import commands, tasks +from discord import ui import datetime import asyncio import aiohttp # Added for webhook sending From 7b5317f06d73d4dea0fcbeae07b29e1aba80f6cc Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 21:45:09 +0000 Subject: [PATCH 24/25] Fix test message to use view --- cogs/logging_cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index ff24071..45f4ce7 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -337,13 +337,13 @@ class LoggingCog(commands.Cog): await ctx.send(f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.") # Test send (optional) try: - test_embed = self._create_log_embed( + test_view = self._create_log_embed( "โœ… Logging Setup Complete", f"Logs will now be sent to this channel via the webhook `{new_webhook.name}`.", color=discord.Color.green(), ) await new_webhook.send( - embed=test_embed, + view=test_view, username=webhook_name, avatar_url=self.bot.user.display_avatar.url, ) From f365a62b3e7154fb806f2f0db8ae5a93124ef356 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 5 Jun 2025 21:50:40 +0000 Subject: [PATCH 25/25] Ensure logging messages disable pings --- cogs/logging_cog.py | 87 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/cogs/logging_cog.py b/cogs/logging_cog.py index 45f4ce7..57833e9 100644 --- a/cogs/logging_cog.py +++ b/cogs/logging_cog.py @@ -1,6 +1,6 @@ import discord from discord.ext import commands, tasks -from discord import ui +from discord import ui, AllowedMentions import datetime import asyncio import aiohttp # Added for webhook sending @@ -203,6 +203,7 @@ class LoggingCog(commands.Cog): view=embed, username=f"{self.bot.user.name} Logs", avatar_url=self.bot.user.display_avatar.url, + allowed_mentions=AllowedMentions.none(), ) # log.debug(f"Sent log embed via webhook for guild {guild.id}") # Can be noisy except ValueError as e: @@ -285,10 +286,16 @@ class LoggingCog(commands.Cog): # 1. Check bot permissions if not channel.permissions_for(me).manage_webhooks: - await ctx.send(f"โŒ I don't have the 'Manage Webhooks' permission in {channel.mention}. Please grant it and try again.") + await ctx.send( + f"โŒ I don't have the 'Manage Webhooks' permission in {channel.mention}. Please grant it and try again.", + allowed_mentions=AllowedMentions.none(), + ) return if not channel.permissions_for(me).send_messages: - await ctx.send(f"โŒ I don't have the 'Send Messages' permission in {channel.mention}. Please grant it and try again (needed for webhook creation confirmation).") + await ctx.send( + f"โŒ I don't have the 'Send Messages' permission in {channel.mention}. Please grant it and try again (needed for webhook creation confirmation).", + allowed_mentions=AllowedMentions.none(), + ) return # 2. Check existing webhook setting @@ -299,15 +306,27 @@ class LoggingCog(commands.Cog): if not self.session or self.session.closed: self.session = aiohttp.ClientSession() # Ensure session exists existing_webhook = await discord.Webhook.from_url(existing_url, session=self.session).fetch() if existing_webhook.channel_id == channel.id: - await ctx.send(f"โœ… Logging is already configured for {channel.mention} using webhook `{existing_webhook.name}`.") + await ctx.send( + f"โœ… Logging is already configured for {channel.mention} using webhook `{existing_webhook.name}`.", + allowed_mentions=AllowedMentions.none(), + ) return else: - await ctx.send(f"โš ๏ธ Logging webhook is currently set for a different channel (<#{existing_webhook.channel_id}>). I will create a new one for {channel.mention}.") + await ctx.send( + f"โš ๏ธ Logging webhook is currently set for a different channel (<#{existing_webhook.channel_id}>). I will create a new one for {channel.mention}.", + allowed_mentions=AllowedMentions.none(), + ) except (discord.NotFound, discord.Forbidden, ValueError, aiohttp.ClientError): - await ctx.send(f"โš ๏ธ Could not verify the existing webhook URL. It might be invalid or deleted. I will create a new one for {channel.mention}.") + await ctx.send( + f"โš ๏ธ Could not verify the existing webhook URL. It might be invalid or deleted. I will create a new one for {channel.mention}.", + allowed_mentions=AllowedMentions.none(), + ) except Exception as e: 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}.") + 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(), + ) # 3. Create new webhook @@ -324,17 +343,26 @@ class LoggingCog(commands.Cog): log.info(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}") - await ctx.send(f"โŒ Failed to create webhook. Error: {e}. This could be due to hitting the channel webhook limit (15).") + 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(), + ) return except Exception as e: log.exception(f"Unexpected error creating webhook in {channel.mention} for guild {guild.id}") - await ctx.send("โŒ An unexpected error occurred while creating the webhook.") + await ctx.send( + "โŒ An unexpected error occurred while creating the webhook.", + allowed_mentions=AllowedMentions.none(), + ) return # 4. Save webhook URL success = await settings_manager.set_logging_webhook(guild.id, new_webhook.url) if success: - await ctx.send(f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.") + await ctx.send( + f"โœ… Successfully configured logging to send messages to {channel.mention} via the new webhook `{new_webhook.name}`.", + allowed_mentions=AllowedMentions.none(), + ) # Test send (optional) try: test_view = self._create_log_embed( @@ -346,13 +374,20 @@ class LoggingCog(commands.Cog): view=test_view, username=webhook_name, avatar_url=self.bot.user.display_avatar.url, + allowed_mentions=AllowedMentions.none(), ) except Exception as 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.") + await ctx.send( + "โš ๏ธ Could not send a test message via the new webhook, but the URL has been saved.", + allowed_mentions=AllowedMentions.none(), + ) else: log.error(f"Failed to save webhook URL {new_webhook.url} to database for guild {guild.id}") - await ctx.send("โŒ Successfully created the webhook, but failed to save its URL to my settings. Please try again or contact support.") + await ctx.send( + "โŒ Successfully created the webhook, but failed to save its URL to my settings. Please try again or contact support.", + allowed_mentions=AllowedMentions.none(), + ) # Attempt to delete the created webhook to avoid orphans try: await new_webhook.delete(reason="Failed to save URL to settings") @@ -374,7 +409,10 @@ class LoggingCog(commands.Cog): event_key = event_key.lower() # Ensure case-insensitivity if event_key not in ALL_EVENT_KEYS: - await ctx.send(f"โŒ Invalid event key: `{event_key}`. Use `{ctx.prefix}log list_keys` to see valid keys.") + await ctx.send( + f"โŒ Invalid event key: `{event_key}`. Use `{ctx.prefix}log list_keys` to see valid keys.", + allowed_mentions=AllowedMentions.none(), + ) return # Determine the new status @@ -390,9 +428,15 @@ class LoggingCog(commands.Cog): if success: status_str = "ENABLED" if new_status else "DISABLED" - await ctx.send(f"โœ… Logging for event `{event_key}` is now **{status_str}**.") + await ctx.send( + f"โœ… Logging for event `{event_key}` is now **{status_str}**.", + allowed_mentions=AllowedMentions.none(), + ) else: - await ctx.send(f"โŒ Failed to update setting for event `{event_key}`. Please check logs or try again.") + await ctx.send( + f"โŒ Failed to update setting for event `{event_key}`. Please check logs or try again.", + allowed_mentions=AllowedMentions.none(), + ) @log_group.command(name="status") @commands.has_permissions(administrator=True) @@ -414,7 +458,7 @@ class LoggingCog(commands.Cog): for line in lines: if len(description) + len(line) + 1 > 4000: # Embed description limit (approx) embed.description = description - await ctx.send(embed=embed) + 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 else: @@ -422,7 +466,7 @@ class LoggingCog(commands.Cog): if description: # Send the last embed page embed.description = description.strip() - await ctx.send(embed=embed) + await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none()) @log_group.command(name="list_keys") @@ -446,12 +490,15 @@ class LoggingCog(commands.Cog): parts.append(current_part) embed.description = parts[0] - await ctx.send(embed=embed) + await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none()) for part in parts[1:]: - await ctx.send(embed=discord.Embed(description=part, color=discord.Color.purple())) + await ctx.send( + embed=discord.Embed(description=part, color=discord.Color.purple()), + allowed_mentions=AllowedMentions.none(), + ) else: embed.description = keys_text - await ctx.send(embed=embed) + await ctx.send(embed=embed, allowed_mentions=AllowedMentions.none()) # --- Thread Events ---