🛡️ Скрепа

Боты в Скрепе: BotFather, E2E-диалоги и запуск gateway

Этот туториал поможет создать бота через @botfather, безопасно сохранить его реквизиты, запустить skrepa-bot-gateway и ответить на первое сообщение. Исходники Скрепы, Rust toolchain и protobuf-файлы для этого сценария не требуются.

Справочник методов, схем, ошибок и примеров запросов находится в интерактивной документации Bot API.

Что понадобится

Все команды ниже можно выполнить в пустом каталоге.

Как устроен gateway

skrepa-bot-gateway — локальный адаптер между кодом вашего бота и защищённым транспортом Скрепы. Приложение бота работает с привычным Telegram-compatible HTTP API. Gateway подключается к серверу по gRPC, регистрируется как отдельное устройство бота и берёт на себя E2E-криптографию.

flowchart LR
    U["Пользователь<br/>в клиенте Скрепы"] <-->|"E2E-DM"| S["Транспорт Скрепы"]
    S <-->|"gRPC:<br/>device inbox и ciphertext"| G["skrepa-bot-gateway"]
    G <-->|"Telegram-compatible HTTP:<br/>polling, webhook, text, media и callbacks"| B["Код вашего бота"]
    G --- D[("state_dir:<br/>identity, ratchets, durable queue,<br/>file_id и E2E-ключи файлов")]

Путь входящего сообщения выглядит так:

  1. gateway получает зашифрованное сообщение из device inbox;
  2. расшифровывает его в сессии Double Ratchet;
  3. сохраняет update в локальную durable queue;
  4. gateway отдаёт update через getUpdates либо отправляет его на webhook.

Для исходящего сообщения код вызывает sendMessage или один из media-методов. Gateway находит активные устройства адресата, создаёт отдельный ciphertext для каждого устройства и повторяет отправку до подтверждения сервера. Файлы шифруются отдельным случайным GOST-ключом; ciphertext хранится в file stack Скрепы, а ключ доступен адресатам только внутри E2E-envelope. Код бота работает с plaintext на локальной HTTP-границе.

Конфигурация gateway связывает четыре части:

Каталог state_dir должен переживать перезапуски. Размещайте его на постоянном диске, выдавайте доступ только процессу gateway и не подключайте один каталог одновременно к нескольким экземплярам. Bot token можно заменить через BotFather; identity recovery secret при ротации token остаётся прежним.

Gateway слушает два локальных адреса. Порт 8081 предоставляет обычный Bot API: получение updates, webhook, текст, файлы, replies, редактирование и inline-кнопки. Порт 8082 предназначен для дополнительных возможностей платформы. Для первого echo-бота достаточно порта 8081, однако healthcheck удобно выполнять на обоих.

Команды BotFather

КомандаРезультат
/start или /helpсправка
/newbotпошаговое создание бота
/mybotsсписок собственных ботов и их статусы
/token @usernameотзыв прежнего credential и выпуск нового token
/disable @usernameотключение бота и отзыв всех активных credentials
/cancelсброс незавершённого сценария

Туториал: создаём echo_bot

1. Откройте диалог

В клиенте Скрепы найдите @botfather и откройте личный диалог. Поиск не зависит от регистра; начальный символ @ можно опустить. В профиле должна отображаться отметка бота.

Если @botfather отсутствует в результатах или долго не отвечает, обновите клиент и повторите попытку. Сохраняющаяся ошибка означает проблему сервиса, о которой следует сообщить оператору стенда.

2. Запустите мастер

Отправьте:

/newbot

BotFather последовательно запросит:

  1. username длиной 3–32 ASCII-символа; разрешены латинские буквы, цифры и _;
  2. отображаемое имя длиной до 128 Unicode-символов;
  3. набор возможностей.

Для учебного бота диалог выглядит так:

Вы: /newbot
BotFather: Пришлите username нового бота…

Вы: echo_bot
BotFather: Теперь пришлите отображаемое имя бота.

Вы: Эхо
BotFather: Выберите возможности: default либо список dm,groups,channels через запятую.

Вы: default
BotFather: Готово: @echo_bot. … 7100001:<64 hex символа секрета>
BotFather: Bot ID: <64 hex символа>
BotFather: Ключ восстановления identity: <64 hex символа>

Для текущего echo-сценария выберите default: этого достаточно для приватных сообщений. Если бот должен работать в группах или публиковать записи в каналах, укажите groups, channels либо оба значения. Gateway проверяет эти возможности при подключении бота к соответствующему разговору.

3. Сохраните token и recovery secret

BotFather показывает два разных секрета и публичный идентификатор:

Bot token показывается при создании и каждой ротации. Identity recovery secret выдаётся при создании и остаётся постоянным после ротации token. Сохраните оба значения в secret manager или файле с правами 0600. Не помещайте их в Git, shell history, issue tracker и логи.

При утрате или подозрении на компрометацию отправьте:

/token @echo_bot

Прежний token становится недействительным сразу после ответа.

4. Подготовьте идентификаторы

Новый token имеет Telegram SDK-compatible форму:

<положительный 52-битный numeric id>:<64 hex символа секрета>

Скопируйте отдельный Bot ID из ответа BotFather. Числовая часть token служит telegram_user_id и безопасно представима в Python и JavaScript:

set +x
read -rsp 'Bot token: ' SKREPA_BOT_TOKEN; printf '\n'
read -rsp 'Identity recovery secret: ' SKREPA_BOT_IDENTITY_RECOVERY_SECRET; printf '\n'
read -rp 'Bot ID: ' SKREPA_BOT_ID_HEX
export SKREPA_BOT_TOKEN
export SKREPA_BOT_IDENTITY_RECOVERY_SECRET
export SKREPA_BOT_ID_HEX
export SKREPA_TELEGRAM_USER_ID="${SKREPA_BOT_TOKEN%%:*}"
test "${#SKREPA_BOT_ID_HEX}" -eq 64
test "${SKREPA_TELEGRAM_USER_ID}" -gt 0

Gateway принимает только SDK-compatible token из актуального ответа BotFather. Если token был выдан до перехода на numeric id, выполните /token @echo_bot и замените credential в secret manager и конфигурации gateway.

5. Скачайте gateway

Готовые дистрибутивы публикуются вместе с серверным релизом. На Mac с Apple Silicon скачайте нативный бинарный файл:

mkdir -p var/echo-bot/bin
cd var/echo-bot/bin

curl --fail-with-body -LO \
  https://skrepa.fomkin.org/download/bot-gateway/skrepa-bot-gateway-darwin-arm64.tar.gz
curl --fail-with-body -LO \
  https://skrepa.fomkin.org/download/bot-gateway/skrepa-bot-gateway-darwin-arm64.tar.gz.sha256
shasum -a 256 -c skrepa-bot-gateway-darwin-arm64.tar.gz.sha256
tar -xzf skrepa-bot-gateway-darwin-arm64.tar.gz
chmod 700 skrepa-bot-gateway
cd -

Для Linux amd64 и для остальных систем с Docker доступен образ в архиве:

mkdir -p var/echo-bot/image
cd var/echo-bot/image

curl --fail-with-body -LO \
  https://skrepa.fomkin.org/download/bot-gateway/skrepa-bot-gateway-linux-amd64.docker.tar.gz
curl --fail-with-body -LO \
  https://skrepa.fomkin.org/download/bot-gateway/skrepa-bot-gateway-linux-amd64.docker.tar.gz.sha256
shasum -a 256 -c skrepa-bot-gateway-linux-amd64.docker.tar.gz.sha256
gzip -dc skrepa-bot-gateway-linux-amd64.docker.tar.gz | docker load
cd -

Checksum-файл содержит имя соседнего архива. Поэтому команды проверки следует запускать из каталога, куда скачаны оба файла. На Linux shasum -a 256 -c можно заменить на sha256sum -c.

6. Создайте конфигурацию gateway

umask 077
mkdir -p var/echo-bot

jq -n \
  --arg endpoint "https://skrepa.fomkin.org" \
  --arg state_dir "${PWD}/var/echo-bot/state" \
  --arg bot_id_hex "${SKREPA_BOT_ID_HEX}" \
  --arg token "${SKREPA_BOT_TOKEN}" \
  --arg telegram_user_id "${SKREPA_TELEGRAM_USER_ID}" \
  --arg identity_recovery_secret "${SKREPA_BOT_IDENTITY_RECOVERY_SECRET}" \
  '{
    compatibility_profile_id: "telegram-v1-core-preview-4",
    bot_platform_endpoint: $endpoint,
    telegram_listen_addr: "127.0.0.1:8081",
    extensions_listen_addr: "127.0.0.1:8082",
    state_dir: $state_dir,
    bots: [{
      bot_id_hex: $bot_id_hex,
      token: $token,
      identity_recovery_secret_hex: $identity_recovery_secret,
      telegram_user_id: ($telegram_user_id | tonumber),
      username: "echo_bot",
      first_name: "Эхо"
    }]
  }' > var/echo-bot/gateway.json

chmod 600 var/echo-bot/gateway.json

telegram_user_id берётся из token и остаётся стабильным при ротации credential. identity_recovery_secret_hex — отдельный 256-битный recovery secret, который BotFather показывает вместе с первым token. Храните его в secret manager и сохраняйте прежним при ротации bot token. state_dir содержит стабильный bot_runtime_id и должен переживать перезапуски.

7. Запустите gateway

На Mac с Apple Silicon:

./var/echo-bot/bin/skrepa-bot-gateway serve var/echo-bot/gateway.json

Для Docker подготовьте конфигурацию с контейнерными адресами и каталогом state:

jq \
  '.telegram_listen_addr = "0.0.0.0:8081"
   | .extensions_listen_addr = "0.0.0.0:8082"
   | .state_dir = "/state"' \
  var/echo-bot/gateway.json > var/echo-bot/gateway.docker.json
chmod 600 var/echo-bot/gateway.docker.json

mkdir -p var/echo-bot/state
docker run --rm --name skrepa-echo-bot \
  --user "$(id -u):$(id -g)" \
  -p 127.0.0.1:8081:8081 \
  -p 127.0.0.1:8082:8082 \
  -v "${PWD}/var/echo-bot/gateway.docker.json:/config/gateway.json:ro" \
  -v "${PWD}/var/echo-bot/state:/state" \
  skrepa-bot-gateway:0.1.0 serve /config/gateway.json

Команда работает в foreground и пишет только служебные сведения о runtime. Для постоянного запуска перенесите те же параметры в systemd, Compose или вашу систему оркестрации. Секретный URL Bot API стоит исключить из access logs.

В другом терминале проверьте listeners и server-backed getMe:

curl --fail-with-body -sS http://127.0.0.1:8081/healthz | jq .
curl --fail-with-body -sS http://127.0.0.1:8082/healthz | jq .
curl --fail-with-body -sS \
  "http://127.0.0.1:8081/bot${SKREPA_BOT_TOKEN}/getMe" | jq .

Ожидаемый профиль:

{
  "ok": true,
  "result": {
    "id": 7100001,
    "is_bot": true,
    "first_name": "Эхо",
    "username": "echo_bot"
  }
}

После /token или /disable старый URL начинает возвращать HTTP 401. Gateway пока не перечитывает credential на лету: обновите gateway.json, перезапустите процесс и повторите getMe.

Gateway сам создаёт стабильное bot device, регистрирует подписанные prekey и запускает delivery loop. Входящие E2E-DM появляются в getUpdates:

curl --fail-with-body -sS \
  "http://127.0.0.1:8081/bot${SKREPA_BOT_TOKEN}/getUpdates?limit=100" | jq .

update_id подтверждается обычной Telegram-семантикой: следующий запрос передаёт offset, равный последнему обработанному update_id + 1. До этого момента update остаётся в локальной durable queue и переживает перезапуск gateway.

Ответ в приватный E2E-диалог отправляется через sendMessage. Возьмите chat.id из update:

curl --fail-with-body -sS \
  -H 'content-type: application/json' \
  -d '{"chat_id": 217020518514230, "text": "Привет из бота"}' \
  "http://127.0.0.1:8081/bot${SKREPA_BOT_TOKEN}/sendMessage" | jq .

Gateway сверяет активные устройства адресата, шифрует отдельный ciphertext для каждого и сохраняет исходящее сообщение в local outbox до подтверждения платформы.

Форматирование, replies и inline-кнопки

sendMessage принимает parse_mode со значениями HTML или MarkdownV2, а также явный массив entities. Смещения и длины entities измеряются в UTF-16 code units, как в Telegram Bot API. Передавайте либо parse_mode, либо entities: сочетание двух вариантов gateway отклоняет.

MarkdownV2 включает fenced code blocks с необязательным языком, например ```rust. Gateway создаёт для такого блока entity типа pre.

Для ответа используйте настоящий message_id входящего сообщения. Inline keyboard поддерживает callback-кнопки; нажатие приходит отдельным durable callback_query update и подтверждается методом answerCallbackQuery. Контекст кнопки подписан общей identity бота. Нажатие сможет обработать другой активный gateway runtime этого же бота, даже если исходное сообщение отправил первый runtime; изменённый клиентом контекст проверку подписи не проходит.

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyParameters


async def menu(update, _context):
    message = update.message
    await message.reply_text(
        "<b>Выберите действие</b>",
        parse_mode="HTML",
        reply_parameters=ReplyParameters(message_id=message.message_id),
        reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(
            "Дальше", callback_data="next"
        )]]),
    )


async def button(update, _context):
    query = update.callback_query
    await query.answer("Готово")
    await query.edit_message_text("<i>Шаг выполнен</i>", parse_mode="HTML")

Кроме sendMessage, текстовый профиль публикует sendChatAction, editMessageText, deleteMessage, forwardMessage и copyMessage. Редактирование и удаление разрешены для сообщений, созданных этим ботом и доступных в durable history текущего runtime. forwardMessage сохраняет сведения об источнике, copyMessage переносит содержимое без отметки о пересылке.

Gateway хранит до 2048 обработанных сообщений и callbacks. Pending updates, ожидающие ответа callbacks и активные callback targets не удаляются до доставки. Reply snapshots содержат только один уровень вложенности, поэтому длинная цепочка ответов не раздувает durable state квадратично.

Группы и каналы

Бота добавляют в обычную или защищённую группу через интерфейс Скрепы. Gateway сам обнаруживает новое участие и выдаёт разговору стабильный отрицательный chat.id; приложение бота не работает с 32-байтовыми внутренними ID. Обычная группа отображается как Telegram supergroup. Защищённая группа выглядит так же для bot SDK, при этом её сообщения шифруются общей MLS-реализацией Скрепы: bot device публикует key package, входит внешним commit и сохраняет epoch state в своём зашифрованном durable state.

Командный обработчик можно зарегистрировать привычным способом:

from telegram import Update
from telegram.ext import CommandHandler, ContextTypes


async def status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    chat = await context.bot.get_chat(update.effective_chat.id)
    members = await context.bot.get_chat_member_count(chat.id)
    await update.message.reply_text(f"{chat.title}: участников {members}")


application.add_handler(CommandHandler("status", status))

Публикации канала приходят в update.channel_post, а sendMessage с его chat.id создаёт новый пост при наличии права post. Публикация сначала фиксируется в durable gateway outbox. Повтор после потерянного ответа использует тот же client_message_id, поэтому PostgreSQL channel outbox создаёт ровно один пост.

Административный сценарий использует my_chat_member, chat_member и chat_join_request. Доступны getChat, getChatMember, getChatMemberCount, getChatAdministrators, leaveChat, методы блокировки и выдачи прав, ссылки-приглашения, одобрение и отклонение заявок. При явном allowed_updates перечислите нужные типы; без фильтра gateway доставляет их по умолчанию. В текущем preview текстовые групповые и каналовые сценарии являются основными; media-публикации канала и forum topics входят в последующие профильные срезы.

Inline mode

Пользователь может вызвать бота прямо в поле ввода приватного диалога: начните сообщение с @username, добавьте пробел и поисковый запрос. Клиент открывает пятиминутную inline-сессию, а gateway доставляет боту обычный inline_query update. Результаты появляются над полем ввода; выбранный текст отправляется в диалог по обычному E2E message path.

Inline mode поддерживает article results с InputTextMessageContent. До выбора бот может заменить весь список повторным answerInlineQuery. После выбора или истечения TTL platform отклоняет новый ответ, а повторный выбор того же result остаётся идемпотентным.

Пример для python-telegram-bot:

from telegram import InlineQueryResultArticle, InputTextMessageContent, Update
from telegram.ext import InlineQueryHandler


async def inline_search(update: Update, _context):
    query = update.inline_query.query.strip()
    await update.inline_query.answer([
        InlineQueryResultArticle(
            id="result-1",
            title=f"Отправить: {query or 'пустой запрос'}",
            description="Текст будет отправлен в текущий диалог",
            input_message_content=InputTextMessageContent(
                f"Результат поиска: {query}"
            ),
        )
    ], is_personal=True, cache_time=0)


application.add_handler(InlineQueryHandler(inline_search))

Для polling добавьте inline_query и chosen_inline_result в allowed_updates, если передаёте явный фильтр. Gateway сохраняет server cursor в state_dir: перезапуск не создаёт второй Telegram update для уже импортированной platform-сессии.

Реакции

В private E2E-диалоге бот может поставить одну обычную emoji-реакцию методом setMessageReaction. Пустой список снимает её:

from telegram import ReactionTypeEmoji


await context.bot.set_message_reaction(
    chat_id=update.effective_chat.id,
    message_id=update.effective_message.message_id,
    reaction=[ReactionTypeEmoji("👍")],
)

# Снять реакцию
await context.bot.set_message_reaction(
    chat_id=update.effective_chat.id,
    message_id=update.effective_message.message_id,
    reaction=[],
)

Когда пользователь меняет реакцию в диалоге с ботом, gateway выдаёт message_reaction с массивами old_reaction и new_reaction. Telegram исключает такие updates из default-набора, поэтому укажите allowed_updates=["message", "message_reaction"], если бот их обрабатывает.

Реакции пока охватывают private DM. Установка custom_emoji, paid reactions, нескольких реакций и is_big=true получает Telegram-shaped 400. Входящая custom emoji может прийти как ReactionTypeCustomEmoji, если её выбрал обычный клиент Скрепы.

Файлы и media

Методы sendPhoto, sendDocument, sendAudio, sendVideo и sendVoice принимают обычный multipart upload стандартных Telegram SDK. Лимит одного файла составляет 50 МиБ. Gateway шифрует bytes до загрузки в Скрепу, сохраняет canonical file reference и возвращает opaque file_id в объекте сообщения.

Полученный file_id можно передать тому же media-методу после перезапуска gateway. Поэтому каталог state_dir обязан находиться на постоянном диске: там хранится связь публичного file_id с canonical file reference и E2E-ключом. Ссылка на внешний HTTP URL в media-параметре отклоняется; это исключает SSRF из профиля. Загружайте локальный файл либо повторно используйте file_id.

Пример для python-telegram-bot:

from pathlib import Path


async def send_report(update, _context):
    sent = await update.message.reply_document(
        document=Path("report.pdf"),
        caption="Отчёт",
    )

    # file_id остаётся пригодным для повторной отправки после restart gateway.
    await update.message.reply_document(sent.document.file_id)

    descriptor = await sent.get_bot().get_file(sent.document.file_id)
    await descriptor.download_to_drive("downloaded-report.pdf")

При собственной настройке SDK задайте обе базы URL. base_url указывает на методы, base_file_url — на контролируемое gateway скачивание:

application = (
    Application.builder()
    .token(os.environ["SKREPA_BOT_TOKEN"])
    .base_url("http://127.0.0.1:8081/bot")
    .base_file_url("http://127.0.0.1:8081/file/bot")
    .build()
)

getFile возвращает Telegram-compatible file_path. Download сначала полностью получает ciphertext из file stack и проверяет GOST authentication tag. При обрыве backend-потока или повреждении файла gateway отвечает ошибкой и не выдаёт частичный plaintext.

Echo-бот на python-telegram-bot

Gateway поддерживает стандартный polling lifecycle python-telegram-bot 22.8, включая служебный вызов deleteWebhook, long polling и form-urlencoded запросы. Установите SDK в virtualenv:

python3 -m venv .venv
.venv/bin/python -m pip install 'python-telegram-bot==22.8'

Создайте echo_bot.py:

import os

from telegram import Update
from telegram.ext import Application, ContextTypes, MessageHandler, filters


async def echo(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None:
    if update.message and update.message.text:
        await update.message.reply_text(update.message.text)


application = (
    Application.builder()
    .token(os.environ["SKREPA_BOT_TOKEN"])
    .base_url("http://127.0.0.1:8081/bot")
    .base_file_url("http://127.0.0.1:8081/file/bot")
    .build()
)
application.add_handler(MessageHandler(filters.TEXT, echo))
application.run_polling(allowed_updates=["message"])

Запустите gateway, затем бота:

.venv/bin/python echo_bot.py

Суффикс /bot в base_url обязателен: SDK самостоятельно добавляет к нему token и имя метода. Этот сценарий входит в release tests и проходит через настоящий E2E transport Скрепы.

Echo-бот на aiogram

aiogram 3.29.1 использует тот же token и base URL, а запросы отправляет через штатную AiohttpSession:

import asyncio
import os

from aiogram import Bot, Dispatcher, F
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.types import Message

dp = Dispatcher()


@dp.message(F.text)
async def echo(message: Message) -> None:
    await message.answer(message.text)


async def main() -> None:
    session = AiohttpSession(
        api=TelegramAPIServer.from_base("http://127.0.0.1:8081")
    )
    bot = Bot(token=os.environ["SKREPA_BOT_TOKEN"], session=session)
    await dp.start_polling(bot, allowed_updates=["message"])


asyncio.run(main())

Echo-бот на Telegraf

Telegraf 4.16.3 получает корневой адрес API через штатную настройку telegram.apiRoot:

const { Telegraf } = require('telegraf')

const bot = new Telegraf(process.env.SKREPA_BOT_TOKEN, {
  telegram: { apiRoot: 'http://127.0.0.1:8081' },
})

bot.on('text', (ctx) => ctx.reply(ctx.message.text))
bot.launch({ allowedUpdates: ['message'] })

Все три SDK проходят release-сценарий с reply, HTML entities, chat action, редактированием, удалением, копированием, пересылкой и inline keyboard. Gateway сохраняет User.id, Chat.id и Message.message_id в 52-битном диапазоне, поэтому Node.js передаёт их без округления. update_id остаётся отдельным курсором доставки.

Push-доставка через webhook

Webhook подходит сервисам с постоянно доступным HTTP endpoint. Gateway остаётся владельцем E2E device и durable queue, а приложение получает уже расшифрованный Telegram-compatible Update методом POST.

Для production понадобится HTTPS URL, доступный из network namespace gateway. Обычный HTTP принимается только для localhost и loopback IP, что удобно при локальной разработке. Сгенерируйте отдельный проверочный секрет:

export SKREPA_WEBHOOK_URL='https://bot.example.org/skrepa/update'
export SKREPA_WEBHOOK_SECRET="$(openssl rand -hex 32)"

curl --fail-with-body -sS \
  -H 'content-type: application/json' \
  -d "$(jq -n \
    --arg url "$SKREPA_WEBHOOK_URL" \
    --arg secret "$SKREPA_WEBHOOK_SECRET" \
    '{url: $url, secret_token: $secret, allowed_updates: ["message"]}')" \
  "http://127.0.0.1:8081/bot${SKREPA_BOT_TOKEN}/setWebhook" | jq .

После успешного setWebhook вызовы getUpdates получают HTTP 409: у одного bot runtime в каждый момент действует один delivery mode. Текущий URL, число pending updates и последнюю ошибку можно проверить без чтения локальных файлов:

curl --fail-with-body -sS \
  "http://127.0.0.1:8081/bot${SKREPA_BOT_TOKEN}/getWebhookInfo" | jq .

Каждый запрос содержит JSON-объект Update и заголовок X-Telegram-Bot-Api-Secret-Token. Сравнивайте секрет без записи заголовка в логи. Возвращайте 2xx лишь после того, как бизнес-эффект или задание для него надёжно сохранены. При timeout, сетевой ошибке либо другом HTTP status gateway оставляет головной update в очереди и повторяет его с ограниченной экспоненциальной задержкой; следующие updates ждут своей очереди. Поэтому обработчик должен быть идемпотентным по update_id.

Перед каждой попыткой gateway перепроверяет bot credential на платформе. Отзыв token или отключение бота останавливает отправку даже для уже накопленной очереди. После плановой ротации обновите token в gateway.json и перезапустите gateway: прежние pending updates останутся в state_dir.

python-telegram-bot выполняет тот же lifecycle штатными методами:

import os
from telegram import Bot


async def configure_webhook() -> None:
    async with Bot(
        token=os.environ["SKREPA_BOT_TOKEN"],
        base_url="http://127.0.0.1:8081/bot",
    ) as bot:
        await bot.set_webhook(
            url=os.environ["SKREPA_WEBHOOK_URL"],
            secret_token=os.environ["SKREPA_WEBHOOK_SECRET"],
            allowed_updates=["message"],
        )
        info = await bot.get_webhook_info()
        print(info.url, info.pending_update_count, info.last_error_message)

Gateway доставляет updates последовательно: max_connections в preview-профиле равен 1. Файлы сертификатов и фиксированный ip_address пока не поддерживаются. Конфигурация, очередь и последняя ошибка переживают restart gateway.

Для возврата к polling удалите webhook. Очередь сохранится и станет доступна через getUpdates в прежнем порядке:

curl --fail-with-body -sS -X POST \
  "http://127.0.0.1:8081/bot${SKREPA_BOT_TOKEN}/deleteWebhook" | jq .

Передача drop_pending_updates=true необратимо очищает очередь одновременно со сменой режима; используйте её только при сознательном отказе от накопленных updates.

Что сохранять между перезапусками

В state_dir находятся криптографические сессии и локальные очереди конкретного экземпляра gateway. Это рабочие данные, сопоставимые с небольшой базой данных:

Потеря state_dir заставит gateway зарегистрировать новое устройство и построить криптографические сессии заново. Updates, которые уже были приняты с сервера и оставались только в локальной очереди, восстановить будет нельзя. Identity recovery secret сохраняет identity бота, однако локальные очереди он не заменяет.

Безопасность эксплуатации

Диагностика

СимптомЧто проверить
@botfather отсутствует в поискепроверьте написание, авторизацию клиента и соединение; затем сообщите оператору стенда
BotFather не отвечаетобновите клиент, повторите команду новым сообщением и проверьте соединение
gateway завершается при стартепроверьте JSON-конфигурацию, доступность bot_platform_endpoint и права на state_dir
gateway getMe возвращает 401token устарел, отозван, содержит лишние символы или бот отключён
регистрация устройства отклоненапроверьте соответствие bot token, recovery secret и каталога state одному боту
getUpdates пуст после сообщенияубедитесь, что пользователь написал этому боту, gateway продолжает работать и переданный offset корректен
getUpdates возвращает 409у runtime настроен webhook; проверьте getWebhookInfo или вызовите deleteWebhook
webhook не получает updatesпроверьте доступность URL из сети gateway, HTTPS-сертификат и last_error_message в getWebhookInfo
sendMessage или media-метод сообщает unknown private chatсначала должен прийти update из этого диалога; используйте его chat.id
media-метод отклоняет URLпередайте multipart upload или ранее полученный file_id; загрузка произвольных URL отключена
getFile сообщает file_id is unknownиспользуйте file_id из update или ответа этого runtime и проверьте сохранность state_dir

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

Что дальше

Замените ручные вызовы getUpdates циклом вашего приложения, сохраняйте последний подтверждённый update_id и обрабатывайте повторную доставку идемпотентно. Полный контракт запросов и ответов смотрите в Bot API Reference.