TG Selfhost Bot API

Миграция

При переносе меняется Bot API endpoint приложения и, при необходимости, webhook. До начала нужно знать старый endpoint. Облачный getMe не подходит для нейтрального определения текущего сервера: запросы с токеном могут влиять на сессию.

Остановите приложение или фоновый процесс, который обращается к старому API. Это включает pollers, фоновые задания, retries и supervisor. Webhook-приёмник — отдельный процесс: остановка API-клиента не замораживает очередь и доставку Bot API. Оставьте приёмник доступным, пока обрабатываются прежние обновления. Подготовьте новый сервис без авторизации этого бота, сохраните состояние и профиль webhook. getWebhookInfo не возвращает прежний secret_token; храните его в своей конфигурации.

Локальный Bot API с --local допускает HTTP- и HTTPS-адреса webhook; официальный облачный Bot API требует HTTPS.

Выбор сценария

Сценарий Подход
Тот же Bot API, другой webhook Изменить webhook и проверить фактический результат; сервер не переносится
Облако Telegram → локальный Выполнить облачный logOut до обращения к локальному API
Локальный → локальный Следовать официальной процедуре с переносом состояния, описанной ниже
Локальный → облако Выйти из старого локального сервера перед переключением; учесть очередь и доступность облака

Основной источник — README официального сервера. Также см. методы logOut и close. После выхода из облака немедленное возвращение в него ограничено правилом Telegram о десяти минутах.

Облако → локальный сервер

  1. Проверьте доступность и настройки целевой инфраструктуры.
  2. Остановите приложение, использующее облачный endpoint.
  3. Сохраните профиль webhook и выберите поведение для накопившихся обновлений.
  4. Вызовите logOut в облаке, затем измените Bot API endpoint приложения.
  5. Установите полный профиль webhook на новом сервере либо явно выберите polling.
  6. Проверьте фактические настройки, ошибки доставки и тестовое обновление, затем возобновите работу.

Утилита проверяет корень target без токена, с таймаутом и без перехода по redirects. Характерный ответ 404 не подтверждает подлинность узла. До запросов с токеном подтвердите source, target, webhook и политику сброса. Отключение webhook и logOut на source должны успешно завершиться до первого target getMe. Ошибка HTTP/API, некорректный ответ или неизвестный результат изменения останавливают цепочку. Не проверяйте обе стороны входом и не повторяйте потерянный logOut автоматически.

Локальный → локальный с переносом состояния

Официальная процедура для отдельного бота: deleteWebhook без намеренного сброса, успешный close на старом сервере, затем перенос подкаталога бота (найденного по user ID) в рабочий каталог совместимой цели. Сначала остановите старые API-клиенты. Ошибка close, включая 429/retry_after, или неизвестный ответ запрещают перенос и вход на target. Не повторяйте запрос вслепую. Имя каталога может содержать токен: не публикуйте его в журналах.

В закреплённом ClientManager.cpp есть важная граница хранения: tqueue.binlog и webhooks_db.binlog находятся в общем рабочем каталоге, вне каталогов отдельных ботов. Перенос одного подкаталога не доказывает сохранность накопленной локальной очереди. На общем сервере сначала обработайте backlog или явно учтите его потерю. Объединение shared binlogs с действующим многоботным target не поддерживается. drop_pending_updates=false не переносит данные и не гарантирует их сохранность. Простой выход через source logOut и новый вход на target допустимы при принятой потере локального backlog.

Для выделенной установки предусмотрен отдельный cold-transfer:

  1. Выберите окно обслуживания. Остановите все API-клиенты, retries и supervisor; сохраните полный профиль webhook и согласованную резервную копию.
  2. Отключите source webhook без намеренного сброса и получите успешный close. Любая ошибка или неизвестный результат останавливают процедуру.
  3. Остановите все source writers и контейнеры Bot API до копирования рабочего каталога. Не допускайте повторного запуска старого сервиса через restart: always или внешнюю автоматику. Остановка общего контейнера затронет всех его ботов.
  4. Перенесите весь согласованный рабочий каталог, включая общие хранилища, только на пустую совместимую цель. Сохраните владельца и права; проверьте версии, mounts и обработку абсолютных путей медиа в приложении. Не копируйте живую БД и не перезаписывайте общие binlogs другой установки.
  5. Отдельно проверьте перенос и отсутствие прежних writers. Только затем запустите target и выполните первый запрос с токеном (getMe).
  6. Восстановите нужный профиль webhook либо явно настройте polling. Проверьте фактическую конфигурацию и тестовое обновление перед возобновлением трафика.

Браузер выдаёт для этого сценария отдельные ручные этапы. Он не копирует state и не формирует единый исполняемый план, перескакивающий через перенос. Mock-тесты не доказывают сохранность обновлений Telegram при реальном переносе.

Локальный сервер → облако

Обработайте backlog или явно примите его потерю, остановите все старые API-клиенты, сохраните профиль, отключите webhook и получите успешный локальный logOut до любого облачного запроса. Локальный logOut может очистить очередь даже после deleteWebhook с drop_pending_updates=false. Облако не импортирует локальный state. После восстановления доступности cloud выполните облачный getMe, восстановите профиль или явный polling и проверьте доставку. При ошибке или неизвестном исходе остановитесь.

Этот logout-first рецепт — вывод из реализации upstream и общих правил переноса, а не отдельная дословная инструкция upstream для local → cloud. Ограничение возврата в cloud на десять минут после облачного logOut и окно 429 локального close — разные ограничения. Браузер выдаёт ручной план, не исполняя этот переход.

Bash-утилита

Текущий скрипт поддерживает переход из облака на локальный сервер. Он читает .env рядом со скриптом либо файл ENV_FILE (относительный путь считается от рабочего каталога вызова). Отсутствие файла по умолчанию допустимо; ошибка явно указанного файла останавливает запуск. Файл исполняется как доверенный код Bash, это не песочница. Приоритет: аргументы, окружение процесса, файл, значения по умолчанию. В одном источнике SELF_BOT_URL и WEBHOOK_URL имеют приоритет над старыми именами TELEGRAM_BOT_URL и WEB_HOOK; старое имя в окружении сильнее значения из файла. Пустой webhook сохраняет прежнюю логику выбора, описанную ниже. Из другого каталога можно вызвать ENV_FILE="/path/to/settings.env" bash /path/to/migrate_off_to_self.sh "$BOT_TOKEN".

Порядок аргументов: BOT_TOKEN, SELF_BOT_URL, затем WEBHOOK_URL:

set -euo pipefail
read -rsp 'Bot token: ' BOT_TOKEN; printf '\n'
SELF_BOT_URL="http://127.0.0.1:18080" \
WEBHOOK_URL="https://example.com/telegram/webhook" \
  bash ./migrate_off_to_self.sh "$BOT_TOKEN"
unset BOT_TOKEN

Можно передать все три аргумента позиционно. Webhook нельзя ставить вторым аргументом: это место адреса Bot API.

set -euo pipefail
read -rsp 'Bot token: ' BOT_TOKEN; printf '\n'
bash ./migrate_off_to_self.sh "$BOT_TOKEN" \
  "http://127.0.0.1:18080" "https://example.com/telegram/webhook"
unset BOT_TOKEN

Обе формы используют внешний API-порт 18080; nginx передаёт запросы на botapi:8081. Webhook — адрес приложения. Команды не запускают polling consumer. Из другого рабочего каталога используйте абсолютный путь к утилите:

read -rsp 'Bot token: ' BOT_TOKEN; printf '\n'
ENV_FILE="/path/to/settings.env" \
  bash /path/to/migrate_off_to_self.sh "$BOT_TOKEN"
unset BOT_TOKEN

DROP_PENDING_UPDATES=true удаляет накопившиеся обновления. По умолчанию — false.

Текущие ограничения утилиты

Исправления отслеживаются в #2, #7 и #9. Перед использованием с работающей установкой проверьте процедуру на тестовом боте.

Браузерный инструмент

Открыть мигратор. Это статическая страница, а не серверный исполнитель миграции. Запросы идут из вашего браузера; API должен разрешать соответствующий origin и доступ.

Локальные проверки и построение плана не отправляют запросов. Проверка состояния требует согласия и читает только явно выбранный source, не определяя текущий сервер бота. Cloud → local проверяет target без токена, запрашивает согласие, читает source, отключает там webhook и выходит, затем обращается к target. Webhook-only не вызывает logOut и close. Изменение формы сбрасывает прочитанное состояние. Ошибка или неизвестный исход изменения блокирует дальнейшее выполнение на странице до ручной сверки состояния. Локальные переходы выполняются по чеклистам.

В выполнении и генераторе плана остаются ограничения обработки ошибок и shell-экранирования. CORS-ошибка может скрыть ответ уже выполненного изменяющего запроса. Перед новым планом выясните текущее состояние. Проверяйте команды перед выполнением.

Polling, профиль webhook и файлы

Независимые исправления #5, #7 и #9 не интегрированы в этот кандидат. Их незавершённые модули не являются зависимостями утилит. Пустой webhook сохраняет legacy-наследование; это не действие polling. Для явного polling после завершения перехода между серверами вручную удалите webhook на target и проверьте, что result.url — строка, равная "":

