YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
- 🕵️ Dialog Spy Bot
- 1. Как это работает
- 2. Структура проекта
- 3. Схема БД (SQLite, WAL)
- 4. Запуск
- 5. TTL-медиа (сгорающие фото/видео) — главное место, где все ошибаются
- 6. Логика по событиям
- 6.5 Монетизация, квоты и роли
- 7. Надёжность
- 8. Что доработать для продакшена (roadmap)
- 9. Юридическая заметка
- 10. Развёртывание 24/7
- 1. Как это работает
🕵️ Dialog Spy Bot
Telegram-бот — полный аналог «Dialog Spy Bot»: архивирует личные чаты пользователя
через официальный функционал Telegram Business («Автоматизация чатов» /
business_connection), возвращает удалённые сообщения и показывает правки
в формате «Было → Стало».
Никаких юзерботов (Pyrogram/Telethon), никакого чтения чужих аккаунтов в обход API — только Bot API 7.1+ / aiogram 3.x.
1. Как это работает
┌──────────────────────┐ Business Connection ┌───────────────────────┐
│ Пользователь Premium │ ───────────────────────►│ Telegram Bot API │
│ Настройки → Telegram │ │ updates: │
│ Business → Автомати- │ │ business_connection │
│ зация чатов → бот │ │ business_message │
└──────────────────────┘ │ edited_business_msg │
│ deleted_business_msgs│
└───────────┬───────────┘
│ polling / webhook
▼
┌────────────────────────────────┐
│ Dialog Spy Bot │
│ aiogram Router (business.py) │
│ ├─ архив → SQLite │
│ ├─ TTL-медиа → диск сразу │
│ ├─ edit → diff → алерт │
│ └─ delete → оригинал → алерт │
└────────────────────────────────┘
- Пользователь с Telegram Premium подключает бота к своим личным чатам.
- Бот получает
business_message(и входящие, и исходящие) и складывает их в БД. deleted_business_messages→ находим сообщения в БД → отправляем владельцу оригинал (текст + скачанный файл).edited_business_message→ сравниваем с версией из БД → отправляем diff.
2. Структура проекта
dialog-spy-bot/
├── bot/
│ ├── main.py # точка входа: Bot + Dispatcher + polling/webhook
│ ├── config.py # настройки (pydantic-settings, .env)
│ ├── logging_config.py # формат логов
│ ├── db/
│ │ ├── schema.sql # DDL: owners, connections, chats, messages,
│ │ │ # message_versions, events_log
│ │ └── database.py # aiosqlite: подключение + все запросы (репозиторий)
│ ├── filters/
│ │ └── business.py # HasBusinessConnection / ConnectionKnown / IsOutgoingBusiness
│ ├── handlers/
│ │ ├── business.py # ★ 4 business-хэндлера (основная логика)
│ │ ├── user.py # /start /status /settings /search /export /help
│ │ └── errors.py # глобальный error-handler
│ ├── middlewares/
│ │ └── throttling.py # антиспам на команды пользователя
│ ├── services/
│ │ ├── media_extractor.py # content_type / file_id / текст из Message
│ │ ├── ttl_media.py # ★ политика и немедленное скачивание сгорающих медиа
│ │ ├── text_diff.py # пословный + unified diff
│ │ ├── notifier.py # ★ доставка алертов владельцу (ретраи, flood-wait)
│ │ └── background.py # RetentionWorker: чистка БД и диска
│ └── utils/
│ └── formatting.py # имена, даты, HTML-экранирование, truncate
├── tests/
│ ├── test_units.py # diff + извлечение медиа
│ ├── test_business_flow.py # сквозной сценарий на реальном SQLite
│ └── test_smoke_main.py # реальный Bot + фейковый Telegram API (aiohttp)
├── data/ # создаётся автоматически: spy.db + media/
├── .env.example
├── requirements.txt
├── pyproject.toml
└── Dockerfile
3. Схема БД (SQLite, WAL)
| Таблица | Назначение | Ключевые колонки |
|---|---|---|
owners |
владелец бизнес-аккаунта + его настройки алертов | user_id, notify_chat_id, alert_on_deleted/edited/outgoing, retention_days |
connections |
связка business_connection_id → user_id |
id (PK), user_id, user_chat_id, status, is_enabled, can_reply, rights_json, connected_at, last_event_at |
chats |
кэш метаданных диалога (имя собеседника для алертов) | chat_id (PK), chat_type, title, username, first_name, last_name |
messages |
ядро архива | PK (chat_id, message_id), business_connection_id, owner_user_id, is_outgoing, sender_id, sender_name, content_type, text, caption, file_id, file_unique_id, file_size, mime_type, media_group_id, local_path, is_ttl_media, has_protected_content, media_download_state, edit_count, is_deleted, deleted_at, date, raw_json |
message_versions |
история версий для diff | (chat_id, message_id, version), text, file_id, edited_at |
events_log |
аудит всех событий и ошибок | event_type, owner_id, chat_id, message_id, payload |
Индексы: по business_connection_id, owner_user_id+created_at, is_deleted+deleted_at,
media_download_state+created_at, created_at (для retention).
Полный DDL — bot/db/schema.sql.
4. Запуск
git clone <repo> && cd dialog-spy-bot
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # вписать BOT_TOKEN
python -m bot.main # long polling
Подключение к аккаунту (на телефоне пользователя)
- Нужен Telegram Premium.
- Настройки → Telegram Business → Автоматизация чатов (Chat automation).
- Выбрать нашего бота, разрешить доступ к личным чатам.
- Бот получит
business_connectionи пришлёт подтверждение в личку.
⚠️ Бот должен быть создан у @BotFather как обычный бот. Право «Business bots» включается автоматически при первом
business_connection. Для продакшена рекомендуетсяBOT_MODE=webhook(HTTPS обязателен).
Тесты
pip install pytest pytest-asyncio
pytest -q # 12 тестов, сеть не нужна (фейковый Telegram API)
5. TTL-медиа (сгорающие фото/видео) — главное место, где все ошибаются
Если сохранить только file_id, после срабатывания таймера самоуничтожения
getFile вернёт ошибку и контент будет потерян навсегда. Поэтому в
services/ttl_media.py файл выкачивается в момент получения апдейта.
Важная деталь API. В business-событиях Telegram не отдаёт поле таймера
самуничтожения: в схеме Message (проверено на aiogram 3.31 / Bot API 9.x) нет
self_destruct / message_auto_delete_timer. Доступные маркеры риска:
| Маркер | Что означает |
|---|---|
has_protected_content = True |
запрет пересылки/копирования — типично для одноразового контента |
has_media_spoiler = True |
скрытое медиа, косвенный признак |
отсутствие file_id у медиа |
контент уже недоступен боту |
Политика настраивается через MEDIA_DOWNLOAD_MODE:
| Значение | Поведение |
|---|---|
protected |
качать только при has_protected_content |
smart (по умолчанию) |
has_protected_content или has_media_spoiler |
always |
качать всё входящее медиа — максимальная надёжность, расходует диск |
Для гарантированного перехвата именно секретных фото/видео используйте always:
это единственный режим, который не зависит от эвристик.
Ограничения Bot API:
- файл больше 20 МБ скачать нельзя (
getFileне отдаёт) → статусtoo_large, владельцу уходит текст сообщения и пометка; - обход — собственный local Bot API server (
is_local=True, лимит 2 ГБ).
6. Логика по событиям
business_connection
upsert_owner + upsert_connection (сохраняем business_connection_id → user_id
и user_chat_id — цель для алертов), пишем права из BusinessBotRights,
отправляем владельцу подтверждение/уведомление об отключении.
business_message
- Фильтр
HasBusinessConnectionотбрасывает апдейты неизвестных/выключенных подключений и инжектит в хэндлер объектconn. media_extractorопределяетcontent_type,file_id(для фото — крупнейшийPhotoSize),text/caption,entities.should_download_now()→ при риске TTL немедленноbot.download()на диск (сначала пишем строку со статусомpending, чтобы не потерять факт получения).upsert_message+message_versions(version=0)+events_log.- Опционально (
alert_on_outgoing) — уведомление о собственных исходящих.
edited_business_message
- Достаём старую версию из БД. Если её нет (сообщение старше подключения) — архивируем текущую и честно сообщаем, что оригинала нет.
text_diff.compare_texts()→ пословный diff ([− было −] → [+ стало +]) и unified.apply_edit()+ новая строка вmessage_versions.- Алерт владельцу: «✏️ Изменено сообщение от [Имя]. Было: … → Стало: …» + разница.
deleted_business_messages
- Пачка
message_idsв рамках одного чата →get_messages(). - Для каждого найденного:
mark_deleted()→ алерт с оригинальным текстом → отправка файла (локальная копия → иначеfile_id→ иначе «медиа недоступно»). - Для ненайденных id — отдельное сообщение «Удаление без архива» (важно для отладки: значит, сообщение пришло до подключения бота).
6.5 Монетизация, квоты и роли
Подписка за Telegram Stars (/subscribe): неделя = 50⭐, месяц = 120⭐, год = 500⭐
(значения в config.py: SUB_STARS_WEEK/MONTH/YEAR). Оплата через sendInvoice(currency="XTR"),
активация по message.successful_payment, хранение в таблице subscriptions.
Бесплатный лимит восстановлений удалённых сообщений:
- первые
FREE_RESTORE_TOTAL(10) восстановлений — бесплатно; - после исчерпания —
FREE_RESTORE_DAILY(1) раз в сутки; - дальше вместо контента приходит тизер «🙈 Кто-то в вашем чате удалил сообщение…
подключите подписку» с кнопками тарифов.
Квота списывается только при фактической выдаче контента (
services/access.py).
Роли: ADMIN (id из ADMIN_IDS, по умолчанию 7603742317) → TESTER
(безлимит на N дней, выдаёт админ: /testers add <id|@username> <дней>,
/testers remove, /testers list) → SUBSCRIBER → FREE.
Админская статистика: /stats.
Копия сгорающего медиа по ответу: владелец отвечает любым текстом на
фото/видео/ГС/кружок в личном чате → бот сохраняет файл на сервер (скачивает
немедленно, если копии ещё нет) и присылает её в личку бота. Работает через
business_message с reply_to_message.
7. Надёжность
- Retry/flood-wait в
notifier._send_with_retry:TelegramRetryAfter(сон наretry_after),TelegramServerError/TelegramNetworkError(3 попытки),TelegramForbiddenError(пользователь не нажимал /start → логируем, не спамяем). - Нарезка текста по 4096 символов с экранированием HTML.
- Пауза между отправками (
SEND_INTERVAL) — Telegram может прислать до 100 удалённых id одним апдейтом, а лимит в личку ~1 msg/sec. - Идемпотентность:
upsert_messageпри повторной доставке апдейта не «воскрешает» удалённое сообщение и не затираетlocal_path. - RetentionWorker: фоновая задача чистит БД и файлы старше N дней
(настраивается
MESSAGE_RETENTION_DAYS/MEDIA_RETENTION_DAYS). - Error-handler (
handlers/errors.py) логирует любое необработанное исключение и пишет его вevents_log— polling не падает.
8. Что доработать для продакшена (roadmap)
| Задача | Как |
|---|---|
| PostgreSQL | заменить Database._connect() на asyncpg/SQLAlchemy; схема в schema.sql диалекто-независима (кроме strftime в DEFAULT) |
| Webhook + несколько воркеров | BOT_MODE=webhook, nginx + HTTPS; RetentionWorker вынести в отдельный процесс/cron |
| Очередь на отправку алертов | вместо asyncio.sleep — per-chat очередь (Redis/aiostream) |
| Группировка media_group | messages.media_group_id уже сохраняется — склеивать в sendMediaGroup |
| Поиск | LIKE → FTS5 / PostgreSQL FTS |
| Кэширование настроек | Redis вместо запроса в БД на каждый апдейт |
| Авто-ответ от имени аккаунта | bot.send_message(..., business_connection_id=...), если rights.can_reply |
| Пометка прочитанным | readBusinessMessage (нужно право can_read_messages) |
9. Юридическая заметка
Бот обрабатывает личные сообщения третьих лиц. Перед запуском:
- добавьте политику конфиденциальности и явное согласие пользователя при
/start; - храните данные в юрисдикции, соответствующей GDPR (право на удаление —
/export+ полная очистка по запросу); - ограничьте срок хранения (
MESSAGE_RETENTION_DAYS) и шифруйте диск; - не перепродавайте и не передавайте архив третьим лицам.
10. Развёртывание 24/7
GitHub ≠ сервер. GitHub хранит код и может триггерить деплой (Actions), но сам процесс бота должен жить на машине, которая не выключается.
| Вариант | Цена | Подходит |
|---|---|---|
| VPS (Timeweb, Aeza, RuVDS…) | ~150–300 ₽/мес | ✅ рекомендуется: polling, без домена |
| Домашний ПК / Raspberry Pi | 0 | ✅ то же самое, канал стабильный |
Render.com Free (см. deploy/render/) |
0, без карты | ✅ демо: Docker + keep-alive пингом /healthz; диск эфемерный |
| Hugging Face Spaces | PRO $9/мес | ❌ с 2025 Docker Spaces только платные (free — Static) |
| PaaS (Fly.io, Railway) | от 0–5 $ | ⚠️ бесплатные тарифы усыпляют процесс |
| GitHub Actions как «хост» | — | ❌ раннеры эфемерны, 6 ч лимит |
Быстрый старт на VPS (5 команд)
scp -r dialog-spy-bot root@<HOST>:/opt/
ssh root@<HOST>
cd /opt/dialog-spy-bot
nano .env.production.example # вписать BOT_TOKEN → сохранить как .env
sudo bash deploy/install_vps.sh
Скрипт создаст пользователя spybot, venv, systemd-юнит
(deploy/dialog-spy-bot.service, Restart=always, харденинг) и поднимет бота.
Лог: journalctl -u dialog-spy-bot -f.
Автодеплой через GitHub Actions
- Создайте приватый репозиторий, запушьте проект (
.envв git не попадает — он в.gitignore). - Settings → Secrets and variables → Actions:
DEPLOY_HOST,DEPLOY_USER,DEPLOY_SSH_KEY. - Каждый
git pushвmain→ workflow.github/workflows/deploy.ymlскопирует код на VPS и перезапустит systemd-сервис (git-авторизация на сервере не нужна).
Чек-лист безопасности
.envиdata/никогда не коммитятся (проверьте.gitignore);- токен бота живёт только на сервере; после передачи токена кому-либо —
/revokeв @BotFather; - SSH-ключ деплоя — отдельный, не ваш основной;
- резервная копия:
sqlite3 data/spy.db ".backup spy-backup.db"+tar data/media.