Ctrl K
ComunidadGuías…

bot.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import discord
from discord.ext import commands
import logging
import os
import signal
import sys
from datetime import datetime, timezone
from dotenv import load_dotenv
from db.database import initialize_databases, close_all_databases
from utils.log import quiet_duplicate_loggers, setup_logging
from utils.cogs import discover_cog_paths
from utils.uptime import format_uptime
import asyncio
import internal_api

_shutdown_event = None

if sys.platform == "win32":
    # run.py spawns this process in its own console process group so it can send
    # CTRL_BREAK_EVENT for a graceful stop without also signalling the parent.
    # Python's default handler for SIGBREAK just kills the process, so trigger the
    # clean-shutdown path in main() instead. This handler can fire deep inside
    # uvicorn's own signal handling (it re-raises via signal.raise_signal so outer
    # code sees it too), so it must not raise an exception here directly — doing so
    # previously confused asyncio's task bookkeeping ("Task exception was never
    # retrieved"). Setting an Event is safe from a signal handler since Python signal
    # handlers always run on the main thread, same as the event loop.
    def _handle_sigbreak(signum, frame):
        if _shutdown_event is not None:
            _shutdown_event.set()

    signal.signal(signal.SIGBREAK, _handle_sigbreak)

load_dotenv()
TOKEN = os.getenv("DISCORD_BOT_TOKEN")
if not TOKEN:
    raise SystemExit("DISCORD_BOT_TOKEN is not set. Add it to your .env file")

setup_logging(os.getenv("LOG_LEVEL", "INFO"))
quiet_duplicate_loggers()
logger = logging.getLogger("bot")

intents = discord.Intents.default()
intents.members = True
intents.guilds = True
intents.message_content = True
bot = commands.Bot(command_prefix="ç!", intents=intents)
bot.launch_time = datetime.now(timezone.utc)

# A custom /help command is loaded from cogs, so the built-in one is removed to avoid a name clash.
bot.remove_command("help")

async def load_cogs(bot):
    cogs_dir = os.path.join(os.path.dirname(__file__), "cogs")
    for cog_path in discover_cog_paths(cogs_dir):
        try:
            await bot.load_extension(cog_path)
            logger.info(f"Loaded: {cog_path}")
        except commands.NoEntryPointError:
            pass
        except Exception as e:
            logger.error(f"Failed to load cog {cog_path}: {e}")

has_synced = False
has_greeted = False

@bot.event
async def on_ready():
    global has_synced, has_greeted

    # on_ready can fire again after a reconnect; only banner the first one so
    # a flaky connection doesn't spam the console with repeated startup blocks.
    if not has_greeted:
        logger.info("=" * 56)
        logger.info(f"{bot.user.name} online and ready with devtools prefix {bot.command_prefix}")
        logger.info("=" * 56)
        has_greeted = True
    else:
        logger.info(f"{bot.user.name} reconnected")

    await bot.change_presence(activity=discord.CustomActivity(name="/help", state="/help"))

    # on_ready can fire again after a reconnect, so this only syncs once per process.
    # Use ç!sync (developer_tools.py) or the dashboard's Sync button to force a resync.
    # Set SYNC_ON_STARTUP=false in .env to skip this entirely, e.g. during restart testing
    # where every crash/restart would otherwise trigger a fresh command sync.
    sync_on_startup = os.getenv("SYNC_ON_STARTUP", "true").lower() != "false"
    if sync_on_startup and not has_synced:
        try:
            synced = await bot.tree.sync()
            logger.info(f"Synced {len(synced)} slash commands")
            has_synced = True
        except Exception as e:
            logger.error(f"Failed to sync commands: {e}")

if __name__ == "__main__":
    async def main():
        global _shutdown_event
        await initialize_databases()
        await load_cogs(bot)

        _shutdown_event = asyncio.Event()
        bot_task = asyncio.create_task(bot.start(TOKEN))
        api_task = asyncio.create_task(internal_api.start(bot))
        shutdown_task = asyncio.create_task(_shutdown_event.wait())

        try:
            done, pending = await asyncio.wait(
                {bot_task, api_task, shutdown_task}, return_when=asyncio.FIRST_COMPLETED,
            )
            for task in pending:
                task.cancel()
            if pending:
                await asyncio.gather(*pending, return_exceptions=True)
            for task in done:
                if task is shutdown_task:
                    continue
                exc = task.exception()
                if exc:
                    raise exc
        finally:
            # Unloading each extension explicitly ensures cog_unload hooks run before exit.
            for extension in list(bot.extensions.keys()):
                try:
                    await bot.unload_extension(extension)
                    logger.info(f"Unloaded: {extension}")
                except Exception as e:
                    logger.error(f"Failed to unload extension {extension}: {e}")

            await close_all_databases()

            # Gives any straggling background tasks a moment to finish before the process exits.
            await asyncio.sleep(2)

            if not bot.is_closed():
                await bot.close()

            logger.info("=" * 56)
            logger.info(f"Bot shut down cleanly after running for {format_uptime(bot.launch_time)}")
            logger.info("=" * 56)

    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass