Telegram бот через webhook
Я тогда давно написал статью про создание Telegram бота, и обещал дополнить её описанием настройки работы через webhook, но так и не дополнил. Вот только сейчас дошли руки.

Что это такое
Как пишут в документации, общаться с серверами Telegram бот может двумя способами:
- getUpdates — pull: ваш бот постоянно дёргает сервер Telegram и проверяет есть ли новые сообщения;
- setWebhook — push: по мере поступления новых сообщений сервер Telegram отправляет их вашему боту.
Разницу можно изобразить следующим образом:

Очевидно, что второй способ ( setWebhook ) рациональнее для всех участников процесса. Однако в нём присутствует неявная сложность: кто-то должен принимать сообщения от Telegram на стороне бота, то есть необходим веб-сервер или его эквивалент.
Как настроить
Что нужно сделать:
- Заиметь доменное имя для сервера и получить на него сертификат (например, от Let’s Encrypt). Документация также говорит, что в случае самоподписанного сертификата можно обойтись и просто IP адресом, но этого я не пробовал;
- Запилить серверную часть на стороне бота (куда будет ломиться Telegram);
- Зарегистрировать адрес серверной части в Telegram (зацепить webhook на endpoint), чтобы Telegram знал, куда ломиться с сообщениями.
Сертификат
С доменом и сертификатом просто. Домен у меня уже был, а сертификат я получил по этой инструкции.
Вариант с самоподписанным сертификатом на прямой IP адрес я оставляю вам на самостоятельное изучение.
Серверная часть
Серверная часть чуть посложнее. Я переделал текущую реализацию бота на pyTelegramBotAPI, используя пример для AIOHTTP.
Ставим необходимые пакеты:
pip install pyTelegramBotAPI pip install aiohttp pip install cchardet pip install aiodns
И сокращённо код бота теперь такой:
import config import telebot from aiohttp import web import ssl WEBHOOK_LISTEN = "0.0.0.0" WEBHOOK_PORT = 8443 WEBHOOK_SSL_CERT = "/etc/letsencrypt/live/YOUR.DOMAIN/fullchain.pem" WEBHOOK_SSL_PRIV = "/etc/letsencrypt/live/YOUR.DOMAIN/privkey.pem" API_TOKEN = config.token bot = telebot.TeleBot(API_TOKEN) app = web.Application() # process only requests with correct bot token async def handle(request): if request.match_info.get("token") == bot.token: request_body_dict = await request.json() update = telebot.types.Update.de_json(request_body_dict) bot.process_new_updates([update]) return web.Response() else: return web.Response(status=403) app.router.add_post("/ /", handle) help_string = [] help_string.append("*Some bot* - just a bot.\n\n") help_string.append("/start - greetings\n") help_string.append("/help - shows this help") # - - - messages @bot.message_handler(commands=["start"]) def send_welcome(message): bot.send_message(message.chat.id, "Ololo, I am a bot") @bot.message_handler(commands=["help"]) def send_help(message): bot.send_message(message.chat.id, "".join(help_string), parse_mode="Markdown") # - - - context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) context.load_cert_chain(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV) # start aiohttp server (our bot) web.run_app( app, host=WEBHOOK_LISTEN, port=WEBHOOK_PORT, ssl_context=context, )
Что здесь происходит: мы запускаем мини-веб-сервер, который слушает порт 8443 и отвечает на запросы через определённый endpoint, который образован токеном бота. Токен используется здесь как достаточно уникальный идентификатор, чтобы какой-нибудь мимокрокодил из интернета не навызывал бота и не натворил дел. Полный адрес endpoint’а будет выглядеть вот так: https://YOUR.DOMAIN:8443/YOUR-TOKEN/ .
Обратите также внимание на отличия от стандартного примера из репозитория:
- в качестве файла сертификата указан fullchain.pem , а не cert.pem ;
- удалён код снятия и установки webhook’а.
Так как бота я запускаю не из-под root’а, сервис начал валиться с такой ошибкой:
python-bot[1824]: Traceback (most recent call last): python-bot[1824]: File "/usr/local/bin/bot/bot.py", line 142, in python-bot[1824]: context.load_cert_chain(WEBHOOK_SSL_CERT, WEBHOOK_SSL_PRIV) python-bot[1824]: PermissionError: [Errno 13] Permission denied systemd[1]: telegram-bot.service: Main process exited, code=exited, status=1/FAILURE systemd[1]: telegram-bot.service: Failed with result 'exit-code'.
То есть, у пользователя, из-под которого выполняется скрипт, нет доступа к /etc/letsencrypt/ , чтобы открыть файл сертификата. Я попытался дать доступ к каталогу для новой группы, включив в неё этого пользователя:
groupadd letsencrypt usermod -a -G letsencrypt userforbot chgrp -R letsencrypt /etc/letsencrypt/
Но он один фиг не мог открыть файлы оттуда, даже простой ls выдавал ошибку доступа. В общем, или мои познания Linux полный отстой, или одно из двух. Пришлось тупо назначить его владельцем:
chown -R userforbot:letsencrypt /etc/letsencrypt/
Тогда сервис запустился нормально.
Регистрация
Теперь осталось самое, как оказалось, сложное — зарегистрировать endpoint бота в Telegram. Сложности возникли потому, что я сначала неправильно понял принцип составления endpoint’а, а также из-за проблем с проверкой сертификата.
Для установки/регистрации webhook’а нужно выполнить следующий HTTP запрос (можно просто открыть этот URL в браузере):
https://api.telegram.org/botYOUR-TOKEN/setWebhook?url=https://YOUR.DOMAIN:8443/YOUR-TOKEN/
Пока я экспериментировал и разбирался с форматом endpoint’а, Telegram возвращал мне нормальный результат:
"description": "Webhook was set", "ok": true, "result": true >
Но потом я его видимо задолбал, и он стал возвращать мне следующее:
"ok": false, "error_code": 504, "description": "Gateway Timeout" >
Но оказалось, что это ни на что не влияет, и webhook нормально устанавливается, так что можно даже не дожидаться таймаута, а просто отменять запрос через пару секунд.
Проверить статус webhook’а можно таким запросом:
https://api.telegram.org/botYOUR-TOKEN/getWebhookInfo
Если всё нормально, должно вернуть такое:
"ok": true, "result": "url": "https://YOUR.DOMAIN:8443/YOUR-TOKEN/", "has_custom_certificate": false, "pending_update_count": 0, "max_connections": 40 > >
Как видим, в поле url стоит наш endpoint.
Однако, мне оно сейчас возвращает такое:
"ok": true, "result": "url": "https://YOUR.DOMAIN:8443/YOUR-TOKEN/", "has_custom_certificate": false, "pending_update_count": 0, "last_error_date": 1543762687, "last_error_message": "SSL error ", "max_connections": 40 > >
Что указывает на некие проблемы с сертификатом. При этом бот работает нормально, то есть эта ошибка ни на что не влияет. Однако, если вместо fullchain.pem оставить cert.pem (как было указано в примере), то бот работать перестанет.
Стоит также отметить, что если вы установили webhook, то опрос Telegram через getUpdates работать больше будет. Чтобы снять webhook, надо отправить тот же самый запрос, что и для установки, но на этот раз без параметра url :
https://api.telegram.org/botYOUR-TOKEN/setWebhook
"ok": true, "result": true, "description": "Webhook was deleted" >
Ну и всё, не так уж и сложно. Если бы в документации (и сторонних манулах из интернетов) была указана такая простая вещь, что для webhook’а всего-то нужен лишь веб-сервер на стороне бота, я бы это сделал уже сто лет назад. Конечно, продвинутым чувакам это скорее всего было очевидно сразу, но мнe — нет.
Telegram-бот, webhook и 50 строк кода
Как, опять? Ещё один туториал, пережёвывающий официальную документацию от Telegram, подумали вы? Да, но нет! Это скорее рассуждения на тему того, как построить функциональный бот-сервис используя Python3.5+, asyncio и aiohttp. Тем интереснее, что заголовок на самом деле лукавит…
Так в чём же лукавство заголовка? Во-первых, кода не 50 строк, а всего 39, а во-вторых, и бот не такой сложный, просто эхо-бот. Но, как мне кажется, этого достаточно, чтобы поверить в то, что сделать свой собственный бот-сервис не столь сложно, как может показаться.
Telegram-bot в 39 строк кода
import asyncio import aiohttp from aiohttp import web import json TOKEN = '111111111:AAHKeYAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' API_URL = 'https://api.telegram.org/bot%s/sendMessage' % TOKEN async def handler(request): data = await request.json() headers = < 'Content-Type': 'application/json' >message = < 'chat_id': data['message']['chat']['id'], 'text': data['message']['text'] >async with aiohttp.ClientSession(loop=loop) as session: async with session.post(API_URL, data=json.dumps(message), headers=headers) as resp: try: assert resp.status == 200 except: return web.Response(status=500) return web.Response(status=200) async def init_app(loop): app = web.Application(loop=loop, middlewares=[]) app.router.add_post('/api/v1', handler) return app if __name__ == '__main__': loop = asyncio.get_event_loop() try: app = loop.run_until_complete(init_app(loop)) web.run_app(app, host='0.0.0.0', port=23456) except Exception as e: print('Error create server: %r' % e) finally: pass loop.close()
Далее, в нескольких словах, что для чего и как сделать лучше из того, что уже есть.
1. Что используем
- во-первых, Python 3.5+. Почему именно 3.5+, потому что asyncio [2] и потому что сахарные async, await etc;
- во-вторых, aiohttp. Так как сервис на вебхуках, то он одновременно и HTTP-сервер и HTTP-клиент, а что для этого использовать, как не aiohttp [3];
- в-третьих, почему webhook, а не long polling? Если не планируется изначально бот-рассыльщик, то интерактивность является его основной функцией. Выскажу своё мнение, что для этой задачи, бот в роли HTTP-сервера подходит лучше, чем в роли клиента. Да, и отдадим часть работы (доставку сообщений) сервисам Telegram.
2. Как используем
Сервер
Состояние библиотеки aiohttp на текущий момент таково, что с её использованием можно построить полноценный web-сервер в Джанго-стиле [4].
Для standalone-сервиса вся мощь не пригодится, поэтому создание сервера ограничивается несколькими строками.
async def init_app(loop): app = web.Application(loop=loop, middlewares=[]) app.router.add_post('/api/v1', handler) return app
N.B. Обратите внимание, что здесь мы определяем роутинг и задаём обработчик входящих сообщений handler.
И стартуем веб-сервер:
app = loop.run_until_complete(init_app(loop)) web.run_app(app, host='0.0.0.0', port=23456)
Клиент
Для отправки сообщения используем метод sendMessage из Telegram API, для этого необходимо отправить на оформленный должным образом URL POST-запрос с параметрами в виде JSON-объекта. И это мы делаем с помощью aiohttp:
TOKEN = '111111111:AAHKeYAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' API_URL = 'https://api.telegram.org/bot%s/sendMessage' % TOKEN . async def handler(request): data = await request.json() headers = < 'Content-Type': 'application/json' >message = < 'chat_id': data['message']['chat']['id'], 'text': data['message']['text'] >async with aiohttp.ClientSession(loop=loop) as session: async with session.post(API_URL, data=json.dumps(message), headers=headers) as resp: try: assert resp.status == 200 except: return web.Response(status=500) return web.Response(status=200)
N.B. Обратите внимание, что в случае успешной обработки входящего сообщения и удачной отправки «эха», обработчик возвращает пустой ответ со статусом HTTP 200. Если этого не сделать, сервисы Telegram продолжат в течение какого-то времени «дёргать» запросами хук, либо пока не получат в ответ 200, либо пока не истечёт определённое для сообщения время.
3. Что можно улучшить
Совершенству нет предела, пара идей, как сделать сервис функциональней.
Используем middleware
Допустим, возникла необходимость фильтровать входящие сообщения. Препроцессинг сообщений можно сделать на специальных веб-обработчиках, в терминах aiohtttp — это middlewares [5].
Пример, определяем мидлварь для игнора сообщений от пользователей из черного списка:
async def middleware_factory(app, handler): async def middleware_handler(request): data = await request.json() if data['message']['from']['id'] in black_list: return web.Response(status=200) return await handler(request) return middleware_handler
И добавляем обработчик при инициализации web-приложения:
async def init_app(loop): app = web.Application(loop=loop, middlewares=[]) app.router.add_post('/api/v1', handler) app.middlewares.append(middleware_factory) return app
Мысли по поводу обработки входящих сообщений
Если бот будет сложнее, чем репитер-попугай, то можно предложить следующую иерархию объектов Api → Conversation → CustomConversation.
class Api(object): URL = 'https://api.telegram.org/bot%s/%s' def __init__(self, token, loop): self._token = token self._loop = loop async def _request(self, method, message): headers = < 'Content-Type': 'application/json' >async with aiohttp.ClientSession(loop=self._loop) as session: async with session.post(self.URL % (self._token, method), data=json.dumps(message), headers=headers) as resp: try: assert resp.status == 200 except: pass async def sendMessage(self, chatId, text): message = < 'chat_id': chatId, 'text': text >await self._request('sendMessage', message) class Conversation(Api): def __init__(self, token, loop): super().__init__(token, loop) async def _handler(self, message): pass async def handler(self, request): message = await request.json() asyncio.ensure_future(self._handler(message['message'])) return aiohttp.web.Response(status=200) class EchoConversation(Conversation): def __init__(self, token, loop): super().__init__(token, loop) async def _handler(self, message): await self.sendMessage(message['chat']['id'], message['text'])
Наследуя от Conversation и переопределяя _handler получаем кастомные обработчики, в зависимости от функциональности бота — погодный, финансовый etc.
И наш сервис превращается в ферму:
echobot = EchoConversation(TOKEN1, loop) weatherbot = WeatherConversation(TOKEN2, loop) finbot = FinanceConversation(TOKEN3, loop) . app.router.add_post('/api/v1/echo', echobot.handler) app.router.add_post('/api/v1/weather', weatherbot.handler) app.router.add_post('/api/v1/finance', finbot.handler)
4. Реальный мир
Регистрация webhook
Создаём data.json:
И вызываем соответствующий метод API любым доступным способом, например:
curl -X POST -d @data.json -H "Content-Type: application/json" "https://api.telegram.org/botYOURBOTTOKEN/setWebhook"
N.B. Ваш домен, хук на который вы устанавливаете, должен резолвится, иначе метод setWebhook не отработает.
Используем прокси-сервер
Как говорит документация: ports currently supported for Webhooks: 443, 80, 88, 8443.
Как же быть в случае self-hosted, когда необходимые порты уже скорее всего заняты веб-сервером, да и соединение по HTTPS мы в нашем сервисе не настроили?
Ответ простой, запуск сервиса на любом доступном локальном интерфейсе и использование реверс-прокси, и лучше nginx здесь сложно найти что-то другое, пусть он возьмёт на себя задачу организации HTTPS-соединения и переадресацию запросов нашему сервису.
Заключение
Надеюсь, что работа с ботом через вебхуки не показалась сильно сложнее long polling, как по мне так даже проще, гибче и прозрачнее. Дополнительные расходы на организацию сервера не должны пугать настоящего ботовода.
Пусть ваши идеи находят достойный инструмент для реализации.
Полезное:
- Telegram Bot API
- 18.5. asyncio — Asynchronous I/O, event loop, coroutines and tasks
- aiohttp: Asynchronous HTTP Client/Server
- aiohttp: Server Tutorial
- aiohttp: Server Usage — Middlewares
Настройка WebHook Telegram
Пробовал даже на разных доменах. Но все мои попытки приводили лишь к одному.
< "ok": true, "result": < "url": "URL", "has_custom_certificate": false, "pending_update_count": 35, "last_error_date": 1516221264, "last_error_message": "SSL error ", "max_connections": 40 > >
Отслеживать
8,582 4 4 золотых знака 29 29 серебряных знаков 53 53 бронзовых знака
задан 17 янв 2018 в 20:42
Yevgeny Ignatyev Yevgeny Ignatyev
73 11 11 бронзовых знаков
Браузер при переходе по url вебхука показывает, что подключение защищено? Если нажать на просмотр защищенности подключения — нет ли там проблем?
16 июл 2018 в 0:22
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
если сертификат от Let’s Encrypt, тогда без @ перед именем ключа:
curl -F "url=https://www.domain.ru" -F "certificate=cert.pem" "https://api.telegram.org/botТОКЕН/setwebhook"
Отслеживать
ответ дан 29 янв 2018 в 13:20
- telegram-bot
- telebot
-
Важное на Мете
Похожие
Подписаться на ленту
Лента вопроса
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.1.17.3574
Telegram Webhook
Telegram Webhook – это технология, позволяющая отслеживать в чате события в реальном времени и отправлять информацию о нем на указанный адрес. Если вы создали бот, то метод необходим, чтобы система могла реагировать на сообщения и действия пользователей. Получая информацию о них, сервер отправляет ее программе бота, в алгоритм которой заложена обработка.

Что нужно для рабочего коннекта Webhook
- Адрес (URL) страницы с программным кодом;
- Поддержка IPv4, но рекомендуем поддержка IPv6 из-за частой блокировки телеграмма по IPv4;
- Возможность сервера обрабатывать HTTPS трафик;
- Наличие SSL сертификата.
Команды работы с методом описаны в Telegram Webhook Bot API. К ним относятся:
- setWebhook – установка соединения . При этом необходимо передать адрес, на который будут отправляться сообщения из чата;
- deleteWebhook – удаление соединения . Указание предыдущей команды без параметров также удалит его;
- getWebhookInfo – получение текущего статуса соединения.
Активация Webhook Telegram
Чтобы использовать метод постоянного коннекта сервера и бота, следует запустить программный код, в котором выполняется команда API для Телеграм.
Для официального сертификата SSL :
https://api.telegram.org/botТОКЕН/setWebhook?url=ВАША_ССЫЛКА
Для самоподписанного сертификата :
https://api.telegram.org/botТОКЕН/setWebhook?url=ВАША_ССЫЛКА&certificate=ФАЙЛ_СЕРТИФИКАТА
ВАША_ССЫЛКА — это ссылка на скрипт, обрабатывающий запросы от сервера Telegram, например сообщения или команды для вашего бота.
Какая команда будет использоваться для отправки сообщения в мессенджер зависит от языка программирования.
Например, для PHP это будет curl_exec(), а для Python — requests.get().
Для установки сертификата на сервера чаще всего используется криптографический пакет с открытым исходным кодом OpenSSL . Его можно скачать с официального сайта. Настройка зависит от операционной системы и сервера.

Официальные цифровые подписи устанавливаются на хостинге провайдером. Эта одна из услуг большинства компаний.
Прежде чем использовать Webhooks убедитесь, что вам доступен этот функционал.
Создание бота для Telegram на Webhooks предпочтительнее, чем использования метода getUpdates. Во втором случае приходится закладывать в код постоянное обращение этой команды к боту. Это делается в циклическом режиме. В конце концов начинаются баги, и он подвисает. Webhook же держит постоянное соединение, но запросы отправляются только когда произошло событие, например, пользователь ввел сообщение.