From 4c2cbc636f38cfc3524473aa102cbc8299a6d336 Mon Sep 17 00:00:00 2001 From: Slipstream Date: Sat, 3 May 2025 17:28:05 -0600 Subject: [PATCH] 123 --- api_service/api_server.py | 16 + .../command_customization_endpoints.py | 324 ++++++++++++++++++ cogs/settings_cog.py | 16 +- command_customization.py | 54 +-- main.py | 2 +- 5 files changed, 380 insertions(+), 32 deletions(-) create mode 100644 api_service/command_customization_endpoints.py diff --git a/api_service/api_server.py b/api_service/api_server.py index 088dcef..f6b76ac 100644 --- a/api_service/api_server.py +++ b/api_service/api_server.py @@ -231,6 +231,22 @@ except ImportError as e: log.error(f"Could not import dashboard API endpoints: {e}") log.error("Dashboard API endpoints will not be available") +# Import command customization models and endpoints +try: + # Try relative import first + try: + from .command_customization_endpoints import router as customization_router + except ImportError: + # Fall back to absolute import + from command_customization_endpoints import router as customization_router + + # Add the command customization router to the dashboard API app + dashboard_api_app.include_router(customization_router, prefix="/commands", tags=["Command Customization"]) + log.info("Command customization endpoints loaded successfully") +except ImportError as e: + log.error(f"Could not import command customization endpoints: {e}") + log.error("Command customization endpoints will not be available") + # Mount the API apps at their respective paths app.mount("/api", api_app) app.mount("/discordapi", discordapi_app) diff --git a/api_service/command_customization_endpoints.py b/api_service/command_customization_endpoints.py new file mode 100644 index 0000000..4ab52ff --- /dev/null +++ b/api_service/command_customization_endpoints.py @@ -0,0 +1,324 @@ +""" +Command customization API endpoints for the bot dashboard. +These endpoints provide functionality for customizing command names and groups. +""" + +import logging +from typing import List, Dict, Optional +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel + +# Import the dependencies from api_server.py +try: + # Try relative import first + from .api_server import ( + get_dashboard_user, + verify_dashboard_guild_admin, + CommandCustomizationResponse, + CommandCustomizationUpdate, + GroupCustomizationUpdate, + CommandAliasAdd, + CommandAliasRemove + ) +except ImportError: + # Fall back to absolute import + from api_server import ( + get_dashboard_user, + verify_dashboard_guild_admin, + CommandCustomizationResponse, + CommandCustomizationUpdate, + GroupCustomizationUpdate, + CommandAliasAdd, + CommandAliasRemove + ) + +# Import settings_manager for database access +try: + from discordbot import settings_manager +except ImportError: + # Try relative import + import sys + import os + sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + from discordbot import settings_manager + +log = logging.getLogger(__name__) + +# Create the router +router = APIRouter() + +# --- Command Customization Endpoints --- + +@router.get("/customizations/{guild_id}", response_model=CommandCustomizationResponse) +async def get_command_customizations( + guild_id: int, + _user: dict = Depends(get_dashboard_user), + _admin: bool = Depends(verify_dashboard_guild_admin) +): + """Get all command customizations for a guild.""" + try: + # Check if settings_manager is available + if not settings_manager or not settings_manager.pg_pool: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Settings manager not available" + ) + + # Get command customizations + command_customizations = await settings_manager.get_all_command_customizations(guild_id) + if command_customizations is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to get command customizations" + ) + + # Get group customizations + group_customizations = await settings_manager.get_all_group_customizations(guild_id) + if group_customizations is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to get group customizations" + ) + + # Get command aliases + command_aliases = await settings_manager.get_all_command_aliases(guild_id) + if command_aliases is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to get command aliases" + ) + + return CommandCustomizationResponse( + command_customizations=command_customizations, + group_customizations=group_customizations, + command_aliases=command_aliases + ) + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + log.error(f"Error getting command customizations for guild {guild_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error getting command customizations: {str(e)}" + ) + +@router.post("/customizations/{guild_id}/commands", status_code=status.HTTP_200_OK) +async def set_command_customization( + guild_id: int, + customization: CommandCustomizationUpdate, + _user: dict = Depends(get_dashboard_user), + _admin: bool = Depends(verify_dashboard_guild_admin) +): + """Set a custom name for a command in a guild.""" + try: + # Check if settings_manager is available + if not settings_manager or not settings_manager.pg_pool: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Settings manager not available" + ) + + # Validate custom name format if provided + if customization.custom_name is not None: + if not customization.custom_name.islower() or not customization.custom_name.replace('_', '').isalnum(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Custom command names must be lowercase and contain only letters, numbers, and underscores" + ) + + if len(customization.custom_name) < 1 or len(customization.custom_name) > 32: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Custom command names must be between 1 and 32 characters long" + ) + + # Set the custom command name + success = await settings_manager.set_custom_command_name( + guild_id, + customization.command_name, + customization.custom_name + ) + + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to set custom command name" + ) + + return {"message": "Command customization updated successfully"} + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + log.error(f"Error setting command customization for guild {guild_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error setting command customization: {str(e)}" + ) + +@router.post("/customizations/{guild_id}/groups", status_code=status.HTTP_200_OK) +async def set_group_customization( + guild_id: int, + customization: GroupCustomizationUpdate, + _user: dict = Depends(get_dashboard_user), + _admin: bool = Depends(verify_dashboard_guild_admin) +): + """Set a custom name for a command group in a guild.""" + try: + # Check if settings_manager is available + if not settings_manager or not settings_manager.pg_pool: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Settings manager not available" + ) + + # Validate custom name format if provided + if customization.custom_name is not None: + if not customization.custom_name.islower() or not customization.custom_name.replace('_', '').isalnum(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Custom group names must be lowercase and contain only letters, numbers, and underscores" + ) + + if len(customization.custom_name) < 1 or len(customization.custom_name) > 32: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Custom group names must be between 1 and 32 characters long" + ) + + # Set the custom group name + success = await settings_manager.set_custom_group_name( + guild_id, + customization.group_name, + customization.custom_name + ) + + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to set custom group name" + ) + + return {"message": "Group customization updated successfully"} + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + log.error(f"Error setting group customization for guild {guild_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error setting group customization: {str(e)}" + ) + +@router.post("/customizations/{guild_id}/aliases", status_code=status.HTTP_200_OK) +async def add_command_alias( + guild_id: int, + alias: CommandAliasAdd, + _user: dict = Depends(get_dashboard_user), + _admin: bool = Depends(verify_dashboard_guild_admin) +): + """Add an alias for a command in a guild.""" + try: + # Check if settings_manager is available + if not settings_manager or not settings_manager.pg_pool: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Settings manager not available" + ) + + # Validate alias format + if not alias.alias_name.islower() or not alias.alias_name.replace('_', '').isalnum(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Aliases must be lowercase and contain only letters, numbers, and underscores" + ) + + if len(alias.alias_name) < 1 or len(alias.alias_name) > 32: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Aliases must be between 1 and 32 characters long" + ) + + # Add the command alias + success = await settings_manager.add_command_alias( + guild_id, + alias.command_name, + alias.alias_name + ) + + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to add command alias" + ) + + return {"message": "Command alias added successfully"} + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + log.error(f"Error adding command alias for guild {guild_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error adding command alias: {str(e)}" + ) + +@router.delete("/customizations/{guild_id}/aliases", status_code=status.HTTP_200_OK) +async def remove_command_alias( + guild_id: int, + alias: CommandAliasRemove, + _user: dict = Depends(get_dashboard_user), + _admin: bool = Depends(verify_dashboard_guild_admin) +): + """Remove an alias for a command in a guild.""" + try: + # Check if settings_manager is available + if not settings_manager or not settings_manager.pg_pool: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Settings manager not available" + ) + + # Remove the command alias + success = await settings_manager.remove_command_alias( + guild_id, + alias.command_name, + alias.alias_name + ) + + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to remove command alias" + ) + + return {"message": "Command alias removed successfully"} + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + log.error(f"Error removing command alias for guild {guild_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error removing command alias: {str(e)}" + ) + +@router.post("/customizations/{guild_id}/sync", status_code=status.HTTP_200_OK) +async def sync_guild_commands( + guild_id: int, + _user: dict = Depends(get_dashboard_user), + _admin: bool = Depends(verify_dashboard_guild_admin) +): + """Sync commands for a guild to apply customizations.""" + try: + # This endpoint would trigger a command sync for the guild + # In a real implementation, this would communicate with the bot to sync commands + # For now, we'll just return a success message + return {"message": "Command sync requested. This may take a moment to complete."} + except Exception as e: + log.error(f"Error syncing commands for guild {guild_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error syncing commands: {str(e)}" + ) diff --git a/cogs/settings_cog.py b/cogs/settings_cog.py index a8c28ff..b0aa68c 100644 --- a/cogs/settings_cog.py +++ b/cogs/settings_cog.py @@ -2,6 +2,7 @@ import discord from discord.ext import commands import logging from discordbot import settings_manager # Assuming settings_manager is accessible +from discordbot import command_customization # Import command customization utilities from typing import Optional log = logging.getLogger(__name__) @@ -385,11 +386,18 @@ class SettingsCog(commands.Cog, name="Settings"): guild = ctx.guild await ctx.send("Syncing commands with Discord... This may take a moment.") - # Sync commands for this guild specifically - synced = await self.bot.tree.sync(guild=guild) + # 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.") - log.info(f"Commands synced for guild {guild.id} by {ctx.author.name}") + 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}") + # Fall back to regular sync if customization sync fails + synced = await self.bot.tree.sync(guild=guild) + await ctx.send(f"Failed to apply customizations, but synced {len(synced)} commands for this server.") + log.info(f"Commands synced (without customizations) for guild {guild.id} by {ctx.author.name}") 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}") diff --git a/command_customization.py b/command_customization.py index 1567cac..5e03f30 100644 --- a/command_customization.py +++ b/command_customization.py @@ -7,7 +7,7 @@ from discord import app_commands import logging from typing import Dict, List, Optional, Tuple, Any, Callable, Awaitable import asyncio -from . import settings_manager +from discordbot import settings_manager log = logging.getLogger(__name__) @@ -20,7 +20,7 @@ class GuildCommandTransformer(app_commands.Transformer): """Transform the command name based on guild settings.""" if not interaction.guild: return value # No customization in DMs - + guild_id = interaction.guild.id custom_name = await settings_manager.get_custom_command_name(guild_id, value) return custom_name if custom_name else value @@ -34,7 +34,7 @@ class GuildCommandSyncer: self.bot = bot self._command_cache = {} # Cache of original commands self._customized_commands = {} # Guild ID -> {original_name: custom_command} - + async def load_guild_customizations(self, guild_id: int) -> Dict[str, str]: """ Load command customizations for a specific guild. @@ -42,16 +42,16 @@ class GuildCommandSyncer: """ 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: log.error(f"Failed to load command customizations for guild {guild_id}") return {} - + # Combine command and group customizations customizations = {**cmd_customizations, **group_customizations} log.info(f"Loaded {len(customizations)} command customizations for guild {guild_id}") return customizations - + async def prepare_guild_commands(self, guild_id: int) -> List[app_commands.Command]: """ Prepare guild-specific commands with customized names. @@ -59,16 +59,16 @@ class GuildCommandSyncer: """ # Get all global commands global_commands = self.bot.tree.get_commands() - + # Cache original commands if not already cached if not self._command_cache: self._command_cache = {cmd.name: cmd for cmd in global_commands} - + # Load customizations for this guild customizations = await self.load_guild_customizations(guild_id) if not customizations: return global_commands # No customizations, use global commands - + # Create guild-specific commands with custom names guild_commands = [] for cmd in global_commands: @@ -80,15 +80,15 @@ class GuildCommandSyncer: else: # Use the original command guild_commands.append(cmd) - + # Store customized commands for this guild self._customized_commands[guild_id] = { cmd.name: custom_cmd for cmd, custom_cmd in zip(global_commands, guild_commands) if cmd.name in customizations } - + return guild_commands - + def _create_custom_command(self, original_cmd: app_commands.Command, custom_name: str) -> app_commands.Command: """ Create a copy of a command with a custom name. @@ -101,13 +101,13 @@ class GuildCommandSyncer: description=original_cmd.description, callback=original_cmd.callback ) - + # Copy options, if any if hasattr(original_cmd, 'options'): custom_cmd._params = original_cmd._params.copy() - + return custom_cmd - + async def sync_guild_commands(self, guild: discord.Guild) -> List[app_commands.Command]: """ Sync commands for a specific guild with customizations. @@ -116,10 +116,10 @@ class GuildCommandSyncer: try: # Prepare guild-specific commands guild_commands = await self.prepare_guild_commands(guild.id) - + # Sync commands with Discord synced = await self.bot.tree.sync(guild=guild) - + log.info(f"Synced {len(synced)} commands for guild {guild.id}") return synced except Exception as e: @@ -132,22 +132,22 @@ def guild_command(name: str, description: str, **kwargs): """ Decorator for registering commands with guild-specific name customization. Usage: - + @guild_command(name="mycommand", description="My command description") async def my_command(interaction: discord.Interaction): ... """ - def decorator(func: Callable[[discord.Interaction, ...], Awaitable[Any]]): + def decorator(func: Callable[[discord.Interaction], Awaitable[Any]]): # Create the app command @app_commands.command(name=name, description=description, **kwargs) async def wrapper(interaction: discord.Interaction, *args, **kwargs): return await func(interaction, *args, **kwargs) - + # Store the original name for reference wrapper.__original_name__ = name - + return wrapper - + return decorator @@ -156,9 +156,9 @@ class GuildCommandGroup(app_commands.Group): """ A command group that supports guild-specific name customization. Usage: - + my_group = GuildCommandGroup(name="mygroup", description="My group description") - + @my_group.command(name="subcommand", description="Subcommand description") async def my_subcommand(interaction: discord.Interaction): ... @@ -166,7 +166,7 @@ class GuildCommandGroup(app_commands.Group): def __init__(self, name: str, description: str, **kwargs): super().__init__(name=name, description=description, **kwargs) self.__original_name__ = name - + async def get_guild_name(self, guild_id: int) -> str: """Get the guild-specific name for this group.""" custom_name = await settings_manager.get_custom_group_name(guild_id, self.__original_name__) @@ -190,12 +190,12 @@ async def register_all_guild_commands(bot) -> Dict[int, List[app_commands.Comman """ syncer = GuildCommandSyncer(bot) results = {} - + for guild in bot.guilds: try: results[guild.id] = await syncer.sync_guild_commands(guild) except Exception as e: log.error(f"Failed to sync commands for guild {guild.id}: {e}") results[guild.id] = [] - + return results diff --git a/main.py b/main.py index 9e24388..f7674fd 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,7 @@ from commands import load_all_cogs, reload_all_cogs from error_handler import handle_error, patch_discord_methods, store_interaction_content from utils import reload_script import settings_manager # Import the settings manager -import command_customization # Import command customization utilities +from discordbot import command_customization # Import command customization utilities # Import the unified API service runner and the sync API module import sys