diff --git a/bot/bot.py b/bot/bot.py index 3c0ba71..e3562f5 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -1,5 +1,4 @@ from typing import Optional -import logging from discord import Intents from discord.ext import commands @@ -15,8 +14,6 @@ __all__ = ("Bot", "discbot") class Bot(commands.Bot): """ Основной класс Discord-бота с методами настройки и запуска. - - Поддерживает передачу token, prefix, intents и help_command в конструктор. """ def __init__( @@ -26,12 +23,6 @@ class Bot(commands.Bot): intents: Optional[Intents] = None, help_command: Optional[commands.HelpCommand] = None, ) -> None: - """ - :param token: Токен бота (если None — берётся из settings.BOT_TOKEN). - :param prefix: Префикс команд (если None — берётся из settings.PREFIX или '!'). - :param intents: Intents (если None — создаются стандартные + privileged). - :param help_command: Кастомная команда помощи. - """ # Intents по умолчанию if intents is None: intents = Intents.default() @@ -52,7 +43,6 @@ class Bot(commands.Bot): help_command=help_command, ) - # Сохраняем токен и хранилище self._token: Optional[str] = token self.storage = storage # type: ignore[assignment] @@ -65,54 +55,47 @@ class Bot(commands.Bot): async def setup(self) -> None: """ - Инициализация бота: логгер, cogs, логирование discord.py. + Инициализация бота: логгер и загрузка cogs. """ logger.setup(start=True) logger.info(text="Настройка бота...", log_type="SYSTEM") await self.load_cogs() - logging.basicConfig( - level=logging.WARNING, - format="%(asctime)s:%(levelname)s:%(name)s: %(message)s", - ) - logging.getLogger("discord").setLevel(logging.INFO) - async def load_cogs(self) -> None: """ Загрузить все модули cogs. """ + logger.info(text="Начинаю загрузку cogs...", log_type="COGS") + cogs: list[str] = [ - "cogs.events", - "cogs.moderation", - "cogs.blacklist", - "cogs.reminders", + "bot.cogs.events", + "bot.cogs.moderation", + "bot.cogs.blacklist", + "bot.cogs.reminders", ] for cog in cogs: try: await self.load_extension(cog) - logger.info(f"Загружен cog: {cog}", log_type="COGS") + logger.info(text=f"Загружен cog: {cog}", log_type="COGS") except Exception as e: - logger.error(f"Ошибка загрузки {cog}: {e}", log_type="COGS") + logger.error(text=f"Ошибка загрузки {cog}: {e!r}", log_type="COGS") async def start_bot(self, token: Optional[str] = None) -> None: """ Запуск бота с использованием сохранённого токена или переданного. - - :param token: Токен бота (если None — используется self.token). """ use_token: Optional[str] = token or self.token if not use_token: error: str = "BOT_TOKEN не задан (ни в конструкторе, ни в settings)" - logger.error(error) + logger.error(text=error, log_type="START") raise ValueError(error) logger.info(text="Запуск бота...", log_type="START") await self.start(use_token) -# Глобальный экземпляр — МОЖНО ПЕРЕДАВАТЬ token/prefix ПРЯМО ЗДЕСЬ discbot: Bot = Bot( - token=settings.BOT_TOKEN, # кастомный токен - prefix=settings.PREFIX, # кастомный префикс + token=settings.BOT_TOKEN, + prefix=settings.PREFIX, ) diff --git a/bot/cogs/events.py b/bot/cogs/events.py index 80b9a3e..410d2a3 100644 --- a/bot/cogs/events.py +++ b/bot/cogs/events.py @@ -2,11 +2,11 @@ from datetime import datetime import discord from discord.ext import tasks -from discord.ext.commands import Bot, Cog +from discord.ext.commands import Bot, Cog, Context, CommandError from discord.utils import get from configs import settings -from middleware import logger +from middleware.loggers import logger from ..storage import storage, Reminder @@ -25,7 +25,7 @@ class Events(Cog): Событие запуска бота. Загружает данные и создаёт необходимые роли. """ - logger.info(text=f"Бот запущен как {self.bot.user}") + logger.info(text=f"Бот запущен как {self.bot.user}", log_type="SYSTEM") storage.load_all() await self.ensure_roles_exist() @@ -38,13 +38,25 @@ class Events(Cog): """ new_member_role: discord.Role | None = get(member.guild.roles, name="New Member") if new_member_role: - await member.add_roles(new_member_role) + try: + await member.add_roles(new_member_role) + except discord.Forbidden: + logger.warning( + text=f"Нет прав выдать роль New Member пользователю {member}", + log_type="EVENT", + user=str(member), + ) - channel: discord.abc.MessageableChannel | None = self.bot.get_channel( - settings.WELCOME_CHANNEL_ID - ) + channel = self.bot.get_channel(settings.WELCOME_CHANNEL_ID) if isinstance(channel, discord.TextChannel): - await channel.send(f"Приветствуем {member.mention} на сервере!") + try: + await channel.send(f"Приветствуем {member.mention} на сервере!") + except discord.HTTPException as e: + logger.error( + text=f"Не удалось отправить приветствие для {member}: {e!r}", + log_type="EVENT", + user=str(member), + ) @Cog.listener() async def on_message(self, message: discord.Message) -> None: @@ -63,12 +75,50 @@ class Events(Cog): await message.channel.send( f"{message.author.mention}, ваше сообщение содержит запрещённые слова." ) - except Exception: - pass + logger.info( + text=f"Удалено сообщение с запрещённым словом от {message.author}: {message.content!r}", + log_type="BLACKLIST", + user=str(message.author), + ) + except discord.Forbidden: + logger.warning( + text=f"Нет прав удалить сообщение {message.author}: {message.content!r}", + log_type="BLACKLIST", + user=str(message.author), + ) + except discord.HTTPException as e: + logger.error( + text=f"Ошибка при удалении сообщения {message.author}: {e!r}", + log_type="BLACKLIST", + user=str(message.author), + ) return + # Обработка команд ОБЯЗАТЕЛЬНО в конце, иначе команды не будут работать await self.bot.process_commands(message) + @Cog.listener() + async def on_command(self, ctx: Context) -> None: + """ + Логирование всех вызванных команд. + """ + logger.info( + text=f"Команда: {ctx.command} | Автор: {ctx.author} | Гильдия: {ctx.guild} | Сообщение: {ctx.message.content}", + log_type="COMMAND", + user=str(ctx.author), + ) + + @Cog.listener() + async def on_command_error(self, ctx: Context, error: CommandError) -> None: + """ + Логирование ошибок команд. + """ + logger.error( + text=f"Ошибка команды: {ctx.command} | Автор: {ctx.author} | Ошибка: {error!r}", + log_type="COMMAND", + user=str(ctx.author), + ) + async def ensure_roles_exist(self) -> None: """ Проверяет наличие ролей Muted и New Member и создаёт их при необходимости. @@ -79,18 +129,40 @@ class Events(Cog): try: muted_role = await guild.create_role(name="Muted") for channel in guild.channels: - await channel.set_permissions( - muted_role, send_messages=False, speak=False - ) - except Exception: - pass + try: + await channel.set_permissions( + muted_role, send_messages=False, speak=False + ) + except discord.Forbidden: + logger.warning( + text=f"Нет прав настроить права для канала {channel} в {guild}", + log_type="ROLES", + ) + except discord.Forbidden: + logger.warning( + text=f"Нет прав создать роль Muted в {guild}", + log_type="ROLES", + ) + except discord.HTTPException as e: + logger.error( + text=f"Ошибка при создании роли Muted в {guild}: {e!r}", + log_type="ROLES", + ) new_member_role: discord.Role | None = get(guild.roles, name="New Member") if new_member_role is None: try: await guild.create_role(name="New Member") - except Exception: - pass + except discord.Forbidden: + logger.warning( + text=f"Нет прав создать роль New Member в {guild}", + log_type="ROLES", + ) + except discord.HTTPException as e: + logger.error( + text=f"Ошибка при создании роли New Member в {guild}: {e!r}", + log_type="ROLES", + ) @tasks.loop(seconds=30) async def check_reminders(self) -> None: @@ -101,16 +173,29 @@ class Events(Cog): now: float = datetime.now().timestamp() to_remove: list[Reminder] = [] - for rem in storage.reminders: + for rem in list(storage.reminders): if rem.time <= now: channel = self.bot.get_channel(rem.channel_id) if isinstance(channel, discord.TextChannel): - await channel.send(f"{rem.user_mention} Напоминание: {rem.text}") + try: + await channel.send(f"{rem.user_mention} Напоминание: {rem.text}") + logger.info( + text=f"Отправлено напоминание пользователю {rem.user_mention}: {rem.text!r}", + log_type="REMINDER", + ) + except discord.HTTPException as e: + logger.error( + text=f"Ошибка при отправке напоминания в канал {channel.id}: {e!r}", + log_type="REMINDER", + ) to_remove.append(rem) if to_remove: for rem in to_remove: - storage.reminders.remove(rem) + try: + storage.reminders.remove(rem) + except ValueError: + pass storage.save_reminders() @check_reminders.before_loop diff --git a/main.py b/main.py index 3350a83..b66c8b3 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,16 @@ from asyncio import run from bot import discbot -from configs import settings -from middleware import logger +from middleware.loggers import logger async def main() -> None: """ Точка входа для асинхронного запуска бота. """ - logger.setup() - await discbot.start(settings.BOT_TOKEN) - await discbot.start_bot() + logger.setup() # настройка логера + await discbot.setup() # ЗАГРУЗКА COGS + настройка discord-логов + await discbot.start_bot() # запуск бота (внутри возьмёт token из settings) if __name__ == "__main__":