229 lines
14 KiB
Markdown
229 lines
14 KiB
Markdown
# DispelEvilBot
|
||
|
||
Telegram-бот клана Dispel Evil (aiogram 3 + Postgres). Монорепозиторий:
|
||
общий пакет `common` используется сервисом бота `bot` (и, в будущем, админкой `admin`).
|
||
|
||
## Структура
|
||
|
||
```
|
||
common/ # общий пакет: config, logging, db, repositories, models, texts, analytics
|
||
bot/ # сервис бота: handlers, services, keyboards, middlewares, notifications
|
||
admin/ # сервис админки: FastAPI + Jinja2/HTMX (Блок 4), свой Dockerfile
|
||
sql/ # миграции Flyway (V__*.sql, R__*.sql)
|
||
clickhouse/ # init-скрипты ClickHouse (схема аналитики)
|
||
docs/ # документация (docs/events.md — события аналитики)
|
||
scripts/ # эксплуатационные скрипты (проверка контура, бэкап событий)
|
||
images/ # статичные картинки (приветствие)
|
||
deploy/ # материалы деплоя (Caddyfile, инструкции)
|
||
tests/ # тесты
|
||
Dockerfile # образ бота
|
||
docker-compose.yml / docker-compose.prod.yml
|
||
.gitea/ # CI (Gitea Actions)
|
||
```
|
||
|
||
## Требования
|
||
|
||
- Python 3.12
|
||
- PostgreSQL 14+
|
||
- ClickHouse 25.3 (аналитика событий; можно выключить — `CLICKHOUSE_ENABLED=false`)
|
||
|
||
## Настройка
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
# заполнить BOT_TOKEN, DB_*, чаты и ADMIN_IDS
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
## Запуск
|
||
|
||
```bash
|
||
APP_ENV=dev python -m bot # dev
|
||
APP_ENV=prod python -m bot # prod
|
||
```
|
||
|
||
или через `./start.sh` (режим берётся из `APP_ENV`).
|
||
|
||
Админка (FastAPI) запускается отдельным процессом: `python -m admin` (зависимости —
|
||
`requirements-admin.txt`), по умолчанию слушает `0.0.0.0:8000`, проверка живучести — `GET /healthz`.
|
||
|
||
## База данных
|
||
|
||
Схема управляется миграциями **Flyway** в `sql/` (`V__*.sql` — versioned, `R__*.sql` — repeatable).
|
||
При Docker-развёртывании накат выполняется автоматически сервисом `migrate` перед ботом.
|
||
|
||
## Админка (Блок 4)
|
||
|
||
Отдельный сервис `admin/` (FastAPI + Jinja2/HTMX), запуск `python -m admin`.
|
||
Все тексты бота — **последовательности** (`bot.message_sequences`): миграция `V24`
|
||
переносит каждый старый шаблон в последовательность из одного сообщения, а бот
|
||
отправляет их через общий sender. Старые `message_templates` остаются фолбэком в коде.
|
||
Правки применяются без перезапуска; предпросмотр использует тот же `render()`, что и бот.
|
||
|
||
**Вход** — через Telegram Login Widget: подпись `hash` проверяется токеном бота, доступ
|
||
есть только у записей в `bot.admin_users` (роль проверяется в БД на каждом запросе).
|
||
Первые администраторы засеваются из `ADMIN_IDS` при старте. Сессия — подписанная
|
||
HttpOnly-cookie (`ADMIN_SESSION_SECRET`), все формы защищены CSRF. Для входа нужны
|
||
`BOT_TOKEN`, `BOT_USERNAME` (без @) и HTTPS-адрес `ADMIN_PUBLIC_URL`.
|
||
|
||
Дополнительно — **вход через установленный Telegram**: кнопка «Открыть Telegram»
|
||
создаёт одноразовый токен (5 минут) и открывает `t.me/<bot>?start=login_<token>`;
|
||
бот подтверждает вход, сайт забирает токен и открывает сессию. Токен в БД хранится
|
||
как SHA-256 и используется один раз.
|
||
|
||
Раздел «Расписания» — CRUD рассылок (`bot.schedules` + история): время, дни недели,
|
||
таймзона, шаблон и вкл/выкл. Бот раз в минуту читает активные расписания из БД; если
|
||
база недоступна — использует расписание из кода (совпадает с сидом `V16`).
|
||
|
||
Раздел «Пользователи» — список с поиском по нику/username/имени/Telegram-ID и карточка:
|
||
уровень, опыт и серия, баланс (`SUM(money.amount)`), яички, сообщения, очищения,
|
||
подписка на рассылки, дата вступления и последние операции по балансу. В карточке можно
|
||
сменить ник и звание, включить/выключить подписку и скорректировать баланс (запись в
|
||
журнал `money` с действием «Корректировка админом»). Каждое ручное изменение пишется в
|
||
`bot.user_changes`: кто, когда, было → стало и причина.
|
||
|
||
Раздел «Логи» — события из ClickHouse: фильтры по типу, пользователю (ID или username)
|
||
и датам, пагинация по 50. Время показывается в `TIMEZONE`, payload раскрывается как JSON,
|
||
ID пользователя ведёт в его карточку. ClickHouse читается отдельным ленивым клиентом;
|
||
если он недоступен, страница показывает ошибку, а не падает.
|
||
|
||
Раздел «Админы» — выдача и отзыв доступа к панели по Telegram-ID, роли и вкл/выкл.
|
||
В карточке пользователя есть исключение из чата (бан + разбан) с удалением данных и
|
||
уведомлением «кто кого выгнал»; историю изменений показывает, кто менял ник — сам
|
||
пользователь через `/ник` или админ.
|
||
|
||
Раздел «Последовательности» — набор сообщений по порядку (приветствие и любые другие):
|
||
у каждого свой текст, картинка из общего volume `media`, вкл/выкл и несколько кнопок.
|
||
Редактор текста — textarea с тулбаром: жирный, курсив, подчёркивание, зачёркивание,
|
||
код, блок кода, цитата, спойлер и ссылка; есть предпросмотр и счётчик символов
|
||
(1024 для подписи к фото, 4096 для обычного сообщения).
|
||
|
||
Интерфейс адаптирован под мобильные: бургер-меню, второстепенные колонки таблиц
|
||
скрываются на узких экранах, формы складываются в одну колонку.
|
||
|
||
Раздел «Отправить» — ручное сообщение в основной чат от имени бота: текст с тем же
|
||
редактором, картинка и URL-кнопки, предпросмотр и подтверждение. Каждая отправка
|
||
(успешная или нет) пишется в `bot.admin_messages`.
|
||
|
||
## Аналитика (ClickHouse)
|
||
|
||
Все значимые действия бота пишутся в ClickHouse в единую таблицу `analytics.events`.
|
||
Контракт событий — [`docs/events.md`](docs/events.md) (типы, payload, примеры запросов).
|
||
|
||
- схема — `clickhouse/init/001_events.sql` (применяется при первой инициализации контейнера);
|
||
- клиент и батчинг — `common/analytics/` (запись неблокирующая, сбой ClickHouse не влияет на бота);
|
||
- сырые события живут на VPS 7 дней и ежедневно уезжают архивом на домашний сервер,
|
||
на VPS остаются агрегаты (`events_daily`, `money_daily`) — см. [DEPLOY.md](DEPLOY.md);
|
||
- выключается флагом `CLICKHOUSE_ENABLED=false`.
|
||
|
||
## Уведомления в чат логов
|
||
|
||
В `ALERT_CHAT_ID` (если задан) уходят: вход и выход участников, успешные очищения и
|
||
ежечасный пинг «бот живой». Отправка не критична: ошибки только пишутся в лог.
|
||
|
||
## Docker
|
||
|
||
Локальный запуск (postgres + migrate + clickhouse + bot + admin):
|
||
|
||
```bash
|
||
cp .env.example .env # заполнить
|
||
docker compose up --build
|
||
```
|
||
|
||
CI собирает два образа: `dispelevilbot-bot` и `dispelevilbot-admin`. Админка в compose
|
||
по умолчанию публикуется только на loopback (`127.0.0.1:${ADMIN_HTTP_PORT}`, test 8100 /
|
||
prod 8101); для nginx-proxy-manager задаётся `ADMIN_BIND_IP=172.17.0.1`, наружу выйдет
|
||
вместе с доменом и TLS (Блок 4.2). Картинки последовательностей лежат в общем
|
||
Docker-volume `media` (`/app/media`), который монтируется и боту, и админке.
|
||
|
||
Прод/test-контуры — один `docker-compose.prod.yml`, разделяются Compose-проектом и env-файлом:
|
||
|
||
```bash
|
||
docker compose -p prod --env-file env/prod.env -f docker-compose.prod.yml up -d
|
||
docker compose -p test --env-file env/test.env -f docker-compose.prod.yml up -d
|
||
```
|
||
|
||
Пошаговое развёртывание контуров `test`/`prod` — см. [`DEPLOY.md`](DEPLOY.md).
|
||
|
||
## Разработка
|
||
|
||
```bash
|
||
pip install -r requirements-dev.txt
|
||
ruff check .
|
||
black --check .
|
||
pytest
|
||
```
|
||
|
||
## Бэкап событий
|
||
|
||
Сырые события ClickHouse ежедневно забираются с VPS домашним сервером (`pull`), на VPS
|
||
остаются только агрегаты и последние дни сырых данных:
|
||
|
||
```bash
|
||
make backup E=test # на домашнем сервере: ./scripts/clickhouse_backup.sh test
|
||
```
|
||
|
||
Настройка, cron и SSH-доступ — см. [`DEPLOY.md`](DEPLOY.md).
|
||
|
||
## Версионирование
|
||
|
||
- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.6.0`.
|
||
- Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
|
||
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
|
||
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.
|
||
|
||
## Процесс работы с git
|
||
|
||
Проект ведётся в режиме **trunk-based** (один разработчик): основная работа — прямо в `master`.
|
||
|
||
- `master` — интеграционная ветка, всегда в рабочем состоянии.
|
||
- Релизы — аннотированные теги `vX.Y` (см. «Версионирование»).
|
||
- Для крупных/рискованных изменений — короткая ветка `feature/...` и merge обратно в `master`.
|
||
- Push в `master` → CI (Gitea Actions) собирает образ; **авто-выката в прод нет**.
|
||
- Выкатка: сначала контур `test`, затем `prod` по подтверждению (см. Блок 2.5 плана).
|
||
- Переход на «ветки + PR» и защиту ветки — когда появится второй разработчик.
|
||
|
||
```bash
|
||
# обычная работа: сообщение без префиксов, по существу изменения
|
||
git add -A
|
||
git commit -m "Краткое описание изменения"
|
||
git push origin master
|
||
|
||
# выпуск релиза
|
||
# 1) поднять __version__ в common/version.py
|
||
git commit -am "Версия X.Y"
|
||
git tag -a vX.Y -m "Release vX.Y"
|
||
git push origin master --tags
|
||
```
|
||
|
||
## Приветствие новых участников
|
||
|
||
Бот отправляет фото с короткой персональной подписью (первая строка шаблона `welcome.text`),
|
||
а затем одно сообщение со всем остальным текстом (`welcome.text` без первой строки +
|
||
`welcome.text2` + `welcome.text3`) и кнопкой на канал. Так приветствие остаётся одним
|
||
смысловым блоком, но не упирается в лимит подписи к фото (1024 символа).
|
||
|
||
## Пасхальное событие «Яички»
|
||
|
||
`/яички` работает только в сезон: по умолчанию с **1 апреля по православную Пасху**
|
||
(в 2026 году — 12 апреля). Вне окна бот отвечает, что ещё рано, а строка про яички
|
||
не показывается ни в `/инфо`, ни в карточке пользователя в админке.
|
||
Даты можно зафиксировать переменными `EGG_EVENT_START`/`EGG_EVENT_END` (ISO,
|
||
например `2027-04-01` и `2027-05-02`).
|
||
|
||
## Команды бота
|
||
|
||
`/ник`, `/список`, `/кто`, `/инфо`, `/помощь`, `/очищение`, `/яички`,
|
||
`/магазин`, `/рейтинг`, `/отписка`, `/подписка`.
|
||
|
||
В меню Telegram показываются латинские алиасы (`/help`, `/info`, `/nick`, `/list`,
|
||
`/who`, `/purify`, `/eggs`, `/shop`, `/top`, `/sub`, `/unsub`) с русскими описаниями:
|
||
Telegram не принимает кириллицу в именах команд. Русские команды работают при вводе
|
||
вручную. Набор сверяется с ожидаемым при каждом старте бота (`ensure_commands`).
|
||
|
||
## Переменные окружения
|
||
|
||
- общие — `.env.example` (включая `CLICKHOUSE_*` и параметры батчинга `ANALYTICS_*`);
|
||
- контуры — `env/prod.env.example`, `env/test.env.example`;
|
||
- бэкап событий — `env/backup.env.example`.
|