RE:NODE

Приложения13 мин чтения

Хостинг Telegram-бота: polling, webhook и деплой

Как обновления доходят до Telegram-бота, когда выбрать long polling, а когда webhook, и как запустить бота на сервере без ошибки 409, на которую натыкаются все.

0 прочтений

Telegram-бот - это программа, которая запрашивает у Bot API обновления или которой их присылают. Это и есть всё архитектурное решение, и от него зависит всё остальное в хостинге: long polling не требует ни домена, ни сертификата, ни открытого порта, а webhook требует всего этого и взамен даёт меньшую задержку и отсутствие простаивающих запросов. Для большинства ботов правильный ответ - polling, а за webhook хватаются потому, что он звучит солиднее.

В этом руководстве честно разобраны оба пути: что на самом деле настраивает BotFather, чем getUpdates и setWebhook отличаются на практике, правило одного экземпляра, из-за которого рано или поздно появляется знаменитая ошибка, код для python-telegram-bot, grammY и Telegraf, а также как выглядят команда запуска и деплой на сервере. Если вы запускаете ещё и бота для Discord, то части, общие для любого долгоживущего процесса бота, разобраны в статье как хостить Discord-бота 24/7.

Как обновление доходит до вашего бота#

Всё, что пользователь делает с вашим ботом, становится объектом Update на серверах Telegram. Получить его можно ровно двумя способами, и они взаимоисключающие.

getUpdates, держится открытымPOST /secret-pathX-Forwarded-ForTelegram APIapi.telegram.orgПроцесс ботаlong pollingСлот проксиHTTPS на 443Процесс ботаHTTP-сервер
Два пути обновления Telegram к вашему коду

Long polling означает, что ваш процесс вызывает getUpdates с таймаутом, а Telegram держит запрос открытым, пока что-нибудь не случится или не истечёт таймаут, после чего вы вызываете снова. Соединение исходящее, так что бот работает с ноутбука, из-за NAT, откуда угодно, где есть исходящий HTTPS. Цена - постоянный запрос в полёте и небольшая дополнительная задержка в момент прихода обновления.

Webhook означает, что вы один раз вызываете setWebhook с публичным HTTPS-адресом, а Telegram присылает каждое обновление на этот адрес как HTTP POST. Простаивающего запроса нет, доставка мгновенная, и бот масштабируется вширь, если когда-нибудь понадобится. Цена - теперь вы запускаете веб-сервер, вам нужны имя хоста и сертификат, а публичную точку входа может нащупать кто угодно.

Использовать оба способа сразу нельзя. Если webhook задан, getUpdates отказывает с 409 Conflict: can't use getUpdates method while webhook is active. Сначала вызовите deleteWebhook. Тот же 409 появляется, с другой формулировкой, когда две копии вашего бота опрашивают один токен, - это самая частая проблема Telegram-ботов, которую создают себе сами, и причина, по которой тестовому боту нужен собственный токен, а не копия рабочего.

Создание бота: BotFather и настройки, о которых забывают#

Напишите @BotFather в Telegram, отправьте /newbot, выберите отображаемое имя и username, оканчивающийся на bot. Вы получите токен вида 123456789:AAH..., где часть до двоеточия - числовой ID вашего бота. Это пароль. /revoke выдаёт новый и мгновенно убивает старый: так лечится утечка, и одновременно это простой, пока вы не выкатите новый токен.

Три настройки BotFather меняют поведение бота, и их легко упустить:

  • Режим приватности в группах включён по умолчанию. В группах бот с включённой приватностью получает только сообщения, которые являются командами, ответами на его собственные сообщения или упоминаниями его. Если боту нужно читать всё в группе - боту-модератору или бот-логгеру, - выключите её через /setprivacy, затем удалите бота из группы и добавьте снова, потому что настройка применяется в момент вступления бота.
  • Команды, заданные через /setcommands, заполняют меню рядом с полем ввода. Команды, которых вы не зарегистрировали, пользователи не найдут. Это можно сделать и из кода через setMyCommands, и это лучший вариант, потому что список живёт рядом с кодом, который эти команды реализует.
  • Inline-режим выключен, пока вы не включите его через /setinline. Боту, который должен отвечать на @yourbot query в любом чате, он нужен включённым, иначе inline-обработчик никогда не сработает.

Храните токен в переменной окружения. Локально это файл .env, который закрывает .gitignore; на сервере - переменная на вкладке Startup. В статье куда класть секреты на сервере приложений объяснено, почему токен в репозитории остаётся опасным ещё долго после того, как вы его удалили.

Long polling на практике#

Бот с polling делает один HTTP-запрос за раз и блокируется на нём. Параметры, которые важны:

ПараметрРазумное значениеЧто делает
timeout30Секунды, в течение которых Telegram держит запрос открытым. 0 - это short polling, избегайте
offsetпоследний update_id + 1Подтверждает предыдущую пачку. Без него вы получите её снова
limit100Обновлений в ответе, от 1 до 100
allowed_updatesявный списокКакие типы обновлений вам вообще нужны

Каждая библиотека обрабатывает offset за вас, а ошибка при ручной работе с ним - причина, по которой самописный бот обрабатывает одно и то же сообщение бесконечно. allowed_updates стоит задать явно: по умолчанию обновления chat_member не присылаются, а если запросить узкий список, вы перестанете получать редактированные сообщения и посты каналов, на которые никогда не смотрите.

Telegram хранит недоставленные обновления до 24 часов. Это полезное свойство и неприятный сюрприз: бот, который всю ночь был выключен, просыпается и одним залпом обрабатывает сообщения за целую ночь, отвечая на разговоры, закончившиеся несколько часов назад. Если для вашего бота это неправильно, используйте drop_pending_updates при запуске, и накопившееся будет отброшено, а не проиграно заново.

Правило одного экземпляра абсолютно. Два процесса, опрашивающих один токен, дерутся друг с другом: каждый получает случайную половину обновлений, и оба пишут в лог 409. Так бывает, когда деплой запускает новый процесс до того, как завершился старый, когда бот остался работать на ноутбуке и когда тестовый бот использует токен рабочего. Обрабатывайте SIGTERM, останавливайте опрос и давайте процессу завершиться - общий шаблон есть в статье корректное завершение и health checks.

Webhook на практике#

Webhook - это небольшой веб-сервер плюс один вызов API. Требования Telegram конкретны:

  • Только HTTPS, с сертификатом от публичного центра либо самоподписанным, который вы загружаете параметром certificate.
  • Порт 443, 80, 88 или 8443. Никакой другой не работает, и именно это требование топит большинство домашних настроек.
  • Адрес должен быть неугадываемым. Традиционный приём - положить токен в путь; современный ответ - secret_token, при котором Telegram присылает в каждом запросе заголовок X-Telegram-Bot-Api-Secret-Token. Отклоняйте всё, что с ним не совпадает, иначе любой, кто найдёт ваш URL, сможет скормить боту выдуманные обновления.
bash
$ curl -X POST "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \    -d "url=https://bot.example.com/tg/hook" \    -d "secret_token=$WEBHOOK_SECRET" \    -d "max_connections=20" \    -d "allowed_updates=[\"message\",\"callback_query\"]"

max_connections (1-100, по умолчанию 40) ограничивает, сколько доставок Telegram держит в полёте одновременно. На тарифе с 0.5 vCPU 40 одновременных обработчиков - не подарок; снижайте значение, пока оно не совпадёт с тем, что ваше приложение действительно тянет параллельно.

Когда что-то не так, getWebhookInfo говорит, что именно:

bash
$ curl "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"

Он возвращает зарегистрированный url, pending_update_count и - самое полезное - last_error_date и last_error_message: это Telegram цитирует вам сбой вашего же сервера. Растущий pending_update_count при свежей ошибке означает, что доставки проваливаются, а Telegram повторяет попытки. Растущий счётчик без ошибки означает, что ваш обработчик слишком медленный.

Этот последний случай - правило проектирования webhook: отвечайте на HTTP-запрос сразу кодом 200, а работу делайте потом. Обработчик, который вызывает три API перед ответом, держит соединение Telegram открытым, съедает max_connections и в итоге упирается в таймаут. Подтвердить, поставить в очередь, обработать - о стороне очереди, когда работа по-настоящему медленная, рассказано в статье фоновые задачи на маленьком сервере.

Со стороны хостинга требование одного из четырёх портов - причина, по которой важен слот прокси. Ваше приложение слушает свой порт внутри контейнера; прокси терминирует TLS на 443 перед ним, а это один из четырёх портов Telegram. Направьте запись A на адрес, показанный на вкладке прокси, и сертификат будет выпущен и продлён автоматически в окне в 21 день до истечения срока. Адрес клиента приходит в X-Forwarded-For, что для webhook Telegram полезно только если вы хотите ограничивать по опубликованным диапазонам адресов Telegram. Две половины разобраны в статьях что на самом деле делает reverse proxy и как направить домен на сервер; если сторона DNS вам незнакома, введением послужит статья DNS-записи простыми словами.

