"""Entry point for the Discord bot. Usage: python main.py """ import asyncio import os import sys import logging import logging.handlers # Add project root to path project_root = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, project_root) # Load environment variables from dotenv import load_dotenv load_dotenv() # ─── Debug Mode ────────────────────────────────────────────────────────────── DEBUG = os.getenv("APP_DEBUG", "0") == "1" if DEBUG: print("[DEBUG] Debug mode is ENABLED. Logs will be printed.") else: print("[DEBUG] Debug mode is DISABLED. Logs will NOT be printed.") # ─── Logging Configuration ─────────────────────────────────────────────────── log_file = os.path.join(project_root, "bot.log") # Create a rotating file handler (max 10 MB, keep 5 files) file_handler = logging.handlers.RotatingFileHandler( log_file, maxBytes=10 * 1024 * 1024, # 10 MB backupCount=5, ) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" )) # Console handler (only when DEBUG is enabled) console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.DEBUG if DEBUG else logging.WARNING) console_handler.setFormatter(logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" )) # Root logger root_logger = logging.getLogger() root_logger.addHandler(file_handler) root_logger.addHandler(console_handler) root_logger.setLevel(logging.DEBUG if DEBUG else logging.WARNING) # Suppress noisy third-party logs logging.getLogger("discord").setLevel(logging.DEBUG if DEBUG else logging.INFO) logging.getLogger("discord.http").setLevel(logging.DEBUG if DEBUG else logging.WARNING) logging.getLogger("discord.gateway").setLevel(logging.DEBUG if DEBUG else logging.WARNING) logging.getLogger("discord.utils").setLevel(logging.DEBUG if DEBUG else logging.WARNING) logging.getLogger("discord.app_commands").setLevel(logging.DEBUG if DEBUG else logging.WARNING) logging.getLogger("discord.ext.commands").setLevel(logging.DEBUG if DEBUG else logging.WARNING) logging.getLogger("discord.errors").setLevel(logging.DEBUG if DEBUG else logging.WARNING) logging.getLogger("discord.client").setLevel(logging.DEBUG if DEBUG else logging.WARNING) # ─── Logging Configuration ─────────────────────────────────────────────────── # Configure logging logging.basicConfig( level=logging.DEBUG if DEBUG else logging.WARNING, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", handlers=[file_handler, console_handler], ) logger = logging.getLogger(__name__) import discord from discord.ext import commands from discord.app_commands import Command as AppCommand, CommandTree from handlers.gif_emotes import setup as setup_gif_emotes from handlers.reactions import setup as setup_reactions from handlers.voice import setup as setup_voice from cogs.admin import setup as setup_admin from commands.ping import ping, info from commands.hello import hello from commands.emoji import emoji, app_emoji from commands.admin import upload_emoji, list_emojis, delete_emoji # ─── Bot Setup ─────────────────────────────────────────────────────────────── intents = discord.Intents.default() intents.message_content = True intents.members = True bot = commands.Bot( command_prefix="!", intents=intents, ) tree: CommandTree = bot.tree # ─── Slash Commands ────────────────────────────────────────────────────────── # Register slash commands from modules tree.add_command(AppCommand(name="ping", callback=ping, description="Check bot latency.")) tree.add_command(AppCommand(name="info", callback=info, description="Show bot information.")) tree.add_command(AppCommand(name="hello", callback=hello, description="Say hello to someone.")) tree.add_command(AppCommand(name="emoji", callback=emoji, description="Send a custom emoji by name.")) tree.add_command(AppCommand(name="app-emoji", callback=app_emoji, description="Fetch an app-owned emoji by ID.")) tree.add_command(AppCommand(name="upload-emoji", callback=upload_emoji, description="Upload a custom emoji to the guild.")) tree.add_command(AppCommand(name="list-emojis", callback=list_emojis, description="List all custom emojis in the guild.")) tree.add_command(AppCommand(name="delete-emoji", callback=delete_emoji, description="Delete a custom emoji.")) # ─── Prefix Commands ───────────────────────────────────────────────────────── @bot.command(name="ping", description="Check bot latency.") async def ping_command(ctx: commands.Context) -> None: """Check the bot's latency.""" latency = round(bot.latency * 1000) await ctx.send(f"Pong! Latency: {latency}ms") @bot.command(name="info", description="Show bot information.") async def info_command(ctx: commands.Context) -> None: """Show bot information.""" await ctx.send( f"**{ctx.guild.name if ctx.guild else 'DM'}**\n" f"Bot: `{bot.user.name}` (ID: {bot.user.id})\n" f"Latency: `{bot.latency:.3f}s`" ) @bot.command(name="hello", description="Say hello to someone.") async def hello_command(ctx: commands.Context, name: str = "World") -> None: """Say hello to someone.""" await ctx.send(f"Hello, {name}!") @bot.command(name="list-commands", description="List all available commands.") async def list_commands(ctx: commands.Context) -> None: """List all available commands.""" cmds = list(bot.commands.values()) cmd_list = [] for cmd in cmds: cmd_list.append(f" `{cmd.name}` — {cmd.description}") await ctx.send( f"**{bot.user.name} — Command List**\n\n" + "\n".join(cmd_list) ) # ─── Event Handlers ────────────────────────────────────────────────────────── @bot.event async def on_ready() -> None: """Entry point after successful login.""" assert bot.user is not None print(f"Logged in as {bot.user} (ID: {bot.user.id})") print(f"Bot is running with {len(bot.guilds)} guilds") # Sync slash commands await tree.sync() print("Slash commands synced.") # Register handlers await setup_gif_emotes(bot) await setup_reactions(bot) await setup_voice(bot) await setup_admin(bot) print("All handlers registered.") @bot.event async def on_command_error(ctx: commands.Context, error: commands.CommandError) -> None: """Global error handler for all commands.""" logger.debug(f"Command error for {ctx.author.name} ({ctx.author.id}): {error}") if isinstance(error, commands.CommandNotFound): await ctx.send("Command not found. Type `!list-commands` for a list of commands.") elif isinstance(error, commands.CheckFailure): await ctx.send("You do not have permission to use this command.") elif isinstance(error, commands.MissingPermissions): await ctx.send("You don't have permission to use this command.") elif isinstance(error, commands.MissingRequiredArgument): await ctx.send(f"Missing argument: {error.param.name}.") elif isinstance(error, commands.BadArgument): await ctx.send(f"Bad argument: {error.argument}.") elif isinstance(error, commands.CommandOnCooldown): remaining = int(error.retry_after) await ctx.send( f"This command is on cooldown. Try again in {remaining} seconds." ) else: await ctx.send(f"An error occurred: {error}") @bot.event async def on_message(message: discord.Message) -> None: """Handle text messages sent in channels (prefix commands).""" # Ignore bot's own messages if message.author == bot.user: return # Ignore messages in DMs if message.guild is None: return # Call bot.process_commands() — this handles PREFIX COMMANDS # Note: In discord.py 2.7.x, process_commands() is a coroutine # and MUST be awaited. await bot.process_commands(message) @bot.event async def on_voice_state_update( member: discord.Member, before: discord.VoiceState, after: discord.VoiceState, ) -> None: """Handle voice state changes.""" if after.channel_id is None and before.channel_id is not None: print(f"{member.name} left voice channel.") elif after.channel_id is not None and before.channel_id is None: print(f"{member.name} joined voice channel.") # ─── Entry Point ───────────────────────────────────────────────────────────── def main() -> None: """Run the bot.""" token = os.getenv("DISCORD_TOKEN") if not token: print("Error: DISCORD_TOKEN environment variable is not set.") print("Create a .env file with your Discord bot token.") sys.exit(1) asyncio.run(bot.start(token)) if __name__ == "__main__": main()