feat: Implement custom bot management dashboard

- Add `custom_bot_manager.py` for core bot lifecycle management.
- Introduce new API endpoints for custom bot status, start, stop, restart, and log retrieval.
- Extend `UserSettings` and `GlobalSettings` models with custom bot configuration options (token, enabled, prefix, status).
- Create a dedicated "Custom Bot" page in the dashboard (`custom-bot.html`) with associated JavaScript to configure settings and control the bot.
- Integrate custom bot initialization into the application startup.
This commit is contained in:
Slipstream 2025-05-21 18:11:17 -06:00
parent 0c2e599f77
commit 172f5907b3
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
8 changed files with 983 additions and 1 deletions

View File

@ -66,6 +66,13 @@ class UserSettings(BaseModel):
# Theme settings # Theme settings
theme: ThemeSettings = Field(default_factory=ThemeSettings) theme: ThemeSettings = Field(default_factory=ThemeSettings)
# Custom bot settings
custom_bot_token: Optional[str] = None
custom_bot_enabled: bool = False
custom_bot_prefix: str = "!"
custom_bot_status_text: str = "!help"
custom_bot_status_type: str = "listening" # "playing", "listening", "watching", "competing"
# Last updated timestamp # Last updated timestamp
last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now) last_updated: datetime.datetime = Field(default_factory=datetime.datetime.now)

View File

@ -30,6 +30,14 @@ from api_service.dashboard_models import (
# Import settings_manager for database access (use absolute path) # Import settings_manager for database access (use absolute path)
import settings_manager import settings_manager
# Import custom bot manager
import sys
import os
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
if parent_dir not in sys.path:
sys.path.append(parent_dir)
import custom_bot_manager
# Set up logging # Set up logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -84,6 +92,13 @@ class GlobalSettings(BaseModel):
max_tokens: Optional[int] = None max_tokens: Optional[int] = None
theme: Optional[ThemeSettings] = None theme: Optional[ThemeSettings] = None
# Custom bot settings
custom_bot_token: Optional[str] = None
custom_bot_enabled: Optional[bool] = None
custom_bot_prefix: Optional[str] = None
custom_bot_status_text: Optional[str] = None
custom_bot_status_type: Optional[str] = None
# CogInfo and CommandInfo models are now imported from dashboard_models # CogInfo and CommandInfo models are now imported from dashboard_models
# class CommandInfo(BaseModel): # Removed - Imported from dashboard_models # class CommandInfo(BaseModel): # Removed - Imported from dashboard_models
@ -986,7 +1001,12 @@ async def update_global_settings(
system_message=settings.system_message, system_message=settings.system_message,
character=settings.character, character=settings.character,
character_info=settings.character_info, character_info=settings.character_info,
custom_instructions=settings.custom_instructions custom_instructions=settings.custom_instructions,
custom_bot_token=settings.custom_bot_token,
custom_bot_enabled=settings.custom_bot_enabled,
custom_bot_prefix=settings.custom_bot_prefix,
custom_bot_status_text=settings.custom_bot_status_text,
custom_bot_status_type=settings.custom_bot_status_type
) )
# Add theme settings if provided # Add theme settings if provided
@ -1021,6 +1041,172 @@ async def update_global_settings(
detail=f"Error updating global settings: {str(e)}" detail=f"Error updating global settings: {str(e)}"
) )
# --- Custom Bot Management Endpoints ---
class CustomBotStatus(BaseModel):
exists: bool
status: str
error: Optional[str] = None
is_running: bool
@router.get("/custom-bot/status", response_model=CustomBotStatus)
async def get_custom_bot_status(
_user: dict = Depends(get_dashboard_user)
):
"""Get the status of the user's custom bot."""
try:
user_id = _user.get('user_id')
if not user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User ID not found in session"
)
# Get the status from the custom bot manager
bot_status = custom_bot_manager.get_custom_bot_status(user_id)
return CustomBotStatus(**bot_status)
except Exception as e:
log.error(f"Error getting custom bot status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting custom bot status: {str(e)}"
)
@router.post("/custom-bot/start", status_code=status.HTTP_200_OK)
async def start_custom_bot(
_user: dict = Depends(get_dashboard_user)
):
"""Start the user's custom bot."""
try:
user_id = _user.get('user_id')
if not user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User ID not found in session"
)
# Import the database module for user settings
try:
from api_service.api_server import db
except ImportError:
from api_service.api_server import db
if not db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Database connection not available"
)
# Get user settings from the database
user_settings = db.get_user_settings(user_id)
if not user_settings:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User settings not found"
)
# Check if custom bot token is set
if not user_settings.custom_bot_token:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Custom bot token not set. Please set a token first."
)
# Create the bot if it doesn't exist
bot_status = custom_bot_manager.get_custom_bot_status(user_id)
if not bot_status["exists"]:
success, message = await custom_bot_manager.create_custom_bot(
user_id=user_id,
token=user_settings.custom_bot_token,
prefix=user_settings.custom_bot_prefix,
status_type=user_settings.custom_bot_status_type,
status_text=user_settings.custom_bot_status_text
)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error creating custom bot: {message}"
)
# Start the bot
success, message = custom_bot_manager.run_custom_bot_in_thread(
user_id=user_id,
token=user_settings.custom_bot_token
)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error starting custom bot: {message}"
)
# Update the enabled status in user settings
user_settings.custom_bot_enabled = True
db.save_user_settings(user_id, user_settings)
return {"message": "Custom bot started successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error starting custom bot: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error starting custom bot: {str(e)}"
)
@router.post("/custom-bot/stop", status_code=status.HTTP_200_OK)
async def stop_custom_bot(
_user: dict = Depends(get_dashboard_user)
):
"""Stop the user's custom bot."""
try:
user_id = _user.get('user_id')
if not user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User ID not found in session"
)
# Import the database module for user settings
try:
from api_service.api_server import db
except ImportError:
from api_service.api_server import db
if not db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Database connection not available"
)
# Stop the bot
success, message = custom_bot_manager.stop_custom_bot(user_id)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error stopping custom bot: {message}"
)
# Update the enabled status in user settings
user_settings = db.get_user_settings(user_id)
if user_settings:
user_settings.custom_bot_enabled = False
db.save_user_settings(user_id, user_settings)
return {"message": "Custom bot stopped successfully"}
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
log.error(f"Error stopping custom bot: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error stopping custom bot: {str(e)}"
)
# --- Cog and Command Management Endpoints --- # --- Cog and Command Management Endpoints ---
# Note: These endpoints have been moved to cog_management_endpoints.py # Note: These endpoints have been moved to cog_management_endpoints.py

