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:
parent
0c2e599f77
commit
172f5907b3
@ -66,6 +66,13 @@ class UserSettings(BaseModel):
|
||||
# Theme settings
|
||||
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: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
||||
|
||||
|
@ -30,6 +30,14 @@ from api_service.dashboard_models import (
|
||||
# Import settings_manager for database access (use absolute path)
|
||||
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
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -84,6 +92,13 @@ class GlobalSettings(BaseModel):
|
||||
max_tokens: Optional[int] = 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
|
||||
|
||||
# class CommandInfo(BaseModel): # Removed - Imported from dashboard_models
|
||||
@ -986,7 +1001,12 @@ async def update_global_settings(
|
||||
system_message=settings.system_message,
|
||||
character=settings.character,
|
||||
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
|
||||
@ -1021,6 +1041,172 @@ async def update_global_settings(
|
||||
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 ---
|
||||
# Note: These endpoints have been moved to cog_management_endpoints.py
|
||||
|
||||
|
126
api_service/dashboard_web/custom-bot.html
Normal file
126
api_service/dashboard_web/custom-bot.html
Normal 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>
|
@ -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>
|
||||
Theme Settings
|
||||
</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">
|
||||
<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
|
||||
@ -526,6 +530,14 @@
|
||||
</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 -->
|
||||
<template id="command-item-template">
|
||||
<div class="command-item">
|
||||
@ -611,6 +623,7 @@
|
||||
<script src="js/main.js"></script>
|
||||
<!-- <script src="js/ai-settings.js"></script> --> <!-- Removed AI settings 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/cog-management.js"></script>
|
||||
</body>
|
||||
|
275
api_service/dashboard_web/js/custom-bot.js
Normal file
275
api_service/dashboard_web/js/custom-bot.js
Normal 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;
|
@ -343,6 +343,12 @@ function showSection(sectionId) {
|
||||
// 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
|
||||
if (sectionId === 'cog-management' && typeof loadCogManagementData === 'function') {
|
||||
// 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) {
|
||||
// Welcome message test button
|
||||
const testWelcomeButton = document.getElementById('test-welcome-button');
|
||||
|
269
custom_bot_manager.py
Normal file
269
custom_bot_manager.py
Normal 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
70
main.py
@ -23,6 +23,7 @@ import settings_manager # Import the settings manager
|
||||
from db import mod_log_db # Import the new mod log db functions
|
||||
import command_customization # Import command customization utilities
|
||||
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 sys
|
||||
@ -252,6 +253,59 @@ async def on_ready():
|
||||
import traceback
|
||||
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
|
||||
async def on_shard_disconnect(shard_id):
|
||||
print(f"Shard {shard_id} disconnected. Attempting to reconnect...")
|
||||
@ -597,6 +651,22 @@ async def main(args): # Pass parsed args
|
||||
else:
|
||||
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
|
||||
if bot.pg_pool:
|
||||
log.info("Closing Postgres pool in main finally block...")
|
||||
|
Loading…
x
Reference in New Issue
Block a user