feat(tradein/tgbot): Telegram support-мост @MERAsupport_bot #2526

Merged
lekss361 merged 1 commit from feat/tradein-tg-support-bridge into main 2026-07-16 18:26:58 +00:00
Owner

Что это

Клиент пишет @MERAsupport_bot в личку → воркер зеркалит сообщение в топик супергруппы-форума → оператор отвечает реплаем на зеркало → бот доставляет ответ клиенту. Текст и медиа (фото/файлы) в обе стороны. Полный лог переписки в Postgres.

Целевой топик: t.me/c/3742528982/4414 (группа «Идейные и бедные», форум). Бот в группе уже состоит.

Решения

Отдельный контейнер на long-polling, а не webhook в tradein-backend. Webhook потребовал бы дырку в auth-middleware (_PUBLIC_PATHS, тот самый, что закрывали в #2213) и маршрут в Caddy. Так — нулевая внешняя поверхность, падение бота не задевает API. Паттерн тот же, что у scheduler_main.py/scraper.

Без aiogramhttpx уже в зависимостях, из Bot API нужны только getUpdates/copyMessage/sendMessage.

Модуль в этой репе, не отдельная. Бот живёт в той же БД и едет тем же деплой-трактом; отдельная репа дала бы распределённую связность без выигрыша. Изоляция достигается контейнером.

Маршрутизация — по topic_message_id. message_id в Telegram уникален в пределах чата сквозь все топики, а все зеркала лежат в одном support-чате → спутать адресата нельзя (подпёрто частичным UNIQUE-индексом). Реплай на шапку или на ответ другого оператора не резолвится → тихий игнор, обычная болтовня в топике бота не трогает.

Найдено ревью и починено (воспроизведено эмпирически, не по догадке)

Проблема Итог
🔴 Токен утекал в GlitchTip. Токен Telegram лежит в PATH URL, поэтому sanitize_url его не режет. Уезжал через locals стек-фреймов (include_local_variables по умолчанию True) и через span data HttpxIntegration include_local_variables=False + regex-редактор в before_send (обе формы: /bot<id>:<secret> и голая <id>:<secret>), поверх существующего PII-scrub
🔴 Токен в docker logs каждые 30с. httpx-логгер печатает полный URL на INFO, воркер поллит бесконечно логгер httpx приглушён до WARNING
🟠 Рестарт-луп после мержа. exit(0) при пустом токене + restart: unless-stopped (рестартует независимо от кода) kill-switch → idle-блокировка. unless-stopped оставлен сознательно: только он даёт автозапуск после ребута VPS, а у on-failure код выхода — гонка с long-poll
🟠 Drain на 100с был фикцией. stop_grace_period потеряли при копировании паттерна у scraper → дефолтные 10с stop_grace_period: 120s
🟠 Вечный спам дублей в топик. Сбой SQL (обрыв коннекта при деплое) → сессия в failed-transaction → offset не сохраняется → апдейт переигрывается и зеркалится по кругу SQLAlchemyError ловится отдельно, rollback() перед сдвигом offset

152-ФЗ

Переписка — ПДн. ON DELETE CASCADE по chat_id: удаление клиента = один DELETE. Таблицы прокомментированы с пометкой. Политика ретенции — follow-up, на MVP не блокирует.

Тесты

51 passed — маршрутизация обоих направлений, дедуп по update_id, 403 → is_blocked, throttle-окно шапки, redaction токена во всех формах event (frame locals, span data, breadcrumb, голая форма) + отсутствие ложных срабатываний на chat_id:12345 и подобном. Сеть не трогается (MockTransport).

Деплой — требуется ручной шаг

Бот не включится сам: TELEGRAM_BOT_TOKEN / TELEGRAM_SUPPORT_CHAT_ID / TELEGRAM_SUPPORT_TOPIC_ID задаются в runtime-env на VPS (в git не хранится). Без них воркер штатно висит в idle — это норма, не сбой. Порядок описан в DEPLOY.md.

⚠️ Токен для этого бота светился в переписке — на VPS класть перевыпущенный через @BotFather.

Follow-ups (не в этом PR)

  • Политика ретенции переписки (152-ФЗ)
  • Двухфазный протокол против осиротевших зеркал при краше между copyMessage и commit (сейчас логируется WARNING)
  • scheduler_main.py — тот же паттерн include_local_variables=True с секретами в URL (proxy creds, cian login)
  • Heartbeat/алерт «воркер не поллит»
## Что это Клиент пишет `@MERAsupport_bot` в личку → воркер зеркалит сообщение в топик супергруппы-форума → оператор отвечает **реплаем** на зеркало → бот доставляет ответ клиенту. Текст и медиа (фото/файлы) в обе стороны. Полный лог переписки в Postgres. Целевой топик: `t.me/c/3742528982/4414` (группа «Идейные и бедные», форум). Бот в группе уже состоит. ## Решения **Отдельный контейнер на long-polling, а не webhook в `tradein-backend`.** Webhook потребовал бы дырку в auth-middleware (`_PUBLIC_PATHS`, тот самый, что закрывали в #2213) и маршрут в Caddy. Так — нулевая внешняя поверхность, падение бота не задевает API. Паттерн тот же, что у `scheduler_main.py`/`scraper`. **Без `aiogram`** — `httpx` уже в зависимостях, из Bot API нужны только `getUpdates`/`copyMessage`/`sendMessage`. **Модуль в этой репе, не отдельная.** Бот живёт в той же БД и едет тем же деплой-трактом; отдельная репа дала бы распределённую связность без выигрыша. Изоляция достигается контейнером. **Маршрутизация — по `topic_message_id`.** `message_id` в Telegram уникален в пределах чата сквозь все топики, а все зеркала лежат в одном support-чате → спутать адресата нельзя (подпёрто частичным UNIQUE-индексом). Реплай на шапку или на ответ другого оператора не резолвится → тихий игнор, обычная болтовня в топике бота не трогает. ## Найдено ревью и починено (воспроизведено эмпирически, не по догадке) | | Проблема | Итог | |---|---|---| | 🔴 | **Токен утекал в GlitchTip.** Токен Telegram лежит в PATH URL, поэтому `sanitize_url` его не режет. Уезжал через locals стек-фреймов (`include_local_variables` по умолчанию `True`) и через span data `HttpxIntegration` | `include_local_variables=False` + regex-редактор в `before_send` (обе формы: `/bot<id>:<secret>` и голая `<id>:<secret>`), поверх существующего PII-scrub | | 🔴 | **Токен в `docker logs` каждые 30с.** httpx-логгер печатает полный URL на INFO, воркер поллит бесконечно | логгер httpx приглушён до WARNING | | 🟠 | **Рестарт-луп после мержа.** `exit(0)` при пустом токене + `restart: unless-stopped` (рестартует независимо от кода) | kill-switch → idle-блокировка. `unless-stopped` оставлен сознательно: только он даёт автозапуск после ребута VPS, а у `on-failure` код выхода — гонка с long-poll | | 🟠 | **Drain на 100с был фикцией.** `stop_grace_period` потеряли при копировании паттерна у `scraper` → дефолтные 10с | `stop_grace_period: 120s` | | 🟠 | **Вечный спам дублей в топик.** Сбой SQL (обрыв коннекта при деплое) → сессия в failed-transaction → offset не сохраняется → апдейт переигрывается и зеркалится по кругу | `SQLAlchemyError` ловится отдельно, `rollback()` перед сдвигом offset | ## 152-ФЗ Переписка — ПДн. `ON DELETE CASCADE` по `chat_id`: удаление клиента = один `DELETE`. Таблицы прокомментированы с пометкой. **Политика ретенции — follow-up**, на MVP не блокирует. ## Тесты `51 passed` — маршрутизация обоих направлений, дедуп по `update_id`, `403 → is_blocked`, throttle-окно шапки, redaction токена во всех формах event (frame locals, span data, breadcrumb, голая форма) + отсутствие ложных срабатываний на `chat_id:12345` и подобном. Сеть не трогается (`MockTransport`). ## Деплой — требуется ручной шаг Бот **не включится сам**: `TELEGRAM_BOT_TOKEN` / `TELEGRAM_SUPPORT_CHAT_ID` / `TELEGRAM_SUPPORT_TOPIC_ID` задаются в runtime-env на VPS (в git не хранится). Без них воркер штатно висит в idle — это норма, не сбой. Порядок описан в `DEPLOY.md`. ⚠️ Токен для этого бота светился в переписке — на VPS класть **перевыпущенный** через @BotFather. ## Follow-ups (не в этом PR) - Политика ретенции переписки (152-ФЗ) - Двухфазный протокол против осиротевших зеркал при краше между `copyMessage` и commit (сейчас логируется WARNING) - `scheduler_main.py` — тот же паттерн `include_local_variables=True` с секретами в URL (proxy creds, cian login) - Heartbeat/алерт «воркер не поллит»
lekss361 added 1 commit 2026-07-16 13:59:31 +00:00
feat(tradein/tgbot): Telegram support-мост @MERAsupport_bot
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
b579fa4ced
Клиент пишет боту в личку → воркер зеркалит сообщение через copyMessage
в топик супергруппы-форума → оператор отвечает реплаем на зеркало → бот
доставляет ответ клиенту. Полный лог переписки в Postgres.

Отдельный контейнер на long-polling, а не webhook в tradein-backend:
не нужно пробивать дырку в auth-middleware (_PUBLIC_PATHS, #2213) и
маршрут в Caddy, нулевая внешняя поверхность, падение бота не задевает API.
Без aiogram — httpx уже в зависимостях, нужны только getUpdates/copyMessage.

Маршрутизация ответа — по topic_message_id: message_id в Telegram уникален
в пределах чата сквозь все топики, а все зеркала лежат в одном support-чате,
поэтому спутать адресата нельзя. Реплай на шапку/на ответ другого оператора
не резолвится (у direction='out' topic_message_id IS NULL) → тихий игнор.

Безопасность (найдено ревью, воспроизведено эмпирически):
- токен Telegram живёт в PATH URL, поэтому sanitize_url его не режет;
  утекал в GlitchTip через locals стек-фреймов (include_local_variables
  по умолчанию True) и через span data HttpxIntegration. Закрыто
  include_local_variables=False + regex-редактор в before_send (обе формы:
  /bot<id>:<secret> и голая <id>:<secret>), поверх существующего PII-scrub.
- httpx-логгер печатает полный URL на INFO → боевой токен уходил бы в
  docker logs каждые 30с. Приглушён до WARNING.

Надёжность:
- kill-switch при пустом токене — idle-блокировка, не exit(0): при
  restart: unless-stopped выход с любым кодом даёт рестарт-луп.
  unless-stopped выбран сознательно — только он гарантирует автозапуск
  после ребута VPS.
- stop_grace_period: 120s — дефолтные 10с убивали бы контейнер раньше,
  чем докрутится long-poll (30с) и отработает drain (100с).
- сбой SQL теперь ловится отдельно и делает rollback перед сдвигом offset:
  иначе сессия в failed-transaction не давала сохранить offset, апдейт
  переигрывался и зеркалился в топик по кругу.

152-ФЗ: переписка — ПДн, ON DELETE CASCADE по chat_id, удаление клиента
одним DELETE. Ретенция — follow-up.

Бот не включается автоматически: TELEGRAM_* задаются в runtime-env на VPS,
без них воркер штатно висит в idle. Порядок — в DEPLOY.md.

Тесты: 51 passed (маршрутизация обоих направлений, дедуп, 403→is_blocked,
throttle-окно шапки, redaction токена во всех формах event).
lekss361 merged commit fcfc777baa into main 2026-07-16 18:26:58 +00:00
lekss361 deleted branch feat/tradein-tg-support-bridge 2026-07-16 18:26:58 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2526
No description provided.