""" 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