Меня зовут Андрей Устьянцев, я ведущий аналитик направления 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.
Поэтому я выбрал второе решение — облачное.
Идеальный вариант — виртуальный хостинг.
Круглосуточно работающий сервер Apach, настроенный для запуска приложений на Python.
Защита от ddos-атак и прочих «пакостей» извне.
Автоматический выпуск качественного SSL-сертификата.
Качественный url webhook, соответствующий требованиям Telegram.
Удобный интерфейс для написания кода на Python прямо из браузера.
Итак, напишем простенького бота, который при любом взаимодействии с ним будет всегда писать в ответ «Ну привет, {{имя}}»
Очень кратко
Берем виртуальный хостинг.
Регистрируем бота.
Определяем URL-адрес, который сообщим Telegram — что это webhook, соответствующий нашему чат-боту.
Пишем код на Python.
«Сообщаем» 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» |
Отправка простого текста |
|
|
При отправке боту команды «/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?
Кратко:
Определить имя страницы, которая будет соответствовать коду на Python, реализующему желание бота продолжить общение. Пусть это будет ‘whant_pogovorit’.
Настроить задание в CRON на сервере — обращение к странице {{адрес/домен}}/whant_pogovorit с нужной периодичностью.
Добавить блок кода для условия env['PATH_INFO'].lower() == '/whant_pogovorit' и написать внутри код, реализующий логику отправки сообщений (и проч.) в чат или чаты.
_______________________________________
Надеюсь, статья оказалась интересной. Пишите в комментариях замечания, уточнения и пожелания. Возможно, полезным будет рассказать о том, как, например, реализовать прием и отправку картинок — учту мнения и постараюсь написать дополнительный материал.
Комментарии (8)
Buchachalo
00.00.0000 00:00+1Не хочу показаться душнилой, но что за нейминг бро?
... def MainProtokol ...
... except Exception as S: ...
... token='5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo' ...
И ладно было бы в приватной репе, но по мануалу же будут учится. В общем подработать этот вопрос.
gudvinr
00.00.0000 00:00В этом месяце кажется ещё не было статей о том, как сделать бота для telegram на питоне
webalex127
00.00.0000 00:00А теперь важный лайфхак! API Telegram устроен таким образом, что не обязательно отправлять запрос именно из кода на Python. Вы можете правильным образом написать строку вызова метода с передачей ему обязательных параметров прямо в строке браузера, и она сработает!
Только если текст не очень большой, рекомендую все же post использовать. Если по какой либо причине очень хочется get, то почитайте про params в requests
sergonom
00.00.0000 00:00Про задержки от использования longpolling мне кажется глупости. Если соединение не рвется, то все будет так же быстро. Но использование longpolling это безумие. Полностью поддерживаю идею минимализма. Фреймворки это дичь.
red-cat-fat
А где код то? Имеется только ссылка на какой-то локальный файл...
Вообще по описанию в статье код содержит в себе не более 100 строк. Может было бы разумнее вставить его в статью напрямую?
Сильно статью не раздует, зато понятнее станет, тема то интересная, нельзя статью губить таким вот образом...
Digital_League Автор
Спасибо! Добавили????
T-473
спасибо за внимательность ! сейчас исправим - будет код !