README.md
Rama main · réplica de lectura del repositorio original.
# 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)