293 lines
14 KiB
Markdown
293 lines
14 KiB
Markdown
# 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 |
|
||
| `!hp` | Список всех команд бота с описанием (автогенерация из `bot.commands`) |
|
||
| `!morning` | Погода + топ-5 статей + топ-5 новостей + котик (утренний дайджест) |
|
||
| `!cat` | Случайный котик |
|
||
| `!msg <текст>` | Повторить текст в чате |
|
||
|
||
## Команды терминала
|
||
|
||
| Номер | Команда | Описание |
|
||
|-------|---------|----------|
|
||
| 1 | `news` | Топ-5 статей + топ-5 новостей с Habr |
|
||
| 2 | `pogoda` | Прогноз погоды для Магнитогорска |
|
||
| 3 | `morning` | Погода + топ-5 статей + топ-5 новостей + котик |
|
||
| 4 | `cat` | Вывести URL случайного котика |
|
||
| 5 | `help` | Показать список всех команд |
|
||
| 0 | `stop` | Остановка бота |
|
||
|
||
> Номера команд генерируются автоматически из `ALL_CONSOLE_COMMANDS` в порядке определения в `console_commands/__init__.py`.
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
bot.py # Точка входа, инициализация бота, console_input()
|
||
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)
|
||
help.py # !hp — список команд (автогенерация из bot.commands)
|
||
console_commands/ # Консольные команды
|
||
__init__.py # ALL_CONSOLE_COMMANDS — явные импорты
|
||
stop.py # stop — остановка бота
|
||
news.py # news — новости с Habr
|
||
pogoda.py # pogoda — погода в терминале
|
||
morning.py # morning — утренний дайджест в терминале
|
||
cat.py # cat — вывод URL котика
|
||
help.py # help — список всех команд
|
||
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_embed()
|
||
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()
|
||
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
|
||
test_commands_pg.py # Pg cog
|
||
test_bot.py # инициализация бота
|
||
test_morning_runner.py# тесты morning runner-а
|
||
test_help_discord.py # команда !hp — проверка формата вывода и контента
|
||
test_help_console.py # консольная help — проверка списка команд
|
||
ISSUES.md # Задачи и баг-трекер проекта
|
||
pytest.ini # Конфигурация pytest (asyncio_mode = auto)
|
||
Dockerfile # Сборка образа бота (Python 3.14-slim, healthcheck)
|
||
docker-compose.yml # Запуск бота в Docker
|
||
.dockerignore # Исключения для Docker-контекста
|
||
```
|
||
|
||
### Добавление Discord команды
|
||
|
||
1. Создать файл `commands/имя.py` с классом, наследующим `commands.Cog`
|
||
2. Добавить импорт в `commands/__init__.py`
|
||
3. Добавить класс в `ALL_COMMANDS`
|
||
|
||
### Добавление консольной команды
|
||
|
||
1. Создать файл `console_commands/имя.py` с функцией `func(stop_event, bot)`
|
||
2. Добавить импорт в `console_commands/__init__.py`
|
||
3. Добавить функцию в `ALL_CONSOLE_COMMANDS`
|
||
|
||
## Запуск тестов
|
||
|
||
```bash
|
||
python -m pytest tests/ -v
|
||
```
|
||
|
||
### Структура тестов
|
||
|
||
| Файл | Что тестирует | Кол-во |
|
||
|------|---------------|--------|
|
||
| `test_pogoda.py` | `translate_weather()`, `pressure_to_mmhg()`, `wmo_to_russian()`, `format_weather_data_for_console()` | 93 |
|
||
| `test_fetch_cat.py` | `fetch_cat()` | 10 |
|
||
| `test_fetch_rss.py` | `fetch_rss()` | 20 |
|
||
| `test_fetch_weather.py` | `fetch_weather()`, `fetch_open_meteo()` | 20 |
|
||
| `test_format_articles.py` | `truncate_title()`, `_parse_date()`, `format_articles()` | 24 |
|
||
| `test_commands_pg.py` | `Pg` cog, команда `!pg` | 13 |
|
||
| `test_bot.py` | инициализация бота | 7 |
|
||
| `test_morning_runner.py` | morning runner-а | 68 |
|
||
| `test_help_discord.py` | команда `!hp` | 2 |
|
||
| `test_help_console.py` | консольная `help` | 2 |
|
||
| `test_rate_limiter.py` | `RateLimiter` (токен-бакет) | 5 |
|
||
|
||
**Итого: 209 тестов.**
|
||
|
||
## Запуск в Docker
|
||
|
||
### Сборка и запуск
|
||
|
||
```bash
|
||
docker-compose up --build
|
||
```
|
||
|
||
Передайте токен через переменную окружения:
|
||
|
||
```bash
|
||
DISCORD_TOKEN=ваш_токен docker-compose up
|
||
```
|
||
|
||
### Особенности
|
||
|
||
- База: `python:3.14-slim`
|
||
- Healthcheck: проверка каждые 30 сек (старт-период 60 сек)
|
||
- Консольный ввод отключён в Docker (stdin недоступен)
|
||
- Версия Python настраивается через `ARG PYTHON_VERSION`
|
||
|
||
## 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` |
|
||
|
||
## Зависимости
|
||
|
||
```txt
|
||
discord.py>=2.3.2
|
||
python-dotenv>=1.0.0
|
||
requests>=2.31.0
|
||
pytest>=7.4.0
|
||
pytest-asyncio>=0.21.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_embed()` | Форматирование погоды для Discord embed (с заголовком) |
|
||
|
||
### utils/news.py
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `fetch_rss()` | Получение RSS ленты (статьи или новости) |
|
||
| `truncate_title()` | Обрезка заголовка до заданной длины |
|
||
| `format_articles()` | Форматирование списка статей для вывода |
|
||
|
||
### utils/cat.py
|
||
|
||
| Функция | Описание |
|
||
|---------|----------|
|
||
| `fetch_cat()` | Получение URL случайного котика |
|
||
|
||
### utils/morning_runner.py
|
||
|
||
| Функция / Класс | Описание |
|
||
|----------------|----------|
|
||
| `MorningData` | dataclass с полями weather, articles, posts, cat_url |
|
||
| `gather_morning()` | Параллельный сбор всех данных для дайджеста |
|
||
| `run_morning()` | Формирование и отправка embed в канал Discord |
|
||
| `Scheduler` | Планировщик ежедневных задач (discord.ext.tasks.loop) |
|
||
|
||
### 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) |
|