Урок 4

Вебхуки

С простым ботом наконец-то разобрались, теперь будем осваивать различные “плюшки”. Первая из них и, пожалуй, самая главная, — вебхуки.

А в чём, собственно, разница?

(Следующий абзац был написан в 2015-2016г., библиотеки с того момента много обновлялись, поэтому в некоторых случаях использовать Long Polling будет не хуже, чем вебхуки).
Давайте для начала разберемся, как боты принимают сообщения. Первый и наиболее простой вариант заключается в периодическом опросе серверов Telegram на предмет наличия новой информации. Всё это осуществляется через т.н. Long Polling, т.е. открывается соединение на непродолжительное время и все обновления тут же прилетают боту. Просто, но не очень надежно. Во-первых, серверы Telegram периодически начинают возвращать ошибку 504 (Gateway Timeout), из-за чего некоторые боты впадают в ступор. Даже pyTelegramBotAPI, используемый мной, не всегда может пережить такое. Во-вторых, если одновременно запущено несколько ботов, вероятность столкнуться с ошибками возрастает. Это вдвойне обидно, если сами боты используются не очень часто.

Вебхуки работают несколько иначе. Устанавливая вебхук, вы как бы говорите серверам Telegram: “Слышь, если кто мне напишет, стукни сюда — (ссылка)”. Отпадает необходимость периодически самому опрашивать серверы, тем самым, исчезает неприятная причина падений ботов. Однако за это приходится платить необходимостью установки полноценного веб-сервера на ту машину, на которой планируется запускать ботов. Что ещё неприятно, надо иметь собственный SSL-сертификат, т.к. вебхуки в телеграме работают только по HTTPS. К счастью, в один прекрасный день появилась поддержка самоподписанных сертификатов. Вот об их применении я и расскажу.

Создаем сертификат

Повторюсь: я не считаю себя супер-мега-крутым специалистом в айти, возможно, я что-то делаю неправильно, тем не менее, это работает и выглядит вполне прилично. Ладно, приступим.
Для начала, установим пакет openssl (для Linux):
sudo apt-get install openssl
Затем сгенерируем приватный ключ:
openssl genrsa -out webhook_pkey.pem 2048
Теперь, внимание, генерируем самоподписанный сертификат вот этой вот длинной командой:
openssl req -new -x509 -days 3650 -key webhook_pkey.pem -out webhook_cert.pem
Нам предложат ввести некоторую информацию о себе: двухбуквенный код страны, имя организации и т.д. Если не хотите ничего вводить, ставьте точку. НО! ВАЖНО! Когда дойдете до предложения ввести Common Name, следует написать IP адрес сервера, на котором будет запущен бот.

Генерация сертификата

В результате получим файлы webhook_cert.pem и webhook_pkey.pem, положим их в какой-нибудь пустой каталог, в котором потом будем создавать бота. Сертификаты готовы, теперь займемся ботом. Чтобы не сильно загружать себе мозги, напишем простого echo-bot’а из урока №1, только теперь с использованием сертификата.

Наш вишнёвый сервер

Выше я упомянул необходимость наличия веб-сервера, для работы с вебхуками. Те, кто умело владеет Apache или Nginx, можете дальше не читать. Лично я никак не мог (и не могу до сих пор) понять, как обрабатывать входящие сообщения от этих серверов в Python. Поэтому, было принято простое и довольно эффективное решение - используем веб-фреймворк CherryPy. Это не самый простой фреймворк по сравнению, например, с Flask, но мы будем использовать именно его.
Итак, установим CherryPy простой командой python3 -m pip install cherrypy

Новый старый бот

Перейдем в каталог с нашими сертификатами и создадим файлы bot.py и config.py. В последнем создадим переменную token, в которую передадим токен нашего бота. Открываем bot.py.
Импортируем 2 библиотеки, зададим необходимые константы и создадим экземпляр бота:

import telebot
import cherrypy
import config

WEBHOOK_HOST = 'IP-адрес сервера, на котором запущен бот'
WEBHOOK_PORT = 443  # 443, 80, 88 или 8443 (порт должен быть открыт!)
WEBHOOK_LISTEN = '0.0.0.0'  # На некоторых серверах придется указывать такой же IP, что и выше

WEBHOOK_SSL_CERT = './webhook_cert.pem'  # Путь к сертификату
WEBHOOK_SSL_PRIV = './webhook_pkey.pem'  # Путь к приватному ключу

WEBHOOK_URL_BASE = "https://%s:%s" % (WEBHOOK_HOST, WEBHOOK_PORT)
WEBHOOK_URL_PATH = "/%s/" % (config.token)

bot = telebot.TeleBot(config.token)

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

Создадим класс, реализующий экземпляр веб-сервера. Это, в принципе, стандартный код, который от бота к боту сильно меняться не будет:

# Наш вебхук-сервер
class WebhookServer(object):
    @cherrypy.expose
    def index(self):
        if 'content-length' in cherrypy.request.headers and \
                        'content-type' in cherrypy.request.headers and \
                        cherrypy.request.headers['content-type'] == 'application/json':
            length = int(cherrypy.request.headers['content-length'])
            json_string = cherrypy.request.body.read(length).decode("utf-8")
            update = telebot.types.Update.de_json(json_string)
            # Эта функция обеспечивает проверку входящего сообщения
            bot.process_new_updates([update])
            return ''
        else:
            raise cherrypy.HTTPError(403)

Посмотрите на название функции: index. Это, по сути, обозначает последнюю часть URL. Поясню на примере: если бы мы хотели получать обновления на адрес 80.100.95.20/webhooksbot, то функцию выше мы бы назвали webhooksbot. index - это аналог отсутствия какой-либо дополнительной маршрутизации. Зачем менять это значение на другое, рассказано здесь 12, сейчас это не нужно.
Итак, что мы видим в коде выше? Принимаем входящие запросы по URL наш.ip.адрес/, получаем содержимое и прогоняем через набор хэндлеров. Кстати, о них. Т.к. мы реализуем простейших echo-бот, хэндлер нам нужен всего один:

# Хэндлер на все текстовые сообщения
@bot.message_handler(func=lambda message: True, content_types=['text'])
def echo_message(message):
    bot.reply_to(message, message.text)

Внимательный читатель всё же заметит одно отличие, о котором я говорить не буду ;) Заодно ещё один повод открыть документацию.

Далее, отправим серверу наш самоподписанный сертификат и “обратный адрес”, по которому просим сообщать обо всех новых сообщениях:

# Снимаем вебхук перед повторной установкой (избавляет от некоторых проблем)
bot.remove_webhook()

# Ставим заново вебхук
bot.set_webhook(url=WEBHOOK_URL_BASE + WEBHOOK_URL_PATH,
                certificate=open(WEBHOOK_SSL_CERT, 'r'))

Наконец, укажем настройки нашего сервера и запустим его!

# Указываем настройки сервера CherryPy
cherrypy.config.update({
    'server.socket_host': WEBHOOK_LISTEN,
    'server.socket_port': WEBHOOK_PORT,
    'server.ssl_module': 'builtin',
    'server.ssl_certificate': WEBHOOK_SSL_CERT,
    'server.ssl_private_key': WEBHOOK_SSL_PRIV
})

 # Собственно, запуск!
cherrypy.quickstart(WebhookServer(), WEBHOOK_URL_PATH, {'/': {}})

Обратите внимание на последнюю строку. Наш сервер в качестве “корня” будет прослушивать адрес вида “ip-адрес/токен_бота”, относительно которого index - это и есть этот адрес. Может, немного криво пояснил, но позднее вам всё станет предельно ясно, сейчас не нужно загромождать голову лишней информацией.

Запустим бота и напишем ему парочку сообщений. Затем посмотрим в окно терминала:

Лог сервера

Если код статуса равен 200 (OK), значит, всё в порядке и бот получил сообщения от сервера.

На сегодня всё.

← Урок №3 Урок №5 →