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.
245 lines
9.5 KiB
Python
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()
|