This commit is contained in:
Slipstream 2025-05-03 17:28:05 -06:00
parent fb2278e986
commit 4c2cbc636f
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
5 changed files with 380 additions and 32 deletions

View File

@ -231,6 +231,22 @@ except ImportError as e:
log.error(f"Could not import dashboard API endpoints: {e}") log.error(f"Could not import dashboard API endpoints: {e}")
log.error("Dashboard API endpoints will not be available") 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 # Mount the API apps at their respective paths
app.mount("/api", api_app) app.mount("/api", api_app)
app.mount("/discordapi", discordapi_app) app.mount("/discordapi", discordapi_app)

View File

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

View File

@ -2,6 +2,7 @@ import discord
from discord.ext import commands from discord.ext import commands
import logging import logging
from discordbot import settings_manager # Assuming settings_manager is accessible from discordbot import settings_manager # Assuming settings_manager is accessible
from discordbot import command_customization # Import command customization utilities
from typing import Optional from typing import Optional
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -385,11 +386,18 @@ class SettingsCog(commands.Cog, name="Settings"):
guild = ctx.guild guild = ctx.guild
await ctx.send("Syncing commands with Discord... This may take a moment.") await ctx.send("Syncing commands with Discord... This may take a moment.")
# Sync commands for this guild specifically # Use the command_customization module to sync commands with customizations
synced = await self.bot.tree.sync(guild=guild) try:
synced = await command_customization.register_guild_commands(self.bot, guild)
await ctx.send(f"Successfully synced {len(synced)} commands for this server.") await ctx.send(f"Successfully synced {len(synced)} commands for this server with customizations.")
log.info(f"Commands synced for guild {guild.id} by {ctx.author.name}") 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: except Exception as e:
await ctx.send(f"Failed to sync commands: {str(e)}") await ctx.send(f"Failed to sync commands: {str(e)}")
log.error(f"Failed to sync commands for guild {ctx.guild.id}: {e}") log.error(f"Failed to sync commands for guild {ctx.guild.id}: {e}")

View File

@ -7,7 +7,7 @@ from discord import app_commands
import logging import logging
from typing import Dict, List, Optional, Tuple, Any, Callable, Awaitable from typing import Dict, List, Optional, Tuple, Any, Callable, Awaitable
import asyncio import asyncio
from . import settings_manager from discordbot import settings_manager
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -20,7 +20,7 @@ class GuildCommandTransformer(app_commands.Transformer):
"""Transform the command name based on guild settings.""" """Transform the command name based on guild settings."""
if not interaction.guild: if not interaction.guild:
return value # No customization in DMs return value # No customization in DMs
guild_id = interaction.guild.id guild_id = interaction.guild.id
custom_name = await settings_manager.get_custom_command_name(guild_id, value) custom_name = await settings_manager.get_custom_command_name(guild_id, value)
return custom_name if custom_name else value return custom_name if custom_name else value
@ -34,7 +34,7 @@ class GuildCommandSyncer:
self.bot = bot self.bot = bot
self._command_cache = {} # Cache of original commands self._command_cache = {} # Cache of original commands
self._customized_commands = {} # Guild ID -> {original_name: custom_command} self._customized_commands = {} # Guild ID -> {original_name: custom_command}
async def load_guild_customizations(self, guild_id: int) -> Dict[str, str]: async def load_guild_customizations(self, guild_id: int) -> Dict[str, str]:
""" """
Load command customizations for a specific guild. Load command customizations for a specific guild.
@ -42,16 +42,16 @@ class GuildCommandSyncer:
""" """
cmd_customizations = await settings_manager.get_all_command_customizations(guild_id) cmd_customizations = await settings_manager.get_all_command_customizations(guild_id)
group_customizations = await settings_manager.get_all_group_customizations(guild_id) group_customizations = await settings_manager.get_all_group_customizations(guild_id)
if cmd_customizations is None or group_customizations is None: if cmd_customizations is None or group_customizations is None:
log.error(f"Failed to load command customizations for guild {guild_id}") log.error(f"Failed to load command customizations for guild {guild_id}")
return {} return {}
# Combine command and group customizations # Combine command and group customizations
customizations = {**cmd_customizations, **group_customizations} customizations = {**cmd_customizations, **group_customizations}
log.info(f"Loaded {len(customizations)} command customizations for guild {guild_id}") log.info(f"Loaded {len(customizations)} command customizations for guild {guild_id}")
return customizations return customizations
async def prepare_guild_commands(self, guild_id: int) -> List[app_commands.Command]: async def prepare_guild_commands(self, guild_id: int) -> List[app_commands.Command]:
""" """
Prepare guild-specific commands with customized names. Prepare guild-specific commands with customized names.
@ -59,16 +59,16 @@ class GuildCommandSyncer:
""" """
# Get all global commands # Get all global commands
global_commands = self.bot.tree.get_commands() global_commands = self.bot.tree.get_commands()
# Cache original commands if not already cached # Cache original commands if not already cached
if not self._command_cache: if not self._command_cache:
self._command_cache = {cmd.name: cmd for cmd in global_commands} self._command_cache = {cmd.name: cmd for cmd in global_commands}
# Load customizations for this guild # Load customizations for this guild
customizations = await self.load_guild_customizations(guild_id) customizations = await self.load_guild_customizations(guild_id)
if not customizations: if not customizations:
return global_commands # No customizations, use global commands return global_commands # No customizations, use global commands
# Create guild-specific commands with custom names # Create guild-specific commands with custom names
guild_commands = [] guild_commands = []
for cmd in global_commands: for cmd in global_commands:
@ -80,15 +80,15 @@ class GuildCommandSyncer:
else: else:
# Use the original command # Use the original command
guild_commands.append(cmd) guild_commands.append(cmd)
# Store customized commands for this guild # Store customized commands for this guild
self._customized_commands[guild_id] = { self._customized_commands[guild_id] = {
cmd.name: custom_cmd for cmd, custom_cmd in zip(global_commands, guild_commands) cmd.name: custom_cmd for cmd, custom_cmd in zip(global_commands, guild_commands)
if cmd.name in customizations if cmd.name in customizations
} }
return guild_commands return guild_commands
def _create_custom_command(self, original_cmd: app_commands.Command, custom_name: str) -> app_commands.Command: 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. Create a copy of a command with a custom name.
@ -101,13 +101,13 @@ class GuildCommandSyncer:
description=original_cmd.description, description=original_cmd.description,
callback=original_cmd.callback callback=original_cmd.callback
) )
# Copy options, if any # Copy options, if any
if hasattr(original_cmd, 'options'): if hasattr(original_cmd, 'options'):
custom_cmd._params = original_cmd._params.copy() custom_cmd._params = original_cmd._params.copy()
return custom_cmd return custom_cmd
async def sync_guild_commands(self, guild: discord.Guild) -> List[app_commands.Command]: async def sync_guild_commands(self, guild: discord.Guild) -> List[app_commands.Command]:
""" """
Sync commands for a specific guild with customizations. Sync commands for a specific guild with customizations.
@ -116,10 +116,10 @@ class GuildCommandSyncer:
try: try:
# Prepare guild-specific commands # Prepare guild-specific commands
guild_commands = await self.prepare_guild_commands(guild.id) guild_commands = await self.prepare_guild_commands(guild.id)
# Sync commands with Discord # Sync commands with Discord
synced = await self.bot.tree.sync(guild=guild) synced = await self.bot.tree.sync(guild=guild)
log.info(f"Synced {len(synced)} commands for guild {guild.id}") log.info(f"Synced {len(synced)} commands for guild {guild.id}")
return synced return synced
except Exception as e: 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. Decorator for registering commands with guild-specific name customization.
Usage: Usage:
@guild_command(name="mycommand", description="My command description") @guild_command(name="mycommand", description="My command description")
async def my_command(interaction: discord.Interaction): 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 # Create the app command
@app_commands.command(name=name, description=description, **kwargs) @app_commands.command(name=name, description=description, **kwargs)
async def wrapper(interaction: discord.Interaction, *args, **kwargs): async def wrapper(interaction: discord.Interaction, *args, **kwargs):
return await func(interaction, *args, **kwargs) return await func(interaction, *args, **kwargs)
# Store the original name for reference # Store the original name for reference
wrapper.__original_name__ = name wrapper.__original_name__ = name
return wrapper return wrapper
return decorator return decorator
@ -156,9 +156,9 @@ class GuildCommandGroup(app_commands.Group):
""" """
A command group that supports guild-specific name customization. A command group that supports guild-specific name customization.
Usage: Usage:
my_group = GuildCommandGroup(name="mygroup", description="My group description") my_group = GuildCommandGroup(name="mygroup", description="My group description")
@my_group.command(name="subcommand", description="Subcommand description") @my_group.command(name="subcommand", description="Subcommand description")
async def my_subcommand(interaction: discord.Interaction): async def my_subcommand(interaction: discord.Interaction):
... ...
@ -166,7 +166,7 @@ class GuildCommandGroup(app_commands.Group):
def __init__(self, name: str, description: str, **kwargs): def __init__(self, name: str, description: str, **kwargs):
super().__init__(name=name, description=description, **kwargs) super().__init__(name=name, description=description, **kwargs)
self.__original_name__ = name self.__original_name__ = name
async def get_guild_name(self, guild_id: int) -> str: async def get_guild_name(self, guild_id: int) -> str:
"""Get the guild-specific name for this group.""" """Get the guild-specific name for this group."""
custom_name = await settings_manager.get_custom_group_name(guild_id, self.__original_name__) 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) syncer = GuildCommandSyncer(bot)
results = {} results = {}
for guild in bot.guilds: for guild in bot.guilds:
try: try:
results[guild.id] = await syncer.sync_guild_commands(guild) results[guild.id] = await syncer.sync_guild_commands(guild)
except Exception as e: except Exception as e:
log.error(f"Failed to sync commands for guild {guild.id}: {e}") log.error(f"Failed to sync commands for guild {guild.id}: {e}")
results[guild.id] = [] results[guild.id] = []
return results return results

View File

@ -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 error_handler import handle_error, patch_discord_methods, store_interaction_content
from utils import reload_script from utils import reload_script
import settings_manager # Import the settings manager 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 the unified API service runner and the sync API module
import sys import sys