Привет! Меня зовут Никита и я пишу от имени небольшой команды студентов, разработчиков проекта 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 решений), однако позволяет сэкономить время на написании однообразного.
Мне будет интересно послушать про ваш опыт в этой области и предложения по поводу библиотеки. Жду ваших комментариев!