Меня зовут Андрей Устьянцев, я ведущий аналитик направления Big Data Лиги Цифровой Экономики, и в этой статье я расскажу, как писал чат‑бот в Telegram на webhook. Если вы знаете, что это такое, и подготовка не вызывает интереса — можете сразу переходить к разделу «Очень кратко». С остальными поделюсь всеми необходимыми шагами.

Почему webhook

Чат‑бот в Telegram может работать в одном из двух режимов.

Один из них называется polling — это когда код, непосредственно реализующий механику бота, опрашивает сервера Telegram с определенной периодичностью («не появилось ли чего новенького»). А если обнаружена активность в чате — реализуется определенная механика взаимодействия (общения).

Большинство материалов в интернете посвящено описанию того, как создавать ботов именно на этой механике, но мне (частное мнение без претензии на истину в последней инстанции) такой подход не очень нравится. Вот почему:

  • не вижу смысла постоянно «дергать» сервера Telegram почем зря;

  • таймаут, установленный для опроса всех чатов, в которых «общается» бот — это суть задержка в коммуникациях с человеком, который «общается» с ботом.

Второй режим работы ботов, webhook, подразумевает, что Telegram сам вызывает обработчик события/сообщения, когда в боте происходит какая‑то активность. Другими словами, код, реализующий механику бота, срабатывает по инициативе человека, который «общается» с ботом. Самый главный плюс от такого режима работы — ответ бота на действие человека происходит мгновенно: человек написал что‑то боту, Telegram тут же вызвал webhook, написанный код сразу «ответил» человеку.

Лирическое отступление: на тему «что лучше — polling или webhook» спорить можно бесконечно долго. Критерий истины: все зависит от решаемой задачи и от user‑story.

Для задачи, которую решал я, лучше подходит режим webhook: заранее не известно, когда кто‑то напишет боту, но тот должен в любой момент быть готовым поддержать диалог.

При чем тут «минимум» внешних библиотек?

Мое личное убеждение — использовать «чужие» библиотеки по минимуму при написании кода. Особенно в процессе изучения языка программирования. Это отнюдь не означает, что надо впадать в крайности и писать вообще все с нуля — везде возможен разумный компромисс.

Ниже я покажу, что можно написать аккуратный, короткий и читаемый код на Python и без использования специализированных библиотек именно для чат‑ботов (особенно работающих в режиме webhook).

Пошаговая инструкция: как и с чего начать

Расскажу, как я писал код на Python, с какими трудностями столкнулся по пути и как их решал.

Ремарка: код написан в процессе изучения Python, поэтому некоторые моменты могут показаться неоптимальными — буду рад конструктивной критике и предложениям в комментариях к статье.

Итак, что необходимо для работы чат-бота в режиме webhook?

_____________________________________________________________

Ссылка на официальную документацию Telegam, где описана работа чат‑ботов в режиме webhook: https://core.telegram.org/bots/api#setwebhook

_____________________________________________________________

1. Бот. Я не буду здесь подробно расписывать, как зарегистрировать бота — подробных мануалов достаточно и в интернете, и на этом портале.

2. Сервер для размещения кода, обрабатывающего механику бота — тот самый webhook, которого будет вызывать Telegram при активностях в боте.

Важные моменты (требования Telegram к таким серверам):

  • Сервер должен работать круглосуточно. Telegram периодически проверяет служебными запросами наличие и работоспособность webhook.

  • Важно, чтобы на сервере был установлен SSL‑сертификат, обеспечивающий (подтверждающий) безопасное соединение с серверами Telegram.

Ну и где брать такой сервер?

Решений два.

Первое — создать свой сервер:

  • настроить собственный компьютер под управлением Linux в режиме сервера,

  • получить, купить «фирменный» SSL-сертификат или сформировать самостоятельно такой, который Telegram примет как достоверный.

Недостатки такого решения:

  • Необходимы дополнительные компетенции по «поднятию» сервера (обычно это еще и на основе Linux) и созданию SSL-сертификата.

Хотя пошаговых инструкций и статей на эту тему достаточно.

  • Нужен постоянный IP-адрес подключения к интернету.

Если компьютер подключен к интернету из домашней сети — скорее всего, у вас динамический IP-адрес, который точно не подойдет.

  • Надо держать компьютер постоянно включенным в 220В.

Мне в рамках этой задачи круглосуточно гудящий под столом системный блок не нужен.

  • Об этом редко пишут, но на поднятый на скорую руку сервер, на который может «постучаться» Telegram, точно будут «ломиться» разные люди, боты, сети в попытке использовать ваш сервер для своих (не всегда хороших) целей.

А это означает, что мало «поднять» сервер, его еще надо и защитить от потенциальных атак из внешних сетей — суровая дополнительная компетенция, скажу я вам.

С профессиональной точки зрения неплохо, конечно, прокачать дополнительные компетенции, но задача состоит в том, чтобы написать на Python код, реализующий механику бота. Причем в процессе изучения самого Python.

Поэтому я выбрал второе решение — облачное.

Идеальный вариант — виртуальный хостинг.

  1. Круглосуточно работающий сервер Apach, настроенный для запуска приложений на Python.

  2. Защита от ddos-атак и прочих «пакостей» извне.

  3. Автоматический выпуск качественного SSL-сертификата.

  4. Качественный url webhook, соответствующий требованиям Telegram.

  5. Удобный интерфейс для написания кода на Python прямо из браузера.

Итак, напишем простенького бота, который при любом взаимодействии с ним будет всегда писать в ответ «Ну привет, {{имя}}»

Очень кратко

  1. Берем виртуальный хостинг.

  2. Регистрируем бота.

  3. Определяем URL-адрес, который сообщим Telegram — что это webhook, соответствующий нашему чат-боту.

  4. Пишем код на Python.

  5. «Сообщаем» Telegram адрес webhook нашего бота.

Важно! Рекомендую не менять местами последовательность действий из пунктов 5 и 4. В чем риск? Если Telegram решит проверить работоспособность адреса webhook, а там еще не отлажен код на Python, неправильный ответ Telegram на запрос может привести к блокировке webhook и бота. Снятие блокировки — отдельная неинтересная история...

Разберем код

import time     
import json
import requests

def MainProtokol(s,ts = 'Запись'):
    dt=time.strftime('%d.%m.%Y %H:%M:')+'00'

    f=open('log.txt','a')
    f.write(dt+';'+str(ts)+';'+str(s)+'\n')
    f.close
    
def application(env, start_response):
    try:
        content=''
        token='5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo'
        
        if env['PATH_INFO'].lower() == '/tg_bot':
            # тут вся механика бота именно этот код будет исполняться, когда Telegram будет вызывать webhook
            wsgi_string=env['wsgi.input'].read()
            x=wsgi_string.decode('UTF-8')
            
            y=x.replace('\n',' ')

            try:
                json_string=json.loads(y)
            except:
                raise ValueError('Не удалось распарсить в JSON полученную строку')
            
            chat_id=json_string['message']['chat']['id']
            from_first_name=json_string['message']['from']['first_name']    # имя отправителя
            
            # отправка сообщения в чат
            msg='Ну, привет, '+str(from_first_name)
            send_message=requests.get('https://api.telegram.org/bot'+token+'/sendMessage?&chat_id='+str(chat_id)+'&text='+str(msg))
            if not send_message: raise ValueError('Не удалось отправить текст в бот')
            
        elif env['PATH_INFO'] == '/':
            # случай, когда кто-то просто из браузера зашел на сайт
            MainProtokol('кто-то просто зашел на сайт')
        else:
            # заглушка для обработки ситуации, когда кто-то методом перебора пытается обратиться к какой-то странице сайта
            raise ValueError('Вызов неизвестного URL :: '+env['PATH_INFO'])
        
        start_response('200 OK', [('Content-Type','text/html')])
        return [content.encode('utf-8')]
    except Exception as S:
        content=str(S)
        MainProtokol(content,'Ошибка')
        
        start_response('200 OK', [('Content-Type','text/html')])
        return [content.encode('utf-8')]

Нам понадобятся следующие библиотеки:

  • time — для работы с функциями даты и времени;

  • JSON — для распарсивания JSON-строки, получаемой от Telegram;

  • requests — для отправки сообщений в бот.

Примечание: в зависимости от настроек виртуальных серверов по умолчанию и инсталляций Python перечисленные библиотеки могут быть уже предустановлены. Если нет — необходимо их установить командой pip install.

Строка 5: Функция MainProtokol написана для логирования происходящего в коде. Задача этой функции — записывать в текстовый файл то, что в обычных условиях выводится на экран компьютера. Поскольку код выполняется по сути на сервере в момент вызова webhook Telegram, на экран ничего вывести нельзя, потому что экрана как такового нет. Так что текстовый файл — это замена вывода на экран ошибок (если они возникнут при выполнении кода), а также всего, что было бы полезно залогировать.

Строка 12: Функция application — основная у Python, которую вызывает сервер Apach при любом обращении к сайту. И при вызове webhook тоже.

Все данные, которые поступают при обращении к сайту (не важно, открывают ли его в браузере при прямом заходе по имени сайта, или происходит вызов webhook), сохраняются в переменной env (тип — словарь) — суть переменные окружения.

Строка 14: Переменная content в текущем коде — это «заглушка», потому что на экран ничего выводиться не должно. Можно в ней разместить HTML-код, который будет отображается в случае захода на сайт из браузера.

Строка 15: Переменная token — в ней хранится токен бота, полученный при его регистрации.

Строка 17: Проверяем, что вызывается именно webhook. В env['PATH_INFO'] хранится имя страницы, к которой было обращение. Я решил, что у меня страница для коммуникаций с ботами будет называться 'tg_bot'. Настоятельно рекомендую придумывать страницу (адрес) webhook, а не сообщать Telegram просто имя домена – это защитит дополнительно ваш сайт от внешних атак.

Примечание: приведение значения переменной окружения к нижнему регистру функцией lower() — это, можно сказать, мой «пунктик»: так я еще больше уверен, что сравнение строк в одинаковом регистре произойдет корректно.

Строка 19: Получаем данные, переданные из Telegram («от бота») — они хранятся в переменной окружения «wsgi.input» в формате WSGI. Для его дальнейшей обработки применяем метод read().

Строки 24–27: Распарсиваем JSON-строку, полученную от бота.

И вот тут меня ждал подводный камень размерами с булыжник. Метод load библиотеки JSON выдавал ошибку «нарушение структуры JSON». Потратив значительное время и логируя в текстовый файл с использованием функции MainProtokol все, что только можно, я обнаружил следующее:

В JSON-строке, передаваемой из Telegram, совершенно непонятно зачем добавляется спецсимвол «\n» (перевод строки). Причем только в одном месте...

Решение проблемы — перед распарсиванием JSON-строки принудительно заменить в ней «мешающий» спецсимвол на пробел: replace('\n',' ').

Ремарка: по-хорошему надо было бы написать кусок кода или функцию, которая «убирает» из JSON-строки все известные спецсимволы... Но я СТОЛЬКО времени потратил на поиск проблемы, что сил на ее полномасштабное устранение со всеми возможными вариантами последствий впредь у меня уже не осталось.

Итого мы получили словарь в переменной json_string, содержащий все, что передал Telegram от бота. В общем-то, блок строк 17-30 — это то место, где необходимо реализовывать логику «общения» бота. Суть — анализировать полученный JSON и в зависимости от полученных данных программировать поведение бота.

Как разбираться с полученным JSON? По сути это объект message, полное описание которого можно найти в официальной документации вместе с описанием всех вложенных объектов типа chat, user и т. д.

Я пошел другим путем: писал в лог получаемые JSON по разным сценариям взаимодействия с ботом (отправка команды боту, отправка текста боту, отправка картинки), потом смотрел их структуру и реализовывал нужную мне логику.

Ремарка: Telegram передает в webhook только те поля, которые имеют значение. Покажу на примере.

(Исходный код – тут log1 с примерами JSON.txt)

Отправка команды «/start»

Отправка простого текста

{'update_id': 732535001,

 'message':

{'message_id': 10,

  'from':

     {'id': 5922617094,

      'is_bot': False,

      'first_name': 'Андрей',

      'last_name': 'Устьянцев',

      'language_code': 'ru'

     },

  'chat':

     {'id': 5922617094,

      'first_name': 'Андрей',

      'last_name': 'Устьянцев',

      'type': 'private'

     },

  'date': 1675173440,

  'text': '/start',

  'entities':

     [{'offset': 0,

       'length': 6,

       'type': 'bot_command'

      }

     ]

}

}

{'update_id': 732535002,

 'message':

{'message_id': 11,

  'from':

     {'id': 5922617094,

      'is_bot': False,

      'first_name': 'Андрей',

      'last_name': 'Устьянцев',

      'language_code': 'ru'

     },

  'chat':

     {'id': 5922617094,

      'first_name': 'Андрей',

      'last_name': 'Устьянцев',

      'type': 'private'},

  'date': 1675173443,

  'text': 'Привет'

}

}

При отправке боту команды «/start» в JSON-строке будет блок ‘entities’, содержащий элемент 'type': 'bot_command'. При отправке просто текста (обычного общения, скажем так) этих блоков уже нет. При отправке боту картинок, видео или файлов будут переданы еще блоки в JSON-строке.

Так что можно или тщательно изучать официальную документацию Telegram, или логировать все полученные JSON по разным сценариям взаимодействия с ботом — аккуратно раскладывать их в текстовом редакторе, как показано выше, и смотреть, что и как передается на самом деле.

Теперь перейдем к самому интересному — общение бота. Для отправки сообщения обратно (поддержания взаимодействия, так сказать) необходимо сформировать строку запроса к API Telegram Bot, в которой обязательны следующие параметры:

  • ИД чата — определяем это в строке 29 кода.

  • Имя собеседника в боте — определяем в строке 30.

  • Текст сообщения — определяем в строке 33.

Ну и, наконец, то, ради чего все это затевалось: в строке 34 отправляем сообщение в бот. 

Разберу подробнее эту строку.

send_message=requests.get('https://api.telegram.org/bot'+token+'/sendMessage?&chat_id='+str(chat_id)+'&text='+str(msg))

В статическом виде запрос выглядел бы вот так:

https://api.telegram.org/bot5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo/sendMessage?&chat_id=5922617094&text=Ну привет, Андрей

Рассмотрим подробнее:

bot5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo — состоит из двух частей: «bot» (обязательный набор символов) и токен бота.

sendMessage — метод API Telegram для отправки сообщений в бот.

сhat_id — обязательный параметр для метода sendMessage (после знака равно — id чата).

text — текст сообщения, которое будет отправлено от имени бота.

А теперь важный лайфхак! API Telegram устроен таким образом, что не обязательно отправлять запрос именно из кода на Python. Вы можете правильным образом написать строку вызова метода с передачей ему обязательных параметров прямо в строке браузера, и она сработает!

То есть можно отправлять сообщения в чаты от имени бота прямо из адресной строки браузера — надо только знать id чата.

Если вставить вот такую строку в адресную строку браузера

https://api.telegram.org/bot5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo/sendMessage?&chat_id=5922617094&text=Пишу я вам прямо из адресной строки браузера

Результат выполнения этого запроса вы увидите прямо там:

{"ok":true,"result":{"message_id":20,"from":{"id":5937929205,"is_bot":true,"first_name":"\u0422\u0435\u0441\u0442\u043e\u0432\u044b\u0439 \u0431\u043e\u0442","username":"t473test2_bot"},"chat":{"id":5922617094,"first_name":"\u0410\u043d\u0434\u0440\u0435\u0439","last_name":"\u0423\u0441\u0442\u044c\u044f\u043d\u0446\u0435\u0432","type":"private"},"date":1675185341,"text":"\u041f\u0438\u0448\u0443 \u044f \u0432\u0430\u043c \u043f\u0440\u044f\u043c\u043e \u0438\u0437 \u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430"}}

Самое важное — это «ок» в полученном результате. Ну а в чате это будет выглядеть примерно так:

Полное описание возможностей метода sendMessage — что вообще можно отправлять в бот — читайте тут.

Особенность работы блока except

Строки 46–51

Да, код написан так, чтобы всегда возвращать код ответа 200 — скрипт работает как бы без ошибок. Это нужно для того, чтобы Telegram не заблокировал ваш webhook — соответственно, чтобы бот не превратился в тыкву.

Чтобы можно было разобраться, что там за ошибка, ее полный текст пишется в соответствующий файл.

Регистрация webhook для чат-бота

Остался последний шаг — сообщить Telegram адрес webhook, который будет обрабатывать «общение» в боте.

Самое простое — это собрать строку запроса и выполнить ее из адресной строки браузера:

https://api.telegram.org/bot5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo/setWebhook?url=https://t473test.na4u.ru/tg_bot

Разберем эту строку подробнее.

bot5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo — описывал выше (ключевое слово «bot» и токен вашего бота).

setWebhook — метод API для передачи url, по которому размещен код, обрабатывающий общение с ботом.

url=https://t473test.na4u.ru/tg_bot — вот тут в обязательный параметр url я передал адрес, по которому размещен код обработки webhook. t473test.na4u.ru — это технический домен, который я развернул на виртуальном хостинге, чтобы написать статью.

/tg_bot — имя страницы.

При успешной регистрации Webhook вы увидите в браузере ответ вот такого вида:

{"ok":true,"result":true,"description":"Webhook was set"}

В общем-то, не вижу необходимости подробно расписывать — все интуитивно понятно.

Важно! Если вы не получили такой ответ в браузере, не стоит много раз пытаться отправить запрос на установку webhook. Проверить статус webhook’а можно таким запросом:

https://api.telegram.org/bot5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo/getWebhookInfo

Должен вернуться ответ в виде

{"ok":true,"result":{"url":"https://t473test.na4u.ru/tg_bot","has_custom_certificate":false,"pending_update_count":0,"max_connections":40,"ip_address":"91.201.52.155"}}

На самом деле, при установке webhook можно передать больше необязательных параметров. Подробная официальная документация — тут.

Справочно: отключение webhook

Что ж, проверяем, как работает наш бот.

  • Находим в Telegram своего бота.

  • Традиционно жмем на кнопку «start»

  • Пишем что-нибудь чат-боту.

Как продолжить диалог по инициативе бота в режиме webhook?

Кратко:

  1. Определить имя страницы, которая будет соответствовать коду на Python, реализующему желание бота продолжить общение. Пусть это будет ‘whant_pogovorit’.

  2. Настроить задание в CRON на сервере — обращение к странице {{адрес/домен}}/whant_pogovorit с нужной периодичностью.

  3. Добавить блок кода для условия env['PATH_INFO'].lower() == '/whant_pogovorit' и написать внутри код, реализующий логику отправки сообщений (и проч.) в чат или чаты.

_______________________________________

Надеюсь, статья оказалась интересной. Пишите в комментариях замечания, уточнения и пожелания. Возможно, полезным будет рассказать о том, как, например, реализовать прием и отправку картинок — учту мнения и постараюсь написать дополнительный материал.

Комментарии (8)


  1. red-cat-fat
    00.00.0000 00:00
    +1

    А где код то? Имеется только ссылка на какой-то локальный файл...

    Вообще по описанию в статье код содержит в себе не более 100 строк. Может было бы разумнее вставить его в статью напрямую?

    print("Типа вот так")

    Сильно статью не раздует, зато понятнее станет, тема то интересная, нельзя статью губить таким вот образом...


    1. Digital_League Автор
      00.00.0000 00:00

      Спасибо! Добавили????


    1. T-473
      00.00.0000 00:00

      спасибо за внимательность ! сейчас исправим - будет код !


  1. Buchachalo
    00.00.0000 00:00
    +1

    Не хочу показаться душнилой, но что за нейминг бро?
    ... def MainProtokol ...
    ... except Exception as S: ...
    ... token='5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo' ...

    И ладно было бы в приватной репе, но по мануалу же будут учится. В общем подработать этот вопрос.


    1. Stas911
      00.00.0000 00:00

      Туда же '/whant_pogovorit'


  1. gudvinr
    00.00.0000 00:00

    В этом месяце кажется ещё не было статей о том, как сделать бота для telegram на питоне


  1. webalex127
    00.00.0000 00:00

    А теперь важный лайфхак! API Telegram устроен таким образом, что не обязательно отправлять запрос именно из кода на Python. Вы можете правильным образом написать строку вызова метода с передачей ему обязательных параметров прямо в строке браузера, и она сработает!

    Только если текст не очень большой, рекомендую все же post использовать. Если по какой либо причине очень хочется get, то почитайте про params в requests


  1. sergonom
    00.00.0000 00:00

    Про задержки от использования longpolling мне кажется глупости. Если соединение не рвется, то все будет так же быстро. Но использование longpolling это безумие. Полностью поддерживаю идею минимализма. Фреймворки это дичь.