Библиотеки в обоих режимах#

Три основные библиотеки выражают одни и те же два режима примерно за одинаковое число строк.

python-telegram-bot
import osfrom telegram import Updatefrom telegram.ext import Application, CommandHandler, ContextTypesasync def start(update: Update, context: ContextTypes.DEFAULT_TYPE):    await update.message.reply_text("Ready.")app = Application.builder().token(os.environ["BOT_TOKEN"]).build()app.add_handler(CommandHandler("start", start))# Long pollingapp.run_polling(allowed_updates=Update.ALL_TYPES)# Or a webhook, on the port the plan allocatedapp.run_webhook(    listen="0.0.0.0",    port=int(os.environ["PORT"]),    url_path="tg/hook",    webhook_url="https://bot.example.com/tg/hook",    secret_token=os.environ["WEBHOOK_SECRET"],)

run_webhook регистрирует webhook в Telegram за вас и запускает небольшой сервер. Обратите внимание на listen="0.0.0.0": привязка к 127.0.0.1 означает, что снаружи контейнера до него ничто не дотянется, и это самая частая причина, по которой приложение с webhook вроде бы стартует правильно и ничего не получает.

grammY
import { Bot, webhookCallback } from "grammy";import express from "express";const bot = new Bot(process.env.BOT_TOKEN);bot.command("start", (ctx) => ctx.reply("Ready."));if (process.env.MODE === "webhook") {  const app = express();  app.use(express.json());  app.use("/tg/hook", webhookCallback(bot, "express", {    secretToken: process.env.WEBHOOK_SECRET,  }));  app.listen(Number(process.env.PORT), "0.0.0.0");} else {  bot.start();}

Telegraf устроен похоже: bot.launch() запускает long polling и не завершается, пока бот не остановится, а bot.createWebhook({ domain }) возвращает middleware, которое подключается к приложению Express. Какую бы библиотеку вы ни выбрали, зарегистрируйте обработчики SIGINT и SIGTERM, останавливающие бота, потому что poller, убитый посреди запроса, оставляет Telegram уверенным, что экземпляр ещё подключён, на несколько секунд - достаточно, чтобы сменяющий процесс получил 409 при старте.

Одна оптимизация существует только для webhook: на POST от Telegram можно ответить телом JSON, описывающим вызов метода, и он будет выполнен так, как если бы вы сделали этот запрос сами. Ответ на сообщение таким способом экономит целый круг обращения. Перестраивать ради этого код не стоит, но это бесплатно, когда ваш обработчик делает ровно одно действие.

Размещение на сервере#

Схема развёртывания та же, что и для любого постоянно работающего процесса. Установите зависимости из lock-файла или requirements.txt, выполните одну команду, перезапускайте при выходе.

bash
# Nodenpm ci --omit=dev && node bot.js# Pythonpip install --no-cache-dir -r requirements.txt && python -u bot.py

python -u или PYTHONUNBUFFERED=1 важнее, чем кажется: без этого строки лога лежат в буфере, а консоль остаётся пустой, хотя бот прекрасно работает. Как отличать такие небеды от настоящих, рассказано в статье как читать консоль сервера, не гадая.

Переменные окружения - BOT_TOKEN, WEBHOOK_SECRET, URL базы данных, переключатель режима - задаются на вкладке Startup. На RE:NODE код приходит из GitHub через GitHub App с короткоживущими токенами, так что приватные репозитории работают, и есть два переключателя: подтягивать ветку при каждом старте и деплоить при push, что перезапускает уже работавший сервер. Каждый деплой - одна запись, которая открывается, когда приходит push, и закрывается, когда контейнер снова замечен запущенным.

У бота с webhook есть одно дополнительное соображение при деплое. Зарегистрированный URL переживает перезапуски, потому что хранится на стороне Telegram, поэтому при каждой загрузке перерегистрировать не нужно. Перерегистрируйте, когда меняется имя хоста или секрет, и после этого проверьте getWebhookInfo, а не полагайтесь на предположение. У бота с polling - обратное соображение: убедитесь, что старый процесс ушёл до старта нового, иначе первые тридцать секунд каждого деплоя полны 409.

Если бот важен, запустите второго. Отдельный бот в BotFather со своим токеном, своим сервером и веткой, которую вы собираетесь слить, стоит совсем немного и ловит кривой обработчик раньше ваших пользователей. Шаблон описан в статье staging и production на одном аккаунте, и это единственный безопасный способ тестировать код для Telegram, потому что песочницы API нет.

Лимиты частоты, размеры файлов и то, что Telegram не позволит#

У Bot API есть ограничения, которые не обсуждаются, и подстраиваться под них поздно обходится дорого.

ОграничениеОпубликованное значение
Сообщения разным пользователямОколо 30 в секунду
Сообщения в одну группуОколо 20 в минуту
Скачивание файла через getFile20 MB
Загрузка файла ботом50 MB (10 MB для фото)

При превышении возвращается 429 со значением retry_after в ответе, и библиотеки, как правило, его выжидают. Поэтому рассылку по большому списку пользователей должен темпировать ваш собственный код: очередь, несколько сообщений в секунду и запись о том, кому уже отправлено, чтобы перезапуск не начинал всё сначала. Массовая отправка в плотном цикле - самый быстрый способ довести бота до того, что его ограничат до полной бесполезности.

Ограничения на файлы удивляют тех, кто делает медиабота. Это ограничения публичного Bot API, а не самого Telegram, и их можно снять, запустив самому локальный сервер Bot API с открытым кодом. Это отдельный скомпилированный сервис со своими учётными данными API, и место ему на машине, которую вы контролируете, а не рядом с вашим приложением. Если вы движетесь к этому, то подходящая для такого вещь - выделенные серверы.

Решение проблем#

`409 Conflict: terminated by other getUpdates request`. Два процесса на одном токене. Найдите второй: старый контейнер, локальный запуск, тестовая копия.

`409 Conflict: can't use getUpdates method while webhook is active`. Вызовите deleteWebhook, затем опрашивайте.

`401 Unauthorized`. Токен неверный, пустой или отозванный. Именно такой результат даёт незаданная переменная окружения.

Webhook задан, но ничего не приходит. Посмотрите last_error_message в getWebhookInfo. Обычные ответы: сертификат, который клиент не может проверить, приложение, привязанное к 127.0.0.1, несовпадение пути между зарегистрированным URL и маршрутом или 404 от фреймворка, который так и не увидел зарегистрированного маршрута.

Обновления приходят, но останавливаются под нагрузкой. Растущий pending_update_count означает, что обработчик медленнее, чем поступают обновления. Сначала отвечайте 200, потом работайте.

Бот отвечает в личных чатах, но игнорирует группы. Режим приватности. Выключите его в BotFather и добавьте бота в группу заново.

Всё работало, а после деплоя бот затих. У poller - остался старый процесс. У webhook - сменилось имя хоста, а регистрация осталась прежней.

FAQ#

Что выбрать: long polling или webhook?

Long polling, если нет причин поступить иначе. Он не требует ни домена, ни сертификата, ни входящего порта, одинаково работает в разработке и в продакшене, а разница в задержке - доли секунды. Переходите на webhook, когда у вас большой поток обновлений, когда нужно несколько экземпляров за одной точкой входа или когда простаивающий исходящий запрос действительно мешает.

Нужен ли Telegram-боту домен?

Только для webhook. Боту с polling нужен исходящий HTTPS и больше ничего. Если вы всё же идёте по пути webhook, слот прокси в тарифе для приложений даёт имя хоста и автоматически продлеваемый сертификат, так что от DNS остаётся одна запись A.

Почему при каждом деплое я получаю ошибку 409?

Потому что новый процесс начал опрос раньше, чем остановился старый. Telegram разрешает одного вызывающего getUpdates на токен. Сделайте завершение корректным и никогда не запускайте тестовый бот на рабочем токене.

Можно ли захостить Telegram-бота бесплатно?

Можно запустить его дома, и он будет офлайн, когда эта машина выключена. Бесплатные платформы, останавливающие простаивающий процесс, не подходят для poller, который по своей природе выглядит простаивающим. Бесплатного тарифа здесь нет; самый маленький тариф для приложений - 1 GB с 0.5 vCPU от $4 в месяц, а это больше, чем нужно текстовому боту.

Как отправить сообщение тысячам пользователей?

Медленно и с записью прогресса. Опубликованная рекомендация Telegram - около 30 сообщений в секунду разным пользователям, так что поставьте рассылку в очередь, темпируйте её, сохраняйте, каким ID уже отправлено, и ждите 429 с retry_after, если нажмёте сильнее. Рассылка, которая после сбоя начинается с нуля, хуже медленной.

Может ли один сервер запускать несколько ботов?

Два небольших бота помещаются в одном контейнере, но у них общие лимит памяти и доля CPU, так что цикл падений одного утянет за собой другого. Отдельные тарифы ограничивают ущерб, а графики консоли остаются читаемыми, потому что каждый показывает один процесс.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000