+262
-475
@@ -1,475 +1,262 @@
|
||||
# Техническое задание: двунаправленный мост 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 система должна:
|
||||
|
||||
1. Распарсить сообщение:
|
||||
- `message_id`, `chat_id`, `sender_name`, `chat_name`, текст;
|
||||
- список `image_urls`, `video_urls`, `file_urls`;
|
||||
- список неизвестных вложений;
|
||||
- данные reply-контекста (`reply_to_max_message_id`, preview).
|
||||
2. Обогатить данные через API MAX (best effort):
|
||||
- попытаться получить человекочитаемое имя отправителя;
|
||||
- попытаться получить реальное название чата;
|
||||
- извлечь дополнительные вложения через типизированные attach-объекты.
|
||||
3. Проверить дедупликацию по паре `(message_id, chat_id)`; дубликаты не отправлять.
|
||||
4. Выбрать Telegram-чат:
|
||||
- сначала через явный route (bind) по нормализованному названию MAX-чата;
|
||||
- затем через кэш чатов Telegram по совпадению заголовка;
|
||||
- если не найдено — отправка в `fallback_user_id`.
|
||||
5. Сформировать текст:
|
||||
- если целевой Telegram-чат найден (не fallback): `"{sender}:\n{text}"`;
|
||||
- если fallback: `"{sender} / {chat}:\n{text}"`;
|
||||
- при неизвестных вложениях добавить уведомление в конец.
|
||||
6. Обработать reply:
|
||||
- попытаться найти Telegram `reply_to_message_id` через mapping;
|
||||
- не дублировать вложения из исходного сообщения, на которое отвечают;
|
||||
- если mapping не найден — добавить текстовый контекст ответа.
|
||||
7. Отправить контент:
|
||||
- при `image+video > 1` — отправить единым альбомом (`sendMediaGroup`);
|
||||
- иначе отправить текст/медиа/файлы поштучно;
|
||||
- при полностью пустом payload отправить служебный fallback-текст.
|
||||
8. Сохранить mapping отправленных сообщений.
|
||||
9. Пометить сообщение как forwarded.
|
||||
10. При ошибке основного пути сделать аварийное best-effort уведомление в fallback-чат Telegram.
|
||||
|
||||
## 3.2 Telegram -> MAX
|
||||
|
||||
Система должна запускать единственный polling-цикл `getUpdates` и:
|
||||
|
||||
1. При старте получить `bot_id` через `getMe`.
|
||||
2. Обновить локальный кэш чатов MAX (`title -> id`) на основе доступного списка.
|
||||
3. В цикле получать updates с `offset` и `allowed_updates=["message","channel_post"]`.
|
||||
4. Для каждого сообщения:
|
||||
- отбросить неподдерживаемые типы;
|
||||
- отбросить сообщения бота (защита от петель);
|
||||
- обработать команду `/bind_max` в приоритетном порядке;
|
||||
- обработать служебные команды управления MAX (только личка + только fallback user);
|
||||
- для обычных сообщений найти чат MAX по нормализованному названию Telegram-чата;
|
||||
- если чат MAX не найден — не пересылать, логировать ошибку.
|
||||
5. Обработать `media_group_id`:
|
||||
- буферизовать элементы альбома по ключу `(telegram_chat_id, media_group_id)`;
|
||||
- после grace-паузы (около 1.2 сек) отправить одним сообщением в MAX с несколькими вложениями.
|
||||
6. Для одиночных сообщений:
|
||||
- сформировать текст `"{sender}:\n{text_or_caption}"`;
|
||||
- для фото/видео получить URL через `getFile` и вложить в MAX как native media attach;
|
||||
- для document/audio/voice/animation/sticker/video_note добавить URL-список в текстовый блок;
|
||||
- при reply попытаться найти соответствующее сообщение MAX через mapping.
|
||||
7. После успешной отправки:
|
||||
- поставить реакцию на 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 Компоненты
|
||||
|
||||
1. **Bootstrap / Main**
|
||||
- загружает конфиг;
|
||||
- создает клиентов MAX и Telegram;
|
||||
- инициализирует storage;
|
||||
- запускает health server;
|
||||
- подключает обработчики входящих сообщений MAX;
|
||||
- запускает Telegram->MAX poller как фоновую задачу.
|
||||
|
||||
2. **MAX->Telegram Bridge**
|
||||
- парсинг + enrich входящих MAX-сообщений;
|
||||
- маршрутизация в Telegram;
|
||||
- отправка текст/медиа/документы;
|
||||
- обработка миграции Telegram chat id;
|
||||
- сохранение mapping и dedup.
|
||||
|
||||
3. **Telegram API Client**
|
||||
- HTTP-обертка над Bot API;
|
||||
- методы отправки всех типов контента;
|
||||
- `getUpdates` + кэширование известных чатов;
|
||||
- `getFile` для медиа URL;
|
||||
- унифицированная модель ошибок с извлечением `migrate_to_chat_id`.
|
||||
|
||||
4. **Telegram->MAX Bridge**
|
||||
- polling updates;
|
||||
- фильтрация собственных сообщений;
|
||||
- обработка команд;
|
||||
- преобразование Telegram payload -> MAX message/attachments;
|
||||
- буферизация media group;
|
||||
- реакция и mapping.
|
||||
|
||||
5. **Parser MAX Message**
|
||||
- универсальный best-effort разбор разнородных форматов вложений;
|
||||
- классификация URL по типам;
|
||||
- извлечение reply-контекста.
|
||||
|
||||
6. **Storage**
|
||||
- таблица дедупликации;
|
||||
- таблица двустороннего mapping;
|
||||
- таблица явных маршрутов (binds).
|
||||
|
||||
7. **Health subsystem**
|
||||
- хранит отметки времени последнего успеха/ошибки по MAX и Telegram;
|
||||
- формирует snapshot;
|
||||
- HTTP endpoint отдает liveness/readiness.
|
||||
|
||||
8. **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 NULL`
|
||||
- `chat_id TEXT NOT NULL`
|
||||
- `forwarded_at DATETIME DEFAULT CURRENT_TIMESTAMP`
|
||||
|
||||
Ключ:
|
||||
|
||||
- `PRIMARY KEY (message_id, chat_id)`
|
||||
|
||||
### 7.2 Таблица `message_mapping`
|
||||
|
||||
Назначение: двусторонняя связка сообщений для reply и трассировки.
|
||||
|
||||
Поля:
|
||||
|
||||
- `telegram_chat_id TEXT NOT NULL`
|
||||
- `telegram_message_id TEXT NOT NULL`
|
||||
- `max_chat_id TEXT NOT NULL`
|
||||
- `max_message_id TEXT NOT NULL`
|
||||
- `media_group_id TEXT NULL`
|
||||
- `created_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 KEY`
|
||||
- `telegram_chat_id TEXT NOT NULL`
|
||||
- `telegram_chat_title TEXT NULL`
|
||||
- `created_at DATETIME DEFAULT CURRENT_TIMESTAMP`
|
||||
|
||||
Нормализация ключа:
|
||||
|
||||
- trim + casefold/lower (без учета регистра).
|
||||
|
||||
## 8. Алгоритмы и правила
|
||||
|
||||
## 8.1 Нормализация названий чатов
|
||||
|
||||
Во всех маршрутизирующих сравнениях:
|
||||
|
||||
- удалить крайние пробелы;
|
||||
- привести к регистронезависимой форме (`casefold`/`lower`);
|
||||
- сравнивать только в нормализованном виде.
|
||||
|
||||
## 8.2 Политика маршрутизации MAX -> Telegram
|
||||
|
||||
Порядок выбора:
|
||||
|
||||
1. `chat_routes` по нормализованному MAX title;
|
||||
2. локальный кэш Telegram title->id (наполняется из `getUpdates`);
|
||||
3. 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.
|
||||
- Для MAX->Telegram:
|
||||
- если MAX message является reply, искать `telegram_message_id` в mapping;
|
||||
- если MAX message является reply, не копировать медиа из `link.message` (исходного сообщения);
|
||||
- если найден, отправлять `reply_to_message_id`;
|
||||
- если не найден, добавлять текстовую пометку с превью исходного сообщения.
|
||||
|
||||
## 8.5 Обработка migration в Telegram
|
||||
|
||||
Если Telegram API возвращает ошибку с `parameters.migrate_to_chat_id`:
|
||||
|
||||
1. обновить маршрут в `chat_routes` на новый chat id;
|
||||
2. повторить отправку в новый chat id;
|
||||
3. считать повтор успешным итоговым результатом.
|
||||
|
||||
## 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-сообщения в каноническую структуру.
|
||||
|
||||
- `BridgeStorage`
|
||||
- `was_forwarded(message_id, chat_id) -> bool`
|
||||
- `mark_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?`
|
||||
|
||||
- `TelegramClient`
|
||||
- `resolve_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) -> url`
|
||||
- `add_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 Локальный запуск
|
||||
|
||||
1. Подготовить `.env`.
|
||||
2. Установить зависимости.
|
||||
3. Один раз пройти auth MAX (интерактивно), сохранить сессию.
|
||||
4. Запустить основной процесс.
|
||||
|
||||
## 13.2 Контейнерный запуск
|
||||
|
||||
- контейнер должен включать runtime + зависимости;
|
||||
- каталог `cache` должен быть volume для сохранения сессии и SQLite;
|
||||
- должен быть healthcheck через `GET /healthz`.
|
||||
|
||||
## 13.3 CI/CD (рекомендованно)
|
||||
|
||||
- сборка Docker image при push в основные ветки;
|
||||
- публикация в registry с тегами для dev/release/latest;
|
||||
- кэширование слоев сборки.
|
||||
|
||||
## 14. Требования к переносимой реализации (на любом языке)
|
||||
|
||||
Чтобы воссоздать проект в другом языке, необходимо сохранить:
|
||||
|
||||
1. Два независимых, но согласованных канала обработки:
|
||||
- event-driven для MAX сообщений;
|
||||
- polling-loop для Telegram updates.
|
||||
2. Единое постоянное хранилище с тремя сущностями:
|
||||
- dedup;
|
||||
- mapping;
|
||||
- routes.
|
||||
3. Идентичные правила нормализации названий чатов и выбора маршрута.
|
||||
4. Reply-механику с fallback-текстом при отсутствии mapping.
|
||||
5. Политику media group и порядок отправки вложений.
|
||||
6. Обработку Telegram migration (`migrate_to_chat_id`) с обновлением маршрута.
|
||||
7. Health-модель с независимыми метками MAX/Telegram.
|
||||
8. Ограничение: только один consumer `getUpdates` на экземпляр бота.
|
||||
|
||||
## 15. Acceptance criteria
|
||||
|
||||
Система считается реализованной, если:
|
||||
|
||||
1. Текст/фото/видео/файлы корректно ходят в обе стороны.
|
||||
2. MAX->Telegram не дублирует уже пересланные сообщения после рестарта.
|
||||
3. Reply в обе стороны сохраняется, если mapping существует.
|
||||
4. При отсутствии Telegram-совпадения сообщение уходит в fallback.
|
||||
5. `/bind_max` меняет маршрут и влияет на последующие MAX->Telegram сообщения.
|
||||
6. Telegram media group приходит в MAX как одно сообщение с множеством вложений.
|
||||
7. `/healthz` возвращает 200 при рабочем MAX+Telegram и 503 при деградации.
|
||||
8. Ошибки отдельных сообщений не приводят к остановке процесса.
|
||||
|
||||
## 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 без повторов между рестартами).
|
||||
|
||||
# Техническая спецификация: max2telegram (v3.2)
|
||||
|
||||
## 1. Назначение продукта
|
||||
|
||||
**max2telegram** — асинхронный сервис-мост для двусторонней синхронизации переписки между мессенджером **MAX** и **Telegram**. Сторона Telegram представлена **одним Форум-каналом** с множеством тематических топиков.
|
||||
|
||||
Сервис использует **WebSocket-подключение** к API MAX для мгновенного получения событий и хранит **маркеры (курсоры) последнего обработанного сообщения** в базе данных. Это гарантирует, что ни одно сообщение не будет потеряно или обработано дважды во время перезапуска сервиса, сетевых сбоев или даунтайма. Вся маршрутизация выполняется **строго по идентификаторам**, а отправка в обе стороны регулируется асинхронными Redis-очередями для соблюдения лимитов API.
|
||||
|
||||
---
|
||||
|
||||
## 2. Участники и роли
|
||||
|
||||
| Участник | Роль в системе |
|
||||
|----------|----------------|
|
||||
| **Аккаунт MAX** | Источник и приёмник сообщений в MAX (групповые чаты и личные диалоги). |
|
||||
| **Telegram-бот** | Администратор Форум-канала; создаёт топики, читает сообщения по `thread_id`, ставит реакции, выполняет команды. |
|
||||
| **Доверенный оператор (fallback)** | Получает системные уведомления об ошибках очередей; выполняет управляющие команды. **В части пересылки сообщений имеет те же права, что и любой другой участник форума.** |
|
||||
| **Участники форума Telegram** | Могут писать в любые маппированные топики; все их сообщения асинхронно пересылаются в MAX. |
|
||||
| **Оператор инфраструктуры** | Настраивает Docker Compose, управляет переменными окружения, уровнем логирования и контролирует логи. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектура и потоки данных
|
||||
|
||||
### 3.1. Общая схема
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph MAX["MAX API (WebSocket)"]
|
||||
MC[Чаты и ЛС MAX]
|
||||
end
|
||||
|
||||
subgraph Bridge["max2telegram (Python App)"]
|
||||
WS_LAYER[Слой клиента MAX]
|
||||
TG_LAYER[Слой клиента Telegram]
|
||||
ROUTER[Слой маршрутизации]
|
||||
STATE[(Слой состояния: маркеры в SQLite)]
|
||||
MAP[(Слой маппинга: ID в SQLite)]
|
||||
QUEUE[(Слой очередей: Redis)]
|
||||
end
|
||||
|
||||
subgraph TG["Telegram Bot API"]
|
||||
FC[Форум-канал]
|
||||
TOPICS[Топики]
|
||||
FB[Личка fallback]
|
||||
end
|
||||
|
||||
MC <-->|WS Stream| WS_LAYER
|
||||
WS_LAYER <--> STATE
|
||||
WS_LAYER --> ROUTER
|
||||
ROUTER <--> MAP
|
||||
ROUTER --> QUEUE
|
||||
QUEUE --> TG_LAYER
|
||||
TG_LAYER --> FC
|
||||
|
||||
FC --> TG_LAYER --> ROUTER
|
||||
ROUTER --> QUEUE
|
||||
QUEUE --> WS_LAYER
|
||||
WS_LAYER --> MC
|
||||
|
||||
FB <--> TG_LAYER
|
||||
```
|
||||
|
||||
### 3.2. Поток «MAX → Telegram» (с маркерами и очередью)
|
||||
1. `Слой клиента MAX` подключается по WebSocket, запрашивает последние маркеры из `Слоя состояния`.
|
||||
2. При получении нового события:
|
||||
- **Дедупликация:** если `message_id` <= `last_message_id`, событие игнорируется.
|
||||
- `Слой маршрутизации` проверяет маппинг в `Слое маппинга`. Если нет – запрашивает у `Слоя клиента Telegram` создание топика, сохраняет маппинг.
|
||||
- Задача сериализуется и передается в `Слой очередей` (`max2tg_queue`).
|
||||
- **Маркер** атомарно обновляется в `Слое состояния`.
|
||||
3. `Слой клиента Telegram` (Воркер) забирает задачу, соблюдает лимит (≥ 3.5 сек), отправляет сообщение и сохраняет связь `max_message_id` ↔ `tg_message_id` в БД.
|
||||
|
||||
### 3.3. Поток «Telegram → MAX» (с очередью)
|
||||
1. `Слой клиента Telegram` получает сообщение из топика (`tg_chat_id` + `message_thread_id`). Игнорирует сообщения бота.
|
||||
2. `Слой маршрутизации` ищет пару `{tg_chat_id, message_thread_id}` в `Слое маппинга`.
|
||||
3. Если найдено: задача передается в `Слой очередей` (`tg2max_queue`). Если нет: игнорируется.
|
||||
4. `Слой клиента MAX` (Воркер) забирает задачу, ждет настраиваемого лимита, отправляет сообщение в MAX.
|
||||
5. **Только после успеха от API MAX**, `Слой клиента Telegram` ставит реакцию 🦄 на исходное сообщение.
|
||||
|
||||
---
|
||||
|
||||
## 4. Технические требования и стек технологий
|
||||
|
||||
### 4.1. Стек разработки
|
||||
| Компонент | Технология | Обоснование / Требования |
|
||||
|-----------|------------|--------------------------|
|
||||
| **Язык** | Python 3.10+ | Нативная поддержка `asyncio`, строгая типизация (type hints). |
|
||||
| **Клиент MAX** | `PyMAX` | **Обязательная поддержка WebSocket** для стриминга событий. Должен поддерживать переподключение и запрос истории по курсору/ID. |
|
||||
| **Клиент Telegram** | `aiogram 3.x` | Предпочтительно для Bot API (работа с форумами, очередями, поллингом/вебхуками). |
|
||||
| **База данных** | SQLite | Хранение маппингов и маркеров состояния. **Обязательно**: режим `PRAGMA journal_mode=WAL` и `PRAGMA synchronous=NORMAL`. Библиотека: `aiosqlite` или `SQLAlchemy` (async). |
|
||||
| **Очереди** | Redis | Хранение очередей `max2tg_queue` и `tg2max_queue`. Персистентность через `appendonly yes` (AOF). Библиотека: `redis-py` (async). |
|
||||
| **Логирование** | `structlog` / `logging` | Структурированные JSON-логи. Уровень логирования настраивается через переменную окружения. |
|
||||
|
||||
### 4.2. Архитектура приложения (Python)
|
||||
Приложение строится на базе `asyncio` и состоит из следующих независимых задач (tasks):
|
||||
1. **MAX WebSocket Listener**: Поддерживает соединение, парсит события, управляет маркерами.
|
||||
2. **Telegram Listener**: Aiogram Dispatcher.
|
||||
3. **TG Worker**: Цикл `BLPOP` из `max2tg_queue` с `asyncio.sleep(3.5)`.
|
||||
4. **MAX Worker**: Цикл `BLPOP` из `tg2max_queue` с настраиваемой задержкой.
|
||||
5. **Admin Handler**: Обработчик команд только от `FALLBACK_USER_ID`.
|
||||
|
||||
### 4.3. Требования к развертыванию (Docker + Docker Compose)
|
||||
Сервис поставляется в виде готового к запуску окружения. Папка `./data` монтируется для сохранения SQLite БД и сессии PyMAX между перезапусками.
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
app:
|
||||
build: .
|
||||
restart: always
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- MAX_TOKEN=${MAX_TOKEN}
|
||||
- MAX_DEVICE_ID=${MAX_DEVICE_ID}
|
||||
- TG_BOT_TOKEN=${TG_BOT_TOKEN}
|
||||
- TG_FORUM_CHANNEL_ID=${TG_FORUM_CHANNEL_ID}
|
||||
- FALLBACK_USER_ID=${FALLBACK_USER_ID}
|
||||
- DATABASE_URL=sqlite+aiosqlite:///app/data/bridge.db
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- TG_RATE_LIMIT_DELAY_SEC=3.5
|
||||
- MAX_RATE_LIMIT_DELAY_SEC=1.0
|
||||
- MAX_RECONNECT_FETCH_LIMIT=50
|
||||
- LOG_LEVEL=INFO
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
### 4.4. Архитектурные принципы и стандарты разработки
|
||||
Проект обязан строго следовать следующим инженерным стандартам:
|
||||
|
||||
1. **Строгое разделение слоёв (Layered Architecture)**:
|
||||
- `MAX Layer` отвечает **только** за взаимодействие с API MAX (подключение WS, отправка/получение сообщений, сессия).
|
||||
- `Telegram Layer` отвечает **только** за взаимодействие с Telegram Bot API (получение обновлений, отправка в топики, реакции, обработка команд).
|
||||
- `Router Layer` отвечает **только** за логику сопоставления идентификаторов и принятие решений о маршруте.
|
||||
- `Queue Layer` отвечает **только** за сериализацию, буферизацию и доставку задач между слоями.
|
||||
- `Storage Layer` отвечает **только** за атомарные операции чтения/записи в SQLite/Redis.
|
||||
- **Запрещено**: вызывать методы одного слоя из другого напрямую. Например, `MAX Layer` не может ставить реакции или создавать топики в Telegram. Вся межслойная коммуникация проходит через интерфейсы абстракций и очереди.
|
||||
|
||||
2. **SOLID**:
|
||||
- **S**ingle Responsibility: Каждый модуль/класс выполняет одну четкую задачу.
|
||||
- **O**pen/Closed: Расширение функционала (новые типы медиа, новые команды) происходит без модификации стабильного кода ядерных компонентов.
|
||||
- **L**iskov Substitution & **I**nterface Segregation: Чёткие контракты между слоями, минимум методов в интерфейсах.
|
||||
- **D**ependency Inversion: Слои зависят от абстракций (протоколов/интерфейсов), а не от конкретных реализаций клиентов.
|
||||
|
||||
3. **KISS**:
|
||||
- Избегать избыточной абстракции и "over-engineering".
|
||||
- Использовать стандартные паттерны `asyncio`, `aiogram` и `redis-py`.
|
||||
- Конфигурация, бизнес-логика и инфраструктурный код должны быть явно разделены, но не размазаны по сотне файлов.
|
||||
|
||||
---
|
||||
|
||||
## 5. Бизнес-функции и правила
|
||||
|
||||
### 5.1. Двусторонняя пересылка и очереди
|
||||
- **Гарантированная доставка**: Сообщения не теряются при перезапуске контейнера благодаря персистентным Redis-очередям.
|
||||
- **Rate Limiting**: В Telegram ≥ 3.5 сек, в MAX — настраиваемая задержка.
|
||||
- **Подтверждение**: Реакция 🦄 ставится **только после** успешного подтверждения отправки из очереди в MAX.
|
||||
|
||||
### 5.2. Маршрутизация и управление топиками
|
||||
- **Строгое сопоставление по ID**: Ключ поиска: `max_chat_id` ↔ `{tg_chat_id, tg_thread_id}`.
|
||||
- **Автоматическое создание**: При отсутствии маппинга создается топик. Имя: `"Название чата"` или `"👤 Имя контакта"`. Имя хранится только для UX (`/list`).
|
||||
- **Игнорирование**: Сообщения из TG-топиков без маппинга игнорируются.
|
||||
|
||||
### 5.3. Формат сообщений и контент
|
||||
- **MAX → TG**: В групповых топиках: `<Имя отправителя>:\n<текст>`. В ЛС-топиках: только текст.
|
||||
- **TG → MAX**: Всегда: `<Имя Фамилия> (@username):\n<текст>`.
|
||||
- **Медиа**: Фото, видео, альбомы, файлы поддерживаются нативно. Ненативные типы передаются ссылками `[Telegram files]`.
|
||||
- **Треды**: Поддерживаются через `max_message_id` ↔ `tg_message_id` в БД.
|
||||
|
||||
### 5.4. Управление и команды
|
||||
Команды обрабатываются **исключительно** в личной переписке с ботом и **только** от `FALLBACK_USER_ID`. В части пересылки сообщений все участники форума равны.
|
||||
|
||||
| Команда | Действие |
|
||||
|---------|----------|
|
||||
| `/start` | Приветствие, краткая справка, проверка статуса подключения |
|
||||
| `/help` | Полная справка по доступным командам управления |
|
||||
| `/list` | Список активных маппингов (ID, названия, статус) |
|
||||
| `/join <ссылка>` | Вступление в MAX-группу или канал |
|
||||
| `/leave <id или название>` | Выход из указанного MAX-канала |
|
||||
| `/last_messages <id или название>` | Последние 10 сообщений из MAX-чата или ЛС |
|
||||
|
||||
### 5.5. Надежность и восстановление (WebSocket и маркеры)
|
||||
1. **Хранение маркеров**: Таблица `sync_markers` в SQLite хранит `last_processed_message_id` для каждого чата.
|
||||
2. **Идемпотентность**: События с `message_id` <= маркера отбрасываются.
|
||||
3. **Восстановление после даунтайма**: При старте читаются маркеры, запрашивается история (до `MAX_RECONNECT_FETCH_LIMIT`), сообщения ставятся в очередь.
|
||||
4. **Атомарность**: Маркер обновляется только после успешной постановки в Redis.
|
||||
|
||||
---
|
||||
|
||||
## 6. Конфигурация (Переменные окружения)
|
||||
|
||||
| Переменная | Описание | Пример / Значение по умолчанию |
|
||||
|------------|----------|--------------------------------|
|
||||
| `MAX_TOKEN` | Токен авторизации аккаунта MAX | `eyJhbGci...` |
|
||||
| `MAX_DEVICE_ID` | Уникальный идентификатор устройства/сессии | `a1b2c3d4...` |
|
||||
| `TG_BOT_TOKEN` | Токен Telegram-бота от @BotFather | `123456:ABC-DEF1234...` |
|
||||
| `TG_FORUM_CHANNEL_ID` | ID Форум-канала (строго с `-100`) | `-1001234567890` |
|
||||
| `FALLBACK_USER_ID` | Telegram ID администратора | `987654321` |
|
||||
| `DATABASE_URL` | Строка подключения к SQLite | `sqlite+aiosqlite:///app/data/bridge.db` |
|
||||
| `REDIS_URL` | Строка подключения к Redis | `redis://redis:6379/0` |
|
||||
| `TG_RATE_LIMIT_DELAY_SEC` | Задержка между отправками в TG | `3.5` |
|
||||
| `MAX_RATE_LIMIT_DELAY_SEC` | Задержка между отправками в MAX | `1.0` |
|
||||
| `LS_TOPIC_PREFIX` | Префикс для именования ЛС-топиков | `👤 ` |
|
||||
| `MAX_RECONNECT_FETCH_LIMIT` | Макс. кол-во сообщений для дозагрузки из истории | `50` |
|
||||
| `LOG_LEVEL` | Уровень логирования (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | `INFO` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Границы ответственности продукта
|
||||
|
||||
**Входит в scope:**
|
||||
- Асинхронная двусторонняя пересылка текста и медиа через Redis-очереди.
|
||||
- Подключение к MAX через **WebSocket** с обработкой разрывов связи.
|
||||
- Хранение и использование **маркеров последнего сообщения** для предотвращения потери данных.
|
||||
- Автоматическое создание топиков и **строгая маршрутизация только по идентификаторам**.
|
||||
- Соблюдение rate limits и настройка уровня логирования.
|
||||
- Поддержка тредов, дедупликация, равенство участников форума.
|
||||
- Удалённое управление MAX (`/start`, `/help`, `/list` и др.) только для fallback-пользователя.
|
||||
- Архитектурное соблюдение слоёв, SOLID, KISS.
|
||||
- Контейнеризация (Docker + Docker Compose).
|
||||
|
||||
**Не входит в scope:**
|
||||
- Маршрутизация по названиям чатов/контактов.
|
||||
- Автоматическое переименование топиков при смене имени в MAX.
|
||||
- Редактирование и удаление доставленных сообщений.
|
||||
- Синхронизация статусов «прочитано», индикаторов набора текста, звонков.
|
||||
|
||||
---
|
||||
|
||||
## 8. Критерии приёмки (Бизнес и Технические)
|
||||
|
||||
| № | Критерий | Тип проверки |
|
||||
|---|----------|--------------|
|
||||
| 1 | Приложение успешно запускается командой `docker compose up -d`. | Технический |
|
||||
| 2 | SQLite работает в режиме WAL, Redis сохраняет очередь между рестартами. | Технический |
|
||||
| 3 | Подключение к MAX осуществляется по **WebSocket**, автоматическое переподключение при обрыве. | Технический |
|
||||
| 4 | **Архитектура**: Отсутствуют прямые вызовы API одного слоя из другого. Код структурирован по слоям, соответствует SOLID и KISS. | Технический/Code Review |
|
||||
| 5 | В SQLite корректно обновляется `last_processed_message_id`. Дубликаты при переподключении WS не попадают в очередь. | Технический |
|
||||
| 6 | **Сценарий даунтайма**: После 5-минутного простоя пропущенные сообщения доставляются без потерь и дублей. | Бизнес/Технический |
|
||||
| 7 | Переименование чата в MAX **не прерывает** пересылку (маршрутизация строго по ID). | Бизнес |
|
||||
| 8 | Сообщение из TG пересылается в MAX **только если** маппинг найден в БД. | Бизнес |
|
||||
| 9 | При флуде (10+ сообщений) соблюдаются интервалы: ≥ 3.5 сек (TG) и ≥ `MAX_RATE_LIMIT_DELAY_SEC` (MAX). Ошибки `429` отсутствуют. | Технический |
|
||||
| 10 | Реакция 🦄 ставится в TG **строго после** успешной отправки из очереди в MAX. | Бизнес |
|
||||
| 11 | Любой участник форума может писать в маппированный ЛС-топик, сообщение доходит до контакта в MAX. | Бизнес |
|
||||
| 12 | Команды `/start`, `/help`, `/list` и др. работают **только** в личной переписке и **только** от `FALLBACK_USER_ID`. | Бизнес |
|
||||
| 13 | Уровень логирования (`LOG_LEVEL`) применяется ко всем компонентам, логи структурированы в JSON. | Технический |
|
||||
| 14 | При критической ошибке отправки fallback-пользователь получает уведомление в ЛС с деталями. | Бизнес |
|
||||
Reference in New Issue
Block a user