discordBot/README.md
2026-07-21 09:46:20 +05:00

362 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Discord Bot
Discord-бот для Магнитогорска. Команды погоды, новостей, котиков и утреннего дайджеста.
## Установка
```bash
pip install -r requirements.txt
```
## Запуск
```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)
```
### Добавление 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 форматов
- Извлечение ссылок из `<guid isPermaLink="true">` и авторов из `<dc:creator>`
- Возвращает до 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
## Зависимости
### Production (`requirements.txt`)
```txt
discord.py~=2.7.1
python-dotenv~=1.2.2
requests~=2.34.2
defusedxml~=0.7.1
```
### Development (`requirements-dev.txt`)
```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 символов с суффиксом `...`.
Ссылки отображаются в виде `<url>` для предотвращения 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-командам