Все примеры кода по пакету python-telegram-bot
запускают Telegram бот с помощью Updater.start_polling()
. Он использует метод Telegram API getUpdates
для получения новых сообщений для бота. Это вполне нормально для небольших ботов и тестирования, но если бот популярен и получает/отправляет много трафика, то такой подход может замедлить время отклика бота.
Опрос Telegram сервера через webhook - это полезная технология для автоматизации процесса общения с пользователями. Как правило, этот функционал используется для экономии ресурсов на отправку/получение обновлений как собственного сервера, так и серверов Telegram.
Различие между polling
и webhook
является:
polling
(через метод .get_updates
) периодически подключается к серверам Telegram для проверки новых обновлений или отправки обработанных сообщений.Webhook
- это URL-адрес, который передается API Telegram. Каждый раз, когда приходит новое обновление для бота, сервер Telegram отправляет это обновление на указанный URL. Аналогично происходит отправка сообщений.webhook
;webhook
;webhook
;webhook
на Heroku;nginx
с одним доменом/портом для всех ботов;haproxy
с одним поддоменом на бота;webhook
.Необходим SSL-сертификат.
Вся связь с серверами Telegram должна быть зашифрована с помощью HTTPS с использованием SSL. В случае подключения polling
, о шифровании трафика заботятся серверы Telegram, но если отправка/получение сообщений идет через Webhook, то шифровании должен заботиться клиент/бот.
Есть два способа сделать это:
Чтобы создать самоподписанный SSL-сертификат с помощью openssl
, выполните следующую команду в терминале:
$ openssl req -newkey rsa:2048 -sha256 -nodes -keyout private.key -x509 -days 3650 -out cert.pem
Утилита openssl
запросит несколько подробностей. Необходимо убедится, что вы ввели правильное полное доменное имя или IP-адрес! Если у сервера есть домен, то введите полное доменное имя (например, sub.example.com
). Если сервер имеет только IP-адрес, то вместо домена введите его IP-адрес. Если введено неверное полное доменное имя или IP-адрес, то бот не получит никаких обновлений от Telegram, при этом не будет никаких ошибок!
webhook
.Библиотека python-telegram-bot
поставляет встроенный HTTP-сервер, основанный на http.server.HTTPServer
. Реализация HTTPServer
, которая плотно интегрирована в модуль расширения telegram.ext
и может быть запущен с помощью updater.start_webhook
/application.run_webhook
. Этот веб-сервер также занимается расшифровкой HTTPS-трафика. Это самый простой способ настроить webhook.
Однако у этого решения есть ограничение. Telegram в настоящее время поддерживает только четыре порта для веб-перехватчиков: 443, 80, 88 и 8443. В результате можно запускать не более четырех ботов на одном домене/IP-адресе.
Если это не проблема, то можно использовать код ниже или аналогичный, чтобы запустить бот с webhook. Адрес прослушивания должен быть либо '0.0.0.0'
, либо, если нет разрешения на это, общедоступный IP-адрес сервера. Порт может быть одним из 443, 80, 88 или 8443. Рекомендуется установить секретный токен в параметре secret_token
, чтобы никто не мог отправить боту поддельные обновления. Аргументы key
и cert
должны содержать путь к файлам, которые создали ранее. Аргумент webhook_url
должен быть фактическим URL-адресом webhook
. При этом необходимо перед URL-адресом webhook
использовать протокол https:// , домен или IP-адрес, которые установлен в качестве полного доменного имени сертификата, а также правильный порт и URL-адрес.
application.run_webhook( listen='0.0.0.0', port=8443, secret_token='ASecretTokenIHaveChangedByNow', key='private.key', cert='cert.pem', webhook_url='https://example.com:8443' ) # или updater.start_webhook( listen='0.0.0.0', port=8443, secret_token='ASecretTokenIHaveChangedByNow', key='private.key', cert='cert.pem', webhook_url='https://example.com:8443' )
webhook
.Чтобы решить эту проблему, можно использовать обратный прокси-сервер, такой как nginx
или haproxy
, а также можно использовать Heroku
.
В этой модели обратный прокси (nginx
), слушает публичный IP-адрес, принимает все запросы webhook и пересылает их на правильный экземпляр локально запущенных встроенных в python-telegram-bot
серверов webhook. Обратный прокси также выполняет завершение SSL, то есть расшифровывает HTTPS-соединение, поэтому серверы webhook получают уже расшифрованный трафик. Эти серверы могут работать на любом порту, а не только на четырех разрешенных Telegram портах, т.к. сервера Telegram напрямую подключается только к обратному прокси-серверу.
В зависимости от того, какой прокси-сервер используется, реализация будет выглядеть немного иначе. Ниже перечислены несколько возможных настроек.
webhook
на Heroku.На Heroku использовать webhook можно на свободном плане, т.к. он будет автоматически управлять временем простоя. Для пользователя Heroku будет настроен обратный прокси и создана среда исполнения. Из этой среды необходимо будет извлечь порт, который бот должен прослушивать. Heroku управляет SSL на стороне прокси-сервера, следовательно не нужно создавать сертификат самостоятельно.
import os TOKEN = "TOKEN" PORT = int(os.environ.get('PORT', '8443')) # добавим обработчики application.run_webhook( listen="0.0.0.0", port=PORT, secret_token='ASecretTokenIHaveChangedByNow', webhook_url="https://<appname>.herokuapp.com/" )
nginx
с одним доменом/портом для всех ботовВсе боты устанавливают свой URL-адрес на один и тот же домен и порт, но с другим url_path
. Встроенный в python-telegram-bot
сервер обычно запускается по адресу localhost
или 127.0.0.1, порт может быть любым.
Примечание: если нет домена, связанного с сервером, то example.com
может быть заменен IP-адресом.
Пример кода для запуска бота:
application.run_webhook( listen='127.0.0.1', port=5000, url_path='1', secret_token='ASecretTokenIHaveChangedByNow', webhook_url='https://example.com/1', cert='cert.pem' )
Пример конфигурации для nginx
с двумя настроенными ботами (представлены важные части конфига):
server { listen 443 ssl; server_name example.com; ssl_certificate cert.pem; ssl_certificate_key private.key; location /TOKEN1 { proxy_pass http://127.0.0.1:5000/1/; } location /TOKEN2 { proxy_pass http://127.0.0.1:5001/2/; } }
haproxy
с одним поддоменом на бота.При таком подходе, каждому боту присваивается свой собственный поддомен. Если сервер имеет домен example.com
, то можно создать поддомены например: bot1.example.com
, bot2.example.com
и т. д. Понадобится один сертификат для каждого бота с полным доменным именем, установленным для соответствующего поддомена. Встроенный в python-telegram-bot
сервер обычно запускается по адресу localhost
или 127.0.0.1, порт может быть любым.
Примечание: Необходимо иметь домен привязанный к IP-адресу сервера.
Пример кода для запуска бота:
application.run_webhook( listen='127.0.0.1', port=5000, secret_token='ASecretTokenIHaveChangedByNow', webhook_url='https://bot1.example.com', cert='cert_bot1.pem') )
Пример конфигурации для haproxy
с двумя настроенными ботами (сведен к важным частям конфига) . Опять же: полное доменное имя обоих сертификатов должно соответствовать значению в ssl_fc_sni
. Кроме того, файлы .pem
представляют собой объединенные файлы private.key
и cert.pem
.
frontend public-https bind 0.0.0.0:443 ssl crt cert_key_bot1.pem crt cert_key_bot2.pem option httpclose use_backend bot1 if { ssl_fc_sni bot1.example.com } use_backend bot2 if { ssl_fc_sni bot2.example.com } backend bot1 mode http option redispatch server bot1.example.com 127.0.0.1:5000 check inter 1000 backend bot2 mode http option redispatch server bot2.example.com 127.0.0.1:5001 check inter 1000
Не обязательно использовать встроенный веб-сервер. Если решите пойти этим путем, то не следует использовать класс Updater
. Модуль telegram.ext
был переработан с учетом этой опции, поэтому все равно можно использовать класс Application
, чтобы извлечь выгоду из фильтрации / сортировки сообщений, которые он предоставляет. НО придется проделать некоторую работу вручную.
Общая идея:
from telegram import Bot from telegram.ext import Application application = Application.builder().token('TOKEN').build() # Регистрация обработчиков здесь # Получаем `update_queue`, из которого приложение получает обновления для обработки. update_queue = application.update_queue start_fetching_updates(update_queue) # Запускаем приложение async with application: application.start() # и останавливаем, когда срабатывает # какой-либо механизм отключения: application.stop()
Здесь start_fetching_updates
- это заполнитель для любого метода, который используется для настройки веб-перехватчика. Важной частью является то, что полученные обновления в update_queue
ставятся в очередь. То есть вызывается await update_queue.put(update)
, где update
- это декодированный объект Update
(используйте Update.de_json(json.loads(text), bot
) для декодирования обновления из полученных данных JSON).
Альтернатива: нет длительных задач.
Если BOT не использует длительные задачи, запущенные с помощью application.start()
, то это не нужно! Вместо того, чтобы помещать обновления в update_queue
, можно напрямую обрабатывать их через application.process_update(update)
.
Простой пример бота, который использует пользовательскую настройку веб-перехватчика и обрабатывает пользовательские обновления.Для пользовательской настройки вебхука используются библиотеки starlette
и uvicorn
. Эти модули необходимо установить как pip install starlette=0.20.0 uvicorn=0.17.0
.
Обратите внимание, что можно использовать любой другой фреймворк веб-сервера на основе asyncio
для пользовательской настройки веб-перехватчика.
Использование:
admin chat_id
и порт в начале основной функции.listen
в конфигурации uvicorn
, чтобы оно соответствовало вашей настройке.Ctrl-C
в командной строке остановит бота.import asyncio import html import logging from dataclasses import dataclass from http import HTTPStatus import uvicorn from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import PlainTextResponse, Response from starlette.routing import Route from telegram import __version__ as TG_VER try: from telegram import __version_info__ except ImportError: __version_info__ = (0, 0, 0, 0, 0) # type: ignore[assignment] if __version_info__ < (20, 0, 0, "alpha", 1): raise RuntimeError( f"Пример несовместим с текущей версией PTB {TG_VER}. " f"Чтобы просмотреть {TG_VER} версия этого примера, " f"посетите https://docs.python-telegram-bot.org/en/v{TG_VER}/examples.html" ) from telegram import Update from telegram.constants import ParseMode from telegram.ext import ( Application, CallbackContext, CommandHandler, ContextTypes, ExtBot, TypeHandler, ) # Enable logging logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) logger = logging.getLogger(__name__) @dataclass class WebhookUpdate: """Класс данных для переноса пользовательского типа обновления""" user_id: int payload: str class CustomContext(CallbackContext[ExtBot, dict, dict, dict]): """ Пользовательский класс CallbackContext, который делает user_data доступным для обновлений типа `WebhookUpdate`. """ @classmethod def from_update( cls, update: object, application: "Application", ) -> "CustomContext": if isinstance(update, WebhookUpdate): return cls(application=application, user_id=update.user_id) return super().from_update(update, application) async def start(update: Update, context: CustomContext) -> None: """Показать сообщение с инструкциями по использованию этого бота.""" url = context.bot_data["url"] payload_url = html.escape(f"{url}/submitpayload?user_id=<your user id>&payload=<payload>") text = ( f"Чтобы проверить, все ли еще запущен бот, вызовите <code>{url}/healthcheck</code>.\n\n" f"Чтобы опубликовать пользовательское обновление, вызовите <code>{payload_url}</code>." ) await update.message.reply_html(text=text) async def webhook_update(update: WebhookUpdate, context: CustomContext) -> None: """Обратный вызов, который обрабатывает пользовательские обновления.""" chat_member = await context.bot.get_chat_member(chat_id=update.user_id, user_id=update.user_id) payloads = context.user_data.setdefault("payloads", []) payloads.append(update.payload) combined_payloads = "</code>\n• <code>".join(payloads) text = ( f"Пользователь {chat_member.user.mention_html()} отправил новую полезную нагрузку. " f"На данный момент они отправили следующее: \n\n• <code>{combined_payloads}</code>" ) await context.bot.send_message( chat_id=context.bot_data["admin_chat_id"], text=text, parse_mode=ParseMode.HTML ) async def main() -> None: """Настройки приложения и пользовательского веб-сервера.""" url = "https://domain.tld" admin_chat_id = 123456 port = 8000 context_types = ContextTypes(context=CustomContext) # Здесь устанавливаем для `updater` значение `None`, потому что нужно, чтобы пользовательский # сервер обрабатывал обновления, и, следовательно, не нужен экземпляр `Updater`. application = ( Application.builder().token("TOKEN").updater(None).context_types(context_types).build() ) # сохраним значения в `bot_data` таким образом, чтобы можно было легко получить # к ним доступ при обратных вызовах application.bot_data["url"] = url application.bot_data["admin_chat_id"] = admin_chat_id # регистрируем обработчики application.add_handler(CommandHandler("start", start)) application.add_handler(TypeHandler(type=WebhookUpdate, callback=webhook_update)) # Передать настройки вебхука в телеграмм await application.bot.set_webhook(url=f"{url}/telegram") # Поднимаем веб-сервер async def telegram(request: Request) -> Response: """Обрабатываем входящие обновления Telegram, помещая их в `update_queue`""" await application.update_queue.put( Update.de_json(data=await request.json(), bot=application.bot) ) return Response() async def custom_updates(request: Request) -> PlainTextResponse: """ Обрабатываем входящие обновления веб-перехватчика, также помещая их в очередь `update_queue`, если необходимые параметры были переданы правильно. """ try: user_id = int(request.query_params["user_id"]) payload = request.query_params["payload"] except KeyError: return PlainTextResponse( status_code=HTTPStatus.BAD_REQUEST, content="Передайте как `user_id`, так и полезную " "нагрузку в качестве параметров запроса.", ) except ValueError: return PlainTextResponse( status_code=HTTPStatus.BAD_REQUEST, content="`user_id` должен быть строкой!", ) await application.update_queue.put(WebhookUpdate(user_id=user_id, payload=payload)) return PlainTextResponse("Спасибо за отправку! Оно пересылается.") async def health(_: Request) -> PlainTextResponse: """Для конечной точки работоспособности отвечаем текстовым сообщением.""" return PlainTextResponse(content="Бот по-прежнему работает нормально :)") starlette_app = Starlette( routes=[ Route("/telegram", telegram, methods=["POST"]), Route("/healthcheck", health, methods=["GET"]), Route("/submitpayload", custom_updates, methods=["POST", "GET"]), ] ) webserver = uvicorn.Server( config=uvicorn.Config( app=starlette_app, port=port, use_colors=False, host="127.0.0.1", ) ) # Запуск приложения и веб-сервера вместе async with application: await application.start() await webserver.serve() await application.stop() if __name__ == "__main__": asyncio.run(main())