This commit is contained in:
Slipstream 2025-05-07 15:58:59 -06:00
parent 80d9f71962
commit 27807e89bb
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
3 changed files with 229 additions and 4 deletions

View File

@ -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"""

View File

@ -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")

View File

@ -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}")