15 KiB
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)
Настройка
cp .env.example .env
# заполнить BOT_TOKEN, DB_*, чаты и ADMIN_IDS
pip install -r requirements.txt
Запуск
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: кто, когда, было → стало и причина.
Раздел «Звания» — справочник магазина: название, цена и видимость. Изменения сразу
видны боту (он читает bot.titles). Удалить можно только звание, которое ни у кого
не стоит; используемое — скрывается (visible = false).
Раздел «Логи» — события из 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 (типы, payload, примеры запросов).
- схема —
clickhouse/init/001_events.sql(применяется при первой инициализации контейнера); - клиент и батчинг —
common/analytics/(запись неблокирующая, сбой ClickHouse не влияет на бота); - сырые события живут на VPS 7 дней и ежедневно уезжают архивом на домашний сервер,
на VPS остаются агрегаты (
events_daily,money_daily) — см. DEPLOY.md; - выключается флагом
CLICKHOUSE_ENABLED=false.
Уведомления в чат логов
В ALERT_CHAT_ID (если задан) уходят: вход и выход участников, успешные очищения и
ежечасный пинг «бот живой». Отправка не критична: ошибки только пишутся в лог.
Docker
Локальный запуск (postgres + migrate + clickhouse + bot + admin):
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-файлом:
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.
Разработка
pip install -r requirements-dev.txt
ruff check .
black --check .
pytest
Бэкап событий
Сырые события ClickHouse ежедневно забираются с VPS домашним сервером (pull), на VPS
остаются только агрегаты и последние дни сырых данных:
make backup E=test # на домашнем сервере: ./scripts/clickhouse_backup.sh test
Настройка, cron и SSH-доступ — см. DEPLOY.md.
Версионирование
- Единый источник версии —
common/version.py(__version__), текущая —0.6.7. - Схема — 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» и защиту ветки — когда появится второй разработчик.
# обычная работа: сообщение без префиксов, по существу изменения
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.