Разработка ТамТам-бота на Python

от автора

Привет, Хабр! Позвольте представиться: меня зовут Сергей Агальцов, и я «программист по жизни». Это значит, что я давно уже IT-менеджер, а вовсе не программист по профессии, но программирование использую постоянно, как в своей основной деятельности, так и как хобби. Как часто говорил один из моих бывших начальников — «Серёга! Ты опять скатился в программирование!» Правда, не могу сказать, что этим был сильно не доволен он или кто-то другой когда-либо.

После появления Bot API у мессенджера ТамТам, я как истинный, а значит ленивый программист, создал 2 библиотеки Python для работы с ним:

  • open API клиента (далее — OAC) — изначально сгенерировал её при помощи OpenAPI Generator на основе схемы API, затем адаптировал с учётом особенностей генератора;
  • оболочку для этого клиента — TamTamBot (далее — TTB), упрощающую работу с OAC.

Так появился некий ТамТам Python SDK.

Сделал это я в первую очередь «для себя, для души», но также предложил и комьюнити ТамТам при желании им воспользоваться. Но, как известно, ни одно доброе дело не остаётся безнаказанным — народ попросил написать обучающую статью. И вот я здесь с этой статьёй. В ней я расскажу, как разработать простого бота при помощи этих библиотек.

Задача

Разработать бот, предназначенный для упрощения действий разработчиков ботов. Бот должен работать в режиме перманентного опроса состояния bot-api (long polling). В этой статье бот будет обучен показывать внутренности пересылаемого ему сообщения, а также настроен в соответствие разработанной функциональности.

Подразумевается, что у читателя имеется установленный Python 3, git, подключенный к среде разработки PyCharm (среда разработки может быть и другой, но рассказ будет на основе PyCharm). Желательно понимание основ ООП.

Получение токена бота

Получение токена происходит через обращение к специализированному боту @PrimeBot

Находим этого бота в ТамТам, вводим команду /create и отвечаем на вопросы:

  • Введите уникальное короткое имя бота латиницей — это юзернейм бота, по которому он будет доступен через @ или по ссылке вида https://tt.me/username. Особых ограничений на юзернейм нет. В частности, слово bot необязательно.
  • Введите имя — это отображаемое имя бота. Здесь уже можно кириллицей.

Если всё введено корректно, то созданный бот будет добавлен в контакты и в ответ мы получим токен — последовательность символов вида: HDyDvomx6TfsXkgwfFCUY410fv-vbf4XVjr8JVSUu4c.

Первичная настройка

Показать

Создаём каталог:

mkdir ttBotDevHelper

Переходим в него:

cd ttBotDevHelper/

Инициализируем хранилище git:

git init

Качаем необходимые библиотеки, добавляя их как подмодули git:

git submodule add https://github.com/asvbkr/openapi_client.git openapi_client git submodule add https://github.com/asvbkr/TamTamBot.git TamTamBot

Открываем созданный каталог в PyCharm (например из проводника по контекстному меню «Open Folder as PyCharm project») и создаём файл, который и будет содержать наш бот — File/New/Python file. В появившемся диалоге вводим имя — ttBotDevHelper, и отвечаем положительно на вопрос о добавлении в git.

Теперь нужно создать виртуальную среду для нашего проекта.

Для создания виртуальной среды выбираем File/Settings и на закладке проекта выбираем подраздел Project Interpreter. Далее, справа нажимаем на иконку шестерёнки и выбираем Add:

image

PyCharm предложит свой вариант размещения.

image

Имеет смысл с ним согласиться.

После создания виртуальной среды откроется предыдущий экран, но на нём уже будет информация о созданной среде. На этом экране необходимо установить необходимые пакеты, нажимая справа иконку с «+» и вводя имена пакетов:

  • requests
  • six

Затем добавляем к проекту файл .gitignore, исключающий файлы, не требующиеся в git, со следующим содержимым:

venv/ .idea/  # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class  *.log *.log.*  .env ttb.sqlite3

Добавим переменную среды с именем TT_BOT_API_TOKEN, в которой укажем значение токена нашего бота, полученного от https://tt.me/primebot и перезапустим PyCharm.

(!) Вместо добавления переменной среды непосредственно в окружение ОС, в PyCharm оптимально использовать специальный файл .env. Его настройка будет рассмотрена ниже.

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

Запуск простейшего бота

Открываем файл ttBotDevHelper.py и пишем первые строки:

# -*- coding: UTF-8 -*- from TamTamBot.TamTamBot import TamTamBot  class BotDevHelper(TamTamBot):     pass

Здесь мы создаём класс своего бота, основываясь на классе TamTamBot.

PyCharm подсказывает, что класс BotDevHelper содержит абстрактные методы, которые необходимо имплементировать. Нажимаем Alt-Enter на названии класса, выбираем «Implement abstract methods», выбираем все методы (их 2), предложенные PyCharm и нажимаем ОК. В результате будут добавлены два пустых метода-свойства: token и description. Модифицируем получившийся код следующим образом:

# -*- coding: UTF-8 -*- import os  from TamTamBot.TamTamBot import TamTamBot from TamTamBot.utils.lng import set_use_django  class BotDevHelper(TamTamBot):     @property     def token(self):         return os.environ.get('TT_BOT_API_TOKEN')      @property     def description(self):         return 'Этот бот помогает в разработке и управлении ботами.\n\n' \                'This bot is an helper in the development and management of bots.'  if __name__ == '__main__':     set_use_django(False)     BotDevHelper().polling()

Свойство token возвращает токен нашего бота, значение которого берётся из переменной окружения TT_BOT_API_TOKEN. Свойство description возвращает расширенное описание нашего бота, которое будет показываться пользователям.

Код в конце файла необходим для запуска нашего бота в режиме опроса состояния.

Отмечу, что базовый класс TamTamBot предполагает использование веб-сервера django для работы в режиме вебхуков. Но сейчас задача проще, и django нам не требуется, о чём мы и сообщаем в строке set_use_django(False). Здесь для объекта нашего класса вызывается метод polling(), который и обеспечивает работу в нужном режиме.

Минимум необходимого сделан. Этот код уже вполне рабочий. Запустим его на выполнение. Для этого нажмём комбинацию клавиш Ctrl-Shift-F10.

Если Вы не добавляли переменную среды ранее, непосредственно в ОС, то при запуске произойдёт ошибка с сообщением «No access_token». Для её устранения настройте PyCharm на использование .env-файла.

Показать как

Создайте текстовый файл .env. Его содержимое в нашем случае должно быть следующим:

TT_BOT_API_TOKEN=токен_нашего_бота

Теперь нужно его подключить к конфигурации запуска в PyCharm:

Выбираем Run/Edit configuration и на закладке EnvFile подключаем наш .env-файл:

image

После чего нажимаем Apply.

После запуска бота можно перейти в ТамТам, открыть диалог с нашим ботом и нажать кнопку «Начать». Бот сообщит информацию о своих скрытых суперспособностях. Это и означает, что бот работает. Пока бот работает в демо-режиме, в котором доступны 4 команды. Просто ознакомьтесь с ними.

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

Приём сообщения-источника и отправка ответного сообщения с внутренним представлением сообщения-источника

Перекроем метод receive_text(), управление которому передаётся при отправке текста в чат с ботом:

    def receive_text(self, update):         res = self.msg.send_message(NewMessageBody(f'Ваше сообщение: {update.message}', link=update.link), user_id=update.user_id)         return bool(res)

Объект update класса UpdateCmn, который передаётся в данный метод, содержит различную полезную информацию и, в частности, всё то, что нам сейчас необходимо:

  • update.message — объект, содержащий само сообщение;
  • update.link — готовая ответная ссылка на это сообщение;
  • update.user_id — идентификатор пользователя, отправившего сообщение.

Для отправки сообщения от бота используем переменную self.msg, в которой содержится объект MessagesApi, реализовывающий функциональность, описанную в разделе messages описания API. Этот объект содержит нужный нам метод send_message(), который и обеспечивает отправку сообщений. По минимуму, в этот метод необходимо передать объект класса NewMessageBody и адресата — идентификатор пользователя, в нашем случае.

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

Перезапускаем нашего бота и проверяем в диалоге с ботом, что на любое наше сообщение бот формирует ответ, содержащий внутреннее представление объекта сообщения-источника.

Исходный код данного состояния см. здесь.

Добавление новой команды бота с параметром — показ внутреннего представления сообщения по его идентификатору

При разработке ботов зачастую требуется посмотреть внутреннее представление сообщения по одному или нескольким известным идентификаторам сообщений (message id — mid). Добавим такую функциональность нашему боту. Для этого, вначале, вынесем в отдельный метод функциональность вывода информации о внутреннем представлении сообщений:

    def view_messages(self, update, list_mid, link=None):         res = False         msgs = self.msg.get_messages(message_ids=list_mid)         if msgs:             for msg in msgs.messages:                 r = self.msg.send_message(NewMessageBody(f'Сообщение {msg.body.mid}:\n`{msg}`'[:NewMessageBody.MAX_BODY_LENGTH], link=link), user_id=update.user_id)                 res = res or r         return res

В этот метод мы передаём список mid.

Для получения объектов сообщений мы используем метод self.msg.get_messages, возвращающий список объектов в свойстве messages.

Далее, текстовое представление каждого из полученных сообщений отправляем в наш диалог отдельными сообщениями. Чтобы избежать ошибки, текст формируемого сообщения обрезается по константе максимальной длины сообщения — NewMessageBody.MAX_BODY_LENGTH.

Затем добавим метод, обрабатывающий команду. Назовём её vmp. В команду можно будет передать список mid через пробел.

ТТБ спроектирован таким образом, что обработчик команды должен создаваться как метод с именем cmd_handler_%s, где %s — имя команды. Т.е. для команды vmp метод будет называться cmd_handler_vmp. В обработчик команды передаётся объект класса UpdateCmn. Дополнительно, для команды он может содержать свойство cmd_args, в котором содержится словарь строк и слов в них, которые были введены вместе с командой

Код будет выглядеть так:

    def cmd_handler_vmp(self, update):         res = None         if not update.this_cmd_response:  # Это прямой вызов команды, а не текстовый ответ на команду             if update.cmd_args:  # Если вместе с командой сразу переданы аргументы                 list_id = []                 parts = update.cmd_args.get('c_parts') or []                 if parts:                     for line in parts:                         for part in line:                             list_id.append(str(part))                 if list_id:                     res = self.view_messages(update, list_id, update.link)         return bool(res)

Перезапускаем бот. Теперь, если в диалоге бота набрать команду вида: /vmp mid1 mid2 (mid’ы можно взять их предыдущих проверок), то в ответ мы получим два сообщения с внутренним представлением объектов сообщений-источников, по каждому из переданных mid.

Исходный код данного состояния см. здесь.

Модификация команды бота для работы с текстовым ответом

Также можно попробовать переслать сообщение из другого канала/чата. Но в этом случае будет показано только то, что содержится в сообщении-источнике, находящемся в диалоге с ботом. В частности, при пересылке сообщения не сохраняется информация о кнопках.

Но что делать, если мы хотим увидеть информацию об оригинальном сообщении? В этом случае нужно брать mid из пересланного сообщения.

Для реализации этого режима модифицируем команду vmp таким образом, чтобы при её вызове без параметров она ожидала пересылки сообщения, а после этого брала mid пересланного сообщения и выводила информацию о нём.

(!) Для корректной работы данного функционала, боту должно быть предоставлено разрешение на чтение из канала/чата источника.

Код команды модифицируем следующим образом:

    def cmd_handler_vmp(self, update):         res = None         if not update.this_cmd_response:  # Это прямой вызов команды, а не текстовый ответ на команду             if update.cmd_args:  # Если вместе с командой сразу переданы аргументы                 list_id = []                 parts = update.cmd_args.get('c_parts') or []                 if parts:                     for line in parts:                         for part in line:                             list_id.append(str(part))                 if list_id:                     res = self.view_messages(update, list_id, update.link)             else:  # Вывод запроса для ожидания ответа                 self.msg.send_message(NewMessageBody(f'Перешлите *одно* сообщение канала/чата для показа его свойств:'), user_id=update.user_id)                 update.required_cmd_response = True  # Сообщаем о необходимости ожидания текстового ответа         else:  # Текстовый ответ команде             message = update.message             link = message.link  # Доступ к пересланному сообщению через свойство link             # Проверим - пересылка ли это.             if link and link.type == MessageLinkType.FORWARD:                 res = self.view_messages(update, [link.message.mid], update.link)             else:                 # Выведем сообщение об ошибке и сообщим в коде возврата, что команда не отработала.                 self.msg.send_message(NewMessageBody(f'Ошибка. Необходимо *переслать* сообщение из канала/чата. Повторите, пожалуйста.'), user_id=update.user_id)                 return False          return bool(res)

А т.к. при таком подходе увеличивается риск из-за отсутствия доступа к сообщениям, то в метод view_messages() добавим проверку на соответствие количества запрошенных/полученных сообщений:

    def view_messages(self, update, list_mid, link=None):         res = False         msgs = self.msg.get_messages(message_ids=list_mid)         if msgs:             # Сравнение количества переданных mid с количеством полученных сообщений             if len(msgs.messages) < len(list_mid):                 self.msg.send_message(NewMessageBody(                     f'Не удалось получить все запрошенные сообщения. Проверьте доступ бота @{self.username} к каналам/чатам этих сообщений.', link=update.link                 ), user_id=update.user_id)                 return False             else:                 for msg in msgs.messages:                     r = self.msg.send_message(NewMessageBody(f'Сообщение {msg.body.mid}:\n`{msg}`'[:NewMessageBody.MAX_BODY_LENGTH], link=link), user_id=update.user_id)                     res = res or r         return res

Перезапускаем бот, даём команду /vmp и после вывода приглашения о необходимости пересылки, пересылаем сообщение из канала/чата. При наличии у бота прав на чтение сообщений в этом канале/чате, будет выведено текстовое представление объекта пересланного сообшения. Если доступа нет, то бот сообщит о возможной проблеме и будет ожидать пересылки из корректного источника.

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

Теперь осталось навести лоск. Перекроем свойство about, возвращающее текст, который бот выводит при начале работы с ним, а также по команде /start.

    @property     def about(self):         return 'Этот бот помогает в разработке и управлении ботами.'

Перекроем метод get_commands(), возвращающий список команд нашего бота, отображающийся в диалоге с ботом.

    def get_commands(self):         # type: () -> [BotCommand]         commands = [             BotCommand('start', 'о боте'),             BotCommand('menu', 'показать меню'),             BotCommand('vmp', 'показать свойства сообщения'),         ]         return commands

Перекроем свойство main_menu_buttons, возвращающее список кнопок главного меню, вызываемого по команде /menu.

    def main_menu_buttons(self):         # type: () -> []         buttons = [             # Кнопка будет выведена цветом по умолчанию - серым             [CallbackButtonCmd('О боте', 'start')],             # Кнопка будет выведена цветом для позитивных действий - синим. Также есть негативная - красная             [CallbackButtonCmd('Показать свойства сообщения', 'vmp', intent=Intent.POSITIVE)],         ]          return buttons

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

Исходный код данного состояния см. здесь.

Работающего бота @devhelpbot можно посмотреть здесь.

На этом, пока, всё. Если тема заинтересовала, то в следующих статьях могу рассмотреть дальнейшее развитие бота. Например, добавление кастомных кнопок (в частности Да/Нет) и их обработку, отсылку различного вида контента (файлы, фото и пр.), работа в режиме webhook и т.п.


ссылка на оригинал статьи https://habr.com/ru/company/mailru/blog/466373/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *