# 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/?start=login_`; бот подтверждает вход, сайт забирает токен и открывает сессию. Токен в БД хранится как 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`](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.4`. - Схема — 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`.