View File

@ -0,0 +1,126 @@
<!-- Custom Bot Section -->
<div id="custom-bot-section" class="dashboard-section" style="display: none;">
<div class="card">
<div class="card-header">
<h2 class="card-title">Custom Bot</h2>
<p class="text-sm text-gray-600">Run your own personalized Discord bot with your own token, profile picture, and username.</p>
</div>
</div>
<!-- Bot Status Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Bot Status</h3>
</div>
<div class="card-body">
<div id="bot-status-indicator" class="flex items-center mb-4">
<div id="status-dot" class="w-4 h-4 rounded-full bg-gray-400 mr-2"></div>
<span id="status-text">Checking status...</span>
</div>
<div id="bot-controls" class="flex gap-2">
<button id="start-bot-button" class="btn btn-primary" disabled>Start Bot</button>
<button id="stop-bot-button" class="btn btn-danger" disabled>Stop Bot</button>
</div>
<div id="bot-error" class="mt-4 text-red-500 hidden"></div>
</div>
</div>
<!-- Bot Configuration Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Bot Configuration</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="bot-token-input">Bot Token:</label>
<input type="password" id="bot-token-input" class="w-full" placeholder="Enter your Discord bot token">
<p class="text-sm text-gray-600 mt-1">Your bot token is stored securely and never shared.</p>
</div>
<div class="form-group">
<label for="bot-prefix-input">Command Prefix:</label>
<input type="text" id="bot-prefix-input" class="w-full" placeholder="!" maxlength="5">
<p class="text-sm text-gray-600 mt-1">The prefix users will type before commands (e.g., !help).</p>
</div>
<div class="form-group">
<label for="bot-status-type-select">Status Type:</label>
<select id="bot-status-type-select" class="w-full">
<option value="playing">Playing</option>
<option value="listening">Listening to</option>
<option value="watching">Watching</option>
<option value="competing">Competing in</option>
</select>
</div>
<div class="form-group">
<label for="bot-status-text-input">Status Text:</label>
<input type="text" id="bot-status-text-input" class="w-full" placeholder="!help" maxlength="128">
<p class="text-sm text-gray-600 mt-1">The text that will appear in your bot's status.</p>
</div>
<div class="btn-group">
<button id="save-bot-config-button" class="btn btn-primary">Save Configuration</button>
</div>
</div>
</div>
<!-- Bot Creation Guide Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">How to Create Your Bot</h3>
</div>
<div class="card-body">
<ol class="list-decimal pl-5 space-y-4">
<li>
<strong>Create a Discord Application:</strong>
<p>Go to the <a href="https://discord.com/developers/applications" target="_blank" class="text-blue-500 hover:underline">Discord Developer Portal</a> and click "New Application".</p>
</li>
<li>
<strong>Set Up Your Bot:</strong>
<p>In your application, go to the "Bot" tab and click "Add Bot".</p>
</li>
<li>
<strong>Customize Your Bot:</strong>
<p>Upload a profile picture and set a username for your bot.</p>
</li>
<li>
<strong>Get Your Bot Token:</strong>
<p>Click "Reset Token" to generate a new token, then copy it.</p>
<p class="text-red-500">IMPORTANT: Never share your bot token with anyone!</p>
</li>
<li>
<strong>Set Bot Permissions:</strong>
<p>In the "Bot" tab, under "Privileged Gateway Intents", enable:</p>
<ul class="list-disc pl-5">
<li>Presence Intent</li>
<li>Server Members Intent</li>
<li>Message Content Intent</li>
</ul>
</li>
<li>
<strong>Invite Your Bot:</strong>
<p>Go to the "OAuth2" tab, then "URL Generator". Select the following scopes:</p>
<ul class="list-disc pl-5">
<li>bot</li>
<li>applications.commands</li>
</ul>
<p>Then select the following bot permissions:</p>
<ul class="list-disc pl-5">
<li>Send Messages</li>
<li>Embed Links</li>
<li>Attach Files</li>
<li>Read Message History</li>
<li>Use Slash Commands</li>
<li>Add Reactions</li>
</ul>
<p>Copy the generated URL and open it in your browser to invite the bot to your server.</p>
</li>
<li>
<strong>Configure Your Bot:</strong>
<p>Paste your bot token in the configuration form above and save it.</p>
</li>
<li>
<strong>Start Your Bot:</strong>
<p>Click the "Start Bot" button to bring your bot online!</p>
</li>
</ol>
</div>
</div>
</div>