# BOT_TOKEN и SELF_BOT_URL относятся к выбранному target после миграции.
set -euo pipefail
curl -q -fsS --connect-timeout 5 --max-time 30 -X POST \
  "$SELF_BOT_URL/bot$BOT_TOKEN/deleteWebhook" \
  --data-urlencode 'drop_pending_updates=false' | jq -e '.ok == true and .result == true'
curl -q -fsS --connect-timeout 5 --max-time 30 \
  "$SELF_BOT_URL/bot$BOT_TOKEN/getWebhookInfo" | \
  jq -e '.ok == true and (.result.url | type == "string") and .result.url == ""'

При ошибке любой проверки остановитесь. Собственный polling-клиент запускайте после сверки; примеры не потребляют обновления. URL readback не доказывает доставку webhook. Восстановите secret_token, сертификат, allowed_updates, max_connections и остальные нужные настройки из сохранённого профиля. Секрет и загруженный сертификат нельзя восстановить из getWebhookInfo.

В local-режиме абсолютный getFile.file_path обозначает путь сервера/контейнера. Нужны совместимые mounts и пути приложения; перенос state сам по себе их не исправляет. nginx передаёт запросы /file/ в Bot API, не открывая каталог состояния. Не приписывайте ему авторизацию X-Telegram-Bot-Token. См. доступ к файлам. Порт 18081 отдельного downloads-сервиса не заменяет API-порт 18080.

Настройки · Обслуживание

Предварительная диагностика миграции doctor

Запуск: python3 tools/doctor.py --scenario official-to-self --migration-input /private/decisions.json. Строгий контракт входа — tools/doctor-input.schema.json. Храните реальный файл вне любого Git checkout и webroot с правами только владельца на POSIX. В нём только решения по подготовке: без токенов, URL, API hash и содержимого сертификатов. Гарантии приватности файла на native Windows остаются UNKNOWN.

Вход v1 начинается с schema_version: 1. Задайте delivery_action: preserve|set|polling, явный boolean drop_pending_updates (в том числе false), file_access: not_used|local_path|shared_volume|remote_authenticated и boolean workflow_prepared. Декларация workflow включает остановку приложения на нужном этапе, backup, переключение endpoint и обработку абсолютных путей при использовании файлов. Для preserve/set добавьте webhook_profile_prepared: полный профиль, обработка secret/certificate, IP, connections и allowed updates должны быть подготовлены в отдельной защищённой конфигурации. Секрет webhook и полный URL во вход doctor не входят.

{
  "schema_version": 1,
  "delivery_action": "polling",
  "drop_pending_updates": false,
  "file_access": "not_used",
  "workflow_prepared": true
}

Пример декларирует подготовленный план, а не уже выполненные действия. remote_authenticated остаётся UNKNOWN без независимой проверки безопасного контракта на границе #5; doctor v1 не реализует download service. Local/shared file access остаётся UNKNOWN до независимой проверки путей/volumes приложения: выбор режима не доказывает наличие mapping.

Для self-to-self отдельно выберите source через --source-file, --source-env-file, --source-profile, --source-project-directory и --source-project-name. Добавьте transfer с явным method, boolean backup_prepared и exclusive_use_plan_prepared, а также backlog_policy: drain или accept-loss. State backings должны различаться.

Измеряются только метаданные. Их изменение, symlinks, вложенные mounts и неподдерживаемые entries дают UNKNOWN. Hard links не удваивают измеренные bytes. Резерв target включает измеренные bytes/entries плюс 10%; необязательные transfer_metadata остаются декларацией и не подтверждают capacity. Baseline совместимости — совпадающие наблюдаемые версии бинарника и доступные source revisions; различающиеся или неизвестные версии дают UNKNOWN.

self-to-official также требует source_backup_prepared: true и source_backlog_policy: drain|accept-loss; подготовленный workflow начинается с logout. Проверяются локальный source и подготовка ручного workflow. Cloud authorization, ограничения session и delivery остаются непроверенными. hook-only даёт BLOCKED/1 с unsupported_scenario (#7/#9). Отсутствующие решения дают UNKNOWN/3 с именем поля; false preparedness или небезопасные POSIX-права — BLOCKED/1; неверный или нечитаемый вход — exit 2. Отчёт сохраняет pending actions и unverified facts. Doctor не запускает миграцию, helpers, методы бота, cloud probes, journals и locks. READY означает готовность начать подготовленную процедуру и не подтверждает helpers, завершённый перенос, авторизацию или доставку.