From 27807e89bb6e9af136a9d55db284958b6d848778 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Wed, 7 May 2025 15:58:59 -0600 Subject: [PATCH] aaaaa --- cogs/mod_application_cog.py | 89 ++++++++++++++++++++++++++-- cogs/real_moderation_cog.py | 115 ++++++++++++++++++++++++++++++++++++ db/mod_log_db.py | 29 +++++++++ 3 files changed, 229 insertions(+), 4 deletions(-) diff --git a/cogs/mod_application_cog.py b/cogs/mod_application_cog.py index 4df40d8..8ead8e9 100644 --- a/cogs/mod_application_cog.py +++ b/cogs/mod_application_cog.py @@ -39,7 +39,8 @@ CREATE TABLE IF NOT EXISTS mod_application_settings ( required_role_id BIGINT NULL, reviewer_role_id BIGINT NULL, custom_questions JSONB NULL, - cooldown_days INTEGER NOT NULL DEFAULT 30 + cooldown_days INTEGER NOT NULL DEFAULT 30, + log_new_applications BOOLEAN NOT NULL DEFAULT FALSE ); """ @@ -370,6 +371,18 @@ class ModApplicationCog(commands.Cog): )(settings_cooldown_command) self.modapp_group.add_command(settings_cooldown_command) + # --- Toggle Log New Applications Command --- + settings_log_new_apps_command = app_commands.Command( + name="settings_lognewapps", + description="Toggle whether new applications are automatically logged in the log channel", + callback=self.toggle_log_new_applications_callback, + parent=self.modapp_group # Direct child of modapp + ) + app_commands.describe( + enabled="Whether new applications should be logged automatically" + )(settings_log_new_apps_command) + self.modapp_group.add_command(settings_log_new_apps_command) + # --- Command Callbacks --- async def apply_callback(self, interaction: discord.Interaction): @@ -703,6 +716,50 @@ class ModApplicationCog(commands.Cog): ephemeral=True ) + async def toggle_log_new_applications_callback(self, interaction: discord.Interaction, enabled: bool): + """Handle the /modapp settings lognewapps command""" + # Check if user has permission to manage applications + if not await self.check_admin_permission(interaction.guild_id, interaction.user.id): + await interaction.response.send_message( + "❌ You don't have permission to manage application settings.", + ephemeral=True + ) + return + + # Get current settings to check if log channel is set + settings = await self.get_application_settings(interaction.guild_id) + log_channel_id = settings.get("log_channel_id") + + if enabled and not log_channel_id: + await interaction.response.send_message( + "❌ You need to set a log channel first using `/modapp settings_logchannel` before enabling this feature.", + ephemeral=True + ) + return + + # Update setting in database + success = await self.update_application_setting(interaction.guild_id, "log_new_applications", enabled) + + if success: + status = "enabled" if enabled else "disabled" + if enabled: + log_channel = interaction.guild.get_channel(log_channel_id) + channel_mention = log_channel.mention if log_channel else "the configured log channel" + await interaction.response.send_message( + f"✅ New applications will now be automatically logged in {channel_mention}.", + ephemeral=True + ) + else: + await interaction.response.send_message( + "✅ New applications will no longer be automatically logged.", + ephemeral=True + ) + else: + await interaction.response.send_message( + "❌ Failed to update application settings.", + ephemeral=True + ) + # --- Database Helper Methods --- async def submit_application(self, guild_id: int, user_id: int, form_data: Dict[str, str]) -> bool: @@ -846,7 +903,8 @@ class ModApplicationCog(commands.Cog): "required_role_id": None, "reviewer_role_id": None, "custom_questions": None, - "cooldown_days": 30 + "cooldown_days": 30, + "log_new_applications": False } # Convert row to dictionary and parse custom_questions JSON if it exists @@ -1012,6 +1070,8 @@ class ModApplicationCog(commands.Cog): # Get application settings settings = await self.get_application_settings(guild.id) review_channel_id = settings.get("review_channel_id") + log_channel_id = settings.get("log_channel_id") + log_new_applications = settings.get("log_new_applications", False) if not review_channel_id: return @@ -1080,7 +1140,7 @@ class ModApplicationCog(commands.Cog): custom_id=f"view_application_{application['application_id']}" )) - # Send the notification + # Send the notification to the review channel try: await review_channel.send( content=f"📝 New moderator application from {user.mention}", @@ -1088,7 +1148,28 @@ class ModApplicationCog(commands.Cog): view=view ) except Exception as e: - logger.error(f"Error sending application notification: {e}") + logger.error(f"Error sending application notification to review channel: {e}") + + # If log_new_applications is enabled and log_channel_id is set, also log to the log channel + if log_new_applications and log_channel_id: + log_channel = guild.get_channel(log_channel_id) + if log_channel: + try: + # Create a simpler embed for the log channel + log_embed = discord.Embed( + title="New Moderator Application Submitted", + description=f"A new moderator application has been submitted by {user.mention}.", + color=discord.Color.blue(), + timestamp=datetime.datetime.now() + ) + log_embed.set_author(name=f"{user.name}", icon_url=user.display_avatar.url) + log_embed.add_field(name="Application ID", value=application["application_id"], inline=True) + log_embed.add_field(name="Status", value="PENDING", inline=True) + log_embed.add_field(name="Submission Time", value=discord.utils.format_dt(datetime.datetime.now()), inline=True) + + await log_channel.send(embed=log_embed) + except Exception as e: + logger.error(f"Error sending application notification to log channel: {e}") async def notify_application_status_change(self, guild: discord.Guild, user_id: int, status: APPLICATION_STATUS, reason: Optional[str] = None) -> None: """Notify the applicant about a status change""" diff --git a/cogs/real_moderation_cog.py b/cogs/real_moderation_cog.py index 5e89531..749ea02 100644 --- a/cogs/real_moderation_cog.py +++ b/cogs/real_moderation_cog.py @@ -7,6 +7,7 @@ from typing import Optional, Union, List # Use absolute import for ModLogCog from discordbot.cogs.mod_log_cog import ModLogCog +from discordbot.db import mod_log_db # Import the database functions # Configure logging logger = logging.getLogger(__name__) @@ -138,6 +139,31 @@ class ModerationCog(commands.Cog): )(dm_banned_command) self.moderate_group.add_command(dm_banned_command) + # --- View Infractions Command --- + view_infractions_command = app_commands.Command( + name="infractions", + description="View moderation infractions for a user", + callback=self.moderate_view_infractions_callback, + parent=self.moderate_group + ) + app_commands.describe( + member="The member whose infractions to view" + )(view_infractions_command) + self.moderate_group.add_command(view_infractions_command) + + # --- Remove Infraction Command --- + remove_infraction_command = app_commands.Command( + name="removeinfraction", + description="Remove a specific infraction by its case ID", + callback=self.moderate_remove_infraction_callback, + parent=self.moderate_group + ) + app_commands.describe( + case_id="The case ID of the infraction to remove", + reason="The reason for removing the infraction" + )(remove_infraction_command) + self.moderate_group.add_command(remove_infraction_command) + # Helper method for parsing duration strings def _parse_duration(self, duration_str: str) -> Optional[datetime.timedelta]: """Parse a duration string like '1d', '2h', '30m' into a timedelta.""" @@ -712,6 +738,95 @@ class ModerationCog(commands.Cog): logger.error(f"Error sending DM to banned user {banned_user} (ID: {banned_user.id}): {e}") await interaction.response.send_message(f"❌ An unexpected error occurred: {e}", ephemeral=True) + async def moderate_view_infractions_callback(self, interaction: discord.Interaction, member: discord.Member): + """View moderation infractions for a user.""" + if not interaction.user.guild_permissions.kick_members: # Using kick_members as a general mod permission + await interaction.response.send_message("❌ You don't have permission to view infractions.", ephemeral=True) + return + + if not self.bot.pg_pool: + await interaction.response.send_message("❌ Database connection is not available.", ephemeral=True) + logger.error("Cannot view infractions: pg_pool is None.") + return + + 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) + return + + embed = discord.Embed( + title=f"Infractions for {member.display_name}", + color=discord.Color.orange() + ) + embed.set_thumbnail(url=member.display_avatar.url) + + for infraction in infractions[:25]: # Display up to 25 infractions + action_type = infraction['action_type'] + reason = infraction['reason'] or "No reason provided" + moderator_id = infraction['moderator_id'] + timestamp = infraction['timestamp'] + case_id = infraction['case_id'] + duration_seconds = infraction['duration_seconds'] + + moderator = interaction.guild.get_member(moderator_id) or f"ID: {moderator_id}" + + value = f"**Case ID:** {case_id}\n" + value += f"**Action:** {action_type}\n" + value += f"**Moderator:** {moderator}\n" + if duration_seconds: + duration_str = str(datetime.timedelta(seconds=duration_seconds)) + value += f"**Duration:** {duration_str}\n" + value += f"**Reason:** {reason}\n" + value += f"**Date:** {discord.utils.format_dt(timestamp, style='f')}" + + embed.add_field(name=f"Infraction #{case_id}", value=value, inline=False) + + if len(infractions) > 25: + embed.set_footer(text=f"Showing 25 of {len(infractions)} infractions.") + + await interaction.response.send_message(embed=embed, ephemeral=True) + + async def moderate_remove_infraction_callback(self, interaction: discord.Interaction, case_id: int, reason: str = None): + """Remove a specific infraction by its case ID.""" + if not interaction.user.guild_permissions.ban_members: # Higher permission for removing infractions + await interaction.response.send_message("❌ You don't have permission to remove infractions.", ephemeral=True) + return + + if not self.bot.pg_pool: + await interaction.response.send_message("❌ Database connection is not available.", ephemeral=True) + logger.error("Cannot remove infraction: pg_pool is None.") + return + + # Fetch the infraction to ensure it exists and to log details + infraction_to_remove = await mod_log_db.get_mod_log(self.bot.pg_pool, case_id) + if not infraction_to_remove or infraction_to_remove['guild_id'] != interaction.guild.id: + await interaction.response.send_message(f"❌ Infraction with Case ID {case_id} not found in this server.", ephemeral=True) + return + + deleted = await mod_log_db.delete_mod_log(self.bot.pg_pool, case_id, interaction.guild.id) + + if deleted: + logger.info(f"Infraction (Case ID: {case_id}) removed by {interaction.user} (ID: {interaction.user.id}) in guild {interaction.guild.id}. Reason: {reason}") + + # Log the removal action itself + mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog') + if mod_log_cog: + target_user_id = infraction_to_remove['target_user_id'] + target_user = await self.bot.fetch_user(target_user_id) # Fetch user for logging + + await mod_log_cog.log_action( + guild=interaction.guild, + moderator=interaction.user, + target=target_user if target_user else Object(id=target_user_id), + action_type="REMOVE_INFRACTION", + reason=f"Removed Case ID {case_id}. Original reason: {infraction_to_remove['reason']}. Removal reason: {reason or 'Not specified'}", + duration=None + ) + await interaction.response.send_message(f"✅ Infraction with Case ID {case_id} has been removed. Reason: {reason or 'Not specified'}", ephemeral=True) + else: + await interaction.response.send_message(f"❌ Failed to remove infraction with Case ID {case_id}. It might have already been removed or an error occurred.", ephemeral=True) + # --- Legacy Command Handlers (for prefix commands) --- @commands.command(name="timeout") diff --git a/db/mod_log_db.py b/db/mod_log_db.py index efb054c..7e1f47d 100644 --- a/db/mod_log_db.py +++ b/db/mod_log_db.py @@ -459,3 +459,32 @@ async def log_action_safe(bot_instance, guild_id: int, target_user_id: int, acti # Otherwise, use the helper function to run in the bot's loop return run_in_bot_loop(bot_instance, _log_action_coro) + +async def delete_mod_log(pool: asyncpg.Pool, case_id: int, guild_id: int) -> bool: + """Deletes a specific moderation log entry by case_id, ensuring it belongs to the guild.""" + query = """ + DELETE FROM moderation_logs + WHERE case_id = $1 AND guild_id = $2; + """ + connection, success = await create_connection_with_retry(pool) + if not success or not connection: + log.error(f"Failed to acquire database connection for deleting mod log for case_id {case_id} in guild {guild_id}") + return False + + try: + async with connection.transaction(): + result = await connection.execute(query, case_id, guild_id) + if result == "DELETE 1": + log.info(f"Deleted mod log entry for case_id {case_id} in guild {guild_id}") + return True + else: + log.warning(f"Could not delete mod log entry for case_id {case_id} in guild {guild_id}. Case might not exist or not belong to this guild.") + return False + except Exception as e: + log.exception(f"Error deleting mod log entry for case_id {case_id} in guild {guild_id}: {e}") + return False + finally: + try: + await pool.release(connection) + except Exception as e: + log.warning(f"Error releasing connection back to pool after deleting mod log: {e}")