Ctrl K
ComunidadGuías…

README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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)