import asyncio import inspect import logging import os import sys import threading import discord from discord.ext import commands from discord.ext.commands import CommandNotFound from dotenv import load_dotenv from commands import ALL_COMMANDS from console_commands import ALL_CONSOLE_COMMANDS from utils.morning_runner import Scheduler logger = logging.getLogger(__name__) load_dotenv() intents = discord.Intents.default() intents.message_content = True bot = commands.Bot(command_prefix="!", intents=intents) stop_event = threading.Event() bot_ready = threading.Event() scheduler: Scheduler | None = None @bot.event async def on_ready(): global scheduler print(f"Бот вошёл как {bot.user}") for cog_class in ALL_COMMANDS: cog = cog_class() await bot.add_cog(cog) for cog in bot.cogs: print(f" Загружен: {cog}") # Запуск планировщика morning_time = os.getenv("MORNING_TIME", "07:00") scheduler = Scheduler(bot, morning_time) print(f" Планировщик запущен (время: {morning_time})") bot_ready.set() @bot.event async def on_command_error(ctx, error): if isinstance(error, CommandNotFound): return # Терминал — детали для разработчика logger.error( f"Ошибка команды {ctx.command.name if ctx else '?'}: {error}", exc_info=True, ) # Discord — только если команда не ответила сама if ctx and ctx.interaction and ctx.interaction.response.is_done(): return try: await ctx.send("Не удалось выполнить команду. Попробуйте позже.") except (discord.NotFound, discord.Forbidden): pass # Бот не может писать в канал — игнорируем @bot.command(name="msg") async def msg(ctx, *, text: str): """Повторяет текст после !msg""" await ctx.send(text) def _print_commands(): """Вывести список доступных консольных команд.""" available = {k: v for k, v in ALL_CONSOLE_COMMANDS.items() if k != "stop"} print("\nДоступные команды:") for idx, (name, func) in enumerate(available.items(), 1): print(f" {idx}. {name}") print(" 0. stop") def console_input(): bot_ready.wait() _print_commands() while not stop_event.is_set(): try: choice = input("\nВыберите команду (номер): ").strip() if choice == "0": print("\nОстановка бота...") stop_event.set() asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result(timeout=5) break try: available = {k: v for k, v in ALL_CONSOLE_COMMANDS.items() if k != "stop"} idx = int(choice) if 0 < idx <= len(available): cmd_name = list(available.keys())[idx - 1] cmd_func = ALL_CONSOLE_COMMANDS[cmd_name] if inspect.iscoroutinefunction(cmd_func): asyncio.run_coroutine_threadsafe(cmd_func(stop_event, bot), bot.loop).result() else: cmd_func(stop_event, bot) else: print(f"Неизвестная команда: {choice}") except (ValueError, IndexError): print(f"Неверный формат: {choice}") _print_commands() except (EOFError, KeyboardInterrupt): stop_event.set() try: asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result(timeout=5) except Exception as e: print(f"Ошибка при остановке бота: {e}") break if __name__ == "__main__": token = os.getenv("DISCORD_TOKEN") if not token: print("Ошибка: токен не найден в .env") sys.exit(1) # Консольный ввод работает только в интерактивном терминале # В Docker stdin недоступен — пропускаем консольный режим if sys.stdin.isatty(): print("Введите 'stop' для остановки бота") thread = threading.Thread(target=console_input, daemon=True) thread.start() try: bot.run(token) except discord.LoginFailure as e: logger.critical(f"Ошибка авторизации бота: {e}", exc_info=True) print(f"❌ Токен неверный или бот отключён. Код ошибки: {e}") sys.exit(1) except discord.HTTPException as e: logger.critical(f"HTTP ошибка при подключении к Discord: {e}", exc_info=True) print(f"❌ Сбой соединения с Discord API. Проверьте доступность сервиса.") sys.exit(1) except Exception as e: logger.critical(f"Непредвиденная ошибка при запуске бота: {e}", exc_info=True) print(f"❌ Критическая ошибка при запуске. Код ошибки: {type(e).__name__}") sys.exit(1) except KeyboardInterrupt: print("\nОстановка бота...") stop_event.set() if scheduler: scheduler.stop() asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result() sys.exit(0)