This commit is contained in:
Slipstream 2025-05-04 13:52:43 -06:00
parent 0c4f00d747
commit 4b50898664
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
18 changed files with 3417 additions and 55 deletions

View File

@ -18,7 +18,7 @@ class Conversation(BaseModel):
messages: List[Message] = [] messages: List[Message] = []
created_at: datetime.datetime = Field(default_factory=datetime.datetime.now) created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now) updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
# Conversation-specific settings # Conversation-specific settings
model_id: str = "openai/gpt-3.5-turbo" model_id: str = "openai/gpt-3.5-turbo"
reasoning_enabled: bool = False reasoning_enabled: bool = False
@ -28,6 +28,15 @@ class Conversation(BaseModel):
web_search_enabled: bool = False web_search_enabled: bool = False
system_message: Optional[str] = None system_message: Optional[str] = None
class ThemeSettings(BaseModel):
"""Theme settings for the dashboard UI"""
theme_mode: str = "light" # "light", "dark", "custom"
primary_color: str = "#5865F2" # Discord blue
secondary_color: str = "#2D3748"
accent_color: str = "#7289DA"
font_family: str = "Inter, sans-serif"
custom_css: Optional[str] = None
class UserSettings(BaseModel): class UserSettings(BaseModel):
# General settings # General settings
model_id: str = "openai/gpt-3.5-turbo" model_id: str = "openai/gpt-3.5-turbo"
@ -53,7 +62,10 @@ class UserSettings(BaseModel):
# UI settings # UI settings
advanced_view_enabled: bool = False advanced_view_enabled: bool = False
streaming_enabled: bool = True streaming_enabled: bool = True
# Theme settings
theme: ThemeSettings = Field(default_factory=ThemeSettings)
# Last updated timestamp # Last updated timestamp
last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now) last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now)

View File

@ -691,14 +691,19 @@ class CommandPermission(BaseModel):
class CommandPermissionsResponse(BaseModel): class CommandPermissionsResponse(BaseModel):
permissions: Dict[str, List[str]] # Command name -> List of allowed role IDs permissions: Dict[str, List[str]] # Command name -> List of allowed role IDs
class CommandCustomizationDetail(BaseModel):
name: str
description: Optional[str] = None
class CommandCustomizationResponse(BaseModel): class CommandCustomizationResponse(BaseModel):
command_customizations: Dict[str, str] = {} # Original command name -> Custom command name command_customizations: Dict[str, Dict[str, Optional[str]]] = {} # Original command name -> {name, description}
group_customizations: Dict[str, str] = {} # Original group name -> Custom group name group_customizations: Dict[str, str] = {} # Original group name -> Custom group name
command_aliases: Dict[str, List[str]] = {} # Original command name -> List of aliases command_aliases: Dict[str, List[str]] = {} # Original command name -> List of aliases
class CommandCustomizationUpdate(BaseModel): class CommandCustomizationUpdate(BaseModel):
command_name: str command_name: str
custom_name: Optional[str] = None # If None, removes customization custom_name: Optional[str] = None # If None, removes customization
custom_description: Optional[str] = None # If None, keeps existing or no description
class GroupCustomizationUpdate(BaseModel): class GroupCustomizationUpdate(BaseModel):
group_name: str group_name: str

View File

@ -12,7 +12,7 @@ from pydantic import BaseModel
try: try:
# Try relative import first # Try relative import first
from .api_server import ( from .api_server import (
get_dashboard_user, get_dashboard_user,
verify_dashboard_guild_admin, verify_dashboard_guild_admin,
CommandCustomizationResponse, CommandCustomizationResponse,
CommandCustomizationUpdate, CommandCustomizationUpdate,
@ -23,7 +23,7 @@ try:
except ImportError: except ImportError:
# Fall back to absolute import # Fall back to absolute import
from api_server import ( from api_server import (
get_dashboard_user, get_dashboard_user,
verify_dashboard_guild_admin, verify_dashboard_guild_admin,
CommandCustomizationResponse, CommandCustomizationResponse,
CommandCustomizationUpdate, CommandCustomizationUpdate,
@ -63,7 +63,7 @@ async def get_command_customizations(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager not available" detail="Settings manager not available"
) )
# Get command customizations # Get command customizations
command_customizations = await settings_manager.get_all_command_customizations(guild_id) command_customizations = await settings_manager.get_all_command_customizations(guild_id)
if command_customizations is None: if command_customizations is None:
@ -71,7 +71,7 @@ async def get_command_customizations(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get command customizations" detail="Failed to get command customizations"
) )
# Get group customizations # Get group customizations
group_customizations = await settings_manager.get_all_group_customizations(guild_id) group_customizations = await settings_manager.get_all_group_customizations(guild_id)
if group_customizations is None: if group_customizations is None:
@ -79,7 +79,7 @@ async def get_command_customizations(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get group customizations" detail="Failed to get group customizations"
) )
# Get command aliases # Get command aliases
command_aliases = await settings_manager.get_all_command_aliases(guild_id) command_aliases = await settings_manager.get_all_command_aliases(guild_id)
if command_aliases is None: if command_aliases is None:
@ -87,9 +87,17 @@ async def get_command_customizations(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get command aliases" detail="Failed to get command aliases"
) )
# Convert command_customizations to the new format
formatted_command_customizations = {}
for cmd_name, cmd_data in command_customizations.items():
formatted_command_customizations[cmd_name] = {
'name': cmd_data.get('name', cmd_name),
'description': cmd_data.get('description')
}
return CommandCustomizationResponse( return CommandCustomizationResponse(
command_customizations=command_customizations, command_customizations=formatted_command_customizations,
group_customizations=group_customizations, group_customizations=group_customizations,
command_aliases=command_aliases command_aliases=command_aliases
) )
@ -110,7 +118,7 @@ async def set_command_customization(
_user: dict = Depends(get_dashboard_user), _user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin) _admin: bool = Depends(verify_dashboard_guild_admin)
): ):
"""Set a custom name for a command in a guild.""" """Set a custom name and/or description for a command in a guild."""
try: try:
# Check if settings_manager is available # Check if settings_manager is available
if not settings_manager or not settings_manager.pg_pool: if not settings_manager or not settings_manager.pg_pool:
@ -118,7 +126,7 @@ async def set_command_customization(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager not available" detail="Settings manager not available"
) )
# Validate custom name format if provided # Validate custom name format if provided
if customization.custom_name is not None: if customization.custom_name is not None:
if not customization.custom_name.islower() or not customization.custom_name.replace('_', '').isalnum(): if not customization.custom_name.islower() or not customization.custom_name.replace('_', '').isalnum():
@ -126,26 +134,48 @@ async def set_command_customization(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Custom command names must be lowercase and contain only letters, numbers, and underscores" 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: if len(customization.custom_name) < 1 or len(customization.custom_name) > 32:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Custom command names must be between 1 and 32 characters long" detail="Custom command names must be between 1 and 32 characters long"
) )
# Validate custom description if provided
if customization.custom_description is not None:
if len(customization.custom_description) > 100:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Custom command descriptions must be 100 characters or less"
)
# Set the custom command name # Set the custom command name
success = await settings_manager.set_custom_command_name( name_success = await settings_manager.set_custom_command_name(
guild_id, guild_id,
customization.command_name, customization.command_name,
customization.custom_name customization.custom_name
) )
if not success: if not name_success:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to set custom command name" detail="Failed to set custom command name"
) )
# Set the custom command description if provided
if customization.custom_description is not None:
desc_success = await settings_manager.set_custom_command_description(
guild_id,
customization.command_name,
customization.custom_description
)
if not desc_success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to set custom command description"
)
return {"message": "Command customization updated successfully"} return {"message": "Command customization updated successfully"}
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions # Re-raise HTTP exceptions
@ -172,7 +202,7 @@ async def set_group_customization(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager not available" detail="Settings manager not available"
) )
# Validate custom name format if provided # Validate custom name format if provided
if customization.custom_name is not None: if customization.custom_name is not None:
if not customization.custom_name.islower() or not customization.custom_name.replace('_', '').isalnum(): if not customization.custom_name.islower() or not customization.custom_name.replace('_', '').isalnum():
@ -180,26 +210,26 @@ async def set_group_customization(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Custom group names must be lowercase and contain only letters, numbers, and underscores" 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: if len(customization.custom_name) < 1 or len(customization.custom_name) > 32:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Custom group names must be between 1 and 32 characters long" detail="Custom group names must be between 1 and 32 characters long"
) )
# Set the custom group name # Set the custom group name
success = await settings_manager.set_custom_group_name( success = await settings_manager.set_custom_group_name(
guild_id, guild_id,
customization.group_name, customization.group_name,
customization.custom_name customization.custom_name
) )
if not success: if not success:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to set custom group name" detail="Failed to set custom group name"
) )
return {"message": "Group customization updated successfully"} return {"message": "Group customization updated successfully"}
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions # Re-raise HTTP exceptions
@ -226,33 +256,33 @@ async def add_command_alias(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager not available" detail="Settings manager not available"
) )
# Validate alias format # Validate alias format
if not alias.alias_name.islower() or not alias.alias_name.replace('_', '').isalnum(): if not alias.alias_name.islower() or not alias.alias_name.replace('_', '').isalnum():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Aliases must be lowercase and contain only letters, numbers, and underscores" detail="Aliases must be lowercase and contain only letters, numbers, and underscores"
) )
if len(alias.alias_name) < 1 or len(alias.alias_name) > 32: if len(alias.alias_name) < 1 or len(alias.alias_name) > 32:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Aliases must be between 1 and 32 characters long" detail="Aliases must be between 1 and 32 characters long"
) )
# Add the command alias # Add the command alias
success = await settings_manager.add_command_alias( success = await settings_manager.add_command_alias(
guild_id, guild_id,
alias.command_name, alias.command_name,
alias.alias_name alias.alias_name
) )
if not success: if not success:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add command alias" detail="Failed to add command alias"
) )
return {"message": "Command alias added successfully"} return {"message": "Command alias added successfully"}
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions # Re-raise HTTP exceptions
@ -279,20 +309,20 @@ async def remove_command_alias(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings manager not available" detail="Settings manager not available"
) )
# Remove the command alias # Remove the command alias
success = await settings_manager.remove_command_alias( success = await settings_manager.remove_command_alias(
guild_id, guild_id,
alias.command_name, alias.command_name,
alias.alias_name alias.alias_name
) )
if not success: if not success:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to remove command alias" detail="Failed to remove command alias"
) )
return {"message": "Command alias removed successfully"} return {"message": "Command alias removed successfully"}
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions # Re-raise HTTP exceptions

View File

@ -8,6 +8,9 @@ from typing import List, Dict, Optional, Any
from fastapi import APIRouter, Depends, HTTPException, status, Body from fastapi import APIRouter, Depends, HTTPException, status, Body
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
# Default prefix for commands
DEFAULT_PREFIX = "!"
# Import the dependencies from api_server.py # Import the dependencies from api_server.py
try: try:
# Try relative import first # Try relative import first
@ -78,6 +81,14 @@ class Message(BaseModel):
role: str # 'user' or 'assistant' role: str # 'user' or 'assistant'
created_at: str created_at: str
class ThemeSettings(BaseModel):
theme_mode: str = "light" # "light", "dark", "custom"
primary_color: str = "#5865F2" # Discord blue
secondary_color: str = "#2D3748"
accent_color: str = "#7289DA"
font_family: str = "Inter, sans-serif"
custom_css: Optional[str] = None
class GlobalSettings(BaseModel): class GlobalSettings(BaseModel):
system_message: Optional[str] = None system_message: Optional[str] = None
character: Optional[str] = None character: Optional[str] = None
@ -86,6 +97,19 @@ class GlobalSettings(BaseModel):
model: Optional[str] = None model: Optional[str] = None
temperature: Optional[float] = None temperature: Optional[float] = None
max_tokens: Optional[int] = None max_tokens: Optional[int] = None
theme: Optional[ThemeSettings] = None
class CogInfo(BaseModel):
name: str
description: Optional[str] = None
enabled: bool = True
commands: List[Dict[str, Any]] = []
class CommandInfo(BaseModel):
name: str
description: Optional[str] = None
enabled: bool = True
cog_name: Optional[str] = None
# --- Endpoints --- # --- Endpoints ---
@router.get("/guilds/{guild_id}/channels", response_model=List[Channel]) @router.get("/guilds/{guild_id}/channels", response_model=List[Channel])
@ -424,6 +448,58 @@ async def remove_command_alias(
detail=f"Error removing command alias: {str(e)}" detail=f"Error removing command alias: {str(e)}"
) )
@router.get("/guilds/{guild_id}/settings", response_model=Dict[str, Any])
async def get_guild_settings(
guild_id: int,
_user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Get settings 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 prefix
prefix = await settings_manager.get_guild_prefix(guild_id, DEFAULT_PREFIX)
# Get welcome/goodbye settings
welcome_channel_id = await settings_manager.get_setting(guild_id, 'welcome_channel_id')
welcome_message = await settings_manager.get_setting(guild_id, 'welcome_message')
goodbye_channel_id = await settings_manager.get_setting(guild_id, 'goodbye_channel_id')
goodbye_message = await settings_manager.get_setting(guild_id, 'goodbye_message')
# Get cog enabled statuses
cogs_enabled = await settings_manager.get_all_enabled_cogs(guild_id)
# Get command enabled statuses
commands_enabled = await settings_manager.get_all_enabled_commands(guild_id)
# Construct response
settings = {
"prefix": prefix,
"welcome_channel_id": welcome_channel_id,
"welcome_message": welcome_message,
"goodbye_channel_id": goodbye_channel_id,
"goodbye_message": goodbye_message,
"cogs": cogs_enabled,
"commands": commands_enabled
}
return settings
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error getting settings for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting settings: {str(e)}"
)
@router.patch("/guilds/{guild_id}/settings", status_code=status.HTTP_200_OK) @router.patch("/guilds/{guild_id}/settings", status_code=status.HTTP_200_OK)
async def update_guild_settings( async def update_guild_settings(
guild_id: int, guild_id: int,
@ -444,7 +520,14 @@ async def update_guild_settings(
log.debug(f"Update data received: {settings_update}") log.debug(f"Update data received: {settings_update}")
success_flags = [] success_flags = []
core_cogs_list = {'SettingsCog', 'HelpCog'} # Core cogs that cannot be disabled
# Get bot instance for core cogs check
try:
from discordbot import discord_bot_sync_api
bot = discord_bot_sync_api.bot_instance
core_cogs_list = bot.core_cogs if bot and hasattr(bot, 'core_cogs') else {'SettingsCog', 'HelpCog'}
except ImportError:
core_cogs_list = {'SettingsCog', 'HelpCog'} # Core cogs that cannot be disabled
# Update prefix if provided # Update prefix if provided
if 'prefix' in settings_update: if 'prefix' in settings_update:
@ -494,6 +577,14 @@ async def update_guild_settings(
else: else:
log.warning(f"Attempted to change status of core cog '{cog_name}' for guild {guild_id} - ignored.") log.warning(f"Attempted to change status of core cog '{cog_name}' for guild {guild_id} - ignored.")
# Update commands if provided
if 'commands' in settings_update and isinstance(settings_update['commands'], dict):
for command_name, enabled_status in settings_update['commands'].items():
success = await settings_manager.set_command_enabled(guild_id, command_name, enabled_status)
success_flags.append(success)
if not success:
log.error(f"Failed to update status for command '{command_name}' for guild {guild_id}")
if all(s is True for s in success_flags): # Check if all operations returned True if all(s is True for s in success_flags): # Check if all operations returned True
return {"message": "Settings updated successfully."} return {"message": "Settings updated successfully."}
else: else:
@ -628,7 +719,7 @@ async def get_global_settings(
) )
# Convert from UserSettings to GlobalSettings # Convert from UserSettings to GlobalSettings
return GlobalSettings( global_settings = GlobalSettings(
system_message=user_settings.get("system_message", ""), system_message=user_settings.get("system_message", ""),
character=user_settings.get("character", ""), character=user_settings.get("character", ""),
character_info=user_settings.get("character_info", ""), character_info=user_settings.get("character_info", ""),
@ -637,6 +728,20 @@ async def get_global_settings(
temperature=user_settings.get("temperature", 0.7), temperature=user_settings.get("temperature", 0.7),
max_tokens=user_settings.get("max_tokens", 1000) max_tokens=user_settings.get("max_tokens", 1000)
) )
# Add theme settings if available
if "theme" in user_settings:
theme_data = user_settings["theme"]
global_settings.theme = ThemeSettings(
theme_mode=theme_data.get("theme_mode", "light"),
primary_color=theme_data.get("primary_color", "#5865F2"),
secondary_color=theme_data.get("secondary_color", "#2D3748"),
accent_color=theme_data.get("accent_color", "#7289DA"),
font_family=theme_data.get("font_family", "Inter, sans-serif"),
custom_css=theme_data.get("custom_css")
)
return global_settings
except HTTPException: except HTTPException:
# Re-raise HTTP exceptions # Re-raise HTTP exceptions
raise raise
@ -688,6 +793,18 @@ async def update_global_settings(
custom_instructions=settings.custom_instructions custom_instructions=settings.custom_instructions
) )
# Add theme settings if provided
if settings.theme:
from discordbot.api_service.api_models import ThemeSettings as ApiThemeSettings
user_settings.theme = ApiThemeSettings(
theme_mode=settings.theme.theme_mode,
primary_color=settings.theme.primary_color,
secondary_color=settings.theme.secondary_color,
accent_color=settings.theme.accent_color,
font_family=settings.theme.font_family,
custom_css=settings.theme.custom_css
)
# Save user settings to the database # Save user settings to the database
updated_settings = db.save_user_settings(user_id, user_settings) updated_settings = db.save_user_settings(user_id, user_settings)
if not updated_settings: if not updated_settings:
@ -708,6 +825,200 @@ async def update_global_settings(
detail=f"Error updating global settings: {str(e)}" detail=f"Error updating global settings: {str(e)}"
) )
# --- Cog and Command Management Endpoints ---
@router.get("/guilds/{guild_id}/cogs", response_model=List[CogInfo])
async def get_guild_cogs(
guild_id: int,
_user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Get all cogs and their commands for a guild."""
try:
# Check if bot instance is available via discord_bot_sync_api
try:
from discordbot import discord_bot_sync_api
bot = discord_bot_sync_api.bot_instance
if not bot:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot instance not available"
)
except ImportError:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot sync API not available"
)
# Get all cogs from the bot
cogs_list = []
for cog_name, cog in bot.cogs.items():
# Get enabled status from settings_manager
is_enabled = await settings_manager.is_cog_enabled(guild_id, cog_name, default_enabled=True)
# Get commands for this cog
commands_list = []
for command in cog.get_commands():
# Get command enabled status
cmd_enabled = await settings_manager.is_command_enabled(guild_id, command.qualified_name, default_enabled=True)
commands_list.append({
"name": command.qualified_name,
"description": command.help or "No description available",
"enabled": cmd_enabled
})
# Add slash commands if any
app_commands = [cmd for cmd in bot.tree.get_commands() if hasattr(cmd, 'cog') and cmd.cog and cmd.cog.qualified_name == cog_name]
for cmd in app_commands:
# Get command enabled status
cmd_enabled = await settings_manager.is_command_enabled(guild_id, cmd.name, default_enabled=True)
if not any(c["name"] == cmd.name for c in commands_list): # Avoid duplicates
commands_list.append({
"name": cmd.name,
"description": cmd.description or "No description available",
"enabled": cmd_enabled
})
cogs_list.append(CogInfo(
name=cog_name,
description=cog.__doc__ or "No description available",
enabled=is_enabled,
commands=commands_list
))
return cogs_list
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error getting cogs for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting cogs: {str(e)}"
)
@router.patch("/guilds/{guild_id}/cogs/{cog_name}", status_code=status.HTTP_200_OK)
async def update_cog_status(
guild_id: int,
cog_name: str,
enabled: bool = Body(..., embed=True),
_user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Enable or disable a cog 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"
)
# Check if the cog exists
try:
from discordbot import discord_bot_sync_api
bot = discord_bot_sync_api.bot_instance
if not bot:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot instance not available"
)
if cog_name not in bot.cogs:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Cog '{cog_name}' not found"
)
# Check if it's a core cog
if cog_name in bot.core_cogs:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Core cog '{cog_name}' cannot be disabled"
)
except ImportError:
# If we can't import the bot, we'll just assume the cog exists
log.warning("Bot sync API not available, skipping cog existence check")
# Update the cog enabled status
success = await settings_manager.set_cog_enabled(guild_id, cog_name, enabled)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update cog '{cog_name}' status"
)
return {"message": f"Cog '{cog_name}' {'enabled' if enabled else 'disabled'} successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error updating cog status for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating cog status: {str(e)}"
)
@router.patch("/guilds/{guild_id}/commands/{command_name}", status_code=status.HTTP_200_OK)
async def update_command_status(
guild_id: int,
command_name: str,
enabled: bool = Body(..., embed=True),
_user: dict = Depends(get_dashboard_user),
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Enable or disable a command 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"
)
# Check if the command exists
try:
from discordbot import discord_bot_sync_api
bot = discord_bot_sync_api.bot_instance
if not bot:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Bot instance not available"
)
# Check if it's a prefix command
command = bot.get_command(command_name)
if not command:
# Check if it's an app command
app_commands = [cmd for cmd in bot.tree.get_commands() if cmd.name == command_name]
if not app_commands:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Command '{command_name}' not found"
)
except ImportError:
# If we can't import the bot, we'll just assume the command exists
log.warning("Bot sync API not available, skipping command existence check")
# Update the command enabled status
success = await settings_manager.set_command_enabled(guild_id, command_name, enabled)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update command '{command_name}' status"
)
return {"message": f"Command '{command_name}' {'enabled' if enabled else 'disabled'} successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error updating command status for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating command status: {str(e)}"
)
# --- Conversations Endpoints --- # --- Conversations Endpoints ---
@router.get("/conversations", response_model=List[Conversation]) @router.get("/conversations", response_model=List[Conversation])

View File

@ -0,0 +1,60 @@
<!-- Cog Management Section -->
<div id="cog-management-section" class="dashboard-section" style="display: none;">
<div class="card">
<div class="card-header">
<h2 class="card-title">Manage Cogs & Commands</h2>
</div>
<div class="form-group">
<label for="cog-guild-select">Select Server:</label>
<select name="guilds" id="cog-guild-select" class="w-full">
<option value="">--Please choose a server--</option>
</select>
</div>
</div>
<div id="cog-management-loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Loading cogs and commands...</p>
</div>
<div id="cog-management-content" style="display: none;">
<div class="card">
<div class="card-header">
<h3 class="card-title">Cogs (Modules)</h3>
<p class="text-sm text-muted">Enable or disable entire modules of functionality</p>
</div>
<div class="cogs-list-container p-4">
<div id="cogs-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Cogs will be populated here -->
</div>
</div>
<div class="btn-group mt-4">
<button id="save-cogs-button" class="btn btn-primary">Save Cog Settings</button>
</div>
<p id="cogs-feedback" class="mt-2"></p>
</div>
<div class="card mt-6">
<div class="card-header">
<h3 class="card-title">Commands</h3>
<p class="text-sm text-muted">Enable or disable individual commands</p>
</div>
<div class="form-group">
<label for="cog-filter">Filter by Cog:</label>
<select id="cog-filter" class="w-full">
<option value="all">All Cogs</option>
<!-- Cog options will be populated here -->
</select>
</div>
<div class="commands-list-container p-4">
<div id="commands-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Commands will be populated here -->
</div>
</div>
<div class="btn-group mt-4">
<button id="save-commands-button" class="btn btn-primary">Save Command Settings</button>
</div>
<p id="commands-feedback" class="mt-2"></p>
</div>
</div>
</div>

View File

@ -0,0 +1,161 @@
<!-- Command Customization Section -->
<div id="command-customization-section" class="dashboard-section" style="display: none;">
<div class="card">
<div class="card-header">
<h2 class="card-title">Command Customization</h2>
</div>
</div>
<div id="command-customization-form">
<!-- Command Customization Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Customize Commands</h3>
<p class="text-muted">Customize the names and descriptions of commands for your server.</p>
</div>
<div class="form-group">
<div class="search-container">
<input type="text" id="command-search" placeholder="Search commands..." class="w-full">
</div>
</div>
<div id="command-list" class="command-list">
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<!-- Command Group Customization Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Customize Command Groups</h3>
<p class="text-muted">Customize the names of command groups for your server.</p>
</div>
<div id="group-list" class="command-list">
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<!-- Command Aliases Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Command Aliases</h3>
<p class="text-muted">Add alternative names for commands.</p>
</div>
<div id="alias-list" class="command-list">
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
<div class="form-group mt-4">
<h4>Add New Alias</h4>
<div class="flex-row">
<div class="flex-col mr-2">
<label for="alias-command-select">Command:</label>
<select id="alias-command-select" class="w-full">
<option value="">Select a command</option>
</select>
</div>
<div class="flex-col">
<label for="alias-name-input">Alias:</label>
<input type="text" id="alias-name-input" placeholder="Enter alias name" class="w-full">
</div>
</div>
<button id="add-alias-button" class="btn btn-primary mt-2">Add Alias</button>
<p id="alias-feedback" class="mt-2"></p>
</div>
</div>
<!-- Sync Commands Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Sync Commands</h3>
<p class="text-muted">Sync command customizations to Discord.</p>
</div>
<div class="form-group">
<p>After making changes to command names, descriptions, or aliases, you need to sync the changes to Discord.</p>
<button id="sync-commands-button" class="btn btn-primary">Sync Commands</button>
<p id="sync-feedback" class="mt-2"></p>
</div>
</div>
</div>
</div>
<!-- Command Customization Template -->
<template id="command-item-template">
<div class="command-item">
<div class="command-header">
<h4 class="command-name"></h4>
<div class="command-actions">
<button class="btn btn-sm btn-primary edit-command-btn">Edit</button>
<button class="btn btn-sm btn-warning reset-command-btn">Reset</button>
</div>
</div>
<div class="command-details">
<p class="command-description"></p>
<div class="command-customization" style="display: none;">
<div class="form-group">
<label>Custom Name:</label>
<input type="text" class="custom-command-name w-full" placeholder="Enter custom name">
</div>
<div class="form-group">
<label>Custom Description:</label>
<input type="text" class="custom-command-description w-full" placeholder="Enter custom description">
</div>
<div class="btn-group">
<button class="btn btn-sm btn-primary save-command-btn">Save</button>
<button class="btn btn-sm btn-secondary cancel-command-btn">Cancel</button>
</div>
</div>
</div>
</div>
</template>
<!-- Group Customization Template -->
<template id="group-item-template">
<div class="command-item">
<div class="command-header">
<h4 class="group-name"></h4>
<div class="command-actions">
<button class="btn btn-sm btn-primary edit-group-btn">Edit</button>
<button class="btn btn-sm btn-warning reset-group-btn">Reset</button>
</div>
</div>
<div class="group-details">
<div class="group-customization" style="display: none;">
<div class="form-group">
<label>Custom Name:</label>
<input type="text" class="custom-group-name w-full" placeholder="Enter custom name">
</div>
<div class="btn-group">
<button class="btn btn-sm btn-primary save-group-btn">Save</button>
<button class="btn btn-sm btn-secondary cancel-group-btn">Cancel</button>
</div>
</div>
</div>
</div>
</template>
<!-- Alias Item Template -->
<template id="alias-item-template">
<div class="alias-item">
<div class="alias-header">
<h4 class="command-name"></h4>
</div>
<div class="alias-list">
<ul class="alias-tags">
<!-- Alias tags will be added here -->
</ul>
</div>
</div>
</template>
<!-- Alias Tag Template -->
<template id="alias-tag-template">
<li class="alias-tag">
<span class="alias-name"></span>
<button class="remove-alias-btn">×</button>
</li>
</template>

View File

@ -0,0 +1,115 @@
/* Cog Management Styles */
.cog-card, .command-card {
background-color: var(--card-bg);
border-color: var(--border-color);
transition: all 0.2s ease;
}
.cog-card:hover, .command-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.cog-badge {
background-color: var(--primary-color-light);
color: var(--primary-color-dark);
}
.command-count {
background-color: var(--secondary-color-light);
color: var(--secondary-color-dark);
}
.cogs-list-container, .commands-list-container {
max-height: 500px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 0.25rem;
}
/* Loading container */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid var(--primary-color);
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Checkbox styling */
input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(--border-color);
border-radius: 0.25rem;
background-color: var(--card-bg);
display: inline-block;
position: relative;
margin-right: 0.5rem;
vertical-align: middle;
cursor: pointer;
}
input[type="checkbox"]:checked {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
input[type="checkbox"]:checked::after {
content: "";
position: absolute;
left: 0.3rem;
top: 0.1rem;
width: 0.5rem;
height: 0.8rem;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
input[type="checkbox"]:disabled {
background-color: var(--disabled-bg);
border-color: var(--disabled-border);
cursor: not-allowed;
}
input[type="checkbox"]:disabled:checked {
background-color: var(--disabled-checked-bg);
}
input[type="checkbox"]:disabled:checked::after {
border-color: var(--disabled-checked-color);
}
/* Grid layout for larger screens */
@media (min-width: 768px) {
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Feedback messages */
.text-green-600 {
color: #059669;
}
.text-red-600 {
color: #dc2626;
}

View File

@ -0,0 +1,162 @@
/* Command Customization CSS */
.command-list {
margin-top: var(--spacing-4);
}
.command-item {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-3);
overflow: hidden;
}
.command-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-3) var(--spacing-4);
background-color: var(--light-bg);
border-bottom: 1px solid var(--border-color);
}
.command-name {
margin: 0;
font-weight: 600;
}
.command-actions {
display: flex;
gap: var(--spacing-2);
}
.command-details, .group-details {
padding: var(--spacing-4);
}
.command-description {
margin-top: 0;
margin-bottom: var(--spacing-3);
color: var(--text-secondary);
}
.command-customization, .group-customization {
margin-top: var(--spacing-3);
padding-top: var(--spacing-3);
border-top: 1px solid var(--border-color);
}
/* Alias styles */
.alias-item {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-3);
overflow: hidden;
}
.alias-header {
padding: var(--spacing-3) var(--spacing-4);
background-color: var(--light-bg);
border-bottom: 1px solid var(--border-color);
}
.alias-list {
padding: var(--spacing-3) var(--spacing-4);
}
.alias-tags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-2);
list-style: none;
padding: 0;
margin: 0;
}
.alias-tag {
display: inline-flex;
align-items: center;
background-color: var(--primary-color);
color: white;
padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius-sm);
font-size: 0.9rem;
}
.alias-name {
margin-right: var(--spacing-1);
}
.remove-alias-btn {
background: none;
border: none;
color: white;
font-size: 1.2rem;
line-height: 1;
padding: 0;
cursor: pointer;
opacity: 0.7;
}
.remove-alias-btn:hover {
opacity: 1;
}
/* Search container */
.search-container {
margin-bottom: var(--spacing-4);
}
#command-search {
padding: var(--spacing-2) var(--spacing-3);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
width: 100%;
}
/* Dark mode styles */
body.dark-mode .command-header,
body.dark-mode .alias-header {
background-color: var(--dark-bg);
border-bottom-color: var(--border-color);
}
body.dark-mode .command-item,
body.dark-mode .alias-item {
border-color: var(--border-color);
}
body.dark-mode .command-description {
color: var(--text-secondary);
}
body.dark-mode .command-customization,
body.dark-mode .group-customization {
border-top-color: var(--border-color);
}
/* Custom mode styles */
body.custom-mode .alias-tag {
background-color: var(--primary-color);
}
/* Responsive styles */
@media (max-width: 768px) {
.command-header {
flex-direction: column;
align-items: flex-start;
}
.command-actions {
margin-top: var(--spacing-2);
}
.flex-row {
flex-direction: column;
}
.flex-col.mr-2 {
margin-right: 0;
margin-bottom: var(--spacing-2);
}
}

View File

@ -0,0 +1,199 @@
/* Theme Settings CSS */
/* Color Picker Container */
.color-picker-container {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
input[type="color"] {
width: 40px;
height: 40px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background-color: white;
cursor: pointer;
padding: 0;
}
.color-text-input {
width: 100px;
font-family: monospace;
}
/* Theme Preview */
.theme-preview {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
overflow: hidden;
margin-top: var(--spacing-4);
}
.preview-header {
background-color: var(--primary-color);
color: white;
padding: var(--spacing-4);
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-title {
font-weight: 600;
font-size: 1.1rem;
}
.preview-button {
background-color: rgba(255, 255, 255, 0.2);
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-md);
cursor: pointer;
}
.preview-content {
padding: var(--spacing-4);
background-color: var(--light-bg);
}
.preview-card {
background-color: white;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.preview-card-header {
padding: var(--spacing-3) var(--spacing-4);
border-bottom: 1px solid var(--border-color);
font-weight: 600;
}
.preview-card-body {
padding: var(--spacing-4);
}
.preview-form-control {
height: 40px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-4);
}
.preview-button-primary {
display: inline-block;
background-color: var(--primary-color);
color: white;
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-md);
margin-right: var(--spacing-2);
cursor: pointer;
}
.preview-button-secondary {
display: inline-block;
background-color: var(--secondary-color);
color: white;
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-md);
cursor: pointer;
}
/* Dark Mode Preview */
.theme-preview.dark-mode {
--preview-bg: #1A202C;
--preview-card-bg: #2D3748;
--preview-text: #F7FAFC;
--preview-border: #4A5568;
}
.theme-preview.dark-mode .preview-content {
background-color: var(--preview-bg);
}
.theme-preview.dark-mode .preview-card {
background-color: var(--preview-card-bg);
color: var(--preview-text);
}
.theme-preview.dark-mode .preview-card-header {
border-bottom-color: var(--preview-border);
}
.theme-preview.dark-mode .preview-form-control {
border-color: var(--preview-border);
background-color: #4A5568;
}
/* Custom Mode Preview */
.theme-preview.custom-mode .preview-header {
background-color: var(--primary-color);
}
.theme-preview.custom-mode .preview-button-primary {
background-color: var(--primary-color);
}
.theme-preview.custom-mode .preview-button-secondary {
background-color: var(--secondary-color);
}
/* Dark Mode for Dashboard */
body.dark-mode {
--light-bg: #1A202C;
--dark-bg: #171923;
--card-bg: #2D3748;
--text-primary: #F7FAFC;
--text-secondary: #CBD5E0;
--border-color: #4A5568;
}
body.dark-mode .card {
background-color: var(--card-bg);
color: var(--text-primary);
}
body.dark-mode input[type="text"],
body.dark-mode input[type="number"],
body.dark-mode input[type="password"],
body.dark-mode input[type="email"],
body.dark-mode input[type="search"],
body.dark-mode select,
body.dark-mode textarea {
background-color: #4A5568;
color: var(--text-primary);
border-color: #718096;
}
body.dark-mode .sidebar {
background-color: #171923;
}
body.dark-mode .header {
background-color: #2D3748;
border-bottom-color: #4A5568;
}
/* Custom Mode for Dashboard */
body.custom-mode {
--primary-color: var(--primary-color);
--secondary-color: var(--secondary-color);
--accent-color: var(--accent-color);
font-family: var(--font-family);
}
body.custom-mode .btn-primary {
background-color: var(--primary-color);
}
body.custom-mode .btn-secondary {
background-color: var(--secondary-color);
}
body.custom-mode .sidebar {
background-color: var(--secondary-color);
}
body.custom-mode .nav-item.active {
background-color: var(--primary-color);
}

View File

@ -12,6 +12,9 @@
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/components.css"> <link rel="stylesheet" href="css/components.css">
<link rel="stylesheet" href="css/layout.css"> <link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/theme-settings.css">
<link rel="stylesheet" href="css/command-customization.css">
<link rel="stylesheet" href="css/cog-management.css">
</head> </head>
<body> <body>
<!-- Auth Section --> <!-- Auth Section -->
@ -46,6 +49,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
Modules Modules
</a> </a>
<a href="#cog-management" class="nav-item" data-section="cog-management-section" id="nav-cog-management">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
Cog Management
</a>
<a href="#permissions-settings" class="nav-item" data-section="permissions-settings-section"> <a href="#permissions-settings" class="nav-item" data-section="permissions-settings-section">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Permissions Permissions
@ -54,6 +61,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
AI Settings AI Settings
</a> </a>
<a href="#theme-settings" class="nav-item" data-section="theme-settings-section">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
Theme Settings
</a>
<a href="#command-customization" class="nav-item" data-section="command-customization-section">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
Command Customization
</a>
</div> </div>
<div class="sidebar-footer"> <div class="sidebar-footer">
<button id="logout-button" class="btn btn-danger w-full"> <button id="logout-button" class="btn btn-danger w-full">
@ -366,6 +381,340 @@
</div> </div>
</div> </div>
<!-- Include Theme Settings Section -->
<div id="theme-settings-template" style="display: none;">
<!-- This template will be used to restore the form after loading -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Theme Mode</h3>
</div>
<div class="form-group">
<div class="radio-group">
<input type="radio" id="theme-mode-light" name="theme_mode" value="light" checked>
<label for="theme-mode-light">Light Mode</label>
</div>
<div class="radio-group">
<input type="radio" id="theme-mode-dark" name="theme_mode" value="dark">
<label for="theme-mode-dark">Dark Mode</label>
</div>
<div class="radio-group">
<input type="radio" id="theme-mode-custom" name="theme_mode" value="custom">
<label for="theme-mode-custom">Custom Mode</label>
</div>
</div>
</div>
<div id="custom-theme-settings" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">Custom Colors</h3>
</div>
<div class="form-group">
<label for="primary-color">Primary Color:</label>
<div class="color-picker-container">
<input type="color" id="primary-color" value="#5865F2">
<input type="text" id="primary-color-text" value="#5865F2" class="color-text-input">
</div>
</div>
<div class="form-group">
<label for="secondary-color">Secondary Color:</label>
<div class="color-picker-container">
<input type="color" id="secondary-color" value="#2D3748">
<input type="text" id="secondary-color-text" value="#2D3748" class="color-text-input">
</div>
</div>
<div class="form-group">
<label for="accent-color">Accent Color:</label>
<div class="color-picker-container">
<input type="color" id="accent-color" value="#7289DA">
<input type="text" id="accent-color-text" value="#7289DA" class="color-text-input">
</div>
</div>
<div class="form-group">
<label for="font-family">Font Family:</label>
<select id="font-family" class="w-full">
<option value="Inter, sans-serif">Inter</option>
<option value="'Roboto', sans-serif">Roboto</option>
<option value="'Open Sans', sans-serif">Open Sans</option>
<option value="'Montserrat', sans-serif">Montserrat</option>
<option value="'Poppins', sans-serif">Poppins</option>
</select>
</div>
<div class="form-group">
<label for="custom-css">Custom CSS (Advanced):</label>
<textarea id="custom-css" rows="6" class="w-full" placeholder="Enter custom CSS here..."></textarea>
<small class="text-muted">Custom CSS will be applied to the dashboard. Use with caution.</small>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Theme Preview</h3>
</div>
<div id="theme-preview" class="theme-preview">
<div class="preview-header">
<div class="preview-title">Header</div>
<div class="preview-button">Button</div>
</div>
<div class="preview-content">
<div class="preview-card">
<div class="preview-card-header">Card Title</div>
<div class="preview-card-body">
<p>This is a preview of how your theme will look.</p>
<div class="preview-form-control"></div>
<div class="preview-button-primary">Primary Button</div>
<div class="preview-button-secondary">Secondary Button</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="btn-group">
<button id="save-theme-settings-button" class="btn btn-primary">Save Theme Settings</button>
<button id="reset-theme-settings-button" class="btn btn-warning">Reset to Defaults</button>
</div>
<p id="theme-settings-feedback" class="mt-2"></p>
</div>
</div>
<!-- Theme Settings Section -->
<div id="theme-settings-section" class="dashboard-section" style="display: none;">
<div class="card">
<div class="card-header">
<h2 class="card-title">Theme Settings</h2>
</div>
</div>
<div id="theme-settings-form">
<!-- Will be populated by JS -->
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<!-- Include Cog Management Section -->
<!-- Cog Management Section -->
<div id="cog-management-section" class="dashboard-section" style="display: none;">
<div class="card">
<div class="card-header">
<h2 class="card-title">Manage Cogs & Commands</h2>
</div>
<div class="form-group">
<label for="cog-guild-select">Select Server:</label>
<select name="guilds" id="cog-guild-select" class="w-full">
<option value="">--Please choose a server--</option>
</select>
</div>
</div>
<div id="cog-management-loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Loading cogs and commands...</p>
</div>
<div id="cog-management-content" style="display: none;">
<div class="card">
<div class="card-header">
<h3 class="card-title">Cogs (Modules)</h3>
<p class="text-sm text-muted">Enable or disable entire modules of functionality</p>
</div>
<div class="cogs-list-container p-4">
<div id="cogs-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Cogs will be populated here -->
</div>
</div>
<div class="btn-group mt-4">
<button id="save-cogs-button" class="btn btn-primary">Save Cog Settings</button>
</div>
<p id="cogs-feedback" class="mt-2"></p>
</div>
<div class="card mt-6">
<div class="card-header">
<h3 class="card-title">Commands</h3>
<p class="text-sm text-muted">Enable or disable individual commands</p>
</div>
<div class="form-group">
<label for="cog-filter">Filter by Cog:</label>
<select id="cog-filter" class="w-full">
<option value="all">All Cogs</option>
<!-- Cog options will be populated here -->
</select>
</div>
<div class="commands-list-container p-4">
<div id="commands-list" class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Commands will be populated here -->
</div>
</div>
<div class="btn-group mt-4">
<button id="save-commands-button" class="btn btn-primary">Save Command Settings</button>
</div>
<p id="commands-feedback" class="mt-2"></p>
</div>
</div>
</div>
<!-- Include Command Customization Section -->
<div id="command-customization-section" class="dashboard-section" style="display: none;">
<div class="card">
<div class="card-header">
<h2 class="card-title">Command Customization</h2>
</div>
</div>
<div id="command-customization-form">
<!-- Command Customization Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Customize Commands</h3>
<p class="text-muted">Customize the names and descriptions of commands for your server.</p>
</div>
<div class="form-group">
<div class="search-container">
<input type="text" id="command-search" placeholder="Search commands..." class="w-full">
</div>
</div>
<div id="command-list" class="command-list">
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<!-- Command Group Customization Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Customize Command Groups</h3>
<p class="text-muted">Customize the names of command groups for your server.</p>
</div>
<div id="group-list" class="command-list">
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<!-- Command Aliases Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Command Aliases</h3>
<p class="text-muted">Add alternative names for commands.</p>
</div>
<div id="alias-list" class="command-list">
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
<div class="form-group mt-4">
<h4>Add New Alias</h4>
<div class="flex-row">
<div class="flex-col mr-2">
<label for="alias-command-select">Command:</label>
<select id="alias-command-select" class="w-full">
<option value="">Select a command</option>
</select>
</div>
<div class="flex-col">
<label for="alias-name-input">Alias:</label>
<input type="text" id="alias-name-input" placeholder="Enter alias name" class="w-full">
</div>
</div>
<button id="add-alias-button" class="btn btn-primary mt-2">Add Alias</button>
<p id="alias-feedback" class="mt-2"></p>
</div>
</div>
<!-- Sync Commands Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Sync Commands</h3>
<p class="text-muted">Sync command customizations to Discord.</p>
</div>
<div class="form-group">
<p>After making changes to command names, descriptions, or aliases, you need to sync the changes to Discord.</p>
<button id="sync-commands-button" class="btn btn-primary">Sync Commands</button>
<p id="sync-feedback" class="mt-2"></p>
</div>
</div>
</div>
</div>
<!-- Command Customization Templates -->
<template id="command-item-template">
<div class="command-item">
<div class="command-header">
<h4 class="command-name"></h4>
<div class="command-actions">
<button class="btn btn-sm btn-primary edit-command-btn">Edit</button>
<button class="btn btn-sm btn-warning reset-command-btn">Reset</button>
</div>
</div>
<div class="command-details">
<p class="command-description"></p>
<div class="command-customization" style="display: none;">
<div class="form-group">
<label>Custom Name:</label>
<input type="text" class="custom-command-name w-full" placeholder="Enter custom name">
</div>
<div class="form-group">
<label>Custom Description:</label>
<input type="text" class="custom-command-description w-full" placeholder="Enter custom description">
</div>
<div class="btn-group">
<button class="btn btn-sm btn-primary save-command-btn">Save</button>
<button class="btn btn-sm btn-secondary cancel-command-btn">Cancel</button>
</div>
</div>
</div>
</div>
</template>
<template id="group-item-template">
<div class="command-item">
<div class="command-header">
<h4 class="group-name"></h4>
<div class="command-actions">
<button class="btn btn-sm btn-primary edit-group-btn">Edit</button>
<button class="btn btn-sm btn-warning reset-group-btn">Reset</button>
</div>
</div>
<div class="group-details">
<div class="group-customization" style="display: none;">
<div class="form-group">
<label>Custom Name:</label>
<input type="text" class="custom-group-name w-full" placeholder="Enter custom name">
</div>
<div class="btn-group">
<button class="btn btn-sm btn-primary save-group-btn">Save</button>
<button class="btn btn-sm btn-secondary cancel-group-btn">Cancel</button>
</div>
</div>
</div>
</div>
</template>
<template id="alias-item-template">
<div class="alias-item">
<div class="alias-header">
<h4 class="command-name"></h4>
</div>
<div class="alias-list">
<ul class="alias-tags">
<!-- Alias tags will be added here -->
</ul>
</div>
</div>
</template>
<template id="alias-tag-template">
<li class="alias-tag">
<span class="alias-name"></span>
<button class="remove-alias-btn">×</button>
</li>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -376,5 +725,8 @@
<script src="js/utils.js"></script> <script src="js/utils.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script src="js/ai-settings.js"></script> <script src="js/ai-settings.js"></script>
<script src="js/theme-settings.js"></script>
<script src="js/command-customization.js"></script>
<script src="js/cog-management.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,466 @@
/**
* Cog Management JavaScript
* Handles cog and command enabling/disabling functionality
*/
// Global variables
let cogsData = [];
let commandsData = {};
let selectedGuildId = null;
let cogManagementLoaded = false;
// Initialize cog management when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
initCogManagement();
});
/**
* Initialize cog management functionality
*/
function initCogManagement() {
// Get DOM elements
const cogGuildSelect = document.getElementById('cog-guild-select');
const cogFilter = document.getElementById('cog-filter');
const saveCogsButton = document.getElementById('save-cogs-button');
const saveCommandsButton = document.getElementById('save-commands-button');
const navCogManagement = document.getElementById('nav-cog-management');
// Add event listener for cog management tab
if (navCogManagement) {
navCogManagement.addEventListener('click', () => {
// Show cog management section
showSection('cog-management');
// Load guilds if not already loaded
if (!cogManagementLoaded) {
loadGuildsForCogManagement();
cogManagementLoaded = true;
}
});
}
// Add event listener for guild select
if (cogGuildSelect) {
cogGuildSelect.addEventListener('change', () => {
selectedGuildId = cogGuildSelect.value;
if (selectedGuildId) {
loadCogsAndCommands(selectedGuildId);
} else {
// Hide content if no guild selected
document.getElementById('cog-management-content').style.display = 'none';
}
});
}
// Add event listener for cog filter
if (cogFilter) {
cogFilter.addEventListener('change', () => {
filterCommands(cogFilter.value);
});
}
// Add event listener for save cogs button
if (saveCogsButton) {
saveCogsButton.addEventListener('click', () => {
saveCogsSettings();
});
}
// Add event listener for save commands button
if (saveCommandsButton) {
saveCommandsButton.addEventListener('click', () => {
saveCommandsSettings();
});
}
}
/**
* Load guilds for cog management
*/
function loadGuildsForCogManagement() {
const cogGuildSelect = document.getElementById('cog-guild-select');
// Show loading state
cogGuildSelect.disabled = true;
cogGuildSelect.innerHTML = '<option value="">Loading servers...</option>';
// Fetch guilds from API
API.get('/dashboard/api/guilds')
.then(guilds => {
// Clear loading state
cogGuildSelect.innerHTML = '<option value="">--Please choose a server--</option>';
// Add guilds to select
guilds.forEach(guild => {
const option = document.createElement('option');
option.value = guild.id;
option.textContent = guild.name;
cogGuildSelect.appendChild(option);
});
// Enable select
cogGuildSelect.disabled = false;
})
.catch(error => {
console.error('Error loading guilds:', error);
cogGuildSelect.innerHTML = '<option value="">Error loading servers</option>';
cogGuildSelect.disabled = false;
Toast.error('Failed to load servers. Please try again.');
});
}
/**
* Load cogs and commands for a guild
* @param {string} guildId - The guild ID
*/
function loadCogsAndCommands(guildId) {
// Show loading state
document.getElementById('cog-management-loading').style.display = 'flex';
document.getElementById('cog-management-content').style.display = 'none';
// Fetch cogs and commands from API
API.get(`/dashboard/api/guilds/${guildId}/cogs`)
.then(data => {
// Store data
cogsData = data;
// Populate cogs list
populateCogsUI(data);
// Populate commands list
populateCommandsUI(data);
// Hide loading state
document.getElementById('cog-management-loading').style.display = 'none';
document.getElementById('cog-management-content').style.display = 'block';
})
.catch(error => {
console.error('Error loading cogs and commands:', error);
document.getElementById('cog-management-loading').style.display = 'none';
Toast.error('Failed to load cogs and commands. Please try again.');
});
}
/**
* Populate cogs UI
* @param {Array} cogs - Array of cog objects
*/
function populateCogsUI(cogs) {
const cogsList = document.getElementById('cogs-list');
const cogFilter = document.getElementById('cog-filter');
// Clear previous content
cogsList.innerHTML = '';
// Clear filter options except "All Cogs"
cogFilter.innerHTML = '<option value="all">All Cogs</option>';
// Add cogs to list
cogs.forEach(cog => {
// Create cog card
const cogCard = document.createElement('div');
cogCard.className = 'cog-card p-4 border rounded';
// Create cog header
const cogHeader = document.createElement('div');
cogHeader.className = 'cog-header flex items-center justify-between mb-2';
// Create cog checkbox
const cogCheckbox = document.createElement('div');
cogCheckbox.className = 'cog-checkbox flex items-center';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `cog-${cog.name}`;
checkbox.className = 'mr-2';
checkbox.checked = cog.enabled;
checkbox.dataset.cogName = cog.name;
// Disable checkbox for core cogs
if (cog.name === 'SettingsCog' || cog.name === 'HelpCog') {
checkbox.disabled = true;
checkbox.title = 'Core cogs cannot be disabled';
}
const label = document.createElement('label');
label.htmlFor = `cog-${cog.name}`;
label.textContent = cog.name;
label.className = 'font-medium';
cogCheckbox.appendChild(checkbox);
cogCheckbox.appendChild(label);
// Create command count badge
const commandCount = document.createElement('span');
commandCount.className = 'command-count bg-gray-200 text-gray-800 px-2 py-1 rounded text-xs';
commandCount.textContent = `${cog.commands.length} commands`;
cogHeader.appendChild(cogCheckbox);
cogHeader.appendChild(commandCount);
// Create cog description
const cogDescription = document.createElement('p');
cogDescription.className = 'cog-description text-sm text-gray-600 mt-1';
cogDescription.textContent = cog.description || 'No description available';
// Add elements to cog card
cogCard.appendChild(cogHeader);
cogCard.appendChild(cogDescription);
// Add cog card to list
cogsList.appendChild(cogCard);
// Add cog to filter options
const option = document.createElement('option');
option.value = cog.name;
option.textContent = cog.name;
cogFilter.appendChild(option);
});
}
/**
* Populate commands UI
* @param {Array} cogs - Array of cog objects
*/
function populateCommandsUI(cogs) {
const commandsList = document.getElementById('commands-list');
// Clear previous content
commandsList.innerHTML = '';
// Create a flat list of all commands with their cog
commandsData = {};
cogs.forEach(cog => {
cog.commands.forEach(command => {
// Store command data with cog name
commandsData[command.name] = {
...command,
cog_name: cog.name
};
// Create command card
const commandCard = createCommandCard(command, cog.name);
// Add command card to list
commandsList.appendChild(commandCard);
});
});
}
/**
* Create a command card element
* @param {Object} command - Command object
* @param {string} cogName - Name of the cog the command belongs to
* @returns {HTMLElement} Command card element
*/
function createCommandCard(command, cogName) {
// Create command card
const commandCard = document.createElement('div');
commandCard.className = 'command-card p-4 border rounded';
commandCard.dataset.cogName = cogName;
// Create command header
const commandHeader = document.createElement('div');
commandHeader.className = 'command-header flex items-center justify-between mb-2';
// Create command checkbox
const commandCheckbox = document.createElement('div');
commandCheckbox.className = 'command-checkbox flex items-center';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `command-${command.name}`;
checkbox.className = 'mr-2';
checkbox.checked = command.enabled;
checkbox.dataset.commandName = command.name;
const label = document.createElement('label');
label.htmlFor = `command-${command.name}`;
label.textContent = command.name;
label.className = 'font-medium';
commandCheckbox.appendChild(checkbox);
commandCheckbox.appendChild(label);
// Create cog badge
const cogBadge = document.createElement('span');
cogBadge.className = 'cog-badge bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs';
cogBadge.textContent = cogName;
commandHeader.appendChild(commandCheckbox);
commandHeader.appendChild(cogBadge);
// Create command description
const commandDescription = document.createElement('p');
commandDescription.className = 'command-description text-sm text-gray-600 mt-1';
commandDescription.textContent = command.description || 'No description available';
// Add elements to command card
commandCard.appendChild(commandHeader);
commandCard.appendChild(commandDescription);
return commandCard;
}
/**
* Filter commands by cog
* @param {string} cogName - Name of the cog to filter by, or "all" for all cogs
*/
function filterCommands(cogName) {
const commandCards = document.querySelectorAll('.command-card');
commandCards.forEach(card => {
if (cogName === 'all' || card.dataset.cogName === cogName) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
}
/**
* Save cogs settings
*/
function saveCogsSettings() {
if (!selectedGuildId) return;
// Show loading state
const saveButton = document.getElementById('save-cogs-button');
saveButton.disabled = true;
saveButton.textContent = 'Saving...';
// Get cog settings
const cogsPayload = {};
const cogCheckboxes = document.querySelectorAll('#cogs-list input[type="checkbox"]');
cogCheckboxes.forEach(checkbox => {
if (!checkbox.disabled) {
cogsPayload[checkbox.dataset.cogName] = checkbox.checked;
}
});
// Send request to API
API.patch(`/dashboard/api/guilds/${selectedGuildId}/settings`, {
cogs: cogsPayload
})
.then(() => {
// Reset button state
saveButton.disabled = false;
saveButton.textContent = 'Save Cog Settings';
// Show success message
document.getElementById('cogs-feedback').textContent = 'Cog settings saved successfully!';
document.getElementById('cogs-feedback').className = 'mt-2 text-green-600';
// Clear message after 3 seconds
setTimeout(() => {
document.getElementById('cogs-feedback').textContent = '';
document.getElementById('cogs-feedback').className = 'mt-2';
}, 3000);
Toast.success('Cog settings saved successfully!');
})
.catch(error => {
console.error('Error saving cog settings:', error);
// Reset button state
saveButton.disabled = false;
saveButton.textContent = 'Save Cog Settings';
// Show error message
document.getElementById('cogs-feedback').textContent = 'Error saving cog settings. Please try again.';
document.getElementById('cogs-feedback').className = 'mt-2 text-red-600';
Toast.error('Failed to save cog settings. Please try again.');
});
}
/**
* Save commands settings
*/
function saveCommandsSettings() {
if (!selectedGuildId) return;
// Show loading state
const saveButton = document.getElementById('save-commands-button');
saveButton.disabled = true;
saveButton.textContent = 'Saving...';
// Get command settings
const commandsPayload = {};
const commandCheckboxes = document.querySelectorAll('#commands-list input[type="checkbox"]');
commandCheckboxes.forEach(checkbox => {
commandsPayload[checkbox.dataset.commandName] = checkbox.checked;
});
// Send request to API
API.patch(`/dashboard/api/guilds/${selectedGuildId}/settings`, {
commands: commandsPayload
})
.then(() => {
// Reset button state
saveButton.disabled = false;
saveButton.textContent = 'Save Command Settings';
// Show success message
document.getElementById('commands-feedback').textContent = 'Command settings saved successfully!';
document.getElementById('commands-feedback').className = 'mt-2 text-green-600';
// Clear message after 3 seconds
setTimeout(() => {
document.getElementById('commands-feedback').textContent = '';
document.getElementById('commands-feedback').className = 'mt-2';
}, 3000);
Toast.success('Command settings saved successfully!');
})
.catch(error => {
console.error('Error saving command settings:', error);
// Reset button state
saveButton.disabled = false;
saveButton.textContent = 'Save Command Settings';
// Show error message
document.getElementById('commands-feedback').textContent = 'Error saving command settings. Please try again.';
document.getElementById('commands-feedback').className = 'mt-2 text-red-600';
Toast.error('Failed to save command settings. Please try again.');
});
}
/**
* Show a specific section and hide others
* @param {string} sectionId - ID of the section to show
*/
function showSection(sectionId) {
// Get all sections
const sections = document.querySelectorAll('.dashboard-section');
// Hide all sections
sections.forEach(section => {
section.style.display = 'none';
});
// Get all nav buttons
const navButtons = document.querySelectorAll('.nav-button');
// Remove active class from all nav buttons
navButtons.forEach(button => {
button.classList.remove('active');
});
// Show the selected section and activate the corresponding nav button
const selectedSection = document.getElementById(`${sectionId}-section`);
const selectedNavButton = document.getElementById(`nav-${sectionId}`);
if (selectedSection) {
selectedSection.style.display = 'block';
}
if (selectedNavButton) {
selectedNavButton.classList.add('active');
}
}

View File

@ -0,0 +1,781 @@
/**
* Command Customization JavaScript
* Handles command customization functionality for the dashboard
*/
// Initialize command customization when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
initCommandCustomization();
});
/**
* Initialize command customization
*/
function initCommandCustomization() {
// Add event listener to command search input
const commandSearch = document.getElementById('command-search');
if (commandSearch) {
commandSearch.addEventListener('input', filterCommands);
}
// Add event listener to sync commands button
const syncCommandsButton = document.getElementById('sync-commands-button');
if (syncCommandsButton) {
syncCommandsButton.addEventListener('click', syncCommands);
}
// Add event listener to add alias button
const addAliasButton = document.getElementById('add-alias-button');
if (addAliasButton) {
addAliasButton.addEventListener('click', addAlias);
}
// Load command customizations when the section is shown
const navItem = document.querySelector('a[data-section="command-customization-section"]');
if (navItem) {
navItem.addEventListener('click', () => {
loadCommandCustomizations();
});
}
}
/**
* Load command customizations from API
*/
async function loadCommandCustomizations() {
try {
// Show loading spinners
document.getElementById('command-list').innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
document.getElementById('group-list').innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
document.getElementById('alias-list').innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
showToast('error', 'Error', 'No guild selected');
return;
}
// Fetch command customizations from API
const response = await fetch(`/dashboard/commands/customizations/${guildId}`);
if (!response.ok) {
throw new Error('Failed to load command customizations');
}
const data = await response.json();
// Render command customizations
renderCommandCustomizations(data.command_customizations);
// Render group customizations
renderGroupCustomizations(data.group_customizations);
// Render command aliases
renderCommandAliases(data.command_aliases);
// Populate command select for aliases
populateCommandSelect(Object.keys(data.command_customizations));
} catch (error) {
console.error('Error loading command customizations:', error);
showToast('error', 'Error', 'Failed to load command customizations');
// Show error message in lists
document.getElementById('command-list').innerHTML = '<div class="alert alert-danger">Failed to load command customizations</div>';
document.getElementById('group-list').innerHTML = '<div class="alert alert-danger">Failed to load group customizations</div>';
document.getElementById('alias-list').innerHTML = '<div class="alert alert-danger">Failed to load command aliases</div>';
}
}
/**
* Render command customizations
* @param {Object} commandCustomizations - Command customizations object
*/
function renderCommandCustomizations(commandCustomizations) {
const commandList = document.getElementById('command-list');
commandList.innerHTML = '';
if (Object.keys(commandCustomizations).length === 0) {
commandList.innerHTML = '<div class="alert alert-info">No commands found</div>';
return;
}
// Sort commands alphabetically
const sortedCommands = Object.keys(commandCustomizations).sort();
// Create command items
sortedCommands.forEach(commandName => {
const customization = commandCustomizations[commandName];
const commandItem = createCommandItem(commandName, customization);
commandList.appendChild(commandItem);
});
}
/**
* Create a command item element
* @param {string} commandName - Original command name
* @param {Object} customization - Command customization object
* @returns {HTMLElement} Command item element
*/
function createCommandItem(commandName, customization) {
// Clone the template
const template = document.getElementById('command-item-template');
const commandItem = template.content.cloneNode(true).querySelector('.command-item');
// Set command name
const nameElement = commandItem.querySelector('.command-name');
nameElement.textContent = commandName;
if (customization.name && customization.name !== commandName) {
nameElement.textContent = `${customization.name} (${commandName})`;
}
// Set command description
const descriptionElement = commandItem.querySelector('.command-description');
descriptionElement.textContent = customization.description || 'No description available';
// Set custom name input value
const customNameInput = commandItem.querySelector('.custom-command-name');
customNameInput.value = customization.name || '';
customNameInput.placeholder = commandName;
// Set custom description input value
const customDescriptionInput = commandItem.querySelector('.custom-command-description');
customDescriptionInput.value = customization.description || '';
// Add event listeners to buttons
const editButton = commandItem.querySelector('.edit-command-btn');
const resetButton = commandItem.querySelector('.reset-command-btn');
const saveButton = commandItem.querySelector('.save-command-btn');
const cancelButton = commandItem.querySelector('.cancel-command-btn');
const customizationDiv = commandItem.querySelector('.command-customization');
editButton.addEventListener('click', () => {
customizationDiv.style.display = 'block';
editButton.style.display = 'none';
});
resetButton.addEventListener('click', () => {
resetCommandCustomization(commandName);
});
saveButton.addEventListener('click', () => {
saveCommandCustomization(
commandName,
customNameInput.value,
customDescriptionInput.value,
customizationDiv,
editButton,
nameElement,
descriptionElement
);
});
cancelButton.addEventListener('click', () => {
customizationDiv.style.display = 'none';
editButton.style.display = 'inline-block';
// Reset input values
customNameInput.value = customization.name || '';
customDescriptionInput.value = customization.description || '';
});
// Add data attribute for filtering
commandItem.dataset.commandName = commandName.toLowerCase();
return commandItem;
}
/**
* Save command customization
* @param {string} commandName - Original command name
* @param {string} customName - Custom command name
* @param {string} customDescription - Custom command description
* @param {HTMLElement} customizationDiv - Command customization div
* @param {HTMLElement} editButton - Edit button
* @param {HTMLElement} nameElement - Command name element
* @param {HTMLElement} descriptionElement - Command description element
*/
async function saveCommandCustomization(
commandName,
customName,
customDescription,
customizationDiv,
editButton,
nameElement,
descriptionElement
) {
try {
// Validate custom name format if provided
if (customName && (!/^[a-z][a-z0-9_]*$/.test(customName) || customName.length > 32)) {
showToast('error', 'Error', 'Custom command names must be lowercase, start with a letter, and contain only letters, numbers, and underscores (max 32 characters)');
return;
}
// Validate custom description if provided
if (customDescription && customDescription.length > 100) {
showToast('error', 'Error', 'Custom command descriptions must be 100 characters or less');
return;
}
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
showToast('error', 'Error', 'No guild selected');
return;
}
// Prepare request data
const requestData = {
command_name: commandName,
custom_name: customName || null,
custom_description: customDescription || null
};
// Send request to API
const response = await fetch(`/dashboard/commands/customizations/${guildId}/commands`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error('Failed to save command customization');
}
// Update UI
customizationDiv.style.display = 'none';
editButton.style.display = 'inline-block';
if (customName) {
nameElement.textContent = `${customName} (${commandName})`;
} else {
nameElement.textContent = commandName;
}
if (customDescription) {
descriptionElement.textContent = customDescription;
} else {
descriptionElement.textContent = 'No description available';
}
showToast('success', 'Success', 'Command customization saved successfully');
} catch (error) {
console.error('Error saving command customization:', error);
showToast('error', 'Error', 'Failed to save command customization');
}
}
/**
* Reset command customization
* @param {string} commandName - Original command name
*/
async function resetCommandCustomization(commandName) {
try {
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
showToast('error', 'Error', 'No guild selected');
return;
}
// Prepare request data
const requestData = {
command_name: commandName,
custom_name: null,
custom_description: null
};
// Send request to API
const response = await fetch(`/dashboard/commands/customizations/${guildId}/commands`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error('Failed to reset command customization');
}
// Reload command customizations
loadCommandCustomizations();
showToast('success', 'Success', 'Command customization reset successfully');
} catch (error) {
console.error('Error resetting command customization:', error);
showToast('error', 'Error', 'Failed to reset command customization');
}
}
/**
* Render group customizations
* @param {Object} groupCustomizations - Group customizations object
*/
function renderGroupCustomizations(groupCustomizations) {
const groupList = document.getElementById('group-list');
groupList.innerHTML = '';
if (Object.keys(groupCustomizations).length === 0) {
groupList.innerHTML = '<div class="alert alert-info">No command groups found</div>';
return;
}
// Sort groups alphabetically
const sortedGroups = Object.keys(groupCustomizations).sort();
// Create group items
sortedGroups.forEach(groupName => {
const customName = groupCustomizations[groupName];
const groupItem = createGroupItem(groupName, customName);
groupList.appendChild(groupItem);
});
}
/**
* Create a group item element
* @param {string} groupName - Original group name
* @param {string} customName - Custom group name
* @returns {HTMLElement} Group item element
*/
function createGroupItem(groupName, customName) {
// Clone the template
const template = document.getElementById('group-item-template');
const groupItem = template.content.cloneNode(true).querySelector('.command-item');
// Set group name
const nameElement = groupItem.querySelector('.group-name');
nameElement.textContent = groupName;
if (customName && customName !== groupName) {
nameElement.textContent = `${customName} (${groupName})`;
}
// Set custom name input value
const customNameInput = groupItem.querySelector('.custom-group-name');
customNameInput.value = customName || '';
customNameInput.placeholder = groupName;
// Add event listeners to buttons
const editButton = groupItem.querySelector('.edit-group-btn');
const resetButton = groupItem.querySelector('.reset-group-btn');
const saveButton = groupItem.querySelector('.save-group-btn');
const cancelButton = groupItem.querySelector('.cancel-group-btn');
const customizationDiv = groupItem.querySelector('.group-customization');
editButton.addEventListener('click', () => {
customizationDiv.style.display = 'block';
editButton.style.display = 'none';
});
resetButton.addEventListener('click', () => {
resetGroupCustomization(groupName);
});
saveButton.addEventListener('click', () => {
saveGroupCustomization(
groupName,
customNameInput.value,
customizationDiv,
editButton,
nameElement
);
});
cancelButton.addEventListener('click', () => {
customizationDiv.style.display = 'none';
editButton.style.display = 'inline-block';
// Reset input value
customNameInput.value = customName || '';
});
return groupItem;
}
/**
* Save group customization
* @param {string} groupName - Original group name
* @param {string} customName - Custom group name
* @param {HTMLElement} customizationDiv - Group customization div
* @param {HTMLElement} editButton - Edit button
* @param {HTMLElement} nameElement - Group name element
*/
async function saveGroupCustomization(
groupName,
customName,
customizationDiv,
editButton,
nameElement
) {
try {
// Validate custom name format if provided
if (customName && (!/^[a-z][a-z0-9_]*$/.test(customName) || customName.length > 32)) {
showToast('error', 'Error', 'Custom group names must be lowercase, start with a letter, and contain only letters, numbers, and underscores (max 32 characters)');
return;
}
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
showToast('error', 'Error', 'No guild selected');
return;
}
// Prepare request data
const requestData = {
group_name: groupName,
custom_name: customName || null
};
// Send request to API
const response = await fetch(`/dashboard/commands/customizations/${guildId}/groups`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error('Failed to save group customization');
}
// Update UI
customizationDiv.style.display = 'none';
editButton.style.display = 'inline-block';
if (customName) {
nameElement.textContent = `${customName} (${groupName})`;
} else {
nameElement.textContent = groupName;
}
showToast('success', 'Success', 'Group customization saved successfully');
} catch (error) {
console.error('Error saving group customization:', error);
showToast('error', 'Error', 'Failed to save group customization');
}
}
/**
* Reset group customization
* @param {string} groupName - Original group name
*/
async function resetGroupCustomization(groupName) {
try {
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
showToast('error', 'Error', 'No guild selected');
return;
}
// Prepare request data
const requestData = {
group_name: groupName,
custom_name: null
};
// Send request to API
const response = await fetch(`/dashboard/commands/customizations/${guildId}/groups`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error('Failed to reset group customization');
}
// Reload command customizations
loadCommandCustomizations();
showToast('success', 'Success', 'Group customization reset successfully');
} catch (error) {
console.error('Error resetting group customization:', error);
showToast('error', 'Error', 'Failed to reset group customization');
}
}
/**
* Render command aliases
* @param {Object} commandAliases - Command aliases object
*/
function renderCommandAliases(commandAliases) {
const aliasList = document.getElementById('alias-list');
aliasList.innerHTML = '';
if (Object.keys(commandAliases).length === 0) {
aliasList.innerHTML = '<div class="alert alert-info">No command aliases found</div>';
return;
}
// Sort commands alphabetically
const sortedCommands = Object.keys(commandAliases).sort();
// Create alias items
sortedCommands.forEach(commandName => {
const aliases = commandAliases[commandName];
if (aliases && aliases.length > 0) {
const aliasItem = createAliasItem(commandName, aliases);
aliasList.appendChild(aliasItem);
}
});
}
/**
* Create an alias item element
* @param {string} commandName - Original command name
* @param {Array} aliases - Command aliases
* @returns {HTMLElement} Alias item element
*/
function createAliasItem(commandName, aliases) {
// Clone the template
const template = document.getElementById('alias-item-template');
const aliasItem = template.content.cloneNode(true).querySelector('.alias-item');
// Set command name
const nameElement = aliasItem.querySelector('.command-name');
nameElement.textContent = commandName;
// Add alias tags
const aliasTagsList = aliasItem.querySelector('.alias-tags');
aliases.forEach(alias => {
const aliasTag = createAliasTag(commandName, alias);
aliasTagsList.appendChild(aliasTag);
});
return aliasItem;
}
/**
* Create an alias tag element
* @param {string} commandName - Original command name
* @param {string} alias - Command alias
* @returns {HTMLElement} Alias tag element
*/
function createAliasTag(commandName, alias) {
// Clone the template
const template = document.getElementById('alias-tag-template');
const aliasTag = template.content.cloneNode(true).querySelector('.alias-tag');
// Set alias name
const nameElement = aliasTag.querySelector('.alias-name');
nameElement.textContent = alias;
// Add event listener to remove button
const removeButton = aliasTag.querySelector('.remove-alias-btn');
removeButton.addEventListener('click', () => {
removeAlias(commandName, alias);
});
return aliasTag;
}
/**
* Populate command select for aliases
* @param {Array} commands - Command names
*/
function populateCommandSelect(commands) {
const commandSelect = document.getElementById('alias-command-select');
commandSelect.innerHTML = '<option value="">Select a command</option>';
// Sort commands alphabetically
const sortedCommands = commands.sort();
// Add command options
sortedCommands.forEach(commandName => {
const option = document.createElement('option');
option.value = commandName;
option.textContent = commandName;
commandSelect.appendChild(option);
});
}
/**
* Add a command alias
*/
async function addAlias() {
try {
// Get input values
const commandSelect = document.getElementById('alias-command-select');
const aliasInput = document.getElementById('alias-name-input');
const feedbackElement = document.getElementById('alias-feedback');
const commandName = commandSelect.value;
const aliasName = aliasInput.value.trim();
// Validate inputs
if (!commandName) {
feedbackElement.textContent = 'Please select a command';
feedbackElement.className = 'mt-2 text-danger';
return;
}
if (!aliasName) {
feedbackElement.textContent = 'Please enter an alias name';
feedbackElement.className = 'mt-2 text-danger';
return;
}
// Validate alias format
if (!/^[a-z][a-z0-9_]*$/.test(aliasName) || aliasName.length > 32) {
feedbackElement.textContent = 'Alias names must be lowercase, start with a letter, and contain only letters, numbers, and underscores (max 32 characters)';
feedbackElement.className = 'mt-2 text-danger';
return;
}
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
feedbackElement.textContent = 'No guild selected';
feedbackElement.className = 'mt-2 text-danger';
return;
}
// Prepare request data
const requestData = {
command_name: commandName,
alias_name: aliasName
};
// Send request to API
const response = await fetch(`/dashboard/commands/customizations/${guildId}/aliases`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error('Failed to add command alias');
}
// Clear input
aliasInput.value = '';
// Show success message
feedbackElement.textContent = 'Alias added successfully';
feedbackElement.className = 'mt-2 text-success';
// Reload command customizations
loadCommandCustomizations();
showToast('success', 'Success', 'Command alias added successfully');
} catch (error) {
console.error('Error adding command alias:', error);
const feedbackElement = document.getElementById('alias-feedback');
feedbackElement.textContent = 'Failed to add command alias';
feedbackElement.className = 'mt-2 text-danger';
showToast('error', 'Error', 'Failed to add command alias');
}
}
/**
* Remove a command alias
* @param {string} commandName - Original command name
* @param {string} aliasName - Command alias
*/
async function removeAlias(commandName, aliasName) {
try {
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
showToast('error', 'Error', 'No guild selected');
return;
}
// Prepare request data
const requestData = {
command_name: commandName,
alias_name: aliasName
};
// Send request to API
const response = await fetch(`/dashboard/commands/customizations/${guildId}/aliases`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error('Failed to remove command alias');
}
// Reload command customizations
loadCommandCustomizations();
showToast('success', 'Success', 'Command alias removed successfully');
} catch (error) {
console.error('Error removing command alias:', error);
showToast('error', 'Error', 'Failed to remove command alias');
}
}
/**
* Sync commands to Discord
*/
async function syncCommands() {
try {
// Get the current guild ID
const guildId = getCurrentGuildId();
if (!guildId) {
showToast('error', 'Error', 'No guild selected');
return;
}
// Show feedback
const feedbackElement = document.getElementById('sync-feedback');
feedbackElement.textContent = 'Syncing commands...';
feedbackElement.className = 'mt-2 text-info';
// Send request to API
const response = await fetch(`/dashboard/commands/customizations/${guildId}/sync`, {
method: 'POST'
});
if (!response.ok) {
throw new Error('Failed to sync commands');
}
// Show success message
feedbackElement.textContent = 'Commands synced successfully';
feedbackElement.className = 'mt-2 text-success';
showToast('success', 'Success', 'Commands synced successfully');
} catch (error) {
console.error('Error syncing commands:', error);
// Show error message
const feedbackElement = document.getElementById('sync-feedback');
feedbackElement.textContent = 'Failed to sync commands';
feedbackElement.className = 'mt-2 text-danger';
showToast('error', 'Error', 'Failed to sync commands');
}
}
/**
* Filter commands by search query
*/
function filterCommands() {
const searchQuery = document.getElementById('command-search').value.toLowerCase();
const commandItems = document.querySelectorAll('#command-list .command-item');
commandItems.forEach(item => {
const commandName = item.dataset.commandName;
if (commandName.includes(searchQuery)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}

View File

@ -312,6 +312,12 @@ function showSection(sectionId) {
if (sectionId === 'ai-settings' && typeof loadAiSettings === 'function' && typeof aiSettingsLoaded !== 'undefined' && !aiSettingsLoaded) { if (sectionId === 'ai-settings' && typeof loadAiSettings === 'function' && typeof aiSettingsLoaded !== 'undefined' && !aiSettingsLoaded) {
loadAiSettings(); loadAiSettings();
} }
// Load cog management if needed
if (sectionId === 'cog-management' && typeof loadGuildsForCogManagement === 'function' && typeof cogManagementLoaded !== 'undefined' && !cogManagementLoaded) {
loadGuildsForCogManagement();
cogManagementLoaded = true;
}
} }
/** /**

View File

@ -0,0 +1,325 @@
/**
* Theme Settings JavaScript
* Handles theme customization for the dashboard
*/
// Initialize theme settings when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
initThemeSettings();
});
/**
* Initialize theme settings
*/
function initThemeSettings() {
// Get theme mode radio buttons
const themeModeRadios = document.querySelectorAll('input[name="theme_mode"]');
// Add event listeners to theme mode radio buttons
themeModeRadios.forEach(radio => {
radio.addEventListener('change', () => {
const customThemeSettings = document.getElementById('custom-theme-settings');
if (radio.value === 'custom') {
customThemeSettings.style.display = 'block';
} else {
customThemeSettings.style.display = 'none';
}
updateThemePreview();
});
});
// Add event listeners to color pickers
const colorInputs = document.querySelectorAll('input[type="color"]');
colorInputs.forEach(input => {
// Get the corresponding text input
const textInput = document.getElementById(`${input.id}-text`);
// Update text input when color input changes
input.addEventListener('input', () => {
textInput.value = input.value;
updateThemePreview();
});
// Update color input when text input changes
textInput.addEventListener('input', () => {
// Validate hex color format
if (/^#[0-9A-F]{6}$/i.test(textInput.value)) {
input.value = textInput.value;
updateThemePreview();
}
});
});
// Add event listener to font family select
const fontFamilySelect = document.getElementById('font-family');
fontFamilySelect.addEventListener('change', updateThemePreview);
// Add event listener to custom CSS textarea
const customCssTextarea = document.getElementById('custom-css');
customCssTextarea.addEventListener('input', updateThemePreview);
// Add event listener to save button
const saveButton = document.getElementById('save-theme-settings-button');
saveButton.addEventListener('click', saveThemeSettings);
// Add event listener to reset button
const resetButton = document.getElementById('reset-theme-settings-button');
resetButton.addEventListener('click', resetThemeSettings);
// Load theme settings from API
loadThemeSettings();
}
/**
* Load theme settings from API
*/
async function loadThemeSettings() {
try {
// Show loading spinner
const themeSettingsForm = document.getElementById('theme-settings-form');
themeSettingsForm.innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
// Fetch theme settings from API
const response = await fetch('/dashboard/api/settings');
if (!response.ok) {
throw new Error('Failed to load theme settings');
}
const data = await response.json();
// Restore the form
themeSettingsForm.innerHTML = document.getElementById('theme-settings-template').innerHTML;
// Initialize event listeners again
initThemeSettings();
// Set theme settings values
if (data && data.theme) {
const theme = data.theme;
// Set theme mode
const themeModeRadio = document.querySelector(`input[name="theme_mode"][value="${theme.theme_mode}"]`);
if (themeModeRadio) {
themeModeRadio.checked = true;
// Show/hide custom theme settings
const customThemeSettings = document.getElementById('custom-theme-settings');
if (theme.theme_mode === 'custom') {
customThemeSettings.style.display = 'block';
} else {
customThemeSettings.style.display = 'none';
}
}
// Set color values
if (theme.primary_color) {
document.getElementById('primary-color').value = theme.primary_color;
document.getElementById('primary-color-text').value = theme.primary_color;
}
if (theme.secondary_color) {
document.getElementById('secondary-color').value = theme.secondary_color;
document.getElementById('secondary-color-text').value = theme.secondary_color;
}
if (theme.accent_color) {
document.getElementById('accent-color').value = theme.accent_color;
document.getElementById('accent-color-text').value = theme.accent_color;
}
// Set font family
if (theme.font_family) {
document.getElementById('font-family').value = theme.font_family;
}
// Set custom CSS
if (theme.custom_css) {
document.getElementById('custom-css').value = theme.custom_css;
}
// Update preview
updateThemePreview();
}
} catch (error) {
console.error('Error loading theme settings:', error);
showToast('error', 'Error', 'Failed to load theme settings');
}
}
/**
* Update theme preview
*/
function updateThemePreview() {
const themeMode = document.querySelector('input[name="theme_mode"]:checked').value;
const primaryColor = document.getElementById('primary-color').value;
const secondaryColor = document.getElementById('secondary-color').value;
const accentColor = document.getElementById('accent-color').value;
const fontFamily = document.getElementById('font-family').value;
const customCss = document.getElementById('custom-css').value;
const preview = document.getElementById('theme-preview');
// Apply theme mode
if (themeMode === 'dark') {
preview.classList.add('dark-mode');
preview.classList.remove('custom-mode');
} else if (themeMode === 'light') {
preview.classList.remove('dark-mode');
preview.classList.remove('custom-mode');
} else if (themeMode === 'custom') {
preview.classList.remove('dark-mode');
preview.classList.add('custom-mode');
// Apply custom colors
preview.style.setProperty('--primary-color', primaryColor);
preview.style.setProperty('--secondary-color', secondaryColor);
preview.style.setProperty('--accent-color', accentColor);
preview.style.setProperty('--font-family', fontFamily);
// Apply custom CSS
const customStyleElement = document.getElementById('custom-theme-style');
if (customStyleElement) {
customStyleElement.textContent = customCss;
} else {
const style = document.createElement('style');
style.id = 'custom-theme-style';
style.textContent = customCss;
document.head.appendChild(style);
}
}
}
/**
* Save theme settings
*/
async function saveThemeSettings() {
try {
const saveButton = document.getElementById('save-theme-settings-button');
saveButton.classList.add('btn-loading');
const themeMode = document.querySelector('input[name="theme_mode"]:checked').value;
const primaryColor = document.getElementById('primary-color').value;
const secondaryColor = document.getElementById('secondary-color').value;
const accentColor = document.getElementById('accent-color').value;
const fontFamily = document.getElementById('font-family').value;
const customCss = document.getElementById('custom-css').value;
// Create theme settings object
const themeSettings = {
theme_mode: themeMode,
primary_color: primaryColor,
secondary_color: secondaryColor,
accent_color: accentColor,
font_family: fontFamily,
custom_css: customCss
};
// Send theme settings to API
const response = await fetch('/dashboard/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
settings: {
theme: themeSettings
}
})
});
if (!response.ok) {
throw new Error('Failed to save theme settings');
}
// Show success message
const feedbackElement = document.getElementById('theme-settings-feedback');
feedbackElement.textContent = 'Theme settings saved successfully!';
feedbackElement.classList.add('text-success');
// Apply theme to the entire dashboard
applyThemeToDocument(themeSettings);
// Show toast notification
showToast('success', 'Success', 'Theme settings saved successfully!');
} catch (error) {
console.error('Error saving theme settings:', error);
// Show error message
const feedbackElement = document.getElementById('theme-settings-feedback');
feedbackElement.textContent = 'Failed to save theme settings. Please try again.';
feedbackElement.classList.add('text-danger');
// Show toast notification
showToast('error', 'Error', 'Failed to save theme settings');
} finally {
// Remove loading state from button
const saveButton = document.getElementById('save-theme-settings-button');
saveButton.classList.remove('btn-loading');
}
}
/**
* Reset theme settings to defaults
*/
function resetThemeSettings() {
// Set theme mode to light
document.getElementById('theme-mode-light').checked = true;
// Hide custom theme settings
document.getElementById('custom-theme-settings').style.display = 'none';
// Reset color values
document.getElementById('primary-color').value = '#5865F2';
document.getElementById('primary-color-text').value = '#5865F2';
document.getElementById('secondary-color').value = '#2D3748';
document.getElementById('secondary-color-text').value = '#2D3748';
document.getElementById('accent-color').value = '#7289DA';
document.getElementById('accent-color-text').value = '#7289DA';
// Reset font family
document.getElementById('font-family').value = 'Inter, sans-serif';
// Reset custom CSS
document.getElementById('custom-css').value = '';
// Update preview
updateThemePreview();
// Show toast notification
showToast('info', 'Reset', 'Theme settings reset to defaults');
}
/**
* Apply theme to the entire document
* @param {Object} theme - Theme settings object
*/
function applyThemeToDocument(theme) {
// Apply theme mode
if (theme.theme_mode === 'dark') {
document.body.classList.add('dark-mode');
document.body.classList.remove('custom-mode');
} else if (theme.theme_mode === 'light') {
document.body.classList.remove('dark-mode');
document.body.classList.remove('custom-mode');
} else if (theme.theme_mode === 'custom') {
document.body.classList.remove('dark-mode');
document.body.classList.add('custom-mode');
// Apply custom colors
document.documentElement.style.setProperty('--primary-color', theme.primary_color);
document.documentElement.style.setProperty('--secondary-color', theme.secondary_color);
document.documentElement.style.setProperty('--accent-color', theme.accent_color);
document.documentElement.style.setProperty('--font-family', theme.font_family);
// Apply custom CSS
const customStyleElement = document.getElementById('global-custom-theme-style');
if (customStyleElement) {
customStyleElement.textContent = theme.custom_css;
} else {
const style = document.createElement('style');
style.id = 'global-custom-theme-style';
style.textContent = theme.custom_css;
document.head.appendChild(style);
}
}
}

View File

@ -0,0 +1,107 @@
<!-- Theme Settings Section -->
<div id="theme-settings-section" class="dashboard-section" style="display: none;">
<div class="card">
<div class="card-header">
<h2 class="card-title">Theme Settings</h2>
</div>
</div>
<div id="theme-settings-form">
<!-- Theme Mode Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Theme Mode</h3>
</div>
<div class="form-group">
<div class="radio-group">
<input type="radio" id="theme-mode-light" name="theme_mode" value="light" checked>
<label for="theme-mode-light">Light Mode</label>
</div>
<div class="radio-group">
<input type="radio" id="theme-mode-dark" name="theme_mode" value="dark">
<label for="theme-mode-dark">Dark Mode</label>
</div>
<div class="radio-group">
<input type="radio" id="theme-mode-custom" name="theme_mode" value="custom">
<label for="theme-mode-custom">Custom Mode</label>
</div>
</div>
</div>
<!-- Color Settings Card -->
<div id="custom-theme-settings" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">Custom Colors</h3>
</div>
<div class="form-group">
<label for="primary-color">Primary Color:</label>
<div class="color-picker-container">
<input type="color" id="primary-color" value="#5865F2">
<input type="text" id="primary-color-text" value="#5865F2" class="color-text-input">
</div>
</div>
<div class="form-group">
<label for="secondary-color">Secondary Color:</label>
<div class="color-picker-container">
<input type="color" id="secondary-color" value="#2D3748">
<input type="text" id="secondary-color-text" value="#2D3748" class="color-text-input">
</div>
</div>
<div class="form-group">
<label for="accent-color">Accent Color:</label>
<div class="color-picker-container">
<input type="color" id="accent-color" value="#7289DA">
<input type="text" id="accent-color-text" value="#7289DA" class="color-text-input">
</div>
</div>
<div class="form-group">
<label for="font-family">Font Family:</label>
<select id="font-family" class="w-full">
<option value="Inter, sans-serif">Inter</option>
<option value="'Roboto', sans-serif">Roboto</option>
<option value="'Open Sans', sans-serif">Open Sans</option>
<option value="'Montserrat', sans-serif">Montserrat</option>
<option value="'Poppins', sans-serif">Poppins</option>
</select>
</div>
<div class="form-group">
<label for="custom-css">Custom CSS (Advanced):</label>
<textarea id="custom-css" rows="6" class="w-full" placeholder="Enter custom CSS here..."></textarea>
<small class="text-muted">Custom CSS will be applied to the dashboard. Use with caution.</small>
</div>
</div>
<!-- Preview Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Theme Preview</h3>
</div>
<div id="theme-preview" class="theme-preview">
<div class="preview-header">
<div class="preview-title">Header</div>
<div class="preview-button">Button</div>
</div>
<div class="preview-content">
<div class="preview-card">
<div class="preview-card-header">Card Title</div>
<div class="preview-card-body">
<p>This is a preview of how your theme will look.</p>
<div class="preview-form-control"></div>
<div class="preview-button-primary">Primary Button</div>
<div class="preview-button-secondary">Secondary Button</div>
</div>
</div>
</div>
</div>
</div>
<!-- Save Button -->
<div class="card">
<div class="btn-group">
<button id="save-theme-settings-button" class="btn btn-primary">Save Theme Settings</button>
<button id="reset-theme-settings-button" class="btn btn-warning">Reset to Defaults</button>
</div>
<p id="theme-settings-feedback" class="mt-2"></p>
</div>
</div>
</div>

View File

@ -54,7 +54,7 @@ class GuildCommandSyncer:
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 and descriptions.
Returns a list of commands with guild-specific customizations applied. Returns a list of commands with guild-specific customizations applied.
""" """
# Get all global commands # Get all global commands
@ -69,12 +69,18 @@ class GuildCommandSyncer:
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 and descriptions
guild_commands = [] guild_commands = []
for cmd in global_commands: for cmd in global_commands:
# Set guild_id attribute for use in _create_custom_command
setattr(cmd, 'guild_id', guild_id)
if cmd.name in customizations: if cmd.name in customizations:
# Create a copy of the command with the custom name # Get the custom name
custom_name = customizations[cmd.name] custom_data = customizations[cmd.name]
custom_name = custom_data.get('name', cmd.name)
# Create a copy of the command with the custom name and description
custom_cmd = self._create_custom_command(cmd, custom_name) custom_cmd = self._create_custom_command(cmd, custom_name)
guild_commands.append(custom_cmd) guild_commands.append(custom_cmd)
else: else:
@ -91,14 +97,23 @@ class GuildCommandSyncer:
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 and description.
This is a simplified version - in practice, you'd need to handle all command attributes. This is a simplified version - in practice, you'd need to handle all command attributes.
""" """
# For simplicity, we're just creating a basic copy with the custom name # Get custom description if available
custom_description = None
if hasattr(original_cmd, 'guild_id') and original_cmd.guild_id:
# This is a guild-specific command, get the custom description
custom_description = asyncio.run_coroutine_threadsafe(
settings_manager.get_custom_command_description(original_cmd.guild_id, original_cmd.name),
asyncio.get_event_loop()
).result()
# For simplicity, we're just creating a basic copy with the custom name and description
# In a real implementation, you'd need to handle all command attributes and options # In a real implementation, you'd need to handle all command attributes and options
custom_cmd = app_commands.Command( custom_cmd = app_commands.Command(
name=custom_name, name=custom_name,
description=original_cmd.description, description=custom_description or original_cmd.description,
callback=original_cmd.callback callback=original_cmd.callback
) )
@ -117,6 +132,11 @@ class GuildCommandSyncer:
# 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)
# Set the commands for this guild
self.bot.tree.clear_commands(guild=guild)
for cmd in guild_commands:
self.bot.tree.add_command(cmd, guild=guild)
# Sync commands with Discord # Sync commands with Discord
synced = await self.bot.tree.sync(guild=guild) synced = await self.bot.tree.sync(guild=guild)

20
main.py
View File

@ -201,6 +201,9 @@ async def on_command_error(ctx, error):
elif isinstance(error, CommandPermissionError): elif isinstance(error, CommandPermissionError):
await ctx.send(str(error), ephemeral=True) # Send the error message from the exception await ctx.send(str(error), ephemeral=True) # Send the error message from the exception
log.warning(f"Command '{ctx.command.qualified_name}' blocked for user {ctx.author.id} in guild {ctx.guild.id}: {error}") log.warning(f"Command '{ctx.command.qualified_name}' blocked for user {ctx.author.id} in guild {ctx.guild.id}: {error}")
elif isinstance(error, CommandDisabledError):
await ctx.send(str(error), ephemeral=True) # Send the error message from the exception
log.warning(f"Command '{ctx.command.qualified_name}' blocked for user {ctx.author.id} in guild {ctx.guild.id}: {error}")
else: else:
# Pass other errors to the original handler # Pass other errors to the original handler
await handle_error(ctx, error) await handle_error(ctx, error)
@ -232,6 +235,12 @@ class CommandPermissionError(commands.CheckFailure):
self.command_name = command_name self.command_name = command_name
super().__init__(f"You do not have the required role to use the command `{command_name}`.") super().__init__(f"You do not have the required role to use the command `{command_name}`.")
class CommandDisabledError(commands.CheckFailure):
"""Custom exception for disabled commands."""
def __init__(self, command_name):
self.command_name = command_name
super().__init__(f"The command `{command_name}` is disabled in this server.")
@bot.before_invoke @bot.before_invoke
async def global_command_checks(ctx: commands.Context): async def global_command_checks(ctx: commands.Context):
"""Global check run before any command invocation.""" """Global check run before any command invocation."""
@ -268,7 +277,14 @@ async def global_command_checks(ctx: commands.Context):
log.warning(f"Command '{command_name}' blocked in guild {guild_id}: Cog '{cog_name}' is disabled.") log.warning(f"Command '{command_name}' blocked in guild {guild_id}: Cog '{cog_name}' is disabled.")
raise CogDisabledError(cog_name) raise CogDisabledError(cog_name)
# 2. Check command permissions based on roles # 2. Check if the Command is enabled
# This only applies if the command has been explicitly disabled
is_cmd_enabled = await settings_manager.is_command_enabled(guild_id, command_name, default_enabled=True)
if not is_cmd_enabled:
log.warning(f"Command '{command_name}' blocked in guild {guild_id}: Command is disabled.")
raise CommandDisabledError(command_name)
# 3. Check command permissions based on roles
# This check only applies if specific permissions HAVE been set for this command. # This check only applies if specific permissions HAVE been set for this command.
# If no permissions are set in the DB, check_command_permission returns True. # If no permissions are set in the DB, check_command_permission returns True.
has_perm = await settings_manager.check_command_permission(guild_id, command_name, member_roles_ids) has_perm = await settings_manager.check_command_permission(guild_id, command_name, member_roles_ids)
@ -276,7 +292,7 @@ async def global_command_checks(ctx: commands.Context):
log.warning(f"Command '{command_name}' blocked for user {ctx.author.id} in guild {guild_id}: Insufficient role permissions.") log.warning(f"Command '{command_name}' blocked for user {ctx.author.id} in guild {guild_id}: Insufficient role permissions.")
raise CommandPermissionError(command_name) raise CommandPermissionError(command_name)
# If both checks pass, the command proceeds. # If all checks pass, the command proceeds.
log.debug(f"Command '{command_name}' passed global checks for user {ctx.author.id} in guild {guild_id}.") log.debug(f"Command '{command_name}' passed global checks for user {ctx.author.id} in guild {guild_id}.")

View File

@ -3,6 +3,7 @@ import redis.asyncio as redis
import os import os
import logging import logging
from dotenv import load_dotenv from dotenv import load_dotenv
from typing import Dict
# Load environment variables # Load environment variables
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '.env')) load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), '.env'))
@ -138,6 +139,17 @@ async def initialize_database():
); );
""") """)
# Enabled Commands table - Stores the explicit enabled/disabled state for individual commands
await conn.execute("""
CREATE TABLE IF NOT EXISTS enabled_commands (
guild_id BIGINT NOT NULL,
command_name TEXT NOT NULL,
enabled BOOLEAN NOT NULL,
PRIMARY KEY (guild_id, command_name),
FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE
);
""")
# Command Permissions table (simple role-based for now) # Command Permissions table (simple role-based for now)
await conn.execute(""" await conn.execute("""
CREATE TABLE IF NOT EXISTS command_permissions ( CREATE TABLE IF NOT EXISTS command_permissions (
@ -149,12 +161,13 @@ async def initialize_database():
); );
""") """)
# Command Customization table - Stores guild-specific command names # Command Customization table - Stores guild-specific command names and descriptions
await conn.execute(""" await conn.execute("""
CREATE TABLE IF NOT EXISTS command_customization ( CREATE TABLE IF NOT EXISTS command_customization (
guild_id BIGINT NOT NULL, guild_id BIGINT NOT NULL,
original_command_name TEXT NOT NULL, original_command_name TEXT NOT NULL,
custom_command_name TEXT NOT NULL, custom_command_name TEXT NOT NULL,
custom_command_description TEXT,
PRIMARY KEY (guild_id, original_command_name), PRIMARY KEY (guild_id, original_command_name),
FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE
); );
@ -428,6 +441,119 @@ async def set_cog_enabled(guild_id: int, cog_name: str, enabled: bool):
log.exception(f"Failed to invalidate Redis cache for cog enabled status '{cog_name}' (Guild: {guild_id}): {redis_err}") log.exception(f"Failed to invalidate Redis cache for cog enabled status '{cog_name}' (Guild: {guild_id}): {redis_err}")
return False return False
async def is_command_enabled(guild_id: int, command_name: str, default_enabled: bool = True) -> bool:
"""Checks if a command is enabled for a guild, checking cache first.
Uses default_enabled if no specific setting is found."""
if not pg_pool or not redis_pool:
log.warning(f"Pools not initialized, returning default for command '{command_name}'.")
return default_enabled
cache_key = _get_redis_key(guild_id, "cmd_enabled", command_name)
try:
cached_value = await redis_pool.get(cache_key)
if cached_value is not None:
log.debug(f"Cache hit for command enabled status '{command_name}' (Guild: {guild_id})")
return cached_value == "True" # Redis stores strings
except Exception as e:
log.exception(f"Redis error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
log.debug(f"Cache miss for command enabled status '{command_name}' (Guild: {guild_id})")
db_enabled_status = None
try:
async with pg_pool.acquire() as conn:
db_enabled_status = await conn.fetchval(
"SELECT enabled FROM enabled_commands WHERE guild_id = $1 AND command_name = $2",
guild_id, command_name
)
except Exception as e:
log.exception(f"Database error getting command enabled status for '{command_name}' (Guild: {guild_id}): {e}")
# Fallback to default on DB error after cache miss
return default_enabled
final_status = db_enabled_status if db_enabled_status is not None else default_enabled
# Cache the result (True or False)
try:
await redis_pool.set(cache_key, str(final_status), ex=3600) # Cache for 1 hour
except Exception as e:
log.exception(f"Redis error setting cache for command enabled status '{command_name}' (Guild: {guild_id}): {e}")
return final_status
async def set_command_enabled(guild_id: int, command_name: str, enabled: bool):
"""Sets the enabled status for a command in a guild and updates the cache."""
if not pg_pool or not redis_pool:
log.error(f"Pools not initialized, cannot set command enabled status for '{command_name}'.")
return False
cache_key = _get_redis_key(guild_id, "cmd_enabled", command_name)
try:
async with pg_pool.acquire() as conn:
# Ensure guild exists
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT (guild_id) DO NOTHING;", guild_id)
# Upsert the enabled status
await conn.execute(
"""
INSERT INTO enabled_commands (guild_id, command_name, enabled)
VALUES ($1, $2, $3)
ON CONFLICT (guild_id, command_name) DO UPDATE SET enabled = $3;
""",
guild_id, command_name, enabled
)
# Update cache
await redis_pool.set(cache_key, str(enabled), ex=3600)
log.info(f"Set command '{command_name}' enabled status to {enabled} for guild {guild_id}")
return True
except Exception as e:
log.exception(f"Database or Redis error setting command enabled status for '{command_name}' in guild {guild_id}: {e}")
# Attempt to invalidate cache on error
try:
await redis_pool.delete(cache_key)
except Exception as redis_err:
log.exception(f"Failed to invalidate Redis cache for command enabled status '{command_name}' (Guild: {guild_id}): {redis_err}")
return False
async def get_all_enabled_commands(guild_id: int) -> Dict[str, bool]:
"""Gets all command enabled statuses for a guild.
Returns a dictionary of command_name -> enabled status."""
if not pg_pool:
log.error(f"Database pool not initialized, cannot get command enabled statuses for guild {guild_id}.")
return {}
try:
async with pg_pool.acquire() as conn:
records = await conn.fetch(
"SELECT command_name, enabled FROM enabled_commands WHERE guild_id = $1",
guild_id
)
return {record['command_name']: record['enabled'] for record in records}
except Exception as e:
log.exception(f"Database error getting command enabled statuses for guild {guild_id}: {e}")
return {}
async def get_all_enabled_cogs(guild_id: int) -> Dict[str, bool]:
"""Gets all cog enabled statuses for a guild.
Returns a dictionary of cog_name -> enabled status."""
if not pg_pool:
log.error(f"Database pool not initialized, cannot get cog enabled statuses for guild {guild_id}.")
return {}
try:
async with pg_pool.acquire() as conn:
records = await conn.fetch(
"SELECT cog_name, enabled FROM enabled_cogs WHERE guild_id = $1",
guild_id
)
return {record['cog_name']: record['enabled'] for record in records}
except Exception as e:
log.exception(f"Database error getting cog enabled statuses for guild {guild_id}: {e}")
return {}
# --- Command Permission Functions --- # --- Command Permission Functions ---
async def add_command_permission(guild_id: int, command_name: str, role_id: int) -> bool: async def add_command_permission(guild_id: int, command_name: str, role_id: int) -> bool:
@ -680,6 +806,39 @@ async def get_custom_command_name(guild_id: int, original_command_name: str) ->
return custom_name return custom_name
async def get_custom_command_description(guild_id: int, original_command_name: str) -> str | None:
"""Gets the custom command description for a guild, checking cache first.
Returns None if no custom description is set."""
if not pg_pool or not redis_pool:
log.warning(f"Pools not initialized, returning None for custom command description '{original_command_name}'.")
return None
cache_key = _get_redis_key(guild_id, "cmd_desc", original_command_name)
try:
cached_value = await redis_pool.get(cache_key)
if cached_value is not None:
log.debug(f"Cache hit for custom command description '{original_command_name}' (Guild: {guild_id})")
return None if cached_value == "__NONE__" else cached_value
except Exception as e:
log.exception(f"Redis error getting custom command description for '{original_command_name}' (Guild: {guild_id}): {e}")
log.debug(f"Cache miss for custom command description '{original_command_name}' (Guild: {guild_id})")
async with pg_pool.acquire() as conn:
custom_desc = await conn.fetchval(
"SELECT custom_command_description FROM command_customization WHERE guild_id = $1 AND original_command_name = $2",
guild_id, original_command_name
)
# Cache the result (even if None)
try:
value_to_cache = custom_desc if custom_desc is not None else "__NONE__"
await redis_pool.set(cache_key, value_to_cache, ex=3600) # Cache for 1 hour
except Exception as e:
log.exception(f"Redis error setting cache for custom command description '{original_command_name}' (Guild: {guild_id}): {e}")
return custom_desc
async def set_custom_command_name(guild_id: int, original_command_name: str, custom_command_name: str | None) -> bool: async def set_custom_command_name(guild_id: int, original_command_name: str, custom_command_name: str | None) -> bool:
"""Sets a custom command name for a guild and updates the cache. """Sets a custom command name for a guild and updates the cache.
Setting custom_command_name to None removes the customization.""" Setting custom_command_name to None removes the customization."""
@ -727,6 +886,74 @@ async def set_custom_command_name(guild_id: int, original_command_name: str, cus
return False return False
async def set_custom_command_description(guild_id: int, original_command_name: str, custom_command_description: str | None) -> bool:
"""Sets a custom command description for a guild and updates the cache.
Setting custom_command_description to None removes the description."""
if not pg_pool or not redis_pool:
log.error(f"Pools not initialized, cannot set custom command description for '{original_command_name}'.")
return False
cache_key = _get_redis_key(guild_id, "cmd_desc", original_command_name)
try:
async with pg_pool.acquire() as conn:
# Ensure guild exists
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT (guild_id) DO NOTHING;", guild_id)
# Check if the command customization exists
exists = await conn.fetchval(
"SELECT 1 FROM command_customization WHERE guild_id = $1 AND original_command_name = $2",
guild_id, original_command_name
)
if custom_command_description is not None:
if exists:
# Update the existing record
await conn.execute(
"""
UPDATE command_customization
SET custom_command_description = $3
WHERE guild_id = $1 AND original_command_name = $2;
""",
guild_id, original_command_name, custom_command_description
)
else:
# Insert a new record with default custom_command_name (same as original)
await conn.execute(
"""
INSERT INTO command_customization (guild_id, original_command_name, custom_command_name, custom_command_description)
VALUES ($1, $2, $2, $3);
""",
guild_id, original_command_name, custom_command_description
)
# Update cache
await redis_pool.set(cache_key, custom_command_description, ex=3600)
log.info(f"Set custom command description for '{original_command_name}' for guild {guild_id}")
else:
if exists:
# Update the existing record to remove the description
await conn.execute(
"""
UPDATE command_customization
SET custom_command_description = NULL
WHERE guild_id = $1 AND original_command_name = $2;
""",
guild_id, original_command_name
)
# Update cache to indicate no description
await redis_pool.set(cache_key, "__NONE__", ex=3600)
log.info(f"Removed custom command description for '{original_command_name}' for guild {guild_id}")
return True
except Exception as e:
log.exception(f"Database or Redis error setting custom command description for '{original_command_name}' in guild {guild_id}: {e}")
# Attempt to invalidate cache on error
try:
await redis_pool.delete(cache_key)
except Exception as redis_err:
log.exception(f"Failed to invalidate Redis cache for custom command description '{original_command_name}' (Guild: {guild_id}): {redis_err}")
return False
async def get_custom_group_name(guild_id: int, original_group_name: str) -> str | None: async def get_custom_group_name(guild_id: int, original_group_name: str) -> str | None:
"""Gets the custom command group name for a guild, checking cache first. """Gets the custom command group name for a guild, checking cache first.
Returns None if no custom name is set.""" Returns None if no custom name is set."""
@ -923,19 +1150,26 @@ async def get_command_aliases(guild_id: int, original_command_name: str) -> list
return None # Indicate error return None # Indicate error
async def get_all_command_customizations(guild_id: int) -> dict[str, str] | None: async def get_all_command_customizations(guild_id: int) -> dict[str, dict[str, str]] | None:
"""Gets all command customizations for a guild. """Gets all command customizations for a guild.
Returns a dictionary mapping original command names to custom names, or None on error.""" Returns a dictionary mapping original command names to a dict with 'name' and 'description' keys,
or None on error."""
if not pg_pool: if not pg_pool:
log.error("Pools not initialized, cannot get command customizations.") log.error("Pools not initialized, cannot get command customizations.")
return None return None
try: try:
async with pg_pool.acquire() as conn: async with pg_pool.acquire() as conn:
records = await conn.fetch( records = await conn.fetch(
"SELECT original_command_name, custom_command_name FROM command_customization WHERE guild_id = $1", "SELECT original_command_name, custom_command_name, custom_command_description FROM command_customization WHERE guild_id = $1",
guild_id guild_id
) )
customizations = {record['original_command_name']: record['custom_command_name'] for record in records} customizations = {}
for record in records:
cmd_name = record['original_command_name']
customizations[cmd_name] = {
'name': record['custom_command_name'],
'description': record['custom_command_description']
}
log.debug(f"Fetched {len(customizations)} command customizations for guild {guild_id}.") log.debug(f"Fetched {len(customizations)} command customizations for guild {guild_id}.")
return customizations return customizations
except Exception as e: except Exception as e: