На Хабре много статей про разработку Телеграм ботов на Python, в том числе при помощи Django. Однако, большинство из них направлено на первичное ознакомление с API Телеграм.
В этой статье я хочу рассказать как можно упростить разработку ботов, сократив количество кода в разы, и не создавать велосипед.
Всем, привет! Меня зовут Александр Алескин, и я участвовал в создании десятках Telegram ботов. Достаточно часто я встречался с одними и теми же проблемами при разработке из-за нехватки инструментов: в Телеграме нет web-форм -невозможно от пользователя одновременно запросить несколько атрибутов; поддержка локализации; учет состояния пользователя для отображения информации; над разными типами данных происходят схожие действия, но нужно писать отдельные обработчики под каждый тип и т.д.
На самом деле все эти проблемы достаточно легко решаются по одиночке. Однако, при расширении функциональности бота это неминуемо приводит к дублированию кода в той или иной степени, а также к путанице среди функций.
В этой статье пойдет речь об инструментах, которые позволяют стандартизировать разработку ботов. Большинство из них широко применяется в Web-разработке и присутствуют в Django (или Django Rest Framework). Данные инструменты кастомизированы для использования под Телеграм ботов в библиотеке Telegram Django Bot, о которой и будет рассказано далее.
Telegram-Django-Bot библиотека
В основе Telegram-Django-Bot лежит использование следующих двух библиотек:
Python-Telegram-Bot – библиотека для взаимодействия с Telegram API (22k звезд на github).
Django – фреймворк для разработки веб-сервисов (в ТОП-10 фреймворков в мире).
По факту Telegram-Django-Bot плотно связывает между собой эти библиотеки и предоставляет схожий с Django Rest Framework инструмент для управления данными.
Viewsets и Формы
Ключевой сущностью Telegram-Django-Bot является TelegramViewSet
– абстрактный класс для управления моделями (таблицами) базы данных, описанных через Django ORM. Данный класс позволяет создавать, отображать, удалять и отображать элементы сущности. Как и в Django Rest Framework Viewsets по необходимости вы можете добавлять свои методы для обработки данных. Однако, в отличие от web аналога, данный класс формирует ответ не в виде json или xml, а на естественном языке в виде текста и кнопок для дальнейшего отображения пользователю в Телеграмме. То есть совмещает обработку данных и формирование интерфейса для пользователей.
Такой подход шаблонизирует обработку данных и общение бота с пользователем. В итоге это позволяет значительно сократить количество кода.
Легче всего продемонстрировать этот подход на примере Telegram_django_bot_template. Данный репозиторий создан как шаблон для создания ботов и содержит небольшие примеры по использованию.
Для более подробного и реального примера можно рассмотреть живого бота @Disk_Drive_Bot (исходный код бота). Основная цель бота – хранение файлов в Телеграме, так же как это делает Яндекс диск или Гугл драйв.
Так, например, в коде шаблона задается управление некоторой сущностью BotMenuElem (таблицей в базе данных) в 40 строчках кода:
class BotMenuElemViewSet(TelegramViewSet):
viewset_name = 'BotMenuElem' # Название Вьюсета для отображения
model_form = BotMenuElemForm # Форма, где указываются поля для забора от клиента и их валидация. Дочерний класс от Django.forms.ModelForm
queryset = BotMenuElem.objects.all() # указываем, что за модель базы данных
foreign_filter_amount = 1 # количество переменных окружения (дополнительный атрибут, детали в документации)
prechoice_fields_values = { # Часто используемые варианты значений атрибутов или просто формат отображения некоторых значений атрибутов
'is_visable': ( # варианты для переменной is_visable в формате (значение, текст)
(True, '???? Visable'),
(False, '???? Disabled'),
)
}
def get_queryset(self): # переопределение вспомогательной функции для учета бизнес логики
queryset = super().get_queryset()
if self.foreign_filters[0]: # хотим, чтобы если указана переменная окружения, то отображали только те элементы, в которых поле команда начинались со значения переменной окружения
queryset = queryset.filter(command__contains=self.foreign_filters[0])
return queryset
def create(self, field=None, value=None, initial_data=None): # переопределение функции создания элемента
initial_data = { # просто хотим указать стартовые значения для некоторых атрибутов
'is_visable': True,
'callbacks_db': '[]',
'buttons_db': '[]',
}
return super().create(field, value, initial_data)
def show_list(self, page=0, per_page=10, columns=1): # переопределение функции отображения списка элементов
reply_action, (mess, buttons) = super().show_list(page, per_page, columns)
buttons += [ # к дефолтному отображению элементов в виде текста и кнопок добавляем кнопки «создание» и «назад»
[InlineKeyboardButtonDJ(
text=_('➕ Add'),
callback_data=self.gm_callback_data('create')
)],
[InlineKeyboardButtonDJ(
text=_('???? Back'),
callback_data=settings.TELEGRAM_BOT_MAIN_MENU_CALLBACK
)],
]
return reply_action, (mess, buttons) # для ответа пользователю возвращаем формат ответа и атрибуты ответа (в данном случае текст сообщения и кнопки)
И эти 40 строчек позволяют создавать, изменять, удалять и просматривать список элементов BotMenuElem
в боте. В результате такого описания BotMenuElemViewSet
получаем необходимую бизнес логику и пользовательский интерфейс в боте:
Магия заключается в том, что базовая логика взаимодействия и отображения элементов описана в родительском классе TelegramViewSet
. В нем присутствуют следующие стандартные функции:
Метод |
Описание |
---|---|
create |
Создание модели |
change |
Изменения атрибутов |
delete |
Удаление модели |
show_elem |
Отображение элемента |
show_list |
Отображение списка элементов |
Для удобства кастомизации бизнес логики эти функции вызывают вспомогательные методы внутри. Так, например, в BotMenuElemViewSet
переопределяется только одна вспомогательная функция get_queryset
под особенности контекста.
Среди функций TelegramViewSet
особый интерес представляют create
и update
, так как они осуществляют плотную работу с формами (структурой из нескольких атрибутов, которые указывает пользователей). Так как Телеграм не умеет работать с формами, то здесь используется последовательный подход (как и ConversationHandler: сначала пользователей указывает одно поле, затем второе и т.д.:
Для этого, как и в Django, создается форма данных, которая указывается в BotMenuElemViewSet:
# base/forms.py
from telegram_django_bot import forms as td_forms
class BotMenuElemForm(td_forms.TelegramModelForm):
class Meta:
model = BotMenuElem
fields = ['command', "is_visable", "callbacks_db", "message", "buttons_db"]
# base/views.py
class BotMenuElemViewSet(TelegramViewSet):
model_form = BotMenuElemForm
…
Роутинг
Структурность кода одна из важных составляющих для поддержания проекта. Для этого Django предлагает разбивать проект на приложения (apps), каждое из которых отвечают за свою задачу (например, оплата или интеграция с каким-нибудь другим сервисом).
Telegram-Django-Bot делает возможным использование этого механизма при разработке ботов через универсальный обработчик RouterCallbackMessageCommandHandler
. Данный класс необходимо добавить в перечень обработчиков веб-сервера Python-Telegram-Bot:
from telegram_django_bot.tg_dj_bot import TG_DJ_Bot
from telegram_django_bot.routing import RouterCallbackMessageCommandHandler
# look in the template: https://github.com/alexanderaleskin/telergam_django_bot_template/blob/main/run_bot.py#L19
# in 13.x version of Python-Telegram-Bot:
updater = Updater(bot=TG_DJ_Bot(settings.TELEGRAM_TOKEN))
updater.dispatcher.add_handler(RouterCallbackMessageCommandHandler())
# or in 20.x version :
bot = TG_DJ_Bot(settings.TELEGRAM_TOKEN)
application = ApplicationBuilder().bot(bot).build()
application.add_handler(RouterCallbackMessageCommandHandler())
После чего можно прописывать пути к конкретным обработчикам (endpoints) и viewsets в стандартной для Django нотации:
# файл bot_conf/utrls.py (основной)
urlpatterns = [
re_path('', include(('base.utrls', 'base'), namespace='base')), # подключаем пути из модуля base
]
# файл base/utrls.py
from django.urls import re_path, include
from .views import start, BotMenuElemViewSet, UserViewSet # импорт veiwset и обычной функции
urlpatterns = [ # добавляем пути
re_path('start', start, name='start'),
re_path('main_menu', start, name='start'),
re_path('sb/', BotMenuElemViewSet, name='BotMenuElemViewSet'),
re_path('us/', UserViewSet, name='UserViewSet'),
]
# Теперь по нажатию на кнопку с callback_data='sb/<suffix>' будут вызываться методы BotMenuElemViewSet
Как и в Django, необходимо указать главный файл с маршрутизацией запросов в настройках проекта (в settings.py фале):
TELEGRAM_ROOT_UTRLCONF = 'bot_conf.utrls'
Как результат таких манипуляций приложение может быть разбито на несколько папок, в каждой из которых храниться своя независимая бизнес логика (а если вы пишите не один бот, а несколько, то такой подход позволяет легко подключать отдельные модули).
Дополнительные инструменты библиотеки
В библиотеке есть также ряд инструментов, которые часто нужны в продакшн ботах:
-
Встроенные модели в базе данных:
ActionLog – хранит действие пользователей (нажатия на кнопки, нажатие на команды и тд). Данная модель позволяет анализировать активность и действия пользователей;
TeleDeepLink – хранит информацию откуда пришел пользователей (если пользователей перешел в бота по deeplink). Это информация помогает строить воронку продаж и отслеживать эффективность рекламы;
BotMenuElem - достаточно часто в боте есть блоки, где указывается статическая информация (например, сообщение о контактах поддержки, база знаний или стартовое сообщение). Данная модель дает возможность создавать такие блоки меню в боте, при этом указывать ссылки на другие элементы или функции/вьюсеты через кнопки. В примере темплейта как раз используется эта модель (стандартное создание предполагается через администраторскую панель).
Trigger – при наступлении определенных событий (смотрит в ActionLog) отправляет определенные сообщения пользователям (BotMenuElem). Так, например, можно отправлять сообщения пользователям, которые стартанули бота и не продолжили взаимодействия, или попросить пользователей, которые пользуются больше 7 дней к примеру, дать обратную связь о взаимодействии с ботом;
-
Дополнительные функции:
telegram_django_bot.utils.handler_decor – обертка для своих обработчиков (Пример использования; вьюсеты автоматически оборачиваются в декоратор). Позволяет логировать действие пользователей, ловить и логировать все ошибки, а также обрабатывать создание пользователей;
Telegram_django_bot.utils.CalendarPagination– класс для генерации календаря в меню бота в виде активных кнопок (по нажатию можно перейти в другие разделы);
Telegram_django_bot.tg_dj_bot.TG_DJ_Bot – дочерний класс telegram.Bot, который добавляет ряд функций в том числе task_send_message_handler для безопасной рассылки сообщений группе пользователей (обрабатывает ошибки при отправке сообщений).
Локализация – используется инструменты локализации Django.
Заключение
Telegram-Django-Bot ускоряет создание Телеграм ботов за счет стандартизации действий. Важным инструментом библиотеки является TelegramViewSet
класс, использование которого позволяет в несколько строк описать как обрабатывать и отображать данные пользователю.
Посмотреть, как это работает, а также запустить самому можно на примерах Telegram_django_bot_template и Disk_Drive_Bot.
Очень интересна ваша обратная связь, а также предложения по улучшению. И особо приветствуются реквесты в репозитории!