# perepost **Telegram ↔ MAX.** Бот зеркалирует посты между каналами в обе стороны. Опубликуйте пост в одном канале, и он появится во втором с форматированием, фото, видео, альбомом и файлами. Можно включить перенос правок текста и подписей, а также удаление копий в Telegram при удалении оригинала в MAX. *English* — self-hosted, bidirectional channel mirroring between Telegram and MAX, with configurable routes, a persistent SQLite queue and loop protection. One Linux binary or Docker image. [Двусторонний запуск в Docker](#быстрый-запуск-в-обе-стороны) · [Только Telegram → MAX](#односторонний-запуск-без-домена) · [Большие файлы](#видео-больше-20-мб) ## Быстрый запуск в обе стороны Нужны Docker Compose и публичный HTTPS-адрес на порту 443 для вебхука MAX. Telegram в этом примере работает через long polling. При желании [переведите и его на вебхук](#вебхук-telegram). ### 1. Подготовьте ботов и каналы - Создайте отдельного бота Telegram у [@BotFather](https://t.me/BotFather) и назначьте его администратором Telegram-канала с правом публикации. - Создайте бота MAX и назначьте его администратором MAX-канала с правами чтения и публикации сообщений. Для обратного маршрута используйте бота с отдельной подпиской на вебхук. - Узнайте числовой `chat_id` канала MAX. Для каналов используйте ID со знаком минус. Его можно взять из события `bot_added`. [Как получить ID канала](#как-узнать-id-канала-max) ### 2. Создайте конфигурацию В новом каталоге скачайте файлы Compose и подготовьте `.env`. Клонировать репозиторий для запуска не требуется. ```bash mkdir -p perepost/config cd perepost curl -fsSLO https://raw.githubusercontent.com/smotim/perepost/v0.3.2/docker-compose.yml curl -fsSL https://raw.githubusercontent.com/smotim/perepost/v0.3.2/.env.example -o .env chmod 600 .env openssl rand -hex 32 ``` Заполните в `.env` токены, адрес вебхука и секрет, полученный на последнем шаге. Переменные `TELEGRAM_CHANNEL` и `MAX_CHANNEL` оставьте пустыми. ```dotenv TELEGRAM_BOT_TOKEN=... MAX_BOT_TOKEN=... MAX_WEBHOOK_URL=https://example.com/perepost/max MAX_WEBHOOK_SECRET=... WEBHOOK_LISTEN=0.0.0.0:8090 ``` Создайте `config/config.yaml` с двумя встречными маршрутами. Замените `@my_channel` и числовой ID на свои. В примере включены правки в обе стороны. ```yaml routes: - name: telegram-to-max from: { platform: telegram, channel: "@my_channel" } to: { platform: max, channel: "-70123456789" } sync_edits: true - name: max-to-telegram from: { platform: max, channel: "-70123456789" } to: { platform: telegram, channel: "@my_channel" } sync_edits: true # sync_deletes: true ``` ### 3. Настройте HTTPS Если nginx работает на хосте, создайте рядом с `docker-compose.yml` файл `docker-compose.override.yml`. Compose подхватит его автоматически. Порт контейнера будет доступен прокси через loopback. ```yaml services: perepost: ports: - "127.0.0.1:8090:8090" ``` Добавьте в существующий HTTPS-сервер nginx блок для пути из `MAX_WEBHOOK_URL` и перечитайте конфигурацию прокси. ```nginx location = /perepost/max { client_max_body_size 2m; proxy_pass http://127.0.0.1:8090; } ``` ### 4. Запустите и проверьте ```bash docker compose up -d docker compose logs -f perepost ``` В журнале `пересылаю посты` должны появиться оба направления. Опубликуйте по одному пробному посту в каждом канале и убедитесь, что на второй площадке появляется одна копия. perepost регистрирует подписку MAX при запуске. ## Односторонний запуск без домена Скачайте `docker-compose.yml` и `.env.example` как в быстром запуске. Для одной пары достаточно заполнить `.env` и выполнить `docker compose up -d`. Telegram-боту нужны права администратора в исходном канале, MAX-боту — право публикации в канале назначения. ```dotenv TELEGRAM_BOT_TOKEN=... MAX_BOT_TOKEN=... TELEGRAM_CHANNEL=@my_channel MAX_CHANNEL=-70123456789 ``` В этом режиме маршрут берётся из `.env`, а Telegram использует long polling. Для нескольких пар или переноса правок оставьте `TELEGRAM_CHANNEL` и `MAX_CHANNEL` пустыми и используйте YAML-маршруты. ## Маршруты В YAML каждый канал из `from` связан со всеми каналами из `to`. Для нескольких каналов одной платформы используйте `channels: [...]` вместо `channel`. У Telegram можно указать `@имя`, ссылку `https://t.me/имя` или числовой ID. Для MAX предпочтителен числовой ID. Сокращённая запись `from: "@my_channel"` и `to: "-70123456789"` задаёт Telegram → MAX. В короткой записи также поддерживаются списки. Маршрут с `enabled: false` выключен. Пример с двумя направлениями есть в [`config.example.yaml`](config.example.yaml). ### Синхронизация правок и удалений **Флаги по умолчанию выключены.** - `sync_edits: true` переносит правки текста и подписей. При изменении длины бот переиспользует части поста, добавляет недостающие и удаляет лишние текстовые части, где это разрешает API. Замена вложений выполняется вручную. - `sync_deletes: true` на маршруте из MAX удаляет известные копии оригинала в Telegram. Telegram Bot API позволяет удалять сообщения младше 48 часов. Если Telegram запрещает удаление старой копии, бот заменяет её текст или подпись на «Содержимое удалено.». Старое вложение при этом остаётся в Telegram; убрать его можно вручную. Это относится и к лишним частям сокращённого поста. Удаление оригинала из Telegram требуется повторить в MAX вручную. [Ограничения Bot API](https://core.telegram.org/bots/api#deletemessage) Редактируйте пост там, где он был опубликован впервые. Правка копии действует только в её канале. Для синхронизации нужна сохранённая связь с копией, которую perepost создаёт при публикации. Если одна пара в одном направлении описана несколько раз, флаг достаточно включить в одном маршруте. Если копию в Telegram удалили, следующая правка оригинала пропускается для этого канала. Бот не публикует удалённую копию заново и не добавляет к ней новые части. Ошибки прав доступа и другие сбои по-прежнему отправляются администратору. После изменения YAML примените его через `docker compose restart perepost`. После изменения `.env` выполните `docker compose up -d --force-recreate perepost`, чтобы контейнер получил новые переменные. ## Что переносится - **В обе стороны** — текст и подписи, жирный и курсив, подчёркивание, зачёркивание, моноширинный текст, цитаты, ссылки, фото, видео, альбомы и файлы. Если текст превышает лимит площадки назначения, бот разбивает его на части. В Telegram продолжения отправляются тихо; в каналах MAX уведомления обязательны для каждой части. - **Telegram → MAX** — MPEG-4-анимации и видеокружки отправляются как видео, настоящие GIF как изображения, живое фото как снимок, голосовое как аудио, музыка как файл. У репостов сохраняется пометка об источнике. Опросы, геоточки, контакты, платные посты и Rich Messages заменяются ссылкой на публичный оригинал. Стикеры и служебные сообщения пропускаются. - **MAX → Telegram** — фото и видео группируются в альбомы до 10 вложений, аудио и файлы отправляются как документы. Изображения с неподходящими размерами или форматом и видео не в MP4 передаются документами с сохранением оригинала. Заголовки становятся жирными, выделение — подчёркиванием, упоминания с публичным именем — ссылками на профиль MAX. Упоминания только по внутреннему ID остаются текстом. При потере части поста бот добавляет ссылку, если MAX прислал публичный адрес. Для MAX бот делит текст по лимиту в 4000 символов вместе с HTML-разметкой. Для Telegram лимит составляет 4096 символов в тексте и 1024 в подписи к вложению. Если HTML-ссылка сама не помещается в лимит, подпись сохраняется, а адрес переносится в обычный текст. Слишком длинный адрес может занять несколько сообщений. Сторис пропускаются. Для закрытого оригинала доступ к каналу нужен и по ссылке. ## Вебхук Telegram Чтобы принимать посты и из Telegram по HTTPS, задайте в `.env` ещё два параметра. ```dotenv TELEGRAM_WEBHOOK_URL=https://example.com/perepost/telegram TELEGRAM_WEBHOOK_SECRET=... ``` Сгенерируйте отдельный секрет командой `openssl rand -hex 32`. Добавьте в nginx такой же блок, как для MAX, с путём `/perepost/telegram`. Оба пути обслуживает один `WEBHOOK_LISTEN`. Старое имя `MAX_WEBHOOK_LISTEN` также поддерживается. Остановите прежний экземпляр perepost перед запуском нового. Бот зарегистрирует вебхук через настроенный Bot API с сохранением ожидающих событий. Для возврата к long polling остановите perepost, вызовите `deleteWebhook` с `drop_pending_updates=false` через тот же Bot API, уберите `TELEGRAM_WEBHOOK_URL` и запустите контейнер заново. ## Видео больше 20 МБ При стандартном Telegram Bot API скачивание ограничено 20 МБ. При отправке в Telegram документы и видео ограничены 50 МБ, фотографии — 10 МБ. С локальным Bot API perepost скачивает файлы Telegram с общего диска. В направлении MAX → Telegram лимит perepost составляет 2 ГБ суммарно на пост. MAX применяет собственные лимиты, в частности 250 МБ на видео, 256 МБ и 60 минут на аудио, 50 МБ и 7680 × 7680 пикселей на изображение, 4 ГБ на файл. perepost проверяет размер и доступные метаданные до загрузки, а затем проверяет фактический размер скачанного файла. [Лимиты Telegram](https://core.telegram.org/bots/api#using-a-local-bot-api-server) · [Лимиты MAX](https://dev.max.ru/docs-api/methods/POST/uploads/) В Compose уже есть [локальный Telegram Bot API](https://github.com/tdlib/telegram-bot-api). Получите `api_id` и `api_hash` на [my.telegram.org](https://my.telegram.org) в разделе API development tools, затем добавьте эти параметры в `.env`. ```dotenv COMPOSE_PROFILES=bot-api TELEGRAM_API_ID=1234567 TELEGRAM_API_HASH=... TELEGRAM_API_URL=http://telegram-bot-api:8081 ``` При переходе на локальный сервер остановите perepost и вызовите [`logOut`](https://core.telegram.org/bots/api#logout) в облачном Bot API для своего бота. Затем выполните `docker compose up -d`. Файлы читаются с общего тома и удаляются после обработки. Оставляйте на диске место под одновременные загрузки. Для возврата в облако остановите perepost, вызовите `logOut` на локальном Bot API, уберите четыре параметра из примера и заново выполните `docker compose up -d`. Если облачный Bot API попросит подождать, повторите запуск после окончания периода ожидания. [Переезд бота между серверами](https://github.com/tdlib/telegram-bot-api#moving) ## Обновление и хранение данных Для обновления загрузите новый образ и пересоздайте контейнер. ```bash docker compose pull perepost docker compose up -d perepost ``` Если переходите с версии до v0.3.0, добавьте в Compose именованный том `perepost-state` для `/var/lib/perepost` по [текущему примеру](docker-compose.yml). Сохраняйте том при обновлениях. В SQLite лежат очередь, идентификаторы копий и статусы доставки. На один файл `STATE_FILE` запускайте один экземпляр perepost. Для фиксации версии укажите, например, `ghcr.io/smotim/perepost:0.3.2`. Образы и бинарники доступны для Linux amd64 и arm64. ### Проверка происхождения сборки Начиная с v0.3.1, у каждого архива и образа есть подписанная attestation: она подтверждает, что файл собран workflow `release` этого репозитория из указанного тега. Проверка через [GitHub CLI](https://cli.github.com/): ```bash gh attestation verify perepost_*_linux_amd64.tar.gz --repo smotim/perepost gh attestation verify oci://ghcr.io/smotim/perepost:<версия> --repo smotim/perepost ``` ## Запуск без Docker Скачайте архив для своей архитектуры из [Releases](https://github.com/smotim/perepost/releases). Для systemd установите бинарник и подготовьте настройки. В примере используется архив для amd64. ```bash tar xzf perepost_*_linux_amd64.tar.gz sudo install perepost /usr/local/bin/ sudo mkdir -p /etc/perepost sudo install -m 600 .env.example /etc/perepost/env sudo cp config.example.yaml /etc/perepost/config.yaml sudo cp perepost.service /etc/systemd/system/ ``` Заполните `/etc/perepost/env` и `/etc/perepost/config.yaml`, затем запустите службу. ```bash sudo systemctl enable --now perepost journalctl -u perepost -f ``` Служба хранит SQLite в `/var/lib/perepost`. При запуске бинарника напрямую можно задать `STATE_FILE` и `CONFIG_FILE` через окружение. Вебхуки напрямую слушают `127.0.0.1:8090`, поэтому nginx на том же хосте может проксировать их без публикации порта Docker. ## Диагностика ### Как узнать ID канала MAX Для нового бота до регистрации вебхука можно получить событие `bot_added` однократным запросом к MAX. Добавьте бота администратором канала и выполните эти команды в Bash. ```bash curl -fsSLO https://raw.githubusercontent.com/smotim/perepost/v0.3.2/certs/russian_trusted_root_ca.pem read -rsp 'Токен бота MAX: ' max_token; printf '\n' printf 'header = "Authorization: %s"\n' "$max_token" | curl -fsS --config - --cacert russian_trusted_root_ca.pem \ 'https://platform-api2.max.ru/updates?types=bot_added&timeout=0' unset max_token ``` Найдите `chat_id` в событии с `is_channel: true`. Если у бота уже есть подписка, ищите событие в сервисе, который принимает этот вебхук. perepost использует существующую подписку MAX с тем же URL и при необходимости добавляет к ней нужные типы событий. В `.env` укажите секрет этой подписки. Если подписка указывает на другой адрес, для обратного маршрута используйте отдельного бота. [Получение ID канала в MAX](https://dev.max.ru/docs-api/methods/GET/chats) ### Журнал и мониторинг - `docker compose logs -f perepost` показывает запуск, маршруты и ошибки. Для расширенного журнала задайте `LOG_LEVEL=debug`. - `HEARTBEAT_URL` получает POST после успешной проверки. Проверка раз в пять минут сверяет доступ к платформам, права ботов и настроенные подписки. В режиме long polling проверяется отсутствие вебхука Telegram и успешный приём ответа за последние десять минут, включая пустой ответ без новых постов. - `TELEGRAM_ADMIN_ID` задаёт получателя уведомлений о запуске, проблемах с доставкой и изменении состояния проверки. Чтобы бот мог написать, нажмите «Старт» в диалоге с ним. - API MAX использует сертификат Минцифры. Корневой сертификат встроен в perepost. Для ручных запросов `curl` указывайте [сертификат из репозитория](certs/russian_trusted_root_ca.pem) через `--cacert`. ### Неизвестный результат публикации Если платформа могла принять сообщение, а ответ потерялся, доставка получает статус `uncertain`. Бот ставит на паузу пересылку из канала назначения, чтобы возможная копия не ушла обратно. Новые события этого канала продолжают записываться в очередь. Проверьте канал назначения и возьмите из журнала поля `source`, `post` и `destination`. Остановите perepost и выполните `perepost -resolve-uncertain SOURCE POST DESTINATION ID [ID...]` с идентификаторами уже опубликованных частей. Для MAX это `body.mid`, для Telegram — `message_id`. Если публикации точно не было, передайте `none` вместо ID. В Docker используйте `docker compose run --rm --no-deps perepost -resolve-uncertain …`. Затем запустите perepost, чтобы продолжить обработку очереди. ### Изменившийся состав незавершённого поста Если после перезапуска доступен другой набор вложений, бот останавливает досылку: ранее опубликованные части нельзя надёжно сопоставить с новым разбиением. То же происходит с незавершённой доставкой старой версии, для которой не сохранён состав частей. Сначала проверьте доступность вложений: следующий запуск продолжит пост, если исходный состав восстановился. Для ручного завершения остановите perepost, проверьте копию и при необходимости дошлите оставшееся вручную. Затем выполните `perepost -resolve-delivery SOURCE POST DESTINATION none|ID [ID...]`. Укажите ID вручную опубликованных частей для защиты от обратной пересылки либо `none`, если новых частей нет. Команда помечает доставку этому адресату обработанной; автоматической досылки для него больше не будет. Уже известные копии сохраняются. Флаг также принимает `uncertain`; старый `-resolve-uncertain` остаётся доступен. ### Сбой при обработке поста Если обработка поста падает с паникой, perepost пишет администратору и завершается: при перезапуске незаконченные публикации получают статус `uncertain`. Число сбоев хранится в SQLite. После третьего сбоя подряд на одном посте бот пропускает этот пост, сообщает об этом администратору и переходит к следующему. ## Как работает доставка Пост подтверждается платформе после записи в SQLite. Каналы обрабатываются параллельно, посты внутри канала идут в порядке приёма. Альбом Telegram собирается две секунды после последней части. Бот хранит ID каждой опубликованной части, чтобы распознавать свои копии и подавлять обратную пересылку. Начатый альбом сохраняет свой состав; поздние части отправляются отдельно и редактируются независимо от первоначальной копии. Медиа скачивается один раз и загружается для каждого получателя. Сетевые сбои при чтении и загрузке вложений можно повторять. При публикации бот автоматически повторяет только явно отклонённые попытки, например после ответа 429. При остановке он досылает посты до 50 секунд, а оставшуюся очередь подхватывает при следующем запуске. Подтверждённые части повторно не публикуются: досылка начинается с первой недоставленной. Второй сигнал остановки немедленно завершает процесс. Запросы всех маршрутов делят общий ограничитель: до 30 запросов в секунду на платформу. Для одного получателя MAX между отправками и правками выдерживается не менее 0,5 секунды, для Telegram — не менее секунды. Ожидание одного канала не останавливает остальные. ## Разработка ```bash go vet ./... go test -race ./... golangci-lint run go run . # переменные из окружения, маршруты из ./config.yaml ``` GitHub Actions выпускает бинарники и образы после отправки подписанного тега `vX.Y.Z`. Библиотеки [go-telegram/bot](https://github.com/go-telegram/bot) и [max-bot-api-client-go](https://github.com/max-messenger/max-bot-api-client-go). ## Лицензия [MIT](LICENSE)