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 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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
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>
|
<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>
|
||||||
|
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
|
// 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
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
|
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...")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user