Telegram бот через webhook
Я тогда давно написал статью про создание Telegram бота, и обещал дополнить её описанием настройки работы через webhook, но так и не дополнил. Вот только сейчас дошли руки.
This article in english 🇺🇸.
Что это такое
Как пишут в документации, общаться с серверами 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("/{token}/", 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 <module>
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 {error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed}",
"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 - нет.
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks