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

Telegram bot webhook

This article in english 🇺🇸.

Что это такое

Как пишут в документации, общаться с серверами Telegram бот может двумя способами:

  1. getUpdates - pull: ваш бот постоянно дёргает сервер Telegram и проверяет есть ли новые сообщения;
  2. setWebhook - push: по мере поступления новых сообщений сервер Telegram отправляет их вашему боту.

Разницу можно изобразить следующим образом:

Telegram bot, getUpdates vs setWebhook

Очевидно, что второй способ (setWebhook) рациональнее для всех участников процесса. Однако в нём присутствует неявная сложность: кто-то должен принимать сообщения от Telegram на стороне бота, то есть необходим веб-сервер или его эквивалент.

Как настроить

Что нужно сделать:

  1. Заиметь доменное имя для сервера и получить на него сертификат (например, от Let’s Encrypt). Документация также говорит, что в случае самоподписанного сертификата можно обойтись и просто IP адресом, но этого я не пробовал;
  2. Запилить серверную часть на стороне бота (куда будет ломиться Telegram);
  3. Зарегистрировать адрес серверной части в 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’а всего-то нужен лишь веб-сервер на стороне бота, я бы это сделал уже сто лет назад. Конечно, продвинутым чувакам это скорее всего было очевидным, но для меня нет.