import discord import logging from logging.handlers import RotatingFileHandler import asyncio import os from discord.ext import commands from in_game.gameplay_models import create_db_and_tables, Session, engine from in_game.gameplay_queries import get_channel_game_or_none from health_server import run_health_server from notify_restart import send_restart_notification raw_log_level = os.getenv("LOG_LEVEL") if raw_log_level == "DEBUG": log_level = logging.DEBUG elif raw_log_level == "INFO": log_level = logging.INFO elif raw_log_level == "WARN": log_level = logging.WARNING else: log_level = logging.ERROR # date = f'{datetime.datetime.now().year}-{datetime.datetime.now().month}-{datetime.datetime.now().day}' # logger.basicConfig( # filename=f'logs/{date}.log', # format='%(asctime)s - %(levelname)s - %(message)s', # level=log_level # ) # logger.getLogger('discord.http').setLevel(logger.INFO) logger = logging.getLogger("discord_app") logger.setLevel(log_level) handler = RotatingFileHandler( filename="logs/discord.log", # encoding='utf-8', maxBytes=32 * 1024 * 1024, # 32 MiB backupCount=5, # Rotate through 5 files ) formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) # dt_fmt = '%Y-%m-%d %H:%M:%S' # formatter = logger.Formatter('[{asctime}] [{levelname:<8}] {name}: {message}', dt_fmt, style='{') # handler.setFormatter(formatter) logger.addHandler(handler) COGS = [ "cogs.owner", "cogs.admins", "cogs.economy", "cogs.players", "cogs.gameplay", "cogs.economy_new.scouting", "cogs.evolution", ] intents = discord.Intents.default() intents.members = True intents.message_content = True bot = commands.Bot( command_prefix=".", intents=intents, # help_command=None, description="The Paper Dynasty Bot\nIf you have questions, feel free to contact Cal.", case_insensitive=True, owner_id=258104532423147520, ) @bot.event async def on_ready(): logger.info("Logged in as:") logger.info(bot.user.name) logger.info(bot.user.id) # Send restart notification if configured send_restart_notification() @bot.tree.error async def on_app_command_error( interaction: discord.Interaction, error: discord.app_commands.AppCommandError ): """Global error handler for all app commands (slash commands).""" logger.error(f"App command error in {interaction.command}: {error}", exc_info=error) # CRITICAL: Release play lock if command failed during gameplay # This prevents permanent user lockouts when exceptions occur try: with Session(engine) as session: game = get_channel_game_or_none(session, interaction.channel_id) if game: current_play = game.current_play_or_none(session) if current_play and current_play.locked and not current_play.complete: logger.warning( f"Releasing stuck play lock {current_play.id} in game {game.id} " f"after command error: {error}" ) current_play.locked = False session.add(current_play) session.commit() except Exception as lock_error: logger.error( f"Failed to release play lock after error: {lock_error}", exc_info=lock_error, ) # Try to respond to the user try: if not interaction.response.is_done(): await interaction.response.send_message( f"❌ An error occurred: {str(error)}", ephemeral=True ) else: await interaction.followup.send( f"❌ An error occurred: {str(error)}", ephemeral=True ) except Exception as e: logger.error(f"Failed to send error message to user: {e}") async def main(): create_db_and_tables() for c in COGS: try: await bot.load_extension(c) logger.info(f"Loaded cog: {c}") except Exception as e: logger.error(f"Failed to load cog: {c}") logger.error(f"{e}") # Start health server and bot concurrently async with bot: # Create health server task health_task = asyncio.create_task(run_health_server(bot)) try: # Start bot (this blocks until bot stops) await bot.start(os.environ.get("BOT_TOKEN", "NONE")) finally: # Cleanup: cancel health server when bot stops health_task.cancel() try: await health_task except asyncio.CancelledError: pass asyncio.run(main())