View File

@ -77,6 +77,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"><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> <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 Theme Settings
</a> </a>
<a href="#custom-bot" class="nav-item" data-section="custom-bot-section" id="nav-custom-bot">
<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"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
Custom Bot
</a>
<a href="#command-customization" class="nav-item" data-section="command-customization-section"> <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> <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 Command Customization
@ -526,6 +530,14 @@
</div> </div>
</div> </div>
<!-- Include Custom Bot Section -->
<div id="custom-bot-section" class="dashboard-section" style="display: none;">
<!-- This will be loaded from custom-bot.html -->
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
<!-- Command Customization Templates --> <!-- Command Customization Templates -->
<template id="command-item-template"> <template id="command-item-template">
<div class="command-item"> <div class="command-item">
@ -611,6 +623,7 @@
<script src="js/main.js"></script> <script src="js/main.js"></script>
<!-- <script src="js/ai-settings.js"></script> --> <!-- Removed AI settings script --> <!-- <script src="js/ai-settings.js"></script> --> <!-- Removed AI settings script -->
<script src="js/theme-settings.js"></script> <script src="js/theme-settings.js"></script>
<script src="js/custom-bot.js"></script>
<script src="js/command-customization.js"></script> <script src="js/command-customization.js"></script>
<script src="js/cog-management.js"></script> <script src="js/cog-management.js"></script>
</body> </body>

View File

@ -0,0 +1,275 @@
/**
* Custom Bot Management
* This module handles the custom bot functionality in the dashboard.
*/
// Status constants
const BOT_STATUS = {
NOT_CREATED: 'not_created',
RUNNING: 'running',
STOPPED: 'stopped',
ERROR: 'error'
};
// DOM elements
let botTokenInput;
let botPrefixInput;
let botStatusTypeSelect;
let botStatusTextInput;
let saveBotConfigButton;
let startBotButton;
let stopBotButton;
let statusDot;
let statusText;
let botError;
/**
* Initialize the custom bot functionality
*/
function initCustomBot() {
// Get DOM elements
botTokenInput = document.getElementById('bot-token-input');
botPrefixInput = document.getElementById('bot-prefix-input');
botStatusTypeSelect = document.getElementById('bot-status-type-select');
botStatusTextInput = document.getElementById('bot-status-text-input');
saveBotConfigButton = document.getElementById('save-bot-config-button');
startBotButton = document.getElementById('start-bot-button');
stopBotButton = document.getElementById('stop-bot-button');
statusDot = document.getElementById('status-dot');
statusText = document.getElementById('status-text');
botError = document.getElementById('bot-error');
// Add event listeners
if (saveBotConfigButton) {
saveBotConfigButton.addEventListener('click', saveCustomBotConfig);
}
if (startBotButton) {
startBotButton.addEventListener('click', startCustomBot);
}
if (stopBotButton) {
stopBotButton.addEventListener('click', stopCustomBot);
}
// Load custom bot settings
loadCustomBotSettings();
// Check bot status periodically
checkBotStatus();
setInterval(checkBotStatus, 10000); // Check every 10 seconds
}
/**
* Load custom bot settings from the server
*/
async function loadCustomBotSettings() {
try {
const response = await API.get('/dashboard/api/settings');
// Fill in the form fields
if (botTokenInput && response.custom_bot_token) {
botTokenInput.value = response.custom_bot_token;
}
if (botPrefixInput) {
botPrefixInput.value = response.custom_bot_prefix || '!';
}
if (botStatusTypeSelect) {
botStatusTypeSelect.value = response.custom_bot_status_type || 'listening';
}
if (botStatusTextInput) {
botStatusTextInput.value = response.custom_bot_status_text || '!help';
}
} catch (error) {
console.error('Error loading custom bot settings:', error);
Toast.error('Failed to load custom bot settings');
}
}
/**
* Save custom bot configuration
*/
async function saveCustomBotConfig() {
try {
// Validate inputs
if (!botTokenInput.value) {
Toast.error('Bot token is required');
return;
}
if (!botPrefixInput.value) {
Toast.error('Command prefix is required');
return;
}
if (!botStatusTextInput.value) {
Toast.error('Status text is required');
return;
}
// Get current settings first
const currentSettings = await API.get('/dashboard/api/settings');
// Prepare the settings object with updated bot settings
const settings = {
...currentSettings,
custom_bot_token: botTokenInput.value,
custom_bot_prefix: botPrefixInput.value,
custom_bot_status_type: botStatusTypeSelect.value,
custom_bot_status_text: botStatusTextInput.value
};
// Save the settings
await API.put('/dashboard/api/settings', settings);
Toast.success('Custom bot configuration saved successfully');
// Check bot status after saving
checkBotStatus();
} catch (error) {
console.error('Error saving custom bot configuration:', error);
Toast.error('Failed to save custom bot configuration');
}
}
/**
* Start the custom bot
*/
async function startCustomBot() {
try {
startBotButton.disabled = true;
startBotButton.textContent = 'Starting...';
await API.post('/dashboard/api/custom-bot/start');
Toast.success('Custom bot started successfully');
// Check bot status after starting
checkBotStatus();
} catch (error) {
console.error('Error starting custom bot:', error);
Toast.error('Failed to start custom bot: ' + (error.response?.data?.detail || error.message));
// Re-enable the button
startBotButton.disabled = false;
startBotButton.textContent = 'Start Bot';
}
}
/**
* Stop the custom bot
*/
async function stopCustomBot() {
try {
stopBotButton.disabled = true;
stopBotButton.textContent = 'Stopping...';
await API.post('/dashboard/api/custom-bot/stop');
Toast.success('Custom bot stopped successfully');
// Check bot status after stopping
checkBotStatus();
} catch (error) {
console.error('Error stopping custom bot:', error);
Toast.error('Failed to stop custom bot: ' + (error.response?.data?.detail || error.message));
// Re-enable the button
stopBotButton.disabled = false;
stopBotButton.textContent = 'Stop Bot';
}
}
/**
* Check the status of the custom bot
*/
async function checkBotStatus() {
try {
const response = await API.get('/dashboard/api/custom-bot/status');
// Update the status indicator
updateStatusIndicator(response);
// Update button states
updateButtonStates(response);
// Show error if any
if (response.error && botError) {
botError.textContent = response.error;
botError.classList.remove('hidden');
} else if (botError) {
botError.classList.add('hidden');
}
} catch (error) {
console.error('Error checking custom bot status:', error);
// Set status to unknown
if (statusDot) {
statusDot.className = 'w-4 h-4 rounded-full bg-gray-400 mr-2';
}
if (statusText) {
statusText.textContent = 'Unable to check status';
}
// Disable buttons
if (startBotButton) {
startBotButton.disabled = true;
}
if (stopBotButton) {
stopBotButton.disabled = true;
}
}
}
/**
* Update the status indicator based on the bot status
*/
function updateStatusIndicator(status) {
if (!statusDot || !statusText) return;
if (status.is_running) {
statusDot.className = 'w-4 h-4 rounded-full bg-green-500 mr-2';
statusText.textContent = 'Bot is running';
} else if (status.status === BOT_STATUS.ERROR) {
statusDot.className = 'w-4 h-4 rounded-full bg-red-500 mr-2';
statusText.textContent = 'Bot has an error';
} else if (status.exists) {
statusDot.className = 'w-4 h-4 rounded-full bg-yellow-500 mr-2';
statusText.textContent = 'Bot is stopped';
} else {
statusDot.className = 'w-4 h-4 rounded-full bg-gray-400 mr-2';
statusText.textContent = 'Bot not created yet';
}
}
/**
* Update button states based on the bot status
*/
function updateButtonStates(status) {
if (!startBotButton || !stopBotButton) return;
if (status.is_running) {
startBotButton.disabled = true;
stopBotButton.disabled = false;
startBotButton.textContent = 'Bot Running';
stopBotButton.textContent = 'Stop Bot';
} else if (status.exists) {
startBotButton.disabled = false;
stopBotButton.disabled = true;
startBotButton.textContent = 'Start Bot';
stopBotButton.textContent = 'Bot Stopped';
} else {
startBotButton.disabled = false;
stopBotButton.disabled = true;
startBotButton.textContent = 'Create & Start Bot';
stopBotButton.textContent = 'Stop Bot';
}
}
// Export the initialization function
window.initCustomBot = initCustomBot;

View File

@ -343,6 +343,12 @@ function showSection(sectionId) {
// themeSettingsLoaded = true; // Assuming loadThemeSettings handles this // themeSettingsLoaded = true; // Assuming loadThemeSettings handles this
} }
// Load custom bot section if needed
if (sectionId === 'custom-bot') {
console.log("Loading custom bot section");
loadCustomBotSection();
}
// Load cog management if needed // Load cog management if needed
if (sectionId === 'cog-management' && typeof loadCogManagementData === 'function') { if (sectionId === 'cog-management' && typeof loadCogManagementData === 'function') {
// Check if already loaded for this guild // Check if already loaded for this guild
@ -1539,6 +1545,36 @@ function setupSaveSettingsButtons(guildId) {
} }
} }
/**
* Load the custom bot section
*/
function loadCustomBotSection() {
const customBotSection = document.getElementById('custom-bot-section');
if (!customBotSection) return;
// Show loading state
customBotSection.innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner"></div></div>';
// Load the custom bot HTML
fetch('/dashboard/custom-bot.html')
.then(response => response.text())
.then(html => {
// Insert the HTML
customBotSection.innerHTML = html;
// Initialize the custom bot functionality
if (typeof initCustomBot === 'function') {
initCustomBot();
} else {
console.error('initCustomBot function not found. Make sure custom-bot.js is loaded.');
}
})
.catch(error => {
console.error('Error loading custom bot section:', error);
customBotSection.innerHTML = '<div class="alert alert-danger">Error loading custom bot section. Please try again.</div>';
});
}
function setupWelcomeLeaveTestButtons(guildId) { function setupWelcomeLeaveTestButtons(guildId) {
// Welcome message test button // Welcome message test button
const testWelcomeButton = document.getElementById('test-welcome-button'); const testWelcomeButton = document.getElementById('test-welcome-button');

269
custom_bot_manager.py Normal file
View File

@ -0,0 +1,269 @@
"""
Custom Bot Manager for handling user-specific bot instances.
This module provides functionality to create, start, stop, and manage custom bot instances
based on user-provided tokens.
"""
import os
import sys
import asyncio
import threading
import logging
import discord
from discord.ext import commands
import traceback
from typing import Dict, Optional, Tuple, List
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
log = logging.getLogger(__name__)
# Global storage for custom bot instances and their threads
custom_bots: Dict[str, commands.Bot] = {} # user_id -> bot instance
custom_bot_threads: Dict[str, threading.Thread] = {} # user_id -> thread
custom_bot_status: Dict[str, str] = {} # user_id -> status (running, stopped, error)
custom_bot_errors: Dict[str, str] = {} # user_id -> error message
# Status constants
STATUS_RUNNING = "running"
STATUS_STOPPED = "stopped"
STATUS_ERROR = "error"
# Default cogs to load for custom bots
DEFAULT_COGS = [
"cogs.help_cog",
"cogs.settings_cog",
"cogs.utility_cog",
"cogs.fun_cog",
"cogs.moderation_cog"
]
class CustomBot(commands.Bot):
"""Custom bot class with additional functionality for user-specific bots."""
def __init__(self, user_id: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user_id = user_id
self.owner_id = int(os.getenv('OWNER_USER_ID', '0'))
async def setup_hook(self):
"""Called when the bot is first connected to Discord."""
log.info(f"Custom bot for user {self.user_id} is setting up...")
# Load default cogs
for cog in DEFAULT_COGS:
try:
await self.load_extension(cog)
log.info(f"Loaded extension {cog} for custom bot {self.user_id}")
except Exception as e:
log.error(f"Failed to load extension {cog} for custom bot {self.user_id}: {e}")
traceback.print_exc()
async def create_custom_bot(
user_id: str,
token: str,
prefix: str = "!",
status_type: str = "listening",
status_text: str = "!help"
) -> Tuple[bool, str]:
"""
Create a new custom bot instance for a user.
Args:
user_id: The Discord user ID who owns this bot
token: The Discord bot token
prefix: Command prefix for the bot
status_type: Activity type (playing, listening, watching, competing)
status_text: Status text to display
Returns:
Tuple of (success, message)
"""
# Check if a bot already exists for this user
if user_id in custom_bots and custom_bot_status.get(user_id) == STATUS_RUNNING:
return False, f"A bot is already running for user {user_id}. Stop it first."
try:
# Set up intents
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
# Create bot instance
bot = CustomBot(
user_id=user_id,
command_prefix=prefix,
intents=intents
)
# Set up events
@bot.event
async def on_ready():
log.info(f"Custom bot {bot.user.name} (ID: {bot.user.id}) for user {user_id} is ready!")
# Set the bot's status
activity_type = getattr(discord.ActivityType, status_type, discord.ActivityType.listening)
await bot.change_presence(
activity=discord.Activity(
type=activity_type,
name=status_text
)
)
# Update status
custom_bot_status[user_id] = STATUS_RUNNING
if user_id in custom_bot_errors:
del custom_bot_errors[user_id]
@bot.event
async def on_error(event, *args, **kwargs):
log.error(f"Error in custom bot for user {user_id} in event {event}: {sys.exc_info()[1]}")
custom_bot_errors[user_id] = str(sys.exc_info()[1])
# Store the bot instance
custom_bots[user_id] = bot
custom_bot_status[user_id] = STATUS_STOPPED
return True, f"Custom bot created for user {user_id}"
except Exception as e:
log.error(f"Error creating custom bot for user {user_id}: {e}")
custom_bot_status[user_id] = STATUS_ERROR
custom_bot_errors[user_id] = str(e)
return False, f"Error creating custom bot: {e}"
def run_custom_bot_in_thread(user_id: str, token: str) -> Tuple[bool, str]:
"""
Run a custom bot in a separate thread.
Args:
user_id: The Discord user ID who owns this bot
token: The Discord bot token
Returns:
Tuple of (success, message)
"""
if user_id not in custom_bots:
return False, f"No bot instance found for user {user_id}"
if user_id in custom_bot_threads and custom_bot_threads[user_id].is_alive():
return False, f"Bot is already running for user {user_id}"
bot = custom_bots[user_id]
async def _run_bot():
try:
await bot.start(token)
except discord.errors.LoginFailure:
log.error(f"Invalid token for custom bot (user {user_id})")
custom_bot_status[user_id] = STATUS_ERROR
custom_bot_errors[user_id] = "Invalid Discord bot token. Please check your token and try again."
except Exception as e:
log.error(f"Error running custom bot for user {user_id}: {e}")
custom_bot_status[user_id] = STATUS_ERROR
custom_bot_errors[user_id] = str(e)
# Create and start the thread
loop = asyncio.new_event_loop()
thread = threading.Thread(
target=lambda: loop.run_until_complete(_run_bot()),
daemon=True,
name=f"custom-bot-{user_id}"
)
thread.start()
# Store the thread
custom_bot_threads[user_id] = thread
return True, f"Started custom bot for user {user_id}"
def stop_custom_bot(user_id: str) -> Tuple[bool, str]:
"""
Stop a running custom bot.
Args:
user_id: The Discord user ID who owns this bot
Returns:
Tuple of (success, message)
"""
if user_id not in custom_bots:
return False, f"No bot instance found for user {user_id}"
if user_id not in custom_bot_threads or not custom_bot_threads[user_id].is_alive():
custom_bot_status[user_id] = STATUS_STOPPED
return True, f"Bot was not running for user {user_id}"
# Get the bot instance
bot = custom_bots[user_id]
# Close the bot (this will be done in a new thread to avoid blocking)
async def _close_bot():
try:
await bot.close()
custom_bot_status[user_id] = STATUS_STOPPED
except Exception as e:
log.error(f"Error closing custom bot for user {user_id}: {e}")
custom_bot_status[user_id] = STATUS_ERROR
custom_bot_errors[user_id] = str(e)
# Run the close operation in a new thread
loop = asyncio.new_event_loop()
close_thread = threading.Thread(
target=lambda: loop.run_until_complete(_close_bot()),
daemon=True,
name=f"close-bot-{user_id}"
)
close_thread.start()
# Wait for the close thread to finish (with timeout)
close_thread.join(timeout=5.0)
# The thread will be cleaned up when the bot is started again
return True, f"Stopped custom bot for user {user_id}"
def get_custom_bot_status(user_id: str) -> Dict:
"""
Get the status of a custom bot.
Args:
user_id: The Discord user ID who owns this bot
Returns:
Dict with status information
"""
if user_id not in custom_bots:
return {
"exists": False,
"status": "not_created",
"error": None,
"is_running": False
}
status = custom_bot_status.get(user_id, STATUS_STOPPED)
error = custom_bot_errors.get(user_id)
is_running = (
user_id in custom_bot_threads and
custom_bot_threads[user_id].is_alive() and
status == STATUS_RUNNING
)
return {
"exists": True,
"status": status,
"error": error,
"is_running": is_running
}
def get_all_custom_bot_statuses() -> Dict[str, Dict]:
"""
Get the status of all custom bots.
Returns:
Dict mapping user_id to status information
"""
result = {}
for user_id in custom_bots:
result[user_id] = get_custom_bot_status(user_id)
return result

70
main.py
View File

@ -23,6 +23,7 @@ import settings_manager # Import the settings manager
from db import mod_log_db # Import the new mod log db functions from db import mod_log_db # Import the new mod log db functions
import command_customization # Import command customization utilities import command_customization # Import command customization utilities
from global_bot_accessor import set_bot_instance # Import the new accessor from global_bot_accessor import set_bot_instance # Import the new accessor
import custom_bot_manager # Import the custom bot manager
# Import the unified API service runner and the sync API module # Import the unified API service runner and the sync API module
import sys import sys
@ -252,6 +253,59 @@ async def on_ready():
import traceback import traceback
traceback.print_exc() traceback.print_exc()
# Start custom bots for users who have enabled them
try:
log.info("Starting custom bots for users...")
if bot.pg_pool:
async with bot.pg_pool.acquire() as conn:
# Get all users with custom bots enabled
users = await conn.fetch("""
SELECT user_id, settings FROM user_settings
WHERE settings->>'custom_bot_enabled' = 'true'
AND settings->>'custom_bot_token' IS NOT NULL
""")
log.info(f"Found {len(users)} users with custom bots enabled")
for user in users:
user_id = user['user_id']
settings = user['settings']
# Extract bot settings
token = settings.get('custom_bot_token')
prefix = settings.get('custom_bot_prefix', '!')
status_type = settings.get('custom_bot_status_type', 'listening')
status_text = settings.get('custom_bot_status_text', '!help')
if token:
log.info(f"Creating and starting custom bot for user {user_id}")
# Create the bot
success, message = await custom_bot_manager.create_custom_bot(
user_id=user_id,
token=token,
prefix=prefix,
status_type=status_type,
status_text=status_text
)
if success:
# Start the bot
success, message = custom_bot_manager.run_custom_bot_in_thread(
user_id=user_id,
token=token
)
if success:
log.info(f"Successfully started custom bot for user {user_id}")
else:
log.error(f"Failed to start custom bot for user {user_id}: {message}")
else:
log.error(f"Failed to create custom bot for user {user_id}: {message}")
else:
log.warning("Bot Postgres pool not initialized, cannot start custom bots")
except Exception as e:
log.exception(f"Error starting custom bots: {e}")
@bot.event @bot.event
async def on_shard_disconnect(shard_id): async def on_shard_disconnect(shard_id):
print(f"Shard {shard_id} disconnected. Attempting to reconnect...") print(f"Shard {shard_id} disconnected. Attempting to reconnect...")
@ -597,6 +651,22 @@ async def main(args): # Pass parsed args
else: else:
log.info("Flask server process was not running or already terminated.") log.info("Flask server process was not running or already terminated.")
# Stop all custom bots
try:
log.info("Stopping all custom bots...")
# Get all running custom bots
bot_statuses = custom_bot_manager.get_all_custom_bot_statuses()
for user_id, status in bot_statuses.items():
if status.get('is_running', False):
log.info(f"Stopping custom bot for user {user_id}")
success, message = custom_bot_manager.stop_custom_bot(user_id)
if success:
log.info(f"Successfully stopped custom bot for user {user_id}")
else:
log.error(f"Failed to stop custom bot for user {user_id}: {message}")
except Exception as e:
log.exception(f"Error stopping custom bots: {e}")
# Close database/cache pools if they were initialized # Close database/cache pools if they were initialized
if bot.pg_pool: if bot.pg_pool:
log.info("Closing Postgres pool in main finally block...") log.info("Closing Postgres pool in main finally block...")