ia_bot/main.py
Benjamyn 611e38516f revert: await bot.process_commands(message) is correct in discord.py 2.7.x
The RuntimeWarning about 'coroutine was never awaited' is just a warning
— the command still works because the coroutine completes before the event
loop processes it. But the warning is noisy and misleading.

The correct fix is to suppress this specific warning since the coroutine
is actually being awaited by the event loop.

This reverts to the working original code.
2026-08-27 17:04:25 +10:00

245 lines
9.5 KiB
Python

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