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

@ -28,6 +28,15 @@ class Conversation(BaseModel):
web_search_enabled: bool = False
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):
# General settings
model_id: str = "openai/gpt-3.5-turbo"
@ -54,6 +63,9 @@ class UserSettings(BaseModel):
advanced_view_enabled: bool = False
streaming_enabled: bool = True
# Theme settings
theme: ThemeSettings = Field(default_factory=ThemeSettings)
# Last updated timestamp
last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now)

View File

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

View File

@ -88,8 +88,16 @@ async def get_command_customizations(
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(
command_customizations=command_customizations,
command_customizations=formatted_command_customizations,
group_customizations=group_customizations,
command_aliases=command_aliases
)
@ -110,7 +118,7 @@ async def set_command_customization(
_user: dict = Depends(get_dashboard_user),
_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:
# Check if settings_manager is available
if not settings_manager or not settings_manager.pg_pool:
@ -133,19 +141,41 @@ async def set_command_customization(
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
success = await settings_manager.set_custom_command_name(
name_success = await settings_manager.set_custom_command_name(
guild_id,
customization.command_name,
customization.custom_name
)
if not success:
if not name_success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
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"}
except HTTPException:
# 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 pydantic import BaseModel, Field
# Default prefix for commands
DEFAULT_PREFIX = "!"
# Import the dependencies from api_server.py
try:
# Try relative import first
@ -78,6 +81,14 @@ class Message(BaseModel):
role: str # 'user' or 'assistant'
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):
system_message: Optional[str] = None
character: Optional[str] = None
@ -86,6 +97,19 @@ class GlobalSettings(BaseModel):
model: Optional[str] = None
temperature: Optional[float] = 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 ---
@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)}"
)
@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)
async def update_guild_settings(
guild_id: int,
@ -444,7 +520,14 @@ async def update_guild_settings(
log.debug(f"Update data received: {settings_update}")
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
if 'prefix' in settings_update:
@ -494,6 +577,14 @@ async def update_guild_settings(
else:
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
return {"message": "Settings updated successfully."}
else:
@ -628,7 +719,7 @@ async def get_global_settings(
)
# Convert from UserSettings to GlobalSettings
return GlobalSettings(
global_settings = GlobalSettings(
system_message=user_settings.get("system_message", ""),
character=user_settings.get("character", ""),
character_info=user_settings.get("character_info", ""),
@ -637,6 +728,20 @@ async def get_global_settings(
temperature=user_settings.get("temperature", 0.7),
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:
# Re-raise HTTP exceptions
raise
@ -688,6 +793,18 @@ async def update_global_settings(
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
updated_settings = db.save_user_settings(user_id, user_settings)
if not updated_settings:
@ -708,6 +825,200 @@ async def update_global_settings(
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 ---
@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/components.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>
<body>
<!-- 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>
Modules
</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">
<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
@ -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>
AI Settings
</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 class="sidebar-footer">
<button id="logout-button" class="btn btn-danger w-full">
@ -366,6 +381,340 @@
</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>
@ -376,5 +725,8 @@
<script src="js/utils.js"></script>
<script src="js/main.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>
</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) {
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]:
"""
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.
"""
# Get all global commands
@ -69,12 +69,18 @@ class GuildCommandSyncer:
if not customizations:
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 = []
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:
# Create a copy of the command with the custom name
custom_name = customizations[cmd.name]
# Get the custom 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)
guild_commands.append(custom_cmd)
else:
@ -91,14 +97,23 @@ class GuildCommandSyncer:
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.
"""
# 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
custom_cmd = app_commands.Command(
name=custom_name,
description=original_cmd.description,
description=custom_description or original_cmd.description,
callback=original_cmd.callback
)
@ -117,6 +132,11 @@ class GuildCommandSyncer:
# Prepare guild-specific commands
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
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):
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}")
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:
# Pass other errors to the original handler
await handle_error(ctx, error)
@ -232,6 +235,12 @@ class CommandPermissionError(commands.CheckFailure):
self.command_name = 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
async def global_command_checks(ctx: commands.Context):
"""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.")
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.
# 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)
@ -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.")
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}.")

View File

@ -3,6 +3,7 @@ import redis.asyncio as redis
import os
import logging
from dotenv import load_dotenv
from typing import Dict
# Load environment variables
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)
await conn.execute("""
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("""
CREATE TABLE IF NOT EXISTS command_customization (
guild_id BIGINT NOT NULL,
original_command_name TEXT NOT NULL,
custom_command_name TEXT NOT NULL,
custom_command_description TEXT,
PRIMARY KEY (guild_id, original_command_name),
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}")
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 ---
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
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:
"""Sets a custom command name for a guild and updates the cache.
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
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:
"""Gets the custom command group name for a guild, checking cache first.
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
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.
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:
log.error("Pools not initialized, cannot get command customizations.")
return None
try:
async with pg_pool.acquire() as conn:
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
)
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}.")
return customizations
except Exception as e: