This commit is contained in:
Slipstream 2025-05-07 16:02:17 -06:00
parent 27807e89bb
commit 318507ca2c
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
2 changed files with 112 additions and 0 deletions

View File

@ -164,6 +164,19 @@ class ModerationCog(commands.Cog):
)(remove_infraction_command) )(remove_infraction_command)
self.moderate_group.add_command(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 # Helper method for parsing duration strings
def _parse_duration(self, duration_str: str) -> Optional[datetime.timedelta]: def _parse_duration(self, duration_str: str) -> Optional[datetime.timedelta]:
"""Parse a duration string like '1d', '2h', '30m' into a timedelta.""" """Parse a duration string like '1d', '2h', '30m' into a timedelta."""
@ -827,6 +840,68 @@ class ModerationCog(commands.Cog):
else: 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) 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) --- # --- Legacy Command Handlers (for prefix commands) ---
@commands.command(name="timeout") @commands.command(name="timeout")

View File

@ -488,3 +488,40 @@ async def delete_mod_log(pool: asyncpg.Pool, case_id: int, guild_id: int) -> boo
await pool.release(connection) await pool.release(connection)
except Exception as e: except Exception as e:
log.warning(f"Error releasing connection back to pool after deleting mod log: {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}")