import discord from discord.ext import commands import logging import settings_manager # Assuming settings_manager is accessible import command_customization # Import command customization utilities from typing import Optional log = logging.getLogger(__name__) # Get CORE_COGS from bot instance def get_core_cogs(bot): return getattr(bot, "core_cogs", {"SettingsCog", "HelpCog"}) class SettingsCog(commands.Cog, name="Settings"): """Commands for server administrators to configure the bot.""" def __init__(self, bot: commands.Bot): self.bot = bot # --- Prefix Management --- @commands.command( name="setprefix", help="Sets the command prefix for this server. Usage: `setprefix `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def set_prefix(self, ctx: commands.Context, new_prefix: str): """Sets the command prefix for the current guild.""" if not new_prefix: await ctx.send("Prefix cannot be empty.") return if len(new_prefix) > 10: # Arbitrary limit await ctx.send("Prefix cannot be longer than 10 characters.") return if new_prefix.isspace(): await ctx.send("Prefix cannot be just whitespace.") return guild_id = ctx.guild.id success = await settings_manager.set_guild_prefix(guild_id, new_prefix) if success: await ctx.send( f"Command prefix for this server has been set to: `{new_prefix}`" ) log.info( f"Prefix updated for guild {guild_id} to '{new_prefix}' by {ctx.author.name}" ) else: await ctx.send("Failed to set the prefix. Please check the logs.") log.error(f"Failed to save prefix for guild {guild_id}") @commands.command( name="showprefix", help="Shows the current command prefix for this server." ) @commands.guild_only() async def show_prefix(self, ctx: commands.Context): """Shows the current command prefix.""" # We need the bot's default prefix as a fallback # This might need access to the bot instance's initial config or a constant default_prefix = ( self.bot.command_prefix ) # This might not work if command_prefix is the callable # Use the constant defined in main.py if possible, or keep a local fallback default_prefix_fallback = ( "!" # TODO: Get default prefix reliably if needed elsewhere ) guild_id = ctx.guild.id current_prefix = await settings_manager.get_guild_prefix( guild_id, default_prefix_fallback ) await ctx.send( f"The current command prefix for this server is: `{current_prefix}`" ) # --- Cog Management --- @commands.command( name="enablecog", help="Enables a specific module (cog) for this server. Usage: `enablecog `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def enable_cog(self, ctx: commands.Context, cog_name: str): """Enables a cog for the current guild.""" # Validate if cog exists if cog_name not in self.bot.cogs: await ctx.send(f"Error: Cog `{cog_name}` not found.") return core_cogs = get_core_cogs(self.bot) if cog_name in core_cogs: await ctx.send(f"Error: Core cog `{cog_name}` cannot be disabled/enabled.") return guild_id = ctx.guild.id success = await settings_manager.set_cog_enabled( guild_id, cog_name, enabled=True ) if success: await ctx.send(f"Module `{cog_name}` has been enabled for this server.") log.info( f"Cog '{cog_name}' enabled for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to enable module `{cog_name}`. Check logs.") log.error(f"Failed to enable cog '{cog_name}' for guild {guild_id}") @commands.command( name="disablecog", help="Disables a specific module (cog) for this server. Usage: `disablecog `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def disable_cog(self, ctx: commands.Context, cog_name: str): """Disables a cog for the current guild.""" if cog_name not in self.bot.cogs: await ctx.send(f"Error: Cog `{cog_name}` not found.") return core_cogs = get_core_cogs(self.bot) if cog_name in core_cogs: await ctx.send(f"Error: Core cog `{cog_name}` cannot be disabled.") return guild_id = ctx.guild.id success = await settings_manager.set_cog_enabled( guild_id, cog_name, enabled=False ) if success: await ctx.send(f"Module `{cog_name}` has been disabled for this server.") log.info( f"Cog '{cog_name}' disabled for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to disable module `{cog_name}`. Check logs.") log.error(f"Failed to disable cog '{cog_name}' for guild {guild_id}") @commands.command( name="listcogs", help="Lists all available modules (cogs) and their status for this server.", ) @commands.guild_only() async def list_cogs(self, ctx: commands.Context): """Lists available cogs and their enabled/disabled status.""" guild_id = ctx.guild.id # Note: Default enabled status might need adjustment based on desired behavior # If a cog has no entry in the DB, should it be considered enabled or disabled by default? # Let's assume default_enabled=True for now. default_behavior = True embed = discord.Embed( title="Available Modules (Cogs)", color=discord.Color.blue() ) lines = [] # Get core cogs from bot instance core_cogs_list = get_core_cogs(self.bot) for cog_name in sorted(self.bot.cogs.keys()): is_enabled = await settings_manager.is_cog_enabled( guild_id, cog_name, default_enabled=default_behavior ) status = "✅ Enabled" if is_enabled else "❌ Disabled" if cog_name in core_cogs_list: status += " (Core)" lines.append(f"`{cog_name}`: {status}") embed.description = "\n".join(lines) if lines else "No cogs found." await ctx.send(embed=embed) # --- Command Permission Management (Basic Role-Based) --- @commands.command( name="allowcmd", help="Allows a role to use a specific command. Usage: `allowcmd <@Role>`", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def allow_command( self, ctx: commands.Context, command_name: str, role: discord.Role ): """Allows a specific role to use a command.""" command = self.bot.get_command(command_name) if not command: await ctx.send(f"Error: Command `{command_name}` not found.") return guild_id = ctx.guild.id role_id = role.id success = await settings_manager.add_command_permission( guild_id, command_name, role_id ) if success: await ctx.send( f"Role `{role.name}` is now allowed to use command `{command_name}`." ) log.info( f"Permission added for command '{command_name}', role '{role.name}' ({role_id}) in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send( f"Failed to add permission for command `{command_name}`. Check logs." ) log.error( f"Failed to add permission for command '{command_name}', role {role_id} in guild {guild_id}" ) @commands.command( name="disallowcmd", help="Disallows a role from using a specific command. Usage: `disallowcmd <@Role>`", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def disallow_command( self, ctx: commands.Context, command_name: str, role: discord.Role ): """Disallows a specific role from using a command.""" command = self.bot.get_command(command_name) if not command: await ctx.send(f"Error: Command `{command_name}` not found.") return guild_id = ctx.guild.id role_id = role.id success = await settings_manager.remove_command_permission( guild_id, command_name, role_id ) if success: await ctx.send( f"Role `{role.name}` is no longer allowed to use command `{command_name}`." ) log.info( f"Permission removed for command '{command_name}', role '{role.name}' ({role_id}) in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send( f"Failed to remove permission for command `{command_name}`. Check logs." ) log.error( f"Failed to remove permission for command '{command_name}', role {role_id} in guild {guild_id}" ) # --- Command Customization Management --- @commands.command( name="setcmdname", help="Sets a custom name for a slash command in this server. Usage: `setcmdname `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def set_command_name( self, ctx: commands.Context, original_name: str, custom_name: str ): """Sets a custom name for a slash command in the current guild.""" # Validate the original command exists command_found = False for cmd in self.bot.tree.get_commands(): if cmd.name == original_name: command_found = True break if not command_found: await ctx.send(f"Error: Slash command `{original_name}` not found.") return # Validate custom name format (Discord has restrictions on command names) if not custom_name.islower() or not custom_name.replace("_", "").isalnum(): await ctx.send( "Error: Custom command names must be lowercase and contain only letters, numbers, and underscores." ) return if len(custom_name) < 1 or len(custom_name) > 32: await ctx.send( "Error: Custom command names must be between 1 and 32 characters long." ) return guild_id = ctx.guild.id success = await settings_manager.set_custom_command_name( guild_id, original_name, custom_name ) if success: await ctx.send( f"Command `{original_name}` will now appear as `{custom_name}` in this server.\n" f"Note: You'll need to restart the bot or use `/sync` for changes to take effect." ) log.info( f"Custom command name set for '{original_name}' to '{custom_name}' in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to set custom command name. Check logs.") log.error( f"Failed to set custom command name for '{original_name}' in guild {guild_id}" ) @commands.command( name="resetcmdname", help="Resets a slash command to its original name. Usage: `resetcmdname `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def reset_command_name(self, ctx: commands.Context, original_name: str): """Resets a slash command to its original name in the current guild.""" guild_id = ctx.guild.id success = await settings_manager.set_custom_command_name( guild_id, original_name, None ) if success: await ctx.send( f"Command `{original_name}` has been reset to its original name in this server.\n" f"Note: You'll need to restart the bot or use `/sync` for changes to take effect." ) log.info( f"Custom command name reset for '{original_name}' in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to reset command name. Check logs.") log.error( f"Failed to reset command name for '{original_name}' in guild {guild_id}" ) @commands.command( name="setgroupname", help="Sets a custom name for a command group. Usage: `setgroupname `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def set_group_name( self, ctx: commands.Context, original_name: str, custom_name: str ): """Sets a custom name for a command group in the current guild.""" # Validate the original group exists group_found = False for cmd in self.bot.tree.get_commands(): # Check if this command is itself a group with the specified name if ( hasattr(cmd, "name") and cmd.name == original_name and hasattr(cmd, "commands") ): group_found = True break # Also check if this is a subcommand of a group with the specified name (for nested groups) elif ( hasattr(cmd, "parent") and cmd.parent and cmd.parent.name == original_name ): group_found = True break if not group_found: await ctx.send(f"Error: Command group `{original_name}` not found.") return # Validate custom name format (Discord has restrictions on command names) if not custom_name.islower() or not custom_name.replace("_", "").isalnum(): await ctx.send( "Error: Custom group names must be lowercase and contain only letters, numbers, and underscores." ) return if len(custom_name) < 1 or len(custom_name) > 32: await ctx.send( "Error: Custom group names must be between 1 and 32 characters long." ) return guild_id = ctx.guild.id success = await settings_manager.set_custom_group_name( guild_id, original_name, custom_name ) if success: await ctx.send( f"Command group `{original_name}` will now appear as `{custom_name}` in this server.\n" f"Note: You'll need to restart the bot or use `/sync` for changes to take effect." ) log.info( f"Custom group name set for '{original_name}' to '{custom_name}' in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to set custom group name. Check logs.") log.error( f"Failed to set custom group name for '{original_name}' in guild {guild_id}" ) @commands.command( name="resetgroupname", help="Resets a command group to its original name. Usage: `resetgroupname `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def reset_group_name(self, ctx: commands.Context, original_name: str): """Resets a command group to its original name in the current guild.""" guild_id = ctx.guild.id success = await settings_manager.set_custom_group_name( guild_id, original_name, None ) if success: await ctx.send( f"Command group `{original_name}` has been reset to its original name in this server.\n" f"Note: You'll need to restart the bot or use `/sync` for changes to take effect." ) log.info( f"Custom group name reset for '{original_name}' in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to reset group name. Check logs.") log.error( f"Failed to reset group name for '{original_name}' in guild {guild_id}" ) @commands.command( name="addcmdalias", help="Adds an alias for a command. Usage: `addcmdalias `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def add_command_alias( self, ctx: commands.Context, original_name: str, alias_name: str ): """Adds an alias for a command in the current guild.""" # Validate the original command exists command = self.bot.get_command(original_name) if not command: await ctx.send(f"Error: Command `{original_name}` not found.") return # Validate alias format if not alias_name.islower() or not alias_name.replace("_", "").isalnum(): await ctx.send( "Error: Aliases must be lowercase and contain only letters, numbers, and underscores." ) return if len(alias_name) < 1 or len(alias_name) > 32: await ctx.send("Error: Aliases must be between 1 and 32 characters long.") return guild_id = ctx.guild.id success = await settings_manager.add_command_alias( guild_id, original_name, alias_name ) if success: await ctx.send( f"Added alias `{alias_name}` for command `{original_name}` in this server." ) log.info( f"Command alias added for '{original_name}': '{alias_name}' in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to add command alias. Check logs.") log.error( f"Failed to add command alias for '{original_name}' in guild {guild_id}" ) @commands.command( name="removecmdalias", help="Removes an alias for a command. Usage: `removecmdalias `", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def remove_command_alias( self, ctx: commands.Context, original_name: str, alias_name: str ): """Removes an alias for a command in the current guild.""" guild_id = ctx.guild.id success = await settings_manager.remove_command_alias( guild_id, original_name, alias_name ) if success: await ctx.send( f"Removed alias `{alias_name}` for command `{original_name}` in this server." ) log.info( f"Command alias removed for '{original_name}': '{alias_name}' in guild {guild_id} by {ctx.author.name}" ) else: await ctx.send(f"Failed to remove command alias. Check logs.") log.error( f"Failed to remove command alias for '{original_name}' in guild {guild_id}" ) @commands.command( name="listcmdaliases", help="Lists all command aliases for this server." ) @commands.guild_only() async def list_command_aliases(self, ctx: commands.Context): """Lists all command aliases for the current guild.""" guild_id = ctx.guild.id aliases_dict = await settings_manager.get_all_command_aliases(guild_id) if aliases_dict is None: await ctx.send("Failed to retrieve command aliases. Check logs.") return if not aliases_dict: await ctx.send("No command aliases are set for this server.") return embed = discord.Embed(title="Command Aliases", color=discord.Color.blue()) for cmd_name, aliases in aliases_dict.items(): embed.add_field( name=f"Command: {cmd_name}", value=", ".join([f"`{alias}`" for alias in aliases]), inline=False, ) await ctx.send(embed=embed) @commands.command( name="listcustomcmds", help="Lists all custom command names for this server." ) @commands.guild_only() async def list_custom_commands(self, ctx: commands.Context): """Lists all custom command names for the current guild.""" guild_id = ctx.guild.id cmd_customizations = await settings_manager.get_all_command_customizations( guild_id ) group_customizations = await settings_manager.get_all_group_customizations( guild_id ) if cmd_customizations is None or group_customizations is None: await ctx.send("Failed to retrieve command customizations. Check logs.") return if not cmd_customizations and not group_customizations: await ctx.send("No command customizations are set for this server.") return embed = discord.Embed( title="Command Customizations", color=discord.Color.blue() ) if cmd_customizations: cmd_text = "\n".join( [ f"`{orig}` → `{custom['name']}`" for orig, custom in cmd_customizations.items() ] ) embed.add_field(name="Custom Command Names", value=cmd_text, inline=False) if group_customizations: group_text = "\n".join( [ f"`{orig}` → `{custom['name']}`" for orig, custom in group_customizations.items() ] ) embed.add_field(name="Custom Group Names", value=group_text, inline=False) await ctx.send(embed=embed) @commands.command( name="listgroups", help="Lists all available command groups for debugging." ) @commands.has_permissions(administrator=True) @commands.guild_only() async def list_groups(self, ctx: commands.Context): """Lists all available command groups for debugging purposes.""" groups = [] commands_list = [] for cmd in self.bot.tree.get_commands(): # Check if this is a group if hasattr(cmd, "commands") and hasattr(cmd, "name"): groups.append( f"`{cmd.name}` - {getattr(cmd, 'description', 'No description')}" ) # Check if this is a regular command elif hasattr(cmd, "name"): commands_list.append(f"`{cmd.name}`") embed = discord.Embed( title="Available Command Groups & Commands", color=discord.Color.green() ) if groups: groups_text = "\n".join( groups[:10] ) # Limit to first 10 to avoid message length issues if len(groups) > 10: groups_text += f"\n... and {len(groups) - 10} more groups" embed.add_field(name="Command Groups", value=groups_text, inline=False) else: embed.add_field( name="Command Groups", value="No groups found", inline=False ) if commands_list: commands_text = ", ".join(commands_list[:20]) # Limit to first 20 if len(commands_list) > 20: commands_text += f", ... and {len(commands_list) - 20} more commands" embed.add_field( name="Individual Commands", value=commands_text, inline=False ) await ctx.send(embed=embed) @commands.command( name="synccmds", help="Syncs slash commands with Discord to apply customizations.", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def sync_commands(self, ctx: commands.Context): """Syncs slash commands with Discord to apply customizations.""" try: guild = ctx.guild await ctx.send("Syncing commands with Discord... This may take a moment.") # Use the command_customization module to sync commands with customizations try: synced = await command_customization.register_guild_commands( self.bot, guild ) await ctx.send( f"Successfully synced {len(synced)} commands for this server with customizations." ) log.info( f"Commands synced with customizations for guild {guild.id} by {ctx.author.name}" ) except Exception as e: log.error(f"Failed to sync commands with customizations: {e}") # Don't fall back to regular sync to avoid command duplication await ctx.send( f"Failed to apply customizations. Please check the logs and try again." ) log.info( f"Command sync with customizations failed for guild {guild.id}" ) except Exception as e: await ctx.send(f"Failed to sync commands: {str(e)}") log.error(f"Failed to sync commands for guild {ctx.guild.id}: {e}") # TODO: Add command to list permissions? # --- Moderation Logging Settings --- @commands.group( name="modlogconfig", help="Configure the integrated moderation logging.", invoke_without_command=True, ) @commands.has_permissions(administrator=True) @commands.guild_only() async def modlog_config_group(self, ctx: commands.Context): """Base command for moderation log configuration. Shows current settings.""" guild_id = ctx.guild.id enabled = await settings_manager.is_mod_log_enabled(guild_id) channel_id = await settings_manager.get_mod_log_channel_id(guild_id) channel = ctx.guild.get_channel(channel_id) if channel_id else None status = "✅ Enabled" if enabled else "❌ Disabled" channel_status = ( channel.mention if channel else ("Not Set" if channel_id else "Not Set") ) embed = discord.Embed( title="Moderation Logging Configuration", color=discord.Color.teal() ) embed.add_field(name="Status", value=status, inline=False) embed.add_field(name="Log Channel", value=channel_status, inline=False) await ctx.send(embed=embed) @modlog_config_group.command( name="enable", help="Enables the integrated moderation logging." ) @commands.has_permissions(administrator=True) @commands.guild_only() async def modlog_enable(self, ctx: commands.Context): """Enables the integrated moderation logging for this server.""" guild_id = ctx.guild.id success = await settings_manager.set_mod_log_enabled(guild_id, True) if success: await ctx.send("✅ Integrated moderation logging has been enabled.") log.info( f"Moderation logging enabled for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send("❌ Failed to enable moderation logging. Please check logs.") log.error(f"Failed to enable moderation logging for guild {guild_id}") @modlog_config_group.command( name="disable", help="Disables the integrated moderation logging." ) @commands.has_permissions(administrator=True) @commands.guild_only() async def modlog_disable(self, ctx: commands.Context): """Disables the integrated moderation logging for this server.""" guild_id = ctx.guild.id success = await settings_manager.set_mod_log_enabled(guild_id, False) if success: await ctx.send("❌ Integrated moderation logging has been disabled.") log.info( f"Moderation logging disabled for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send( "❌ Failed to disable moderation logging. Please check logs." ) log.error(f"Failed to disable moderation logging for guild {guild_id}") @modlog_config_group.command( name="setchannel", help="Sets the channel where moderation logs will be sent. Usage: `setchannel #channel`", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def modlog_setchannel( self, ctx: commands.Context, channel: discord.TextChannel ): """Sets the channel for integrated moderation logs.""" guild_id = ctx.guild.id # Basic check for bot permissions in the target channel if ( not channel.permissions_for(ctx.guild.me).send_messages or not channel.permissions_for(ctx.guild.me).embed_links ): await ctx.send( f"❌ I need 'Send Messages' and 'Embed Links' permissions in {channel.mention} to send logs there." ) return success = await settings_manager.set_mod_log_channel_id(guild_id, channel.id) if success: await ctx.send(f"✅ Moderation logs will now be sent to {channel.mention}.") log.info( f"Moderation log channel set to {channel.id} for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send( "❌ Failed to set the moderation log channel. Please check logs." ) log.error(f"Failed to set moderation log channel for guild {guild_id}") @modlog_config_group.command( name="unsetchannel", help="Unsets the moderation log channel (disables sending logs).", ) @commands.has_permissions(administrator=True) @commands.guild_only() async def modlog_unsetchannel(self, ctx: commands.Context): """Unsets the channel for integrated moderation logs.""" guild_id = ctx.guild.id success = await settings_manager.set_mod_log_channel_id(guild_id, None) if success: await ctx.send( "✅ Moderation log channel has been unset. Logs will not be sent to a channel." ) log.info( f"Moderation log channel unset for guild {guild_id} by {ctx.author.name}" ) else: await ctx.send( "❌ Failed to unset the moderation log channel. Please check logs." ) log.error(f"Failed to unset moderation log channel for guild {guild_id}") # --- Error Handling for this Cog --- @set_prefix.error @enable_cog.error @disable_cog.error @allow_command.error @disallow_command.error @set_command_name.error @reset_command_name.error @set_group_name.error @reset_group_name.error @add_command_alias.error @remove_command_alias.error @list_groups.error @sync_commands.error @modlog_config_group.error # Add error handler for the group @modlog_enable.error @modlog_disable.error @modlog_setchannel.error @modlog_unsetchannel.error async def on_command_error(self, ctx: commands.Context, error): # Check if the error originates from the modlogconfig group or its subcommands if ctx.command and ( ctx.command.name == "modlogconfig" or (ctx.command.parent and ctx.command.parent.name == "modlogconfig") ): if isinstance(error, commands.MissingPermissions): await ctx.send( "You need Administrator permissions to configure moderation logging." ) return # Handled elif isinstance(error, commands.BadArgument): await ctx.send( f"Invalid argument. Usage: `{ctx.prefix}help {ctx.command.qualified_name}`" ) return # Handled elif isinstance(error, commands.MissingRequiredArgument): await ctx.send( f"Missing argument. Usage: `{ctx.prefix}help {ctx.command.qualified_name}`" ) return # Handled elif isinstance(error, commands.NoPrivateMessage): await ctx.send("This command can only be used in a server.") return # Handled # Let other errors fall through to the generic handler below # Generic handlers for other commands in this cog if isinstance(error, commands.MissingPermissions): await ctx.send("You need Administrator permissions to use this command.") elif isinstance(error, commands.BadArgument): await ctx.send( f"Invalid argument provided. Check the command help: `{ctx.prefix}help {ctx.command.name}`" ) elif isinstance(error, commands.MissingRequiredArgument): await ctx.send( f"Missing required argument. Check the command help: `{ctx.prefix}help {ctx.command.name}`" ) elif isinstance(error, commands.NoPrivateMessage): await ctx.send("This command cannot be used in private messages.") else: log.error( f"Unhandled error in SettingsCog command '{ctx.command.name}': {error}" ) await ctx.send("An unexpected error occurred. Please check the logs.") async def setup(bot: commands.Bot): # Ensure pools are initialized before adding the cog if getattr(bot, "pg_pool", None) is None or getattr(bot, "redis", None) is None: log.warning( "Bot pools not initialized before loading SettingsCog. Cog will not load." ) return # Prevent loading if pools are missing await bot.add_cog(SettingsCog(bot)) log.info("SettingsCog loaded.")