discordBot/README.md

308 lines
15 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=ваш_токен
```
Токен получите на [Discord Developer Portal](https://discord.com/developers/applications).
## Команды Discord
| Команда | Описание |
|---------|----------|
| `!pg` | Прогноз погоды для Магнитогорска |
| `!nw` | Топ-5 статей и топ-5 новостей по AI с Habr |
| `!morning` | Погода + топ-5 статей + топ-5 новостей + котик (утренний дайджест) |
| `!cat` | Случайный котик |
| `!status` | Статус бота: пинг к Discord gateway, uptime |
| `!stats` | Количество серверов, каналов, пользователей |
## Архитектура
```
bot.py # Точка входа, инициализация бота
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 утилит
pogoda.py # fetch_weather(), fetch_open_meteo(), 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()
cat.py # fetch_cat()
rate_limiter.py # RateLimiter (токен-бакет), cat/weather/meteo/rss лимитеры
morning_runner.py # Scheduler, MorningData, gather_morning(), run_morning()
logger.py # setup_logging() — консоль + файл с ротацией по размеру
tests/ # pytest-тесты
test_pogoda.py # translate_weather, pressure_to_mmhg, wmo_to_russian, format_weather_data_for_console
test_fetch_cat.py # fetch_cat
test_fetch_rss.py # fetch_rss
test_fetch_weather.py # fetch_weather, fetch_open_meteo
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()` | 27 |
| `test_fetch_cat.py` | `fetch_cat()` | 10 |
| `test_fetch_rss.py` | `fetch_rss()` | 21 |
| `test_fetch_weather.py` | `fetch_weather()`, `fetch_open_meteo()` | 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)
- **Основной**: `wttr.in/Magnitogorsk` (бесплатный, без ключа)
- **Fallback**: `api.open-meteo.com` (бесплатный, без ключа)
- Retry: 3 попытки с экспоненциальной задержкой при SSL/Connection/Timeout ошибках
- Fallback срабатывает автоматически при неуспешных попытках
- Rate-limiting: 1 req/sec, burst 3 (wttr.in); 2 req/sec, burst 5 (Open-Meteo). Настраивается через `.env`
- WMO weather codes → русский перевод в `wmo_to_russian()`
### Конвертации
- Давление: hPa → мм рт. ст. (`* 0.750062`)
- Ветер: км/ч → м/с (`/ 3.6`)
- Погодные описания: английский → русский (`translate_weather()`)
### Новости (!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>`
- Rate-limiting: 1 req/sec, burst 2. Настраивается через `.env`
- Формат вывода: заголовок → дата → ссылка
### Котики (!cat, !morning)
- **API**: `https://api.thecatapi.com/v1/images/search`
- Rate-limiting: 1 req/sec, burst 3. Настраивается через `.env`
- Картинка встраивается в Discord Embed
## Структура данных погоды
Команда `!pg` возвращает:
```
Температура: X°C (ощущается как Y°C)
Описание: Z
Влажность: X%
Ветер: X м/с
Давление: X мм рт. ст.
```
## Формат дат
Даты форматируются как `дд.мм.гггг` через `datetime.strptime` с форматом `%a, %d %b %Y %H:%M:%S %z`.
## Конфигурация
| Переменная | Описание | Где взять |
|------------|----------|-----------|
| `DISCORD_TOKEN` | Токен бота | [Discord Developer Portal](https://discord.com/developers/applications) |
| `MORNING_TIME` | Время запуска утреннего дайджеста | `.env` (формат `ЧЧ:ММ`, по умолчанию `07:00`) |
| `MORNING_CHANNEL_ID` | ID канала для утреннего дайджеста | Правый клик по каналу → Копировать ID |
| `CAT_API_RATE` | Rate-limit TheCatAPI (токенов/сек) | `.env`, по умолчанию `1` |
| `CAT_API_BURST` | Burst-бакет TheCatAPI | `.env`, по умолчанию `3` |
| `WEATHER_API_RATE` | Rate-limit wttr.in (токенов/сек) | `.env`, по умолчанию `1` |
| `WEATHER_API_BURST` | Burst-бакет wttr.in | `.env`, по умолчанию `3` |
| `OPEN_METEO_API_RATE` | Rate-limit Open-Meteo (токенов/сек) | `.env`, по умолчанию `2` |
| `OPEN_METEO_API_BURST` | Burst-бакет Open-Meteo | `.env`, по умолчанию `5` |
| `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) |
## Логирование
При запуске бота автоматически создаётся директория `logs/` и файл `logs/bot.log`.
- **Консоль**: все сообщения выводятся в stdout
- **Файл**: `logs/bot.log` с ротацией по размеру
- **maxBytes**: 5 МБ — при достижении файл архивируется
- **backupCount**: 5 — хранится до 5 бэкапов (`bot.log.1` … `bot.log.5`)
- Максимальный объём: ~25 МБ
- **Уровень**: настраивается через `LOG_LEVEL` в `.env` (по умолчанию `INFO`)
## Зависимости
### Production (`requirements.txt`)
```txt
discord.py>=2.3.2
python-dotenv>=1.0.0
requests>=2.31.0
defusedxml>=0.7.0
```
### 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()` | Основная функция получения погоды с wttr.in |
| `fetch_open_meteo()` | Fallback при ошибках основного API |
| `wmo_to_russian()` | Перевод WMO кодов погоды в русское описание |
| `translate_weather()` | Перевод погодных описаний на русский язык |
| `pressure_to_mmhg()` | Конвертация давления из hPa в мм рт. ст. |
| `format_weather_data_for_console()` | Форматирование данных погоды для вывода в консоль |
| `format_weather_for_message()` | Форматирование погоды для plain text сообщения (с заголовком) |
### utils/news.py
| Функция | Описание |
|---------|----------|
| `fetch_rss()` | Получение RSS ленты (статьи или новости) |
| `truncate_title()` | Обрезка заголовка до заданной длины |
| `format_articles()` | Форматирование списка статей для вывода |
| `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()` | Формирование и отправка embed в канал Discord |
| `Scheduler` | Планировщик ежедневных задач (asyncio.sleep до целевого времени) |
### utils/rate_limiter.py
| Функция / Класс | Описание |
|----------------|----------|
| `RateLimiter` | Токен-бакет: `rate` (токенов/сек), `burst` (макс. бакет) |
| `RateLimiter.acquire()` | Асинхронно ждать освобождения токена перед запросом |
| `cat_limiter` | Лимитер для TheCatAPI (1/s, burst 3) |
| `weather_limiter` | Лимитер для wttr.in (1/s, burst 3) |
| `open_meteo_limiter` | Лимитер для Open-Meteo (2/s, burst 5) |
| `habr_rss_limiter` | Лимитер для Habr RSS (1/s, burst 2) |