Как написать бот на с с меню
Перейти к содержимому

Как написать бот на с с меню

  • автор:

Меню с командами в Telegram боте

В Вашем кабинете BotHelp можно создать меню с командами для Telegram-ботов. Это встроенный функционал Telegram, который позволят пользователю в любой момент вызывать интерактивное меню бота с его командами.

Ключевые особенности меню с командами:

  • Есть только для Telegram ботов.
  • Создается для всего канала. Если для одного Telegram-канала настроено несколько ботов, то созданное меню команд будет отображаться в каждом боте.
  • Каждая команда меню может переводить подписчика на определенный шаг в выбранном боте. Если подписчик запускает команду, во время прохождения бота, то этот бот для него останавливается. Для назначения доступны только боты, созданные для канала, для которого создается меню.
  • При необходимости созданное меню можно деактивировать без его удаления. В таком случае меню команд будет недоступно для подписчиков, но в кабинете BotHelp оно останется.

Создание и настройка меню команд

Есть 2 способа настроить Меню команд в Телеграм

1 способ: в настройках цепочки бота в Ботхелп
Для этого необходимо нажать на шестерёнку в правом верхнем углу, выбрать пункт «Telegram меню для канала» и добавить команду.

2 способ: в настройках кабинета
Для этого перейдите в раздел Каналы, нажмите у нужного Telegram канала на меню «три точки». В выпадающем списке выберите «Создать меню команд»

1. В открывшемся окне добавить первую команду меню, нажав на кнопку «+ Добавить команду»

2. Задать название и описание команды

  • Название команды может содержать только латинские буквы, цифры и «_»

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

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

Остальные команды будут создаваться таким же образом

  • При необходимости можно менять порядок команд в меню. Для этого просто перетащите карточку команды в нужное место внутри созданного меню

4. Обязательно сохраните меню нажатием на кнопку «Сохранить»

  • При дальнейшем редактировании обязательно сохраняйте меню, чтобы изменения вступили в силу

5. Активируйте меню нажатие на кнопку «Активировать». Готово — меню создано и доступно для подписчиков.

Для подписчиков созданное меню будет отображаться в виде выпадающего списка и кнопки Menu:

ВАЖНО! Если Вы создали команду, название которой совпадает с уже созданным ключевым словом, то для подписчика отработает именно ключевое слово, а не команда.

Например, ключевое слово «/test» и команда «test» — ключевое слово должно быть именно со знаком «/», в названии команды этот знак не нужен, он подставляется автоматически. Если подписчик напишет боту «/test», то сработает именно ключевое слово.

ВАЖНО! Telegram требуется время (несколько секунд), чтобы изменения вступили в силу. После создания/изменения и сохранения меню команд попробуйте перезайти в чат с ботом, чтобы меню появилось/обновилось.

ВАЖНО! На данный момент подключение Меню доступно только Администраторам аккаунта, у Агентов нет такой возможности.

Частые вопросы:

Если меню активирует бота, который начинается с блока Вопрос, и подписчик уже проходил другого бота и остановился на шаге Вопрос, то куда запишется данный им ответ?

Ответ запишется в поле Вопрос, придет сообщение по введенной команде, после чего придет сообщение, которое идет после блока Вопрос. Задержка 24 часа берет отсчет от времени получения подписчиком этого блока.

Если вы не нашли ответ на свой вопрос, задайте его нам в чате внутри кабинета либо напишите на hello@bothelp.io ��

Получите 14 дней полного функционала платформы для создания рассылок, автоворонок и чат-ботов BotHelp:

Чат-боты в Telegram на Python. Часть 2. Создаём и настраиваем меню

Продолжаем писать чат-бота для Telegram — добавляем кнопки и интерактив.

Иллюстрация: Катя Павловская для Skillbox Media

Антон Яценко

Антон Яценко
Изучает Python, его библиотеки и занимается анализом данных. Любит путешествовать в горах.

В первой части урока по чат-ботам для Telegram мы создали на Python эхо-бота с помощью библиотеки aiogram. Сам эхо-бот работает просто, а его функция очевидна из названия: в ответ на сообщение пользователя он присылает тот же текст. Если вы ещё не читали первую часть, начните с неё.

Во второй части урока поработаем над меню: добавим для пользователей клавиатуру с быстрыми ответами и инлайн-кнопки для перехода на сайт Skillbox. Начнём с создания меню, но сначала разберёмся с видами возможных клавиатур.

Виды клавиатур

Библиотека aiogram позволяет создать на Python клавиатуры двух видов, отличающиеся друг от друга расположением кнопок:

  • Reply-кнопки для шаблонных ответов, которые закрепляются вместо основной клавиатуры на экране. Часто используются в чат-ботах как меню. Создаются с помощью метода ReplyKeyboardMarkup.
  • Инлайн-кнопки, связанные с сообщениями в чате. При этом пользователь видит и основную клавиатуру. Создаются с помощью метода InlineKeyboardMarkup.

Создание меню

Наш эхо-бот для Telegram сейчас позволяет только отправлять текстовые сообщения и получать их обратно. Давайте проапгрейдим его и добавим кнопки с готовыми сообщениями, которые не надо вводить самому. Это будут reply-кнопки.

Нам понадобится класс ReplyKeyboardMarkup — для начала импортируем его и дополнительные необходимые классы:

Если нажать на любую кнопку, текст кнопки отправится в чат, а Telegram-бот пришлёт в ответ эту же фразу:

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

Инлайн-кнопки на aiogram

Инлайн-кнопки отличаются от обычных тем, что связаны не с областью клавиатуры в мессенджере, а с каким-то сообщением в Telegram-чате. Самый простой пример инлайн-кнопки — это меню в канале @BotFather, с помощью которого мы создавали токен для доступа к API Telegram. Например, вот так в нём выглядит инлайн-меню с уже созданными ботами:

Создадим на Python для нашего бота инлайн-кнопки со ссылками на Skillbox Media и курсы по программированию. Для этого вернёмся к разделу с импортами в коде и добавим ещё одну строку, чтобы можно было использовать необходимые классы:

Всё получилось. Теперь инлайн-клавиатура появляется при отправке команды /ссылки в бот.

Заключение

В нашем эхо-боте для Telegram появилось два вида меню, написанных на Python: reply-кнопки для быстрых сообщений и инлайн-кнопки для перехода на блог и сайт Skillbox. Для создания сложных ботов — например, ботов онлайн-магазинов — можно самостоятельно изучить документацию к библиотеке aiogram: попробовать новые классы, методы и объекты.

Читайте также:

  • С# для новичков: развеиваем мифы и пишем простого чат-бота
  • Тест. Какой язык создадите вы — Java или Python?
  • Что можно сделать на JavaScript и что нельзя

Создание telegram-ботов с интерактивным меню

Однажды меня попросили провести ревью и рефакторинг одного telegram-бота. Увидев файл размером 2000 строк, рассчитанный только на обработку разных меню я понял, что это требует унификации и общих подходов. Так родилась библиотека aiogram-dialog .

В этой статье я бы хотел обратить внимание на некоторые проблемы, которые мы встречаем при создании таких меню, предложить варианты их решения. А во второй половине статьи показать как это решается с помощью aiogram-dialog .

Мы не будем рассматривать архитектуру всего приложения, об этом вы можете прочитать у Фаулера или Мартина. Мы поговорим только про определенную часть UI ботов. Так же это не будет введением в разработку telegram-ботов с нуля. Я предполагаю, что читатель знаком с питоном, ООП и слышал о такой вещи как DRY. В коде примеров я использую aiogram v3.0 и надеюсь, что читатель уже использовал встроенную в библиотеку машину состояний.

Примеры выбраны так, чтобы проще было показать определенные проблемы, но это не единственные сценарии приводящие к ним.

Постановка проблемы

Шаг первый. Меню

Давайте рассмотрим небольшого бота, взаимодействующего с пользователем через сообщение с inline-клавиатурой.

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

Для реализации такого бота нам пока потребуется 3 обработчика событий телеграм:

  • событие для команды /start, отправляющее сообщение
  • обработчик устанавливающий галочку
  • обработчик снимающий галочку
import asyncio import os from aiogram import Router, F, Bot, Dispatcher from aiogram.filters import CommandStart from aiogram.types import ( CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, Message, ) router = Router() STEP1_EXTEND_CB = "extend" STEP1_COLLAPSE_CB = "collapse" STEP1_SETTINGS_CB = "settings" ADDITIONAL_TEXT = "Here is some additional text, which is visible only in extended mode" @router.message(CommandStart()) async def step1(message: Message): keyboard = InlineKeyboardMarkup(inline_keyboard=[[ InlineKeyboardButton(text="[ ] Extended mode", callback_data=STEP1_EXTEND_CB), InlineKeyboardButton(text="Settings", callback_data=STEP1_SETTINGS_CB), ]]) await message.answer( f"Hello, . \n\n" "Extended mode is off.", reply_markup=keyboard, ) @router.callback_query(F.data == STEP1_EXTEND_CB) async def step1_check(callback: CallbackQuery): keyboard = InlineKeyboardMarkup(inline_keyboard=[[ InlineKeyboardButton(text="[x] Extended mode", callback_data=STEP1_COLLAPSE_CB), InlineKeyboardButton(text="Settings", callback_data=STEP1_SETTINGS_CB), ]]) await callback.message.edit_text( f"Hello, . \n\n" "Extended mode is on.\n\n" + ADDITIONAL_TEXT, reply_markup=keyboard, ) @router.callback_query(F.data == STEP1_COLLAPSE_CB) async def step1_uncheck(callback: CallbackQuery): keyboard = InlineKeyboardMarkup(inline_keyboard=[[ InlineKeyboardButton(text="[ ] Extended mode", callback_data=STEP1_EXTEND_CB), InlineKeyboardButton(text="Settings", callback_data=STEP1_SETTINGS_CB), ]]) await callback.message.edit_text( f"Hello, . \n\n" "Extended mode is off.", reply_markup=keyboard, ) async def main(): bot = Bot(token=os.getenv("BOT_TOKEN")) dp = Dispatcher() dp.include_router(router) await dp.start_polling(bot) asyncio.run(main()) 

Проблема 1:

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

Решение проблемы 1:

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

import asyncio import os from aiogram import Bot, Router, F, Dispatcher from aiogram.filters import CommandStart from aiogram.types import ( Message, InlineKeyboardMarkup, InlineKeyboardButton, User, Chat, CallbackQuery, ) router = Router() STEP1_EXTEND_CB = "extend" STEP1_COLLAPSE_CB = "collapse" STEP1_SETTINGS_CB = "settings" ADDITIONAL_TEXT = "Here is some additional text, which is visible only in extended mode" def step1_text(user: User, is_extended: bool) -> str: if is_extended: status = "on" suffix = "\n\n" + ADDITIONAL_TEXT else: status = "off" suffix = "" return ( f"Hello, . \n\n" f"Extended mode is ." f"" ) def step1_keyboard(is_checked: bool) -> InlineKeyboardMarkup: if is_checked: checkbox = InlineKeyboardButton( text="[x] Extended mode", callback_data=STEP1_COLLAPSE_CB, ) else: checkbox = InlineKeyboardButton( text="[ ] Extended mode", callback_data=STEP1_EXTEND_CB, ) return InlineKeyboardMarkup(inline_keyboard=[[ checkbox, InlineKeyboardButton(text="Settings", callback_data=STEP1_SETTINGS_CB), ]]) @router.message(CommandStart()) async def step1(message: Message): await message.answer( text=step1_text(user=message.from_user, is_extended=False), reply_markup=step1_keyboard(is_checked=False) ) @router.callback_query(F.data == STEP1_EXTEND_CB) async def step1_check(callback: CallbackQuery): await callback.message.edit_text( text=step1_text(user=callback.from_user, is_extended=True), reply_markup=step1_keyboard(is_checked=True) ) @router.callback_query(F.data == STEP1_COLLAPSE_CB) async def step1_uncheck(callback: CallbackQuery): await callback.message.edit_text( text=step1_text(user=callback.from_user, is_extended=False), reply_markup=step1_keyboard(is_checked=False) ) async def main(): bot = Bot(token=os.getenv("BOT_TOKEN")) dp = Dispatcher() dp.include_router(router) await dp.start_polling(bot) asyncio.run(main()) 
Шаг второй. Ввод текста

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

Проблема 2:

  • При входящем сообщении мы не знаем какое старое сообщение редактировать, как это было в CallbackQuery
  • При входящем сообщении мы не знаем состояние чекбокса в старом сообщении

Решение проблемы 2:

Необходимо запоминать где-то состояние чата: нажата ли галочка и id последнего сообщения.

import asyncio import os from aiogram import Bot, Router, F, Dispatcher from aiogram.filters import CommandStart from aiogram.fsm.context import FSMContext from aiogram.types import ( Message, InlineKeyboardMarkup, InlineKeyboardButton, User, Chat, CallbackQuery, ) router = Router() STEP1_EXTEND_CB = "extend" STEP1_COLLAPSE_CB = "collapse" STEP1_SETTINGS_CB = "settings" IS_EXTENDED_KEY = "extended" LAST_MSG_ID_KEY = "last_message_id" ADDITIONAL_TEXT = "Here is some additional text, which is visible only in extended mode" def step1_text(user: User, is_extended: bool) -> str: if is_extended: status = "on" suffix = "\n\n" + ADDITIONAL_TEXT else: status = "off" suffix = "" return ( f"Hello, . \n\n" f"Extended mode is ." f"" ) def step1_keyboard(is_checked: bool) -> InlineKeyboardMarkup: if is_checked: checkbox = InlineKeyboardButton( text="[x] Extended mode", callback_data=STEP1_COLLAPSE_CB, ) else: checkbox = InlineKeyboardButton( text="[ ] Extended mode", callback_data=STEP1_EXTEND_CB, ) return InlineKeyboardMarkup(inline_keyboard=[[ checkbox, InlineKeyboardButton(text="Settings", callback_data=STEP1_SETTINGS_CB), ]]) @router.message(CommandStart()) async def step1(message: Message, state: FSMContext): message = await message.answer( text=step1_text(user=message.from_user, is_extended=False), reply_markup=step1_keyboard(is_checked=False) ) await state.set_data(< IS_EXTENDED_KEY: False, LAST_MSG_ID_KEY: message.message_id, >) @router.callback_query(F.data == STEP1_EXTEND_CB) async def step1_check(callback: CallbackQuery, state: FSMContext): await state.update_data() await callback.message.edit_text( text=step1_text(user=callback.from_user, is_extended=True), reply_markup=step1_keyboard(is_checked=True) ) @router.callback_query(F.data == STEP1_COLLAPSE_CB) async def step1_uncheck(callback: CallbackQuery, state: FSMContext): await state.update_data() await callback.message.edit_text( text=step1_text(user=callback.from_user, is_extended=False), reply_markup=step1_keyboard(is_checked=False) ) @router.message() async def step1_nothing(message: Message, bot: Bot, state: FSMContext): data = await state.get_data() await bot.edit_message_reply_markup( chat_id=message.chat.id, message_id=data[LAST_MSG_ID_KEY], ) message = await message.answer( text=step1_text( user=message.from_user, is_extended=data[IS_EXTENDED_KEY], ), reply_markup=step1_keyboard(is_checked=data[IS_EXTENDED_KEY]) ) data[LAST_MSG_ID_KEY] = message.message_id await state.set_data(data) async def main(): bot = Bot(token=os.getenv("BOT_TOKEN")) dp = Dispatcher() dp.include_router(router) await dp.start_polling(bot) asyncio.run(main()) 
Шаг третий и далее

Добавим к боту вторую клавиатуру, которая появляется по нажатию кнопки «Settings». Пусть это будет экран настроек, содержащий ещё пару чекбоксов и кнопки «сохранить» и «отменить». По Нажатию «сохранить» мы сохраняем настройки в БД и возвращаемся в прошлое меню. А по нажатию «отменить» тоже возвращаемся, но без сохранения.

Проблема 3:

Обработчик сообщения не знает что мы перешли в настройки и отправляет нам меню 1.

Решение проблемы 3:

Необходимо запоминать в каком меню мы находимся по аналогии с другими данными чата. Можно использоваться для этого State из aiogram

Проблема 4:

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

Решение проблемы 4:

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

< "last_message_id": 1, "step1": < "extended": true >, "settings": < "option1": false, "option2": true >> 

Проблема 5:

Повторяющиеся паттерны обработки. В разных частях программы могут повторяться чекбоксы, кнопки выбора из нескольких вариантов, календарь, переходы вперед/назад. Приходится дублировать хэндлеры, делающие одну и ту же работу:

  • генерация кнопок
  • сохранение своего состояния
  • вызов показа нового текста и клавиатуры после нажатия

Решение проблемы 5:

  • выносим каждый паттерн в отдельный класс
  • добавляем ему id для генерации callback_data и хранения данных
  • параметризуем экземпляр колбэк-функциями для вызова прикладной логики, не относящейся к обновлению меню

Например, класс Checkbox может выглядеть так:

 class Checkbox(): def __init__( self, checked_text: str, unchecked_text: str, id: str, on_click: Optional[OnStateChanged] = None, ): . def is_checked(self, state: FMSContext) -> bool: . async def render_keyboard( self, state: FMSContext, ) -> List[List[InlineKeyboardButton]]: . async def process_callback( self, callback: CallbackQuery, state: FMSContext, ) -> bool: . 

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

Проблема 6:

В коде имеющем несколько меню возможны переходы по нескольким направлениям. Часто необходимо реализовать переход назад или в главное меню.

  • В некоторые меню можно попасть разными способами и переход назад должен возвращать пользователя правильно
  • Для отрисовки главного меню требуется его импортировать в другие меню, и наоборот из него мы косвенно импортируем их. Возможны циклические импорты

Решение проблемы 6:

  • Заводим стек состояний. При переходе в новое меню мы не просто сохраняем его стейт, а добавляем его в стек.
  • Заводим отдельный класс менеджер, следящий за состоянием стека и вызывающий отрисовку исходя из текущего состояния, очистку при возврате в главное меню
  • Все переходы делается по State, а конкретные объекты окон привязываются к стейтам и регистрируются в менеджере.

Примерный вид класса менеджера стека:

class Manager: def __init__(self, windows: Dict[State, Window]): . def refresh(self, context: FMSContext): . def switch_into(self, state: State, fsm_context: FSMContext): . def switch_up(self, fsm_context: FSMContext): . def reset_stack(self, state: State, fsm_context: FSMContext): . 

Концепции

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

  1. Разделение реакции на события и генерации сообщения
  2. Хранение состояний в виде стека
  3. Изолированные контексты для разных частей UI
  4. Центральный менеджер управляющий переходами состояния
  5. Переиспользуемые компоненты («виджеты»), группирующиеся в «окна» описывающие внешний вид сообщения

Все эти концепции уже реализованы в aiogram_dialog

Создание бота на aiogram-dialog

Установка

Устанавливаем стандартным для Python способом, например с помощью pip.

pip install aiogram-dialog==2.* 
Окна и виджеты

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

    Динамический текст, куда будут подставлены данные из текущего контекста или события.

from aiogram_dialog.widgets.text import Format text = Format( "Hello, . \n\n" "Extended mode is .\n" ) 
 from aiogram_dialog.widgets.text import Const additional = Const( "Here is some additional text, which is visible only in extended mode", when="extended", ) 
from aiogram_dialog.widgets.text import Const from aiogram_dialog.widgets.kbd import Checkbox EXTEND_BTN_ID = "extend" checkbox = Checkbox( checked_text=Const("[x] Extended mode"), unchecked_text=Const("[ ] Extended mode"), ) 
from aiogram_dialog.widgets.text import Const from aiogram_dialog.widgets.kbd import Button button_next = Button(Const("Settings"), ) 
from aiogram_dialog.widgets.kbd import Row row = Row(checkbox, button_next) 

Теперь объединим это всё одно окно. Так же нам потребуется создать State , для того чтобы мы могли переключиться на это окно позднее. Библиотека требует чтобы все стейты были созданы в StatesGroup , таким образом мы достигаем большей гибкости в изоляции контекстов.

from aiogram.fsm.state import State, StatesGroup from aiogram_dialog import Window from aiogram_dialog.widgets.text import Format, Const from aiogram_dialog.widgets.kbd import Checkbox, Button, Row class MainMenu(StatesGroup): START = State() EXTEND_BTN_ID = "extend" window = Window( Format( "Hello, . \n\n" "Extended mode is .\n" ), Const( "Here is some additional text, which is visible only in extended mode", when="extended", ), Row( Checkbox( checked_text=Const("[x] Extended mode"), unchecked_text=Const("[ ] Extended mode"), ), Button(Const("Settings"), ), ), state=MainMenu.START ) 

В процессе рендеринга данного окна туда в дальнейшем будет передан текущий контекст, откуда Checkbox прочитает своё состояние и сможет выбрать какой из двух вариантов текста использовать. Так же будет использовано текущее обрабатываемое событие (Message или CallbackQuery) чтобы подставить имя пользователя. Однако мы не указали пока что подставить в качестве в текст.

Для внедрения дополнительных данных в процессе рендеринга сообщения в окно добавляется функция-getter. Она возвращает словарь, к которому в дальнейшем можно обращаться из Format , использовать внутри виджетов, влиять на их видимость и т.п. В частности, через него делается создание кнопок для выбора из списка.

В данном случае функция-геттер должна вернуть по ключу extended_str строку «on» если галочка снята и «off» в противном случае. А по ключу extended — само булево значение опции.

В качестве параметров она получает всё, что прилетает из middleware. Пока проигнорируем это, поставив **kwargs .

async def getter(**kwargs) -> Dict[str, Any]: if True: # here will be some condition return < "extended_str": "on", "extended": True, >else: return
Менеджер и ограничение контекста

Чтобы избежать конфликтов и автоматически очищать данные, aiogram-dialog ограничивает работу с контекстом не одним State, а одной StatesGroup . Таким образом мы можем иметь более одного окна с общим контекстом, что упрощает реализацию некоторых сценариев.

Так же как стейты объединяются в StatesGroup , окна объединяются в объект Dialog .

from aiogram_dialog import Dialog main_menu = Dialog(window) 

Если быть более точным, новый контекст создается каждый раз, когда вы добавляете что-то в стек переходов, но вы может сохранять контекст меняя текущий State в стеке в пределах одной StatesGroup .

Для управления переходами и контекстом, используется класс DialogManager . Он прилетает в getter и во все обработчики, обычно под именем dialog_manager .

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

from aiogram_dialog import DialogManager async def getter(dialog_manager: DialogManager, **kwargs) -> Dict[str, Any]: if dialog_manager.find(EXTEND_BTN_ID).is_checked(): return < "extended_str": "on", "extended": True, >else: return

Прежде чем запускать бота нам необходимо реализовать переход к нашему диалогу. Делается это с помощью DialogManager.start

router = Router() @router.message(CommandStart()) async def start(message: Message, dialog_manager: DialogManager): await dialog_manager.start(MainMenu.START) 

Остался последний подготовительный подключить конкретные диалоги к боту и настроить сам Dispatcher на работу с библиотекой:

dp = Dispatcher() dp.include_router(main_menu) dp.include_router(router) setup_dialogs(dp) 

Таким образом, целиком всё будет выглядеть так:

import asyncio import os from typing import Dict, Any from aiogram.filters import CommandStart from aiogram.fsm.state import State, StatesGroup from aiogram import Router, F, Bot, Dispatcher from aiogram.types import Message from aiogram_dialog import Dialog, Window, setup_dialogs, DialogManager from aiogram_dialog.widgets.text import Format, Const from aiogram_dialog.widgets.kbd import Checkbox, Button, Row class Step1(StatesGroup): START = State() EXTEND_BTN_ID = "extend" async def getter(dialog_manager: DialogManager, **kwargs) -> Dict[str, Any]: if dialog_manager.find(EXTEND_BTN_ID).is_checked(): return < "extended_str": "on", "extended": True, >else: return < "extended_str": "off", "extended": False, >main_menu = Dialog( Window( Format( "Hello, . \n\n" "Extended mode is .\n" ), Const( "Here is some additional text, which is visible only in extended mode", when="extended", ), Row( Checkbox( checked_text=Const("[x] Extended mode"), unchecked_text=Const("[ ] Extended mode"), ), Button(Const("Settings"), ), ), getter=getter, state=Step1.START ) ) router = Router() @router.message(CommandStart()) async def start(message: Message, dialog_manager: DialogManager): await dialog_manager.start(Step1.START) async def main(): bot = Bot(token=os.getenv("BOT_TOKEN")) dp = Dispatcher() dp.include_router(main_menu) dp.include_router(router) setup_dialogs(dp) await dp.start_polling(bot) asyncio.run(main()) 
Второй диалог

Второй диалог добавляется полностью аналогично первому. Мы воспользуемся новым виджетом Cancel() , который возвращает нас в предыдущее меню (с очисткой контекста, естественно).

class Settings(StatesGroup): START = State() NOTIFICATIONS_BTN_ID = "notify" ADULT_BTN_ID = "adult" settings = Dialog( Window( Const("Setting"), Checkbox( checked_text=Const("[x] Send notifications"), unchecked_text=Const("[ ] Send notifications"), ), Checkbox( checked_text=Const("[x] Adult mode"), unchecked_text=Const("[ ] Adult mode"), ), Row( Cancel(), Cancel(text=Const("Save"), ), ), state=Settings.START, ) ) 

Переход ко второму диалогу из первого мы можем организовать так же вызвав dialog_manager.start из обработчика кнопки next , либо заменить её на специальный виджет

Start(Const("Settings"), , state=Settings.START) 
import asyncio import os from typing import Dict, Any from aiogram.filters import CommandStart from aiogram.fsm.state import State, StatesGroup from aiogram import Router, F, Bot, Dispatcher from aiogram.types import Message from aiogram_dialog import Dialog, Window, setup_dialogs, DialogManager from aiogram_dialog.widgets.text import Format, Const from aiogram_dialog.widgets.kbd import Checkbox, Button, Row, Cancel, Start class MainMenu(StatesGroup): START = State() class Settings(StatesGroup): START = State() EXTEND_BTN_ID = "extend" async def getter(dialog_manager: DialogManager, **kwargs) -> Dict[str, Any]: if dialog_manager.find(EXTEND_BTN_ID).is_checked(): return < "extended_str": "on", "extended": True, >else: return < "extended_str": "off", "extended": False, >main_menu = Dialog( Window( Format( "Hello, . \n\n" "Extended mode is .\n" ), Const( "Here is some additional text, which is visible only in extended mode", when="extended", ), Row( Checkbox( checked_text=Const("[x] Extended mode"), unchecked_text=Const("[ ] Extended mode"), ), Start(Const("Settings"), , state=Settings.START), ), getter=getter, state=MainMenu.START ) ) NOTIFICATIONS_BTN_ID = "notify" ADULT_BTN_ID = "adult" settings = Dialog( Window( Const("Settings"), Checkbox( checked_text=Const("[x] Send notifications"), unchecked_text=Const("[ ] Send notifications"), ), Checkbox( checked_text=Const("[x] Adult mode"), unchecked_text=Const("[ ] Adult mode"), ), Row( Cancel(), Cancel(text=Const("Save"), ), ), state=Settings.START, ) ) router = Router() @router.message(CommandStart()) async def start(message: Message, dialog_manager: DialogManager): await dialog_manager.start(MainMenu.START) async def main(): bot = Bot(token=os.getenv("BOT_TOKEN")) dp = Dispatcher() dp.include_router(main_menu) dp.include_router(settings) dp.include_router(router) setup_dialogs(dp) await dp.start_polling(bot) asyncio.run(main()) 

Заключение

Почему-то про разработку телеграмм-ботов в основном пишут статьи, рассчитанные на изучающих языки программирования, да и фреймворки почти не предлагают высокоуровневых подходов к реализации интерфейса бота. Между тем, тут можно провести параллели и с веб-разработкой и созданием мобильных приложений, перенимая концепции и паттерны (VIPER, HTTP Session, Widget, Back stack). Грамотно подходя к организации кода мы можем вложить свои силы в разработку бизнес-логики или проектирование UX вместо того, чтобы тратить их на очередное повторение реализации чекбокса в новом разделе.

Если вас заинтересовал проект, приглашаю ознакомиться с документацией и github проекта. Так же на гитхабе доступны примеры, показывающие как использовать те или иные возможности и разные виджеты.

Чем хороша библиотека:

  • готовые паттерны обновления сообщений с меню
  • готовые заменяемые виджеты для различных моделей поведения: чекбоксы, радио кнопки, календарь, пагинаторы, форматирование текста и многие другие
  • возможность разделения реакции на события и логики отображения
  • возможность писать переиспользуемые меню
  • сокращение времени разработки

При этом вы можете совмещать код, написанный с использованием «диалогов», с обычным кодом на aiogram.

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

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

Как создать постоянное меню для Telegram чат-бота

Используйте меню бота, чтобы помочь пользователям найти нужную информацию в вашем боте. Создавайте команды, которые запускают определенные цепочки. Открыть меню можно кликнув по иконке со знаком «/» в поле ввода сообщений .

Чтобы создать меню бота, на странице чат-бота откройте вкладку «Меню» и нажмите «Добавить элемент».

Введите название команды. Можно использовать до 30 символов в поле: латинские буквы, цифры и знак «_».

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

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

Нажмите “Добавить”, чтобы добавить команду в меню.

Расширяйте меню дополнительными командами с помощью кнопки «Добавить элемент». После создания команд нажмите «Сохранить», чтобы сохранить созданные элементы.

Меню с командами готово к работе.

Вы также можете создать клавиатурное меню для пользователя из кнопок «Быстрые ответы». Вы можете добавить до 10 таких кнопок и добавить в них эмодзи.

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

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

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