25 KiB
Техническое задание: двунаправленный мост MAX <-> Telegram
1. Назначение системы
Система должна обеспечивать непрерывную двунаправленную синхронизацию сообщений между мессенджером MAX и Telegram:
- направление A: входящие сообщения из MAX пересылаются в Telegram;
- направление B: входящие сообщения из Telegram пересылаются в MAX;
- поддерживаются текст, фото, видео, документы/файлы и ответы (reply) в пределах доступных API;
- реализованы дедупликация, хранение связей между сообщениями в обоих направлениях и базовый health-monitoring;
- предусмотрен fallback-маршрут в Telegram при отсутствии явного соответствия чатов.
Система реализуется как один сервисный процесс (или контейнер), работающий постоянно.
2. Контекст и границы
2.1 Внешние зависимости
Обязательные внешние системы:
- API MAX (через клиентскую библиотеку или собственный API-клиент);
- Telegram Bot API (через клиентскую библиотеку);
- SQLite (или совместимое хранилище) для локального состояния;
- переменные окружения для конфигурации.
2.2 Что входит в систему
- запуск и аутентификация клиента MAX;
- polling Telegram
getUpdates(без webhook); - обработка входящих событий в обоих направлениях;
- маршрутизация по названию чата и по явным биндам;
- хранение mapping/дедупликации/маршрутов;
- HTTP health endpoints (
/livez,/healthz).
2.3 Что НЕ входит в систему
- UI/панель администрирования;
- сложная очередь сообщений (Kafka/RabbitMQ и т.п.);
- гарантированная exactly-once доставка между платформами;
- миграции БД с версионированием (в базовой реализации только
CREATE TABLE IF NOT EXISTS); - хранение медиа в собственной файловой инфраструктуре.
3. Функциональные требования
3.1 MAX -> Telegram
При получении сообщения из MAX система должна:
- Распарсить сообщение:
message_id,chat_id,sender_name,chat_name, текст;- список
image_urls,video_urls,file_urls; - список неизвестных вложений;
- данные reply-контекста (
reply_to_max_message_id, preview).
- Обогатить данные через API MAX (best effort):
- попытаться получить человекочитаемое имя отправителя;
- попытаться получить реальное название чата;
- извлечь дополнительные вложения через типизированные attach-объекты.
- Проверить дедупликацию по паре
(message_id, chat_id); дубликаты не отправлять. - Выбрать Telegram-чат:
- сначала через явный route (bind) по нормализованному названию MAX-чата;
- затем через кэш чатов Telegram по совпадению заголовка;
- если не найдено — отправка в
fallback_user_id.
- Сформировать текст:
- если целевой Telegram-чат найден (не fallback):
"{sender}:\n{text}"; - если fallback:
"{sender} / {chat}:\n{text}"; - при неизвестных вложениях добавить уведомление в конец.
- если целевой Telegram-чат найден (не fallback):
- Обработать reply:
- попытаться найти Telegram
reply_to_message_idчерез mapping; - если mapping не найден — добавить текстовый контекст ответа.
- попытаться найти Telegram
- Отправить контент:
- при
image+video > 1— отправить единым альбомом (sendMediaGroup); - иначе отправить текст/медиа/файлы поштучно;
- при полностью пустом payload отправить служебный fallback-текст.
- при
- Сохранить mapping отправленных сообщений.
- Пометить сообщение как forwarded.
- При ошибке основного пути сделать аварийное best-effort уведомление в fallback-чат Telegram.
3.2 Telegram -> MAX
Система должна запускать единственный polling-цикл getUpdates и:
- При старте получить
bot_idчерезgetMe. - Обновить локальный кэш чатов MAX (
title -> id) на основе доступного списка. - В цикле получать updates с
offsetиallowed_updates=["message","channel_post"]. - Для каждого сообщения:
- отбросить неподдерживаемые типы;
- отбросить сообщения бота (защита от петель);
- обработать команду
/bind_maxв приоритетном порядке; - обработать служебные команды управления MAX (только личка + только fallback user);
- для обычных сообщений найти чат MAX по нормализованному названию Telegram-чата;
- если чат MAX не найден — не пересылать, логировать ошибку.
- Обработать
media_group_id:- буферизовать элементы альбома по ключу
(telegram_chat_id, media_group_id); - после grace-паузы (около 1.2 сек) отправить одним сообщением в MAX с несколькими вложениями.
- буферизовать элементы альбома по ключу
- Для одиночных сообщений:
- сформировать текст
"{sender}:\n{text_or_caption}"; - для фото/видео получить URL через
getFileи вложить в MAX как native media attach; - для document/audio/voice/animation/sticker/video_note добавить URL-список в текстовый блок;
- при reply попытаться найти соответствующее сообщение MAX через mapping.
- сформировать текст
- После успешной отправки:
- поставить реакцию на Telegram-сообщение (emoji, best effort);
- сохранить mapping
(telegram chat/message -> max chat/message).
3.3 Управляющие команды в Telegram
Команды обрабатываются только в приватном чате с ботом и только от пользователя fallback_user_id, кроме /bind_max (она может работать в целевом чате).
Поддерживаемые команды:
/help— список команд;/list— список активных чатов MAX;/join <LINK>— вступление в группу/канал MAX по ссылке;/leave <НАЗВАНИЕ>— выход из канала MAX (только если тип чата определен как канал);/last_messages <НАЗВАНИЕ>— последние 10 сообщений;/bind_max <точное название MAX-чата>— привязка текущего Telegram-чата к MAX-чату.
Требования:
- у команд должны быть понятные текстовые ответы;
- ошибки внешних API должны возвращаться в ответе, не падая процессом;
/bind_maxдолжна валидировать существование MAX-чата перед сохранением маршрута.
4. Нефункциональные требования
- Надежность: сервис работает бесконечно, при ошибках polling применяет backoff.
- Идемпотентность (частичная): дедупликация MAX->Telegram через БД.
- Наблюдаемость: структурированные логи и health endpoint.
- Портируемость: реализация возможна на любом языке при соблюдении контрактов.
- Производительность: обработка событий в near real-time, без тяжелых batch-процессов.
- Отказоустойчивость: best-effort при частичных отказах API/медиа.
5. Конфигурация (env contract)
Обязательные параметры:
MAX_PHONE— телефон аккаунта MAX (для авторизации/сессии);MAX_WORK_DIR— рабочая директория клиента MAX (по умолчаниюcache);TELEGRAM_BOT_TOKEN— токен Telegram-бота;TELEGRAM_FALLBACK_USER_ID— Telegram user/chat id для fallback;SQLITE_PATH— путь до SQLite БД (по умолчанию${MAX_WORK_DIR}/max2telegram.db).
Дополнительно:
TZ— таймзона окружения;PYTHONUNBUFFERED(или аналог) — политика буферизации логов.
Валидация:
- обязательные переменные должны проверяться при старте;
- при отсутствии обязательной переменной процесс завершает запуск с явной ошибкой.
6. Архитектура
6.1 Компоненты
-
Bootstrap / Main
- загружает конфиг;
- создает клиентов MAX и Telegram;
- инициализирует storage;
- запускает health server;
- подключает обработчики входящих сообщений MAX;
- запускает Telegram->MAX poller как фоновую задачу.
-
MAX->Telegram Bridge
- парсинг + enrich входящих MAX-сообщений;
- маршрутизация в Telegram;
- отправка текст/медиа/документы;
- обработка миграции Telegram chat id;
- сохранение mapping и dedup.
-
Telegram API Client
- HTTP-обертка над Bot API;
- методы отправки всех типов контента;
getUpdates+ кэширование известных чатов;getFileдля медиа URL;- унифицированная модель ошибок с извлечением
migrate_to_chat_id.
-
Telegram->MAX Bridge
- polling updates;
- фильтрация собственных сообщений;
- обработка команд;
- преобразование Telegram payload -> MAX message/attachments;
- буферизация media group;
- реакция и mapping.
-
Parser MAX Message
- универсальный best-effort разбор разнородных форматов вложений;
- классификация URL по типам;
- извлечение reply-контекста.
-
Storage
- таблица дедупликации;
- таблица двустороннего mapping;
- таблица явных маршрутов (binds).
-
Health subsystem
- хранит отметки времени последнего успеха/ошибки по MAX и Telegram;
- формирует snapshot;
- HTTP endpoint отдает liveness/readiness.
-
Auth utility
- отдельный скрипт первичной авторизации MAX;
- сохраняет сессию в рабочем каталоге.
6.2 Логическая схема взаимодействия
- MAX event -> Parser -> MAX->TG Bridge -> Telegram API -> Storage update.
- Telegram update -> TG->MAX Bridge -> MAX API -> Storage update.
- TG->MAX Bridge единолично вызывает
getUpdates, одновременно наполняя кэш чатов Telegram. - Storage используется обоими мостами как разделяемый слой состояния.
- Health обновляется из polling-циклов и MAX событий.
7. Модель данных и БД
Используется SQLite (или эквивалент в другой СУБД).
7.1 Таблица forwarded_messages
Назначение: дедупликация MAX->Telegram.
Поля:
message_id TEXT NOT NULLchat_id TEXT NOT NULLforwarded_at DATETIME DEFAULT CURRENT_TIMESTAMP
Ключ:
PRIMARY KEY (message_id, chat_id)
7.2 Таблица message_mapping
Назначение: двусторонняя связка сообщений для reply и трассировки.
Поля:
telegram_chat_id TEXT NOT NULLtelegram_message_id TEXT NOT NULLmax_chat_id TEXT NOT NULLmax_message_id TEXT NOT NULLmedia_group_id TEXT NULLcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP
Ключ:
PRIMARY KEY (telegram_chat_id, telegram_message_id)
Индексы:
(max_chat_id, max_message_id)— поиск Telegram-сообщения по MAX;(telegram_chat_id, media_group_id)— групповые операции альбомов.
7.3 Таблица chat_routes
Назначение: явные маршруты MAX chat title -> Telegram chat id.
Поля:
max_chat_title_norm TEXT PRIMARY KEYtelegram_chat_id TEXT NOT NULLtelegram_chat_title TEXT NULLcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP
Нормализация ключа:
- trim + casefold/lower (без учета регистра).
8. Алгоритмы и правила
8.1 Нормализация названий чатов
Во всех маршрутизирующих сравнениях:
- удалить крайние пробелы;
- привести к регистронезависимой форме (
casefold/lower); - сравнивать только в нормализованном виде.
8.2 Политика маршрутизации MAX -> Telegram
Порядок выбора:
chat_routesпо нормализованному MAX title;- локальный кэш Telegram title->id (наполняется из
getUpdates); - fallback user/chat id.
8.3 Политика медиа
- MAX->Telegram:
- если суммарно фото+видео больше одного, использовать album API;
- документы отправлять отдельными сообщениями;
- caption добавлять только к первому элементу/первому отправляемому сообщению.
- Telegram->MAX:
- фото/видео отправлять как native attachments;
- прочие типы файлов прикладывать ссылками в тексте.
8.4 Reply-семантика
- Для Telegram->MAX:
- если Telegram message является reply, искать соответствующий
max_message_idв mapping; - если найден, отправлять
reply_toв MAX.
- если Telegram message является reply, искать соответствующий
- Для MAX->Telegram:
- если MAX message является reply, искать
telegram_message_idв mapping; - если найден, отправлять
reply_to_message_id; - если не найден, добавлять текстовую пометку с превью исходного сообщения.
- если MAX message является reply, искать
8.5 Обработка migration в Telegram
Если Telegram API возвращает ошибку с parameters.migrate_to_chat_id:
- обновить маршрут в
chat_routesна новый chat id; - повторить отправку в новый chat id;
- считать повтор успешным итоговым результатом.
8.6 Дедупликация
- применяется для MAX->Telegram по ключу
(max_message_id, max_chat_id); - после успешной отправки обязательно mark-forwarded;
- на старте/рестарте состояния берутся из БД.
8.7 Buffering media group (Telegram)
- ключ буфера:
(telegram_chat_id, media_group_id); - каждое сообщение альбома копится в списке;
- по истечении grace-периода группа отправляется одним вызовом в MAX;
- после отправки буфер очищается.
9. API-контракты внутренних модулей
Ниже абстрактные контракты, независимые от языка:
-
Settings load_settings()- читает env;
- валидирует обязательные поля;
- возвращает immutable-конфигурацию.
-
ParsedMessage parse_message(MaxMessage msg)- best-effort преобразование сырого MAX-сообщения в каноническую структуру.
-
BridgeStoragewas_forwarded(message_id, chat_id) -> boolmark_forwarded(message_id, chat_id)save_mapping(telegram_chat_id, telegram_message_id, max_chat_id, max_message_id, media_group_id?)get_max_message_id_for_telegram(telegram_chat_id, telegram_message_id) -> str?get_telegram_message_id_for_max(telegram_chat_id, max_chat_id, max_message_id) -> str?set_chat_route(max_chat_title_norm, telegram_chat_id, telegram_chat_title?)get_chat_route(max_chat_title_norm) -> str?
-
TelegramClientresolve_target_chat_id(max_chat_name) -> (chat_id, matched_by_title)send_text/send_photo/send_video/send_document/send_media_group(...)get_updates(offset, timeout, limit) -> updates[]get_file_url(file_id) -> urladd_reaction(chat_id, message_id, emoji)
-
MaxToTelegramBridge.forward_message(max_message) -
TelegramToMaxBridge.start() -
handle_control_command(message, max_client, telegram) -> str?
10. Health-check модель
Должны храниться timestamps:
- telegram:
last_ok,last_error; - max:
last_ok,last_error,last_event; started_at.
Параметр:
unhealthy_after_sec(по умолчанию ~15 минут).
Правила:
telegram_healthy = last_ok exists && now-last_ok <= unhealthy_after_sec;max_healthy = last_ok exists && now-last_ok <= unhealthy_after_sec;overall_healthy = telegram_healthy && max_healthy.
HTTP:
GET /livez(и/live,/) -> 200,{status:"live", uptime_sec}.GET /healthz(и/health) -> 200 или 503, детальный JSON по компонентам.
11. Поведение при ошибках и устойчивость
- Любая ошибка обработки отдельного сообщения не должна останавливать сервис.
- Polling Telegram при ошибке уходит в backoff (например 10 секунд).
- Ошибка реакции в Telegram не влияет на основную доставку.
- Ошибка аварийного уведомления логируется, но не роняет процесс.
- Проблемы с разрешением URL медиа обрабатываются best-effort:
- что удалось достать — отправляется;
- что не удалось — отражается в тексте/unknown notices.
12. Логирование и диагностика
Обязательные события логов:
- старт/остановка компонентов;
- маршрутизация сообщений;
- обнаружение дубликатов;
- ошибки API и stacktrace;
- успешная пересылка с количеством вложений;
- операции bind и migration chat id.
Рекомендуемый формат:
- timestamp + level + logger + message.
13. Сценарии запуска и деплой
13.1 Локальный запуск
- Подготовить
.env. - Установить зависимости.
- Один раз пройти auth MAX (интерактивно), сохранить сессию.
- Запустить основной процесс.
13.2 Контейнерный запуск
- контейнер должен включать runtime + зависимости;
- каталог
cacheдолжен быть volume для сохранения сессии и SQLite; - должен быть healthcheck через
GET /healthz.
13.3 CI/CD (рекомендованно)
- сборка Docker image при push в основные ветки;
- публикация в registry с тегами для dev/release/latest;
- кэширование слоев сборки.
14. Требования к переносимой реализации (на любом языке)
Чтобы воссоздать проект в другом языке, необходимо сохранить:
- Два независимых, но согласованных канала обработки:
- event-driven для MAX сообщений;
- polling-loop для Telegram updates.
- Единое постоянное хранилище с тремя сущностями:
- dedup;
- mapping;
- routes.
- Идентичные правила нормализации названий чатов и выбора маршрута.
- Reply-механику с fallback-текстом при отсутствии mapping.
- Политику media group и порядок отправки вложений.
- Обработку Telegram migration (
migrate_to_chat_id) с обновлением маршрута. - Health-модель с независимыми метками MAX/Telegram.
- Ограничение: только один consumer
getUpdatesна экземпляр бота.
15. Acceptance criteria
Система считается реализованной, если:
- Текст/фото/видео/файлы корректно ходят в обе стороны.
- MAX->Telegram не дублирует уже пересланные сообщения после рестарта.
- Reply в обе стороны сохраняется, если mapping существует.
- При отсутствии Telegram-совпадения сообщение уходит в fallback.
/bind_maxменяет маршрут и влияет на последующие MAX->Telegram сообщения.- Telegram media group приходит в MAX как одно сообщение с множеством вложений.
/healthzвозвращает 200 при рабочем MAX+Telegram и 503 при деградации.- Ошибки отдельных сообщений не приводят к остановке процесса.
16. Известные ограничения текущей логики
- Автопоиск Telegram-чата зависит от того, что чат уже встречался в
getUpdates. - Для нестандартных вложений возможна частичная деградация в plain text + ссылки.
- Дедупликация реализована только для потока MAX->Telegram.
- Конкурентный доступ к SQLite через множество соединений допустим для небольших нагрузок, но для high-load может потребоваться иной storage backend.
17. Рекомендации для расширения (необязательно)
- добавить миграции схемы БД;
- добавить retry policy с классификацией transient/permanent ошибок;
- добавить метрики (Prometheus/OpenTelemetry);
- добавить интеграционные тесты с моками API MAX/Telegram;
- добавить персистентный offset Telegram (если нужен recovery без повторов между рестартами).