Aiogram extention aiogram-dialog

Aiogram-dialog — это фреймворк графического интерфейса для телеграмм-бота. Он вдохновлен идеями Android SDK и React.js.

Versions

На момент написания статьи последней дев-версией был 2.0b17. Как мигрировать тут.

Пример кода смотри тут.

Version status

  • v1.x stable release, supports aiogram v2.x, bugfix only
  • v2.x beta, future release, supports aiogram v3.x

Основные идеи

  • Раздельное извлечение данных и рендеринг сообщений
  • Объединененные отрисовки кнопок и обработки кликов
  • Улучшенная маршрутизация состояний
  • Виджеты

Основным строительным блоком интерфейса является Window. Каждое окно представляет собой сообщение, отправляемое пользователю, и обработку реакции пользователя на это сообщение.

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

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

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

Quicl start

pip install aiogram_dialog

Создайте [aiogram] группу состояний для вашего диалога:

from aiogram.dispatcher.filters.state import StatesGroup, State

class MySG(StatesGroup):
    main = State()

Создайте хотя бы одно окно с кнопками или текстом:

from aiogram.dispatcher.filters.state import StatesGroup, State

from aiogram_dialog import Window
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const


class MySG(StatesGroup):
    main = State()


main_window = Window(
    Const("Hello, unknown person"),  # just a constant text
    Button(Const("Useless button"), id="nothing"),  # button with text and id
    state=MySG.main,  # state is used to identify window between dialogs
)

Создайте диалог с вашими окнами:

from aiogram_dialog import Dialog

dialog = Dialog(main_window)

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

from aiogram import Bot, Dispatcher, executor
from aiogram.contrib.fsm_storage.memory import MemoryStorage

storage = MemoryStorage()
bot = Bot(token='BOT TOKEN HERE')
dp = Dispatcher(bot, storage=storage)

Чтобы начать использовать ваш диалог, вам необходимо его зарегистрировать. Также библиотека нуждается в некоторых дополнительных регистрациях для своих внутренних процессов. Для этого мы создадим DialogRegistry и используем его для регистрации нашего диалога.

from aiogram_dialog import DialogRegistry

registry = DialogRegistry(dp)  # this is required to use `aiogram_dialog`
registry.register(dialog)  # register a dialog

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

from aiogram.types import Message
from aiogram_dialog import DialogManager, StartMode

@dp.message_handler(commands=["start"])
async def start(m: Message, dialog_manager: DialogManager):
    # Important: always set `mode=StartMode.RESET_STACK` you don't want to stack dialogs
    await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)

Последний шаг, вам нужно запустить бота как обычно:

from aiogram import executor

if __name__ == '__main__':
    executor.start_polling(dp, skip_updates=True)

Полный пример (совместим с [aiogram] 2.0):

from aiogram import Bot, Dispatcher, executor
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher.filters.state import StatesGroup, State
from aiogram.types import Message

from aiogram_dialog import Window, Dialog, DialogRegistry, DialogManager, StartMode
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const

storage = MemoryStorage()
bot = Bot(token='BOT TOKEN HERE')
dp = Dispatcher(bot, storage=storage)
registry = DialogRegistry(dp)


class MySG(StatesGroup):
    main = State()


main_window = Window(
    Const("Hello, unknown person"),
    Button(Const("Useless button"), id="nothing"),
    state=MySG.main,
)
dialog = Dialog(main_window)
registry.register(dialog)


@dp.message_handler(commands=["start"])
async def start(m: Message, dialog_manager: DialogManager):
    await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)


if __name__ == '__main__':
    executor.start_polling(dp, skip_updates=True)

Виджеты и рендеринг

Как передаются данные

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

from aiogram.dispatcher.filters.state import StatesGroup, State

from aiogram_dialog import Window, Dialog
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const, Format


class MySG(StatesGroup):
    main = State()


async def get_data(**kwargs):
    return {
        "name": "Tishka17",
    }


dialog = Dialog(
    Window(
        Format("Hello, {name}!"),
        Button(Const("Useless button"), id="nothing"),
        state=MySG.main,
        getter=get_data,  # here we set our data getter
    )
)

Часть объектов доступно без геттера, а через атрибут хендлера:

  • dialog_data - содержимое соответствующего поля из текущего контекста. Обычно он используется для хранения данных между несколькими вызовами и окнами в одном диалоговом окне.
  • start_data - данные, передаваемые при запуске текущего диалога. Также доступны с помощью current_context
  • middleware_data - данные передаются от промежуточного программного обеспечения к обработчику. Еще можно так dialog_manager.data
  • event - текущее событие обработки, вызвавшее обновление окна. Будьте осторожны, используя его, потому что разные типы событий могут привести к обновлению одного и того же окна.

Типы виджетов

В настоящее время существует 3 вида виджетов: тексты, клавиатуры и медиа

  • Texts, используются для отображения текста в любом месте диалога. Это может быть текст сообщения, заголовок кнопки и так далее.
  • Keyboards представляют собой части InlineKeyboard [aiogram]
  • Media представляет мультимедийный аттачмент к сообщению

Также есть 2 основных типа:

  • Whenable могут быть скрыты или показаны в зависимости от данных или некоторых условий. В настоящее время все виджеты доступны.
  • Actionable любой виджет с действием (в настоящее время только любой тип клавиатуры). Он имеет id и может быть найден по этому идентификатору. Рекомендуется, чтобы все виджеты с состоянием (например, флаги) имели уникальный идентификатор в диалоговом окне. Кнопки с разным поведением также должны иметь разные идентификаторы.

Тексты

  • Const - возвращает текст без промежуточных значений
  • Format - форматирует текст с помощью format. При использовании в окне данные извлекаются через getter.
  • Multi - несколько текстов, соединенных разделителем
  • Case - показывает один из текстов по условию
  • Progress - показывает индикатор выполнения
  • Jinja — представляет собой HTML, отображаемый с использованием шаблона jinja2.

Клавиатуры

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

  • Button — одна встроенная кнопка. Предоставленный пользователем on_click метод вызывается при нажатии.
  • Url — одна встроенная кнопка с URL
  • Group - любая группа клавиатур друг над другом или микс из кнопок
  • ScrollingGroup — то же, что и Group, но с возможностью прокрутки страниц кнопками.
  • ListGroup - группа виджетов применяемая многократно для каждого элемента в списке
  • Row - упрощенный вариант группы. Все кнопки расположены в один ряд.
  • Column — еще одна упрощенная версия группы. Все кнопки размещены в одном столбце по одной в строке.
  • Checkbox — кнопка с двумя состояниями
  • Select - динамическая группа кнопок, предназначенная для использования при выборе.
  • Radio - переключение между несколькими элементами. Подобно select, но сохраняет выбранный элемент и отображает его по-другому.
  • Multiselect - выбор нескольких элементов. Подобно select/radio, но сохраняет все выбранные элементы и отображает их по-разному.
  • Calendar — имитирует календарь в виде клавиатуры.
  • SwitchTo- переключает окно в диалоге, используя предоставленное состояние
  • Next/Back - переключает состояние вперед или назад
  • Start - запускает новый диалог без параметров
  • Cancel - закрывает текущий диалог без результата. Отображается базовый диалог

Transitions

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

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

Стек задач

Для работы с несколькими открытыми диалогами в aiogram_dialog есть стек диалогов. Это позволяет открывать диалоги друг над другом («складывать»), поэтому виден только один из них.

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

Каждый раз, когда вы закрываете диалог, задача и контекст диалога удаляются.

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

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

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

Есть несколько способов сделать это:

  • dialog.switch_to метод. Передайте другое состояние, и окно будет переключено
  • dialog.next метод. Он переключится на следующее окно в том же порядке, в котором они были переданы при создании диалога. Невозможно вызвать, когда последнее окно активно
  • dialog.back метод. Переключиться на противоположное направление (на предыдущее). Невозможно вызвать, когда активно первое окно

Пример

from aiogram.dispatcher.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery

from aiogram_dialog import Dialog, DialogManager, Window
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const


class DialogSG(StatesGroup):
    first = State()
    second = State()
    third = State()


async def to_second(c: CallbackQuery, button: Button, manager: DialogManager):
    await manager.dialog().switch_to(DialogSG.second)


async def go_back(c: CallbackQuery, button: Button, manager: DialogManager):
    await manager.dialog().back()


async def go_next(c: CallbackQuery, button: Button, manager: DialogManager):
    await manager.dialog().next()


dialog = Dialog(
    Window(
        Const("First"),
        Button(Const("To second"), id="sec", on_click=to_second),
        state=DialogSG.first,
    ),
    Window(
        Const("Second"),
        Row(
            Button(Const("Back"), id="back2", on_click=go_back),
            Button(Const("Next"), id="next2", on_click=go_next),
        ),
        state=DialogSG.second,
    ),
    Window(
        Const("Third"),
        Button(Const("Back"), id="back3", on_click=go_back),
        state=DialogSG.third,
    )
)

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

  • SwitchTo - switch_to при нажатии. Состояние предоставляется через атрибут конструктора
  • Next - next при клике
  • Back - back при клике

Пример тут

Несколько полезных инстурментов контроля реализации диалгов

Смотри еще: