Pull to refresh

Конфигурация вместо кода при написании Telegram-бота

Level of difficultyMedium
Reading time4 min
Views9.3K

Привет! Меня зовут Никита и я пишу от имени небольшой команды студентов, разработчиков проекта Cloffer — система онлайн-заказа для кофеен. Мы решили начать наш путь с написания связки бэкенд + набор телеграм-ботов. Эта статья будет посвящена подходу, который мы использовали для реализации именно телеграм-части.

Часть 1. Мотивация

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

Первое — мы пишем на Python, и он исполняемый. Это значит, что для отладки нового сценария нужно повторять этот сценарий целиком после каждого изменения, поскольку новый код может просто не запуститься. С компилируемыми языками проще, поскольку все опечатки исправляются на этапе билда. Хочется какую-то часть кода исполнять при запуске приложения.

Второе — в предыдущем проекте ко мне пришёл менеджер и попросил поменять текст в одном сообщении. А потом ещё раз, но в другом. А потом ещё раз, но в третьем. Что понял: я хочу дать менеджерам возможность кастомизировать ботов; я ОЧЕНЬ не хочу видеть эмоджи в коде приложения.

def get_random_text():
    return f"😜\n\n\n<i>{get_random_quote()}</i>"

Решение, которое пришло — давайте вынесем настройку IO бота в отдельный конфигурационный файл с читабельным синтаксисом. Часть кода будет жить в статическом файле и будет провалидирована на старте, изменять её сможет не-технический специалист.

Такой подход решает еще и проблему локализации - достаточно просто подменить файл на другой на нужном языке.

Часть 2. Имплементация

Посмотрим на конфигурацию, которую мы используем в одном из телеграм-ботов.

dialogues:
  start:
    description: Стартовое сообщение после команды /start. Стартовая картинка с логотипом кофенйи, приветственный текст и 5 inline-кнопок под сообщением.
    message: |
      Привет! 👋🏿
      Это телеграм-бот кофейни {cafe}, с помощью которого ты можешь заказать любые наши напитки или еду онлайн!
      
      — Используй /menu, чтобы найти нужные блюда
      
      — Добавь их в корзину, её можно открыть с помощью /cart, и оплати через СБП
      
      — Дождись готовности заказа в разделе /orders, а затем забери его в кофейне!🧋
      
      По всем вопросам пиши: {support}

    image: start.png
    buttons:
      - text: 📃 Меню
        callback:
          prefix: start_menu
      - text: 🛒 Корзина
        callback:
          prefix: cart
          params:
            d: 0 
      - text: 🧋 Заказы
        callback:
          prefix: history_page
          params:
            page: 0
      - text: 🧑🏿‍💻 Поддержка
        callback:
          prefix: support
      - text: 📙 Оставить фидбэк
        callback:
          prefix: feedback

  menu:
    description: Клик на кнопку "Меню" внизу. Открытие меню.
    message: |
      <b>{title}</b> 
      {description}

      <b>Выбери категорию ⤵️</b>
    image: menu.png

Как теперь выглядят хэндлеры телеграм-бота? Пишем с использованием библиотеки aiogram версии 3.

router = Router()

@router.message(CommandStart(), MessageConfigSection("start"))
async def start(_, answer: Callable):
    await answer()

Мы достигли этого с помощью filters/middlewares фичи библиотеки: она позволяет добавлять объекты в вызов функции, в нашем случае это answer — функция, берущая нужную конфигурацию сообщения (текст, кнопки, картинки) и отправляющая ответ на него. В случае необходимости мы совершаем нужный procedure call и передаём дополнительные аргументы в answer, чтобы он вставил переменные в статический текст:

@menu_router.callback_query(StartMenuPageCallback.filter(), MessageConfigSection("menu"))
@menu_router.message(Command("menu"), MessageConfigSection("menu"))
async def start_menu(_, answer: Callable, api: ApiClient) -> None:
    menu = await api.get("menu")
    await answer(reply_markup=build_markup_from_api(menu, page=0), context=menu)

В конце концов мы получаем такой экран:

— ???
— Profit

Другой пример использования конфига: мы начали поллить уведомления с бэкенда и удачно использовали те же MessageSections. С бэкенда приходит лишь ключ нотификации (к примеру cancelled_payment_too_much_workload), по которому мы можем достать нужную секцию и отправить необходимому пользователю сообщение.

  notification_cancelled_payment_too_much_workload:
    description: |
      Уведомление, которое приходит в случае отмены заказа из-за слишком большой загруженности
      В этом случае заказ будет переведен в статус ожидания возврата (отменен)
    message: |
      ❌ <b>К сожалению, сотрудник {cafe} отменил заказ {orderCode} из-за большой загруженности
      Деньги по твоему заказу будут возвращены в ближайшие несколько минут</b>
    buttons:
      - text: 📃 Перейти в меню
        callback:
          prefix: start_menu

Часть 3. Взгляд в будущее

Аiogram v3 "provides useful mechanisms to modularize your code, and enables the creation of shareable modules via packages on PyPI"

Поэтому мы думаем над переработкой кода этих конфиг-секций и создания отдельной, более гибкой и расширяемой библиотеки.

Например, мы хотим вынести кастомные плагины, такие как buttons, image и прочие, чтобы механика отправки сообщения могла быть изменена. Можно также подумать над авто-визуализацией экранов телеграм-бота. Я думаю этот подход оставляет полный контроль за происходящим в руках программиста (в отличие от существующих no-code решений), однако позволяет сэкономить время на написании однообразного.

Мне будет интересно послушать про ваш опыт в этой области и предложения по поводу библиотеки. Жду ваших комментариев!

ТГ нашего проекта

Tags:
Hubs:
Total votes 11: ↑10 and ↓1+9
Comments28

Articles