На Хабре много статей про разработку Телеграм ботов на Python, в том числе при помощи Django. Однако, большинство из них направлено на первичное ознакомление с API Телеграм.

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

Всем, привет! Меня зовут Александр Алескин, и я участвовал в создании десятках Telegram ботов. Достаточно часто я встречался с одними и теми же проблемами при разработке из-за нехватки инструментов: в Телеграме нет web-форм -невозможно от пользователя одновременно запросить несколько атрибутов; поддержка локализации; учет состояния пользователя для отображения информации; над разными типами данных происходят схожие действия, но нужно писать отдельные обработчики под каждый тип и т.д.

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

В этой статье пойдет речь об инструментах, которые позволяют стандартизировать разработку ботов. Большинство из них широко применяется в Web-разработке и присутствуют в Django (или Django Rest Framework). Данные инструменты кастомизированы для использования под Телеграм ботов в библиотеке Telegram Django Bot, о которой и будет рассказано далее.

Telegram-Django-Bot библиотека

В основе Telegram-Django-Bot лежит использование следующих двух библиотек:

  1. Python-Telegram-Bot – библиотека для взаимодействия с Telegram API (22k звезд на github).

  2. 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'

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

Дополнительные инструменты библиотеки

В библиотеке есть также ряд инструментов, которые часто нужны в продакшн ботах:

  1. Встроенные модели в базе данных:

    1. ActionLog – хранит действие пользователей (нажатия на кнопки, нажатие на команды и тд). Данная модель позволяет анализировать активность и действия пользователей;

    2. TeleDeepLink – хранит информацию откуда пришел пользователей (если пользователей перешел в бота по deeplink). Это информация помогает строить воронку продаж и отслеживать эффективность рекламы;

    3. BotMenuElem - достаточно часто в боте есть блоки, где указывается статическая информация (например, сообщение о контактах поддержки, база знаний или стартовое сообщение). Данная модель дает возможность создавать такие блоки меню в боте, при этом указывать ссылки на другие элементы или функции/вьюсеты через кнопки. В примере темплейта как раз используется эта модель (стандартное создание предполагается через администраторскую панель).

    4. Trigger – при наступлении определенных событий (смотрит в ActionLog) отправляет определенные сообщения пользователям (BotMenuElem). Так, например, можно отправлять сообщения пользователям, которые стартанули бота и не продолжили взаимодействия, или попросить пользователей, которые пользуются больше 7 дней к примеру, дать обратную связь о взаимодействии с ботом;

  2. Дополнительные функции:

    1. telegram_django_bot.utils.handler_decor – обертка для своих обработчиков (Пример использования; вьюсеты автоматически оборачиваются в декоратор). Позволяет логировать действие пользователей, ловить и логировать все ошибки, а также обрабатывать создание пользователей;

    2. Telegram_django_bot.utils.CalendarPagination– класс для генерации календаря в меню бота в виде активных кнопок (по нажатию можно перейти в другие разделы);

    3. Telegram_django_bot.tg_dj_bot.TG_DJ_Bot – дочерний класс telegram.Bot, который добавляет ряд функций в том числе task_send_message_handler для безопасной рассылки сообщений группе пользователей (обрабатывает ошибки при отправке сообщений).

  3. Локализация – используется инструменты локализации Django.

Заключение

Telegram-Django-Bot ускоряет создание Телеграм ботов за счет стандартизации действий. Важным инструментом библиотеки является TelegramViewSet класс, использование которого позволяет в несколько строк описать как обрабатывать и отображать данные пользователю.

Посмотреть, как это работает, а также запустить самому можно на примерах Telegram_django_bot_template и Disk_Drive_Bot.

Очень интересна ваша обратная связь, а также предложения по улучшению. И особо приветствуются реквесты в репозитории!

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