Исправление ошибки совместимости: • bot.py — убран параметр raise_exception=True (не поддерживается в версии discord.py) * bot.run(token, raise_exception=True) → bot.run(token) * Обработка исключений сохранена через try/except вокруг bot.run() • tests/test_bot.py — обновлённый тест проверки кода обработки ошибок * Убрана проверка на 'raise_exception=True' (не поддерживается) * Добавлена проверка на вызов 'bot.run(token)' Преимущества: - Бот корректно запускается без ошибки TypeError - Обработка ошибок сохранена (LoginFailure, HTTPException, Exception) - Все сообщения об ошибках выводятся пользователю и логируются критически - 200 тестов проходят успешно
154 lines
5.5 KiB
Python
154 lines
5.5 KiB
Python
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)
|