# Discord Bot Discord-бот для Магнитогорска. Команды погоды, новостей, котиков и утреннего дайджеста. ## Установка ```bash pip install . ``` Или с dev-зависимостями: ```bash pip install -e ".[dev]" ``` ## Запуск ```bash python bot.py ``` Используйте `!команда` в Discord. ## Настройка 1. Скопируйте `.env.example` в `.env`: ```bash cp .env.example .env ``` 2. Заполните обязательные переменные в `.env`: ```env DISCORD_TOKEN=ваш_токен YANDEX_WEATHER_API_KEY=ваш_ключ_яндекс_погоды ``` `DISCORD_TOKEN` получите на [Discord Developer Portal](https://discord.com/developers/applications). `YANDEX_WEATHER_API_KEY` — в [Яндекс Погода API](https://yandex.ru/dev/weather/). ## Команды Discord | Команда | Описание | |---------|----------| | `!pg` | Прогноз погоды для Магнитогорска | | `!nw` | Топ-5 статей и топ-5 новостей по AI с Habr | | `!morning` | Погода + топ-5 статей + топ-5 новостей + котик (утренний дайджест) | | `!cat` | Случайный котик | | `!status` | Статус бота: пинг к Discord gateway, uptime | | `!stats` | Количество серверов, каналов, пользователей, пинг | ## Архитектура ``` bot.py # Точка входа, BotRunner, TextHelpCommand, валидация конфига commands/ # Discord команды (cogs) __init__.py # ALL_COMMANDS — явные импорты pg.py # !pg — погода (обёртка над utils.pogoda) news.py # !nw — статьи + новости с Habr cat.py # !cat — случайный котик morning.py # !morning — утренний дайджест (обёртка над utils.morning_runner) status.py # !status — статус бота: пинг, uptime stats.py # !stats — серверы, каналы, пользователи, пинг utils/ # Утилиты (API-клиенты, конвертации) __init__.py # __all__ — публичный API утилит + close_all_sessions() pogoda.py # fetch_weather(), yandex_condition_to_russian(), get_weather_description(), wmo_to_russian(), translate_weather(), pressure_to_mmhg(), format_weather_data_for_console(), format_weather_for_message() news.py # fetch_rss(), format_articles(), truncate_title(), truncate_message(), truncate_embed_text(), truncate_embed_field() cat.py # fetch_cat() rate_limiter.py # RateLimiter (токен-бакет), cat/yandex_weather/habr_rss лимитеры morning_runner.py # Scheduler, MorningData, gather_morning(), run_morning() logger.py # setup_logging() — консоль + файл с ротацией по размеру compat.py # Monkey-patch asyncio.iscoroutinefunction (Python 3.14+ / discord.py 2.7.1) tests/ # pytest-тесты test_pogoda.py # translate_weather, pressure_to_mmhg, wmo_to_russian, format_weather_data_for_console, yandex_condition_to_russian test_fetch_cat.py # fetch_cat test_fetch_rss.py # fetch_rss test_fetch_weather.py # fetch_weather (Яндекс Погода API) test_format_articles.py # truncate_title, _parse_date, format_articles, truncate_message, truncate_embed_text, truncate_embed_field test_commands_pg.py # Pg cog test_commands_cat.py # Cat cog, команда !cat test_commands_news.py # News cog, команда !nw test_commands_morning.py # Morning cog, команда !morning test_bot.py # инициализация бота, обработка ошибок запуска test_morning_runner.py# тесты morning runner-а test_help_command.py # TextHelpCommand — текстовая справка по командам test_logger.py # setup_logging — уровни, обработчики, формат test_commands_status.py # команда !status — embed и uptime test_commands_stats.py # команда !stats — подсчёт серверов и каналов test_integration.py # интеграционные тесты (загрузка когов, поток команд) conftest.py # monkey-patch asyncio.iscoroutinefunction (Python 3.14+ / discord.py 2.7.1) ISSUES.md # Задачи и баг-трекер проекта pytest.ini # Конфигурация pytest (asyncio_mode = auto) Dockerfile # Сборка образа бота (Python 3.14-slim, healthcheck) docker-compose.yml # Запуск бота в Docker .dockerignore # Исключения для Docker-контекста .gitignore # Исключения для Git (venv, .env, логи, IDE) .pre-commit-config.yaml # pre-commit хуки (ruff lint + ruff-format) pyproject.toml # Единый файл конфигурации: зависимости, pytest, ruff ``` ### Добавление Discord команды 1. Создать файл `commands/имя.py` с классом, наследующим `commands.Cog` 2. Добавить импорт в `commands/__init__.py` 3. Добавить класс в `ALL_COMMANDS` ## Запуск тестов ```bash python -m pytest tests/ -v ``` ### Структура тестов | Файл | Что тестирует | Кол-во | |------|---------------|--------| | `test_pogoda.py` | `translate_weather()`, `pressure_to_mmhg()`, `wmo_to_russian()`, `format_weather_data_for_console()`, `yandex_condition_to_russian()` | 27 | | `test_fetch_cat.py` | `fetch_cat()` | 10 | | `test_fetch_rss.py` | `fetch_rss()` | 21 | | `test_fetch_weather.py` | `fetch_weather()` (Яндекс Погода) | 19 | | `test_format_articles.py` | `truncate_title()`, `_parse_date()`, `format_articles()`, `truncate_message()`, `truncate_embed_text()`, `truncate_embed_field()` | 20 | | `test_commands_pg.py` | `Pg` cog, команда `!pg` | 13 | | `test_commands_cat.py` | `Cat` cog, команда `!cat` (embed, fallback) | 8 | | `test_commands_news.py` | `News` cog, команда `!nw` (статьи, посты, fallback) | 8 | | `test_commands_morning.py` | `Morning` cog, команда `!morning` (run_morning) | 5 | | `test_bot.py` | инициализация бота, обработка ошибок запуска | 5 | | `test_morning_runner.py` | morning runner | 12 | | `test_logger.py` | `setup_logging` (уровни, обработчики, формат) | 9 | | `test_help_command.py` | `TextHelpCommand` (справка, алиасы, скрытые команды) | 11 | | `test_rate_limiter.py` | `RateLimiter` (токен-бакет) | 5 | | `test_commands_status.py` | команда `!status` (embed, uptime) | 6 | | `test_commands_stats.py` | команда `!stats` (серверы, каналы) | 5 | | `test_integration.py` | загрузка когов, поток команд (моки API) | 9 | **Итого: 206 функций (282 тестов с учётом parametrized).** ## Запуск в Docker ### Сборка и запуск ```bash docker-compose up --build ``` Передайте токен через переменную окружения: ```bash DISCORD_TOKEN=ваш_токен docker-compose up ``` ### Особенности - База: `python:3.14-slim` - Healthcheck: проверка каждые 30 сек (старт-период 60 сек) - Версия Python настраивается через `ARG PYTHON_VERSION` - Часовой пояс: `TZ=Asia/Yekaterinburg` (в docker-compose.yml) - Установлены `tzdata` и `procps` для healthcheck и корректных дат ## API и внешние сервисы ### Погода (!pg, !morning) - **API**: `api.weather.yandex.ru/v1/informers` (Яндекс Погода API) - Требуется API-ключ в `YANDEX_WEATHER_API_KEY` - Retry: 3 попытки с экспоненциальной задержкой при SSL/Connection/Timeout ошибках - Rate-limiting: 1 req/sec, burst 3. Настраивается через `.env` (`YANDEX_WEATHER_API_RATE`, `YANDEX_WEATHER_API_BURST`) - Координаты: Магнитогорск (53.40716, 58.980289) - API возвращает давление в мм рт. ст. и ветер в м/с — конвертация не требуется ### Конвертации | Функция | Описание | |---------|----------| | `yandex_condition_to_russian()` | Перевод Яндекс condition-кодов в русское описание | | `pressure_to_mmhg()` | hPa → мм рт. ст. (`* 0.750062`) — для обратной совместимости с тестами | | `wmo_to_russian()` | WMO weather codes → русский — для обратной совместимости с тестами | ### Новости (!nw, !morning) - **Articles**: `https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru` - **News**: `https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru` - Парсинг RSS 2.0 и Atom форматов - Извлечение ссылок из `` и авторов из `` - Возвращает до 10 статей/постов, выводит топ-5 - Rate-limiting: 1 req/sec, burst 2. Настраивается через `.env` - Формат вывода: заголовок → дата → ссылка ### Котики (!cat, !morning) - **API**: `https://api.thecatapi.com/v1/images/search` - Опциональный ключ `CAT_API_KEY` (заголовок `x-api-key`) - Rate-limiting: 1 req/sec, burst 3. Настраивается через `.env` - Картинка встраивается в Discord Embed ## Структура данных погоды Команда `!pg` возвращает: ``` Температура: X°C (ощущается как Y°C) Описание: Z Влажность: X% Ветер: X м/с (порывы Y м/с), направление Давление: X мм рт. ст. ``` Яндекс Погода API возвращает ветер в м/с, порывы ветра, направление ветра (n/ne/e/se/s/sw/w/nw → русский перевод). ## Формат дат Даты форматируются как `дд.мм.гггг` через `datetime.strptime`: - RFC 822: `"Mon, 01 Jan 2024 12:00:00 GMT"` - ISO 8601: `"2024-01-01T12:00:00+00:00"` или `"2024-01-01"` ## Конфигурация | Переменная | Описание | Где взять | |------------|----------|-----------| | `DISCORD_TOKEN` | Токен бота (обязательна) | [Discord Developer Portal](https://discord.com/developers/applications) | | `YANDEX_WEATHER_API_KEY` | API-ключ Яндекс Погоды (обязателен) | [Яндекс Погода API](https://yandex.ru/dev/weather/) | | `MORNING_TIME` | Время запуска утреннего дайджеста | `.env` (формат `ЧЧ:ММ`, по умолчанию `07:00`) | | `MORNING_CHANNEL_ID` | ID канала для утреннего дайджеста | Правый клик по каналу → Копировать ID | | `CAT_API_KEY` | API-ключ TheCatAPI (опционально) | [TheCatAPI](https://thecatapi.com/) | | `CAT_API_RATE` | Rate-limit TheCatAPI (токенов/сек) | `.env`, по умолчанию `1` | | `CAT_API_BURST` | Burst-бакет TheCatAPI | `.env`, по умолчанию `3` | | `YANDEX_WEATHER_API_RATE` | Rate-limit Яндекс Погоды (токенов/сек) | `.env`, по умолчанию `1` | | `YANDEX_WEATHER_API_BURST` | Burst-бакет Яндекс Погоды | `.env`, по умолчанию `3` | | `HABR_RSS_RATE` | Rate-limit Habr RSS (токенов/сек) | `.env`, по умолчанию `1` | | `HABR_RSS_BURST` | Burst-бакет Habr RSS | `.env`, по умолчанию `2` | | `LOG_LEVEL` | Уровень логирования | `.env`, по умолчанию `INFO` (DEBUG, WARNING, ERROR, CRITICAL) | | `WEATHER_CACHE_TTL` | TTL кэша погоды (сек) | `.env`, по умолчанию `3600` | ## Валидация конфигурации При запуске бот проверяет: 1. `DISCORD_TOKEN` — наличие 2. `YANDEX_WEATHER_API_KEY` — наличие 3. `MORNING_TIME` — формат `ЧЧ:ММ` (0-23, 0-59) 4. `MORNING_CHANNEL_ID` — числовой формат (если задан) При ошибке валидации бот завершается с `sys.exit(1)`. ## Логирование При запуске бота автоматически создаётся директория `logs/` и файл `logs/bot.log`. - **Консоль**: все сообщения выводятся в stdout - **Файл**: `logs/bot.log` с ротацией по размером - **maxBytes**: 5 МБ — при достижении файл архивируется - **backupCount**: 5 — хранится до 5 бэкапов (`bot.log.1` … `bot.log.5`) - Максимальный объём: ~25 МБ - **Уровень**: настраивается через `LOG_LEVEL` в `.env` (по умолчанию `INFO`) - **Шум**: `aiohttp` — WARNING, `discord` — INFO ## Зависимости Зависимости объявлены в `pyproject.toml` (`[project].dependencies` и `[project.optional-dependencies].dev`). Файлы `requirements.txt` и `requirements-dev.txt` сохранены для обратной совместимости. ### Production ```txt discord.py~=2.7.1 python-dotenv~=1.2.2 requests~=2.34.2 defusedxml~=0.7.1 ``` ### Development ```txt pre-commit>=3.5.0 pytest>=7.4.0 pytest-asyncio>=0.21.0 ruff>=0.8.0 ``` ## Безопасность - `.env` в `.gitignore` — токены и ключи никогда не должны попадать в репозиторий - Используйте `.env.example` как шаблон ## Формат новостей Каждая новость выводится в формате: ``` Заголовок статьи дд.мм.гггг https://habr.com/ru/articles/... ``` Заголовки обрезаются до 60 символов с суффиксом `...`. Ссылки отображаются в виде `` для предотвращения embed-превью в Discord. ## Основные функции утилит ### utils/pogoda.py | Функция | Описание | |---------|----------| | `fetch_weather()` | Получение погоды через Яндекс Погода API | | `yandex_condition_to_russian()` | Перевод Яндекс condition-кодов в русское описание | | `get_weather_description()` | Извлечение описания погоды из weatherDesc | | `format_weather_data_for_console()` | Форматирование данных погоды для вывода в консоль | | `format_weather_for_message()` | Форматирование погоды для plain text сообщения (с заголовком) | | `pressure_to_mmhg()` | Конвертация давления из hPa в мм рт. ст. (для обратной совместимости) | | `wmo_to_russian()` | Перевод WMO weather codes в русский (для обратной совместимости) | ### utils/news.py | Функция | Описание | |---------|----------| | `fetch_rss()` | Получение RSS ленты (статьи или новости), до 10 записей | | `truncate_title()` | Обрезка заголовка до заданной длины | | `format_articles()` | Форматирование списка статей для вывода (топ-5) | | `truncate_message()` | Обрезка plain text сообщения до заданной длины (дефолт 2000) | | `truncate_embed_text()` | Обрезка embed.description до заданной длины (дефолт 4096) | | `truncate_embed_field()` | Обрезка embed field value до заданной длины (дефолт 1024) | ### utils/cat.py | Функция | Описание | |---------|----------| | `fetch_cat()` | Получение URL случайного котика | ### utils/morning_runner.py | Функция / Класс | Описание | |----------------|----------| | `MorningData` | dataclass с полями weather, articles, posts, cat_url | | `gather_morning()` | Параллельный сбор всех данных для дайджеста | | `run_morning()` | Формирование и отправка plain text в канал Discord (котик — отдельным сообщением) | | `Scheduler` | Планировщик ежедневных задач (asyncio.sleep до целевого времени) | ### utils/rate_limiter.py | Функция / Класс | Описание | |----------------|----------| | `RateLimiter` | Токен-бакет: `rate` (токенов/сек), `burst` (макс. бакет) | | `RateLimiter.acquire()` | Асинхронно ждать освобождения токена перед запросом | | `cat_limiter` | Лимитер для TheCatAPI (1/s, burst 3) | | `yandex_weather_limiter` | Лимитер для Яндекс Погоды (1/s, burst 3) | | `habr_rss_limiter` | Лимитер для Habr RSS (1/s, burst 2) | | `make_*_limiter()` | Factory-функции для создания изолированных экземпляров в тестах | ### utils/compat.py | Описание | |----------| | Monkey-patch `asyncio.iscoroutinefunction` → `inspect.iscoroutinefunction` для совместимости Python 3.14+ с discord.py 2.7.1 | ### utils/logger.py | Функция | Описание | |---------|----------| | `setup_logging()` | Настройка root-логгера: консоль (stdout) + файл (logs/bot.log) с ротацией | ## BotRunner `BotRunner` управляет жизненным циклом бота: - **Graceful shutdown**: `async with self.bot` (context manager), `on_shutdown` событие, `KeyboardInterrupt` - **Cog loading**: Защита от дублирования при reconnect (`on_ready` может сработать несколько раз) - **Scheduler**: Запуск через `on_guild_available` (после загрузки guild-кэша) - **Error handling**: `on_command_error` — логирование + пользовательское сообщение в Discord - **Session cleanup**: `close_all_sessions()` при завершении для освобождения сокетов ## TextHelpCommand Кастомная команда справки (`!help`) — выводит список команд простым текстом вместо embed. Поддерживает: - Список всех команд с описаниями - Справку по отдельной команде с алиасами - Справку по cog-модулю - Справку по group-командам