diff --git a/cogs/real_moderation_cog.py b/cogs/real_moderation_cog.py index 749ea02..5250b27 100644 --- a/cogs/real_moderation_cog.py +++ b/cogs/real_moderation_cog.py @@ -164,6 +164,19 @@ class ModerationCog(commands.Cog): )(remove_infraction_command) self.moderate_group.add_command(remove_infraction_command) + # --- Clear Infractions Command --- + clear_infractions_command = app_commands.Command( + name="clearinfractions", + description="Clear all moderation infractions for a user", + callback=self.moderate_clear_infractions_callback, + parent=self.moderate_group + ) + app_commands.describe( + member="The member whose infractions to clear", + reason="The reason for clearing all infractions" + )(clear_infractions_command) + self.moderate_group.add_command(clear_infractions_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.""" @@ -827,6 +840,68 @@ class ModerationCog(commands.Cog): 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) + async def moderate_clear_infractions_callback(self, interaction: discord.Interaction, member: discord.Member, reason: str = None): + """Clear all moderation infractions for a user.""" + # This is a destructive action, so require ban_members permission + if not interaction.user.guild_permissions.ban_members: + await interaction.response.send_message("❌ You don't have permission to clear all infractions for a user.", ephemeral=True) + return + + if not self.bot.pg_pool: + await interaction.response.send_message("❌ Database connection is not available.", ephemeral=True) + logger.error("Cannot clear infractions: pg_pool is None.") + return + + # Confirmation step + view = discord.ui.View() + confirm_button = discord.ui.Button(label="Confirm Clear All", style=discord.ButtonStyle.danger, custom_id="confirm_clear_all") + cancel_button = discord.ui.Button(label="Cancel", style=discord.ButtonStyle.secondary, custom_id="cancel_clear_all") + + async def confirm_callback(interaction_confirm: discord.Interaction): + if interaction_confirm.user.id != interaction.user.id: + await interaction_confirm.response.send_message("❌ You are not authorized to confirm this action.", ephemeral=True) + return + + deleted_count = await mod_log_db.clear_user_mod_logs(self.bot.pg_pool, interaction.guild.id, member.id) + + if deleted_count > 0: + logger.info(f"{deleted_count} infractions for user {member} (ID: {member.id}) cleared by {interaction.user} (ID: {interaction.user.id}) in guild {interaction.guild.id}. Reason: {reason}") + + # Log the clear all action + mod_log_cog: ModLogCog = self.bot.get_cog('ModLogCog') + if mod_log_cog: + await mod_log_cog.log_action( + guild=interaction.guild, + moderator=interaction.user, + target=member, + action_type="CLEAR_INFRACTIONS", + 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) + elif deleted_count == 0: + await interaction_confirm.response.edit_message(content=f"ℹ️ No infractions found for {member.mention} 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) + + async def cancel_callback(interaction_cancel: discord.Interaction): + if interaction_cancel.user.id != interaction.user.id: + await interaction_cancel.response.send_message("❌ You are not authorized to cancel this action.", ephemeral=True) + return + await interaction_cancel.response.edit_message(content="🚫 Infraction clearing cancelled.", view=None) + + confirm_button.callback = confirm_callback + cancel_button.callback = cancel_callback + view.add_item(confirm_button) + 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"This action is irreversible. Reason: {reason or 'Not specified'}", + view=view, + 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 7e1f47d..f8c91b8 100644 --- a/db/mod_log_db.py +++ b/db/mod_log_db.py @@ -488,3 +488,40 @@ async def delete_mod_log(pool: asyncpg.Pool, case_id: int, guild_id: int) -> boo await pool.release(connection) except Exception as e: log.warning(f"Error releasing connection back to pool after deleting mod log: {e}") + +async def clear_user_mod_logs(pool: asyncpg.Pool, guild_id: int, target_user_id: int) -> int: + """Deletes all moderation log entries for a specific user in a guild. Returns the number of deleted logs.""" + query = """ + DELETE FROM moderation_logs + WHERE guild_id = $1 AND target_user_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 clearing mod logs for user {target_user_id} in guild {guild_id}") + return 0 + + try: + async with connection.transaction(): + # Execute the delete command and get the status (e.g., "DELETE 5") + result_status = await connection.execute(query, guild_id, target_user_id) + # Parse the number of deleted rows from the status string + deleted_count = 0 + if result_status and result_status.startswith("DELETE"): + try: + deleted_count = int(result_status.split(" ")[1]) + except (IndexError, ValueError) as e: + log.warning(f"Could not parse deleted count from status: {result_status} - {e}") + + if deleted_count > 0: + log.info(f"Cleared {deleted_count} mod log entries for user {target_user_id} in guild {guild_id}") + else: + log.info(f"No mod log entries found to clear for user {target_user_id} in guild {guild_id}") + return deleted_count + except Exception as e: + log.exception(f"Error clearing mod log entries for user {target_user_id} in guild {guild_id}: {e}") + return 0 + finally: + try: + await pool.release(connection) + except Exception as e: + log.warning(f"Error releasing connection back to pool after clearing user mod logs: {e}")