Telegram bots

Telegram bots

LINE

В этой статье мы не будем пользоваться библиотеками, а посмотрим на то как они устроены внутри. Мы разберем сам telegram bot api и будем делать все вручную. В реальных проектах (наверно) стоит использовать готовые решения, которые позволяют уменьшить количество кода и избежать частых ошибок.

Как происходит общение

Разберем, как происходит общение между пользователем и ботом.

Пользователь отправляет сообщение боту [1], которое отправляется на сервера телеграм. Ваш бот может получить это сообщение двумя путями

  • polling - т.е. постоянный опрос телеграм на наличие новых сообщений (условно каждую секунду).
  • webHook - сказать телеграм что как только придет новое сообщение нужно его переслать по определенному адресу.

Нужен какой-то способ, который позволит отличать боты один от другого - для этого существует токен. Он выглядит как строка, примерно так

0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789

Каждый бот связан со своим токеном, и знание токена равносильно знанию пароля от аккаунта - никогда и никому не передавайте свой токен.

Оставим метод webHook на потом, и поработаем с polling.

Ваш бот делает запрос на сервер телеграм, заключая в url свой токен. Т.к. токен это длинная строка из символов и букв, шансы что кто-то угадает такую строку (или просто сгенерирует одну из действующих) близки к нулю. Телеграм принимает запрос с токеном, авторизует по нему вас как владельца бота и высылает вам сообщение пользователя [2].

Кроме сообщения пользователя телеграм присылает и другие данные в формате json, которые можно использовать для обработки запроса [3]. Что такое обработка зависит от того, что делает бот и здесь нет ограничений (все что может ваш язык программирования - любые вычисления, запросы к сторонним API, доступ к базе данных и т.д.).

Обработав запрос бот снова отправляет сообщение телеграм, указав свой токен (как доказательство того, что он имеет право на эту функцию), сообщение и id пользователя, которому принадлежит запрос [4] (как частный случай, результатом невсегда будет сообщение - файл, викторина, кнопки и т.д.)

Получив сообщение телеграм проверяет корректность данных, и если все в порядке выполняет 2 запроса: один - пользователю с ответом, который он увидит в чате, второй - боту, в котором говорится об успешности запроса или сообщение об ошибке в противном случае [5].

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

BotFather

Первым делом нужно получить тот самый токен. По токену телеграм сможет авторизовать вас и дать возможность взаимодействовать с пользователем.

Для этого у телеграмм есть свой бот, который раздает токены, называется Bot Father (будьте внимательны, у настоящего есть галочка верификации).

Токен - это такая же секретная информация, как и пароль. Держите его в секрете, но если кто-то узнал ваш токен, BotFather может поменять текущий токен бота.

Все что нужно делать - вызывать нужные методы API по следующей схеме:

  • красный цвет всегда такой, это домен
  • вместо синего цвета нужно подставить свой токен
  • зеленый цвет представляет метод, который мы хотим выполнить
  • фиолетовый цвет заменяет дополнительные параметры для метода

Методы

Я покажу на примере библиотеки python requests, но можно использовать строку запроса браузера, postman, curl или любой инструмент, который умеет делать http запросы.

getMe

Создаем новый файл main.py и вводим следующее. Начнем с чего нибудь простого - метод getMe. Он возвращает информацию о боте. Итоговый url создаем соединением всех частей (<token> заменить на свой токен), для post запроса у requests есть метод post, в который можно передать дополнительные параметры в виде словаря (data). Пока что он пустой, но позже нам пригодится.

В результате мы получаем json с информацией о боте. Первый объект - "ok" имеет значение true если все хорошо и false в обратном случае. В случае ошибки запроса вместо result будет возвращен объект description с сообщением об ошибке. Попробуйте добавить в токен случайный символ и повторить запрос, получите следующее

getUpdates

Ключевой метод для получения обновлений - getUpdates. Напишите любое слово боту (первое сообщение боту будет команда /start, мы рассмотрим непосредственно сообщения) в телеграм, например "hello". Он не ответит т.к. даже не получит его. Для того чтобы бот получил это сообщение нужно запросить обновления у телеграм.

Запросим обновления, для этого изменим метод на /getUpdates. Получим следующий результат.

Здесь мы видим несколько важных объектов. Во-первых - список объектов Update в result. Каждый такой объект представляет входящее обновление. У каждого объекта должно быть обязательное поле update_id, представляющее идентификатор Update - и множество дополнительных значений. В нашем случае это message - объект, представляющий сообщение. У объекта сообщения есть свои значения, каждое из которых может быть или объектом (например from представляет объект User), или просто значением (например message_id) Значение по ключу date - количество секунд с 1 января 1970 года, т.е. так называемое unix время. Ну и наконец само сообщение находится по ключу text - "hello".

Достанем нужные нам значения - текст сообщения и id пользователя (chat_id, т.к. бот может писать/читать в группы и каналы). Для этого сначала переведем res.text из строки в словарь (json.loads(res.text)), теперь мы можем работать с объектом data как с обычным словарем python. Возьмем массив объектов Update, который находится по ключу result, пробежим по списку и выведем text и id.

sendMessage

Научимся отправлять сообщения обратно пользователю - для этого есть метод sendMessage, который принимает 2 обязательных параметра - id чата (chat_id) и сообщение (text), которое нужно отправить. Можно объединить строку, чтобы получить строку вида "https://api.telegram.org/bot<token>/sendMessage?chat_id=<chat_id>&text=<text>", но requests умеет добавлять параметры из словаря, если передать его вместе с вызовом post. Мы просто вернем сообщение обратно - так называемый "эхо" бот.

В чате с ботом вы увидите сообщение "hello" после вашего "hello". Отправим еще 2 сообщения, "hello 2" и "hello 3", а в вывод добавим update_id:

Мы уже ответили на первое сообщение, т.е. мы его обработали, но с каждым запросам getUpdates снова получаем его. Для того чтобы передать телеграм информацию об уже обработанных сообщениях, можно использовать параметр offset. Он говорит телеграм, что все объекты Update с номерами меньше чем указанный offset можно больше не хранить и не посылать.

Изменим словарь дата (который посылаем с методом getUpdates) и теперь получим только последнее сообщение (его update_id заканчивается на 235).

Все что нам осталось - сделать бесконечный цикл, каждый раз вызывая getUpdates, обрабатывая запрос, возвращая ответ и увеличивая offset после каждого обработанного запроса. В конце цикла я поставил задержку в 2 секунды.

Добавим после получения каждого вызова обновления время в секундах и количество запросов, и получим следующее (я отправил последовательно сообщения "hello", "hello 1" и "hello 2"). Теперь мы получаем только необработанные сообщения.

Можно заметить, что обновление происходит не каждые 2 секунды, а немного больше. Это так потому что при вызове метода post мы должны дождаться ответа от телеграм, а это занимает какое-то время. Все происходит в синхронном режиме.

Вот альтернативный вариант - один поток только делает запросы и складывает запросы в очередь, другой поток берет из очереди запросы, обрабатывает и отправляет ответ (паттерн производитель-потребитель или producer-consumer). Обработчиков может быть больше, для этого можно использовать пул потоков. Но мы остановимся здесь, чтобы все не усложнять.

Как только мы научились отправлять то же самое сообщение, нужно добавить логику обработки запроса. Сообщение пользователя у нас уже есть, осталось понять как обработать этот запрос.

Пусть наш бот выдает погоду и может выполнять 2 команды: /start с описанием бота и по названию города возвращает погоду (мы не будем реализовывать сам функционал, просто вернем строку), а для всех остальных запросов возвращает строку "мы не смогли понять ваш запрос". Для этих целей вполне подойдет простой if elif else (метод is_city должен проверить является ли отправленное сообщение городом, и если да то получить значение температуры и заменить +15 на это значение.

Протестируем

Но мы сделаем вариант более универсальным. Для начала создадим список обработчиков handlers. Элементом списка будут 2 функции - первая функция обрабатывает запрос (handler), а вторая - функция предикат (predicate). Она возвращает true или false в зависимости от того, подходит запрос для обработки данной функции или нет (обычно они называются handlers и filters).

Обработка запроса теперь выглядит следующим образом. Мы получаем объект update, проходим по списку handlers и пытаемся вызвать функцию precicate для данного update. Если метод возвращает true, то метод handle (который идет вместе с этим методом predicate) может обработать данное сообщение и передаем объект update ему. Если нет - переходим к следующему элементу, т.е. к следующей функции predicate. Последний предикат всегда возвращает true, он готов обработать любое сообщение. Так мы можем настроить очень гибкую проверку в функции predicate.

напишем функцию get_handler

И будем вызывать в главном цикле. Результат будет такой же, как и с if elif else, но теперь мы можем добавить predicate и handler, которые обрабатывают сообщения только для определенных chat_id, если время в указанном диапазоне, если в update есть объект callback_data и т.д.

Форматирование текста

Сообщения, которые бот отправляет пользователю можно форматировать. Для этого сначала нужно выбрать режим (parse_mode) - HTML или markdown. После этого можно использовать разметку HTML или markdwown в тексте (текст взят из документации).

В результате придет следующее сообщение

Удаление и изменение сообщений

Сообщения можно изменять и удалять (изменять бот может только свои сообщения, удалять любые). Для изменения существует метод editMessageText, который принимает 3 основных параметра - id чата, id сообщения (можно получить из объекта update) и новый текст.

Для удаления используется метод deleteMessage, которому нужно передать id чата и id сообщения

Клавиатура

Для бота есть возможность создания клавиатуры. Есть 2 вида

  • простая клавиатура

Для ее создания нам нужно создать словарь, в котором прописать вид и параметры клавиатуры. Главный параметр - это keyboard, т.е. сама клавиатура. Она представляет список списка кнопок. Каждый список представляет строку, а каждый элемент в строке - кнопку. У каждой кнопки должен быть параметр text. Создадим клавиатуру из 2-х строк, в 1-ой две кнопки, во 2-ой - одна. Параметр resize_keyboard позволяет изменять размеры клавиатуры для лучшего представления, one_time_keyboard говорит о том, нужно ли убрать клавиатуру после нажатия. Функция json.dumps позволяет сделать из словаря строку.

Вот что получит пользователь. При нажатии на кнопку будет отправлен текст на этой кнопке

  • inline клавиатура

Такая клавиатура напоминает кнопки в виде сообщения. Легче увидеть, чем объяснить. Замените reply_markup на следующее

Здесь есть новые параметры: callback_data - при нажатии на кнопку будет совершен новый запрос с полем callback_data и значением, которое так указано (но никаких сообщений отправлено не будет). Так вы сможете понять, какая кнопка была нажата. url - при нажатии на кнопку вы перейдете на указанный url. Заметим что для кнопки с указанным url в правом верхнем углу есть стрелочка.

Нажмем на кнопку и получим обновления методом getUpdates (большинство объектов заменено на ...).

Здесь мы видим ценную информацию - callback_query id, from - от кого пришел callback, message - оригинальное сообщение, chat_instance - глобальный id, используется для рейтинга в играх и data - это то, что написано в callback_data той кнопки, что нажал пользователь. Так мы можем получить нажатую кнопку и обработать ее (как пример можно использовать при пагинации, добавьте 2 inline кнопки вправо и влево, при нажатии вправо удаляйте сообщение и отсылайте новое с другой страницей результатов).

Сообщения в группы

Бот может отправлять сообщения в каналы/группы. Логика ничем не отличается от личных сообщений, нужно только добавить бота в администраторы (чтобы он имел возможность посылать сообщения в группу) и узнать id чата группы. Для этого создайте группу, добавьте своего бота и сделайте его админом, затем пошлите любое сообщение (например букву "q"). Получите обновление и вы увидите следующее

Sender_chat и chat будут одинаковые в этом случае, но не всегда - sender - тот, кто отправил, chat - в каком чате. Нам нужно значение id, оно начинается с "-". Если вы отправите сообщение с помощью метода sendMessage и укажите этот id в chat_id - сообщение будет отправлено в канал. Так можно настроить отложенную публикацию или публикацию по времени (например с помощью cron). Или просто настроить бота таким образом, чтобы при определенном слове он выводил нужную информацию в группе - может быть полезно в рабочей группе.

Отправка файлов

Для отправки файлов конкретного типа есть свои методы, посмотрим на примере photo, а остальные выглядят аналогично с заменой пары строк.

Для начала я создам 3 изображения, каждое изображение - квадрат 100x100 с одной буквой: A, B и C. Сохраню эти изображения как a.png, b.png и c.png.

Для передачи одного изображения используется метод /sendPhoto. При post запросе есть возможность передать файл. В requests для этого используется параметр files. Он представляет собой словарь, ключом является название файла, а значением или сам файл (набор байт), или файловый дескриптор. Дескриптор - это некоторое число, с которым операционная система связывает данный файл (возможно устройство) с потоком ввода-вывода. Дескриптор можно получить с помощью команды open (в языке python). Open принимает название файла, который нужно открыть и режим. Используем "rb", где r - read, т.е. мы будем читать файл, а b - указывает что данные представлены в двоичном виде. Попробуем открыть файл и выведем на экран файл дескриптора и сам файл (частично)

BufferedReader - это высокоуровневая абстракция дескриптора - что-то, из чего можно читать данные (в нашем случае байты). Теперь есть все, чтобы отправить изображение (caption - подпись изображения). Потоки нужно обязательно закрывать т.к. ОС не может создать бесконечно много дескрипторов (а еще проверять операции чтения/записи проверять на ошибки, что здесь опущено).

В результате получим в сообщении от бота

При отправке мы получим json файл следующего вида.

Теперь это изображение хранится на сервере телеграм, и если вы захотите отправить файл повторно - нужно сохранить значение file_id. Удалим параметр files и добавим photo со значением file_id, а так же изменим caption на again a.png

Ожидаемо получим еще одно такое же изображение

Для отправки нескольких изображений есть метод /sendMediaGroup. Нужно отправить несколько файлов и параметр media, который представляет список объектов InputMediaPhoto. Каждый объект имеет параметр type (для изображений он равен "photo"), media - это ключ в словаре файлов и caption - подпись.

Запускаем и получаем следующее

Аналогично есть методы /sendVideo, /sendDocument и т.д., а также sendMediaGroup для document, audio, video.

Опросы

Для создания опросов есть метод /sendPoll, который принимает chat_id, question и options - параметры говорят сами за себя.

Результат (какой же?)

WebHook

Для работы с webhook у вас должен быть tls сертификат, т.к. телеграм отсылает запросы только по https. Вы можете приобрести виртуальный сервер, купить домен и с помощью let's encrypt получить его бесплатно (для большинства целей его достаточно) или купить. Если просто хотите попробовать webhook - можете использовать ngrok. В двух словах, когда телеграм отправляет сообщения вашему боту, он отправляет их ngrok, а он уже вам. Когда вам нужно отправить сообщение телеграм, вы отправляете сообщение ngrok по http, а ngrok отправляет телеграм по https, т.е. выступает таким посредником.

Для начала скачайте и установите ngrok, запустите свой локальный сервер на определенном порту, запустите ngrok примерно так: ngrok http <port>, где <port> - порт приложения. Вы увидите следующее окно.

Вам нужно скопировать адрес, выделенный зеленым цветом, вставить в следующий url

https://api.telegram.org/bot<token>/setWebhook?url=<вот сюда>. После этого на ваш локальный сервер будут приходить запросы по мере того, как пользователи будут отправлять вам сообщения.

Админ панель

У многих сайтов есть так называемая админ панель. Для бота можно сделать что-то похожее - для этого создайте массив admins с id тех пользователей, которые имеют дополнительные права - например просмотр статистики, а также новый handler и predicate. Когда пользователь отправляет слово "статистика" в зависимости от того, является он админом или нет вернется, или сообщение со "статистикой" или сообщение "запрос не разобран".

Заключение

Конечно есть еще многое в bot api telegram. Например, есть возможность получать уведомления о том, что пользователь изменил свое сообщение, новый пользователь добавился в группу, inline режим, пересылать/копировать сообщения, и т.д. Посмотрите на интересный метод /sendDice - он принимает парамет emoji и возвращает рандомное число от 1 до n (в зависимости от emoji) в виде анимированного emoji. Выглядят они так

Если вам что-то нужно - переходите сюда, ищите справа доступные методы (avaliables methods) и ищите нужную функцию. Если параметр функции объект - ищите его в разделе доступные типы (avaliable types). Или в документации библиотеки, которую вы используете. Для более приятного разбора json формата можете воспользоваться этим сайтом.

- - - - - - - - - - - - - - - -

Для тех, кто дочитал до конца

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

  • поделиться ссылкой на канал [ https://t.me/line_of_code ]
  • дополнить/уточнить статью в комментариях (я не знаю всего и я могу ошибаться) или предложить интересную тему
  • поддержать материально - https://yoomoney.ru/to/4100117706200369

Report Page