import discord from discord.ext import commands import os from dotenv import load_dotenv from database import Database import logging import traceback import sys # OC Bot - Coded by Ball Studios 🎨 # A Discord bot for managing and roleplaying as original characters # Load environment variables load_dotenv() # Setup logging with more verbosity logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Log startup info logger.info('=' * 60) logger.info('Starting OC Bot...') logger.info(f'Python: {sys.version}') logger.info(f'discord.py: {discord.__version__}') # Verify required environment variables required_env_vars = ['DISCORD_TOKEN', 'DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'] missing_vars = [var for var in required_env_vars if not os.getenv(var)] if missing_vars: logger.error(f'❌ Missing required environment variables: {", ".join(missing_vars)}') for var in missing_vars: logger.error(f' - {var}') sys.exit(1) logger.info('✓ All required environment variables found') logger.info(f'Database Host: {os.getenv("DB_HOST")}') logger.info(f'Database Name: {os.getenv("DB_NAME")}') logger.info(f'Database User: {os.getenv("DB_USER")}') logger.info('=' * 60) # Bot setup intents = discord.Intents.default() intents.message_content = True intents.members = True bot = commands.Bot(command_prefix=',', intents=intents) # Database instance db = None @bot.command(name='divide') async def divide_command(ctx): """Sends a Discord GIF link when you type ,divide""" gif_url = "https://cdn.discordapp.com/attachments/1478216070660755569/1516884782188069057/togif.gif?ex=6a34448a&is=6a32f30a&hm=5f505a453c242673c212999ed8f2b666bf455678b3762312fa3eda31b761fc73&" await ctx.send(gif_url) @bot.event async def on_ready(): global db logger.info(f'✓ Discord connection established: {bot.user}') if db is None: logger.info('Initializing database connection...') db = Database() try: await db.connect() bot.db = db # Make database accessible to cogs logger.info('✓✓✓ DATABASE CONNECTION SUCCESSFUL ✓✓✓') except Exception as e: logger.critical(f'❌❌❌ FAILED TO CONNECT TO DATABASE ❌❌❌') logger.critical(f'Error: {type(e).__name__}: {str(e)}') logger.critical(f'Full traceback:\n{traceback.format_exc()}') logger.critical('Bot cannot function without database. Exiting...') await bot.close() return await load_cogs() async def load_cogs(): """Load all command cogs""" cogs_dir = 'cogs' if not os.path.exists(cogs_dir): logger.warning(f'Cogs directory not found: {cogs_dir}') return cog_files = [f for f in os.listdir(cogs_dir) if f.endswith('.py') and not f.startswith('_')] logger.info(f'Found {len(cog_files)} cog(s) to load') for filename in cog_files: try: cog_name = filename[:-3] await bot.load_extension(f'cogs.{cog_name}') logger.info(f'✓ Loaded cog: {cog_name}') except Exception as e: logger.error(f'❌ Failed to load cog {filename}:') logger.error(f' {type(e).__name__}: {str(e)}') logger.error(f' Traceback:\n{traceback.format_exc()}') @bot.event async def on_message(message): """Handle character trigger messages""" # Ignore bot messages if message.author.bot: return # Check if message starts with prefix if message.content.startswith(','): # Extract trigger and content parts = message.content.split(' ', 1) trigger = parts[0][1:] # Remove the comma msg_content = parts[1] if len(parts) > 1 else '' # Check if this is a registered command # ----- ADDED 'divide' TO THE LIST ----- if trigger in ['oc', 'help', 'ping', 'stats', 'invite', 'support', 'about', 'reload', 'debug', 'admin', 'config', 'divide']: await bot.process_commands(message) return # ------------------------------------- # Try to find a character with this trigger if db is None: logger.warning('Database not connected - cannot process character trigger') return try: character = await db.get_character_by_trigger(message.author.id, trigger) if character: logger.info(f'Triggered character: {character.get("name", "Unknown")} (trigger: {trigger}, user: {message.author.id})') # Delete original message try: await message.delete() except Exception as e: logger.warning(f'Could not delete message: {e}') # Create webhook and send message await send_character_message(message.channel, character, msg_content) return except Exception as e: logger.error(f'Error processing character trigger "{trigger}":') logger.error(f' {type(e).__name__}: {str(e)}') logger.error(f' Traceback:\n{traceback.format_exc()}') # Process other commands await bot.process_commands(message) async def send_character_message(channel, character, content): """Send a message through webhook as a character""" try: logger.info(f'Sending message as {character.get("name")} to {channel}') # Get existing webhooks webhooks = await channel.webhooks() webhook = None # Look for existing bot webhook for wh in webhooks: if wh.user.id == bot.user.id: webhook = wh break # Create webhook if doesn't exist if not webhook: logger.info(f'Creating webhook for channel {channel}') webhook = await channel.create_webhook(name='OC-Bot') # Get avatar URL safely (fallback to None if not present) avatar_url = character.get('avatar_url') # can be None # Send message through webhook, wait=True ensures we get a Message object msg = await webhook.send( content=content, username=character['name'], avatar_url=avatar_url, wait=True ) # Defensive check: webhook.send() might return None in some edge cases if msg is None: logger.error(f'❌ webhook.send() returned None for character {character["name"]}') logger.error(f' content: {content[:50]}, avatar_url: {avatar_url}') return logger.info(f'✓ Message sent successfully: {msg.id}') except discord.HTTPException as e: # Specific HTTP error from Discord API logger.error(f'❌ HTTP error sending webhook message: {e.status} {e.text}') except Exception as e: logger.error(f'❌ Failed to send character message:') logger.error(f' Character: {character.get("name", "Unknown")}') logger.error(f' Channel: {channel}') logger.error(f' {type(e).__name__}: {str(e)}') logger.error(f' Traceback:\n{traceback.format_exc()}') @bot.event async def on_command_error(ctx, error): """Error handling for commands""" logger.error(f'❌ Command error in {ctx.command}:') logger.error(f' {type(error).__name__}: {str(error)}') logger.error(f' Traceback:\n{traceback.format_exc()}') # Run bot if __name__ == '__main__': TOKEN = os.getenv('DISCORD_TOKEN') if not TOKEN: logger.critical('❌ DISCORD_TOKEN not found in .env file') sys.exit(1) logger.info('Starting bot with Discord token...') try: bot.run(TOKEN) except KeyboardInterrupt: logger.info('Bot interrupted by user') except Exception as e: logger.critical(f'❌ Fatal error running bot:') logger.critical(f' {type(e).__name__}: {str(e)}') logger.critical(f' Traceback:\n{traceback.format_exc()}') sys.exit(1)