Фреймворк aiogram-dialog. Высокоуровневый интерфейс телеграм бота.

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

  1. Разделение получения данных и отображения сообщений.
  2. Объединение отображения кнопок и обработки нажатий.
  3. Улучшенная маршрутизация состояний.
  4. Виджеты

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

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

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

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

Содержание
  1. Быстрый старт
  2. Виджеты текста и рендеринг
  3. Передача данных
  4. Пользовательские виджеты
  5. Передача данных
  6. Типы текстовых виджетов
  7. Использование виджета  Case
  8. Виджет Progress
  9. Виджет List
  10. HTML код
  11. Виджеты клавиатуры
  12. Простая кнопка
  13. Виджет URL
  14. SwitchInlineQuery
  15. Виджет Group
  16. Виджет Column
  17. Виджет Row
  18. Виджет ListGroup
  19. ScrollingGroup
  20. Виджет Checkbox
  21. Виджет Select
  22. Виджет Radio
  23. Multiselect
  24. Виджет календаря
  25. Счётчик
  26. Switch To
  27. Кнопки «Вперёд» и «Назад»
  28. Кнопка «Старт»
  29. Кнопка «Отмена»
  30. Виджеты для ввода текста
  31. TextInput
  32. Виджеты для работы с медиа
  33. Типы виджетов медиа
  34. 1. StaticMedia (Статическое медиа)
  35. 2. DynamicMedia (Динамическое медиа)
  36. 3. Другие источники медиа
  37. StaticMedia (Статическое медиа)
  38. DynamicMedia (Динамическое медиа)
  39. Другие источники медиа
  40. Скрытие виджетов
  41. Пользовательские виджеты
  42. SwitchInlineQueryCurrentChat
  43. Переходы в диалогах
  44. Типы переходов
  45. Стек задач
  46. Переключение состояния
  47. Инструменты помощи (экспериментальные)
  48. Диаграмма состояний
  49. Подсказки переходов состояний
  50. Предпросмотр диалогов
  51. Веб-предпросмотр
  52. Часто задаваемые вопросы (FAQ)
  53. Собственные виджеты и рендеринг
  54. Базовая информация
  55. Передача данных
  56. Пользовательские виджеты
  57. Примеры использования с объяснением
  58. Пример 1. Работа с медиа и URL
  59. Что делает этот код?
  60. Что делает каждая часть кода?
  61. Чем это полезно?
  62. Пример 2: Группировка медиафайлов с возможностью удаления
  63. Описание кода:
  64. Зачем это нужно?
  65. Пример 3: Управление режимами запуска диалогов
  66. Описание кода:
  67. Зачем это нужно?
  68. Пример 4: Создание списка с возможностью выбора и радио-кнопками
  69. Описание кода:
  70. Зачем это нужно?
  71. Пример 5: Обновление прогресса в фоновом режиме
  72. Описание кода:
  73. Зачем это нужно?
  74. Пример 6: Множественные стеки диалогов
  75. Описание кода:
  76. Зачем это нужно?
  77. Пример 7: Прокрутка
  78. Описание кода:
  79. Зачем это нужно?
  80. Пример 8: Простой диалог
  81. Описание кода:
  82. Зачем это нужно?
  83. Пример 9: Поддиалоги
  84. Описание кода:
  85. Зачем это нужно?
  86. Пример 10: Мастер-диалог
  87. Описание кода:
  88. Зачем это нужно?
  89. Пример 11. Локализация и мультиязычность в боте.
  90. Файлы в проекте:
  91. Зачем это нужно?
  92. Мега пример с демонстрацией работы всех виджетов.
  93. Проектные задания для тренировки

Быстрый старт

Ссылки на документацию:

Установите библиотеку:

pip install aiogram_dialog

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

from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage

storage = MemoryStorage()
bot = Bot(token='ВАШ_ТОКЕН_БОТА')
dp = Dispatcher(storage=storage)

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

from aiogram.filters.state import StatesGroup, State

class MySG(StatesGroup):
    main = State()

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

from aiogram.filters.state import State, StatesGroup
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("Привет, неизвестный человек"),  # просто постоянный текст
    Button(Const("Бесполезная кнопка"), id="nothing"),  # кнопка с текстом и идентификатором
    state=MySG.main,  # состояние используется для идентификации окна между диалогами
)

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

from aiogram_dialog import Dialog

dialog = Dialog(main_window)

Каждый диалог должен быть прикреплен к какому-то маршрутизатору или диспетчеру:

dp.include_router(dialog)

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

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

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)

Перед запуском бота вам нужно настроить middleware и основные обработчики aiogram-dialogs. Для этого используйте функцию setup_dialogs, передавая ваш экземпляр Router или Dispatcher:

from aiogram_dialog import setup_dialogs

setup_dialogs(dp)

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

if __name__ == '__main__':
    dp.run_polling(bot)

Итак, вот итоговый код:

from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import Window
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const
from aiogram_dialog import Dialog
from aiogram.filters import Command
from aiogram.types import Message
from aiogram_dialog import DialogManager, StartMode
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    main = State()

main_window = Window(
    Const("Привет, неизвестный человек"),  # просто постоянный текст
    Button(Const("Бесполезная кнопка"), id="nothing"),  # кнопка с текстом и идентификатором
    state=MySG.main,  # состояние используется для идентификации окна между диалогами
)
dialog = Dialog(main_window)
dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Виджеты текста и рендеринг

В настоящее время существует 4 вида виджетов: текстовые, клавиатурные, ввода и медиа, и вы можете создавать свои собственные виджеты.

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

Клавиатурные виджеты представляют части InlineKeyboard.

Медиа-виджеты представляют собой медиа-вложения к сообщению.

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

Виджеты могут отображать статический (например, Const) и динамический (например, Format) контент. Чтобы использовать динамические данные, вы должны их установить. См. передачу данных.

Также существуют 2 общих типа:

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

Примечание:

Идентификатор виджета может содержать только буквы ASCII, цифры, подчеркивание и точку.

123, com.mysite.id, my_item — допустимые идентификаторы

hello world, my:item, птичка — недопустимые идентификаторы

Передача данных

Текстовые виджеты

  • Const: Отображает статический текст.
  • Format: Позволяет динамически форматировать текст с использованием данных.
  • Multi: Позволяет отображать несколько текстовых виджетов в одной строке.

Клавиатурные виджеты

  • Button: Создает кнопку с заданным текстом и ID.
  • Cancel: Представляет кнопку «Отмена».
  • Row: Группирует кнопки в одну строку.
  • Group: Группирует кнопки в группу.
  • Back: Представляет кнопку «Назад».
  • Next: Представляет кнопку «Далее».
  • SwitchTo: Переключает на указанное состояние при нажатии на кнопку.

Виджеты ввода

  • MessageInput: Ожидает входящее сообщение от пользователя.

Медиа-виджеты

  • StaticMedia: Позволяет отображать медиафайлы по их пути или URL.
  • DynamicMedia: Позволяет отображать динамически созданные медиафайлы.

Скрытие виджетов

Виджеты могут быть скрыты с помощью атрибута when. Он может быть ключом данных, предикатной функцией или F-фильтром (из magic-filter).

Предикат

Predicate — это функция, которая проверяет, должен ли виджет быть показан.

Пользовательские виджеты

Вы можете создавать свои собственные виджеты, если вы не удовлетворены существующими.

Примеры пользовательских виджетов:

  • Создание кастомной кнопки с определенной логикой.
  • Создание специфичного для вашего приложения виджета для ввода данных.

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

Передача данных

В ваших диалогах вы можете отображать статические данные (просто текст) и динамические данные (значение переменной).

У вас есть доступ к следующим динамическим данным:

  • event — текущее обрабатываемое событие, которое вызвало обновление окна. Будьте осторожны при его использовании, потому что разные типы событий могут вызывать обновление одного и того же окна.
  • middleware_data — данные, переданные из middleware в обработчик.
  • start_data — данные, переданные при запуске текущего диалога. Эти данные хранятся в хранилище FSM aiogram.
  • dialog_data — вы можете использовать этот словарь для хранения данных, связанных с диалогом. Он будет доступен во всех окнах текущего диалога. Эти данные также хранятся в хранилище FSM aiogram.

Кроме того, вы можете указать функции-геттеры для диалога и окна. Функция-геттер должна возвращать словарь.

Библиотека собирает все вышеуказанные данные в один объект-словарь и передает этот объект виджетам.

Давайте рассмотрим пример:

from aiogram import Bot, Dispatcher
from aiogram.filters.state import StatesGroup, State
from aiogram.types import CallbackQuery

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

from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import Window
from aiogram_dialog.widgets.kbd import Button
from aiogram.filters import Command
from aiogram.types import Message
from aiogram_dialog import StartMode
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    window1 = State()
    window2 = State()

async def window1_get_data(**kwargs):
    return {
        "something": "данные из геттера Window1",
    }

async def window2_get_data(**kwargs):
    return {
        "something": "данные из геттера Window2",
    }

async def dialog_get_data(**kwargs):
    return {
        "name": "Victor",
    }

async def button1_clicked(callback: CallbackQuery, button: Button, manager: DialogManager):
    """ Добавить данные в `dialog_data` и переключиться на следующее окно текущего диалога """
    manager.dialog_data['user_input'] = 'некоторые данные от пользователя, хранящиеся в `dialog_data`'
    await manager.next()

dialog = Dialog(
    Window(
        Format("Привет, {name}!"),
        Format("Что-то: {something}"),        
        Button(Const("Следующее окно"), id="button1", on_click=button1_clicked),
        state=MySG.window1,
        getter=window1_get_data,  # здесь мы указываем геттер данных для window1
    ),
    Window(
        Format("Привет, {name}!"),
        Format("Что-то: {something}"),        
        Format("Ввод пользователя: {dialog_data[user_input]}"),
        Back(text=Const("Назад")),
        state=MySG.window2,
        getter=window2_get_data,  # здесь мы указываем геттер данных для window2
    ),
    getter=dialog_get_data  # здесь мы указываем геттер данных для диалога
)

#dialog = Dialog(main_window)
dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.window1, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

Результат:aiogram-dialogaiogram-dialog

В обработчиках событий у вас нет доступа к этому словарю, но вы можете получить доступ к event, middleware_data, start_data, dialog_data через dialog_manager:

async def button1_clicked(callback: CallbackQuery, button: Button, manager: DialogManager):
    dialog_data = manager.dialog_data
    event = manager.event
    middleware_data = manager.middleware_data
    start_data = manager.start_data

Типы текстовых виджетов

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

  1. Const (Константа): Возвращает текст без изменений. Пример:aiogram-dialog
    from aiogram_dialog.widgets.text import Const
    
    const_text = Const("Этот текст неизменен.")
    
  2. Format (Формат): Форматирует текст, используя функцию форматирования.

    Примечание:

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

    Пример:aiogram-dialog

    from aiogram_dialog.widgets.text import Format
    Format("Привет, {username}!"),
    state=MySG.test1,
    getter=test1_get_data,
    
  3. Multi (Множественный): Позволяет отображать несколько текстов, объединенных разделителем.Для объединения нескольких текстов вы можете использовать виджет Multi. Внутри него можно использовать любые тексты. Вы также можете указать строку-разделитель. В простых случаях вы можете просто объединять виджеты с использованием оператора +. Пример:aiogram-dialog
    from aiogram_dialog.widgets.text import Multi, Const, Format
    
    Window(
            Multi(
                Const("Первый текст"),
                Format("Второй текст: {username}"),
                sep=" | "
            ),
            state=MySG.test1,
            getter=test1_get_data,
  4. Case (Условие): Отображает один из текстов на основе условия. Пример:aiogram-dialog
    from aiogram_dialog.widgets.text import Case, Const, Format
    
    def get_length_username(data: dict, case: Case, manager: DialogManager):
        return len(data['username']) > 10
    
    async def test1_get_data(dialog_manager: DialogManager,**kwargs):
        dialog_manager.dialog_data['firstname'] = kwargs['event_from_user'].first_name
        return {
            "username": dialog_manager.dialog_data['firstname'],
        }
    
    win = Window(
            Case(
                {
                    True: Format("Значение больше 10: {username}"),
                    False: Const("Длина имени {username} не больше 10"),
                },
                selector =  get_length_username,
            ),
            state=MySG.test1,
            getter=test1_get_data,
  5. Progress (Прогресс): Отображает полосу прогресса. Пример:aiogram-dialog
    from aiogram_dialog.widgets.text import Progress, Multi, Const
    
    async def test1_get_data(dialog_manager: DialogManager,**kwargs):
        dialog_manager.dialog_data['firstname'] = kwargs['event_from_user'].first_name
        return {
            "username": dialog_manager.dialog_data['firstname'],
            "progress": 30,
        }
    
    win = Window(
            Multi(
                Const("Идет обработка данных..."),
                Progress("progress", 10),
            ),
            state=MySG.test1,
            getter=test1_get_data,
        ),
  6. List (Список): Отображает динамическую группу текстов (аналогично виджету клавиатуры Select). Пример:aiogram-dialog
    from aiogram_dialog.widgets.text import Const, Format, List
    
    async def test1_get_data(dialog_manager: DialogManager,**kwargs):
        dialog_manager.dialog_data['firstname'] = kwargs['event_from_user'].first_name
        return {
            "items": (
                ("name", "Victor"),
                ("username", "@Vvkomlev"),    
            ),
        }
    
    win = Window(
            List(
                Format("* {item[0]} - {item[1]}"),
                items="items",
                ),
            state=MySG.test1,
            getter=test1_get_data,
        )
  7. Jinja (Jinja2): Представляет HTML, отображенный с использованием шаблона jinja2. Пример:aiogram-dialog
    from aiogram_dialog.widgets.text import Jinja
    
    win = Window(
            Jinja("<b>Жирный текст</b>"),
            parse_mode="HTML",
            state=MySG.test1,
            getter=test1_get_data,
        )

     

Использование виджета  Case

Виджет Case используется для выбора одного из текстов в зависимости от какого-то условия.

Для использования этого виджета вам нужно указать как минимум два параметра:

  • texts — словарь, который содержит варианты
  • selector — выражение, которое используется для получения значения условия, которое будет использоваться для выбора опции виджета Case.

selector может быть:

  • строковым ключом — этот ключ будет использоваться как ключ словаря данных для получения значения условия
  • magic-filter (F) — результат разрешения магического фильтра будет использован в качестве условия
  • функцией — результат этой функции будет использован в качестве условия

Примечание:

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

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

Пример кода:

from typing import Any, Dict

from magic_filter import F

from aiogram.filters.state import StatesGroup, State

from aiogram_dialog import Window, DialogManager, Dialog
from aiogram_dialog.widgets.text import Const, Format, Case

class MySG(StatesGroup):
    window1 = State()
    window2 = State()

# предположим, что это наш геттер данных окна
async def get_data(**kwargs):
    return {"color": "red", "number": 42}

# Использование строкового селектора в качестве `selector`.
# Значение data["color"] будет использовано для выбора опции виджета Case, которую нужно показать.
#
# `text` произведет текст `Квадрат`
text = Case(
    {
        "red": Const("Квадрат"),
        "green": Const("Единорог"),
        "blue": Const("Луна"),
        ...: Const("Неизвестное существо"),
    },
    selector="color",
)

# Использование функции в качестве `selector`.
# Результат этой функции будет использован для выбора опции виджета Case, которую нужно показать.
#
# `text2` произведет текст `42 - четное!`
def parity_selector(data: Dict, case: Case, manager: DialogManager):
    return data["number"] % 2

text2 = Case(
    {
        0: Format("{number} - четное!"),
        1: Const("Это нечетное"),
    },
    selector=parity_selector,
)

# Использование F-фильтров в качестве селектора.
# Значение data["dialog_data"]["user"]["test_result"] будет использовано для выбора опции виджета Case,
# которую нужно показать.
#
# `text3` произведет текст `Отличная работа!`
text3 = Case(
    {
        True: Const("Отличная работа!"),
        False: Const("Попробуйте еще раз.."),
    },
    selector=F["dialog_data"]["user"]["test_result"],
)

async def on_dialog_start(start_data: Any, manager: DialogManager):
    manager.dialog_data['user'] = {
        'test_result': True,
    }

dialog = Dialog(
    Window(
        text,
        text2,
        text3,
        state=MySG.window1,
        getter=get_data
    ),
    on_start=on_dialog_start
)

Класс aiogram_dialog.widgets.text.Case принимает следующие параметры:

  • texts (Dict[Any, Text]) –
  • selector (str | Callable[[Dict, Case, DialogManager], Hashable] | MagicFilter) –
  • when (str | MagicFilter | Predicate | None) –

Виджет Progress

Виджет Progress используется, когда вам нужно отображать прогресс процесса.

import asyncio

from aiogram.filters import Command
from aiogram.fsm.state import StatesGroup, State
from aiogram.types import Message

from aiogram_dialog import Dialog, Window, DialogManager, BaseDialogManager
from aiogram_dialog.widgets.text import Multi, Const, Progress


class Main(StatesGroup):
    progress = State()


async def get_bg_data(dialog_manager: DialogManager, **kwargs):
    return {
        "progress": dialog_manager.dialog_data.get("progress", 0)
    }


async def background(manager: BaseDialogManager):
    count = 10
    for i in range(1, count + 1):
        await asyncio.sleep(1)
        await manager.update({
            "progress": i * 100 / count,
        })
    await asyncio.sleep(1)
    await manager.done()


dialog = Dialog(
    Window(
        Multi(
            Const("Ваш запрос обрабатывается, пожалуйста, подождите..."),
            Progress("progress", 10),
        ),
        state=Main.progress,
        getter=get_bg_data,
    ),
)


@dp.message(Command("start"))
async def start_handler(message: Message, dialog_manager: DialogManager):
    await dialog_manager.start(state=Main.progress)
    asyncio.create_task(background(dialog_manager.bg()))

Этот пример создает диалог с виджетом Progress, который отображает прогресс выполнения задачи. При отправке команды «/start» начинается диалог, и фоновая задача (функция background) обновляет данные прогресса в течение определенного времени. Пользователь видит изменения в реальном времени в интерфейсе диалога.

Виджет List

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

from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.text import List, Format
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import DialogManager, StartMode
from aiogram.filters import Command
from aiogram.types import Message
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    main = State()

async def getter_example(**kwargs):
    return {
        "user_info": (
            ("name", "Vicor"),
            ("username", "@Vvkomlev"),
        )
    }


dialog = Dialog(
    Window(
        List(
            Format("+ {item[0]} - {item[1]}"),
            items="user_info",
        ),
        getter=getter_example,
        state=MySG.main,
    ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

Дополнительно вы можете указать page_size и id, так что List позволит вам разбивать свои элементы на страницы. Вы можете комбинировать это с виджетами Pager, чтобы переключаться между страницами.

HTML код

Рендеринг безопасного HTML-кода с использованием шаблонов Jinja2 в aiogram-dialog довольно прост. Для использования этой функциональности вам нужно создать текст с использованием класса Jinja вместо Format и установить соответствующий режим парсинга (parse_mode). Если вы не хотите устанавливать режим парсинга по умолчанию для всего бота, вы можете установить его для каждого окна отдельно.

В приведенном ниже примере использованы подстановки переменных окружения, циклы и фильтры:

from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.text import List, Format, Jinja
from aiogram.filters.state import StatesGroup, State
from aiogram_dialog import DialogManager, StartMode
from aiogram.filters import Command
from aiogram.types import Message
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class DialogSG(StatesGroup):
    ANIMALS = State()


# предположим, что это наш геттер данных окна
async def get_data(**kwargs):
    return {
        "title": "Список животных",
        "animals": ["кот", "собака", "черепаха моего брата"]
    }


html_text = Jinja("""
<b>{{title}}</b>
{% for animal in animals %}
* <a href="https://yandex.ru/search/?text={{ animal }}">{{ animal|capitalize }}</a>
{% endfor %}
""")
dialog = Dialog(
        Window(
            html_text,
            parse_mode="HTML",  # не забудьте установить режим парсинга
            state=DialogSG.ANIMALS,
            getter=get_data,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(DialogSG.ANIMALS, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Это будет отображено в следующем HTML:

<b>Список животных</b>
* <a href="https://yandex.ru/search/?text=кот">Кот</a>
* <a href="https://yandex.ru/search/?text=собака">Собака</a>
* <a href="https://yandex.ru/search/?text=черепаха моего брата">Черепаха моего брата</a>

В данном примере используются данные из геттера, но вы также можете получить доступ к данным из dialog_data (например, {{ dialog_data[‘user’][‘weight’] }}).

Если вы хотите добавить собственные фильтры или выполнить какую-то настройку среды Jinja, вы можете сделать это, используя функцию setup_jinja из модуля aiogram_dialog.widgets.text.

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

Виджеты клавиатуры предоставляют одну или несколько кнопок внутри чата. Текст на кнопке формируется с использованием виджетов текста.

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

Простая кнопка

Простейший вариант использования — создание клавиатуры, состоящей из одной кнопки. Кнопка содержит текст, идентификатор, обработчик нажатия и условие when.

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

Обратный вызов — это обычная асинхронная функция. Она вызывается, когда пользователь нажимает на кнопку. В отличие от обычных обработчиков, вам не следует вызывать callback.answer(), поскольку это делается автоматически.

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()
    next = State()

async def go_clicked(callback: CallbackQuery, button: Button,
                     manager: DialogManager):
    await callback.message.answer("Поехали!")


dialog = Dialog(
        Window(
            Const("Привет!"),
            Button(
                Const("Поехали"),
                id="go",  # id используется для определения нажатой кнопки
                on_click=go_clicked,
                ),

            state=MySG.start,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Виджет URL

Виджет Url представляет собой кнопку с URL-ссылкой. У нее нет обратных вызовов, потому что Telegram не предоставляет уведомлений о нажатии.

Сам URL может быть любым текстом (включая Const или Format).

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Button, Url
from aiogram_dialog.widgets.text import Const
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()
    next = State()

async def go_clicked(callback: CallbackQuery, button: Button,
                     manager: DialogManager):
    await callback.message.answer("Поехали!")


dialog = Dialog(
        Window(
            Const("Привет!"),
            Button(
                Const("Поехали"),
                id="go",  # id используется для определения нажатой кнопки
                on_click=go_clicked,
                ),
            Url(
                Const("Github"),
                Const('https://github.com/Tishka17/aiogram_dialog/'),
                ),

            state=MySG.start,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

SwitchInlineQuery

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

Вы также можете указать текст, который будет отображаться в поле запроса (например, @mytestbot некий запрос).

from aiogram_dialog.widgets.kbd import SwitchInlineQuery
from aiogram_dialog.widgets.text import Const

switch_query = SwitchInlineQuery(
    Const("Найти что-то"),  # Текст кнопки
    Const("заказ")  # Дополнительный текст для поиска
)

Виджет Group

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

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Button, Group, Row
from aiogram_dialog.widgets.text import Const
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()
    next = State()



dialog = Dialog(
        Window(
            Const("Привет!"),
            Group(
                Row(
                    Button(Const("Go"), id="go"),
                    Button(Const("Run"), id="run"),
                ),
                Button(Const("Fly"), id="fly"),),

            state=MySG.start,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Также его можно использовать для создания строк фиксированной ширины. Для этого просто установите значение width в желаемое значение. Виджеты Row и Column являются группами с предопределенной шириной.

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Button, Group, Row
from aiogram_dialog.widgets.text import Const
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()
    next = State()



dialog = Dialog(
        Window(
            Const("Привет!"),
            Group(
                Button(Const("Crawl"), id="crawl"),
                Button(Const("Go"), id="go"),
                Button(Const("Run"), id="run"),
                Button(Const("Fly"), id="fly"),
                Button(Const("Teleport"), id="tele"),
                width=2,
            ),

            state=MySG.start,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Виджет Column

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

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Button, Column
from aiogram_dialog.widgets.text import Const
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()
    next = State()



dialog = Dialog(
        Window(
            Const("Привет!"),
            Column(
                Button(Const("Go"), id="go"),
                Button(Const("Run"), id="run"),
                Button(Const("Fly"), id="fly"),
            ),

            state=MySG.start,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Виджет Row

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

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Button, Row
from aiogram_dialog.widgets.text import Const
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

async def go_clicked(callback: CallbackQuery, button: Button,
                     manager: DialogManager):
    await callback.message.answer("Поехали!")


async def run_clicked(callback: CallbackQuery, button: Button,
                      manager: DialogManager):
    await callback.message.answer("Бегу!")



dialog = Dialog(
        Window(
            Const("Привет!"),
            Row(
                Button(Const("Поехали"), id="go", on_click=go_clicked),
                Button(Const("Бегу"), id="run", on_click=run_clicked),
                Button(Const("Летим"), id="fly"),
            ),

            state=MySG.start,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Виджет ListGroup

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

Чтобы идентифицировать элемент внутри события клавиатуры, вы можете использовать dialog_manager.item_id. Это возможно, потому что вместо DialogManager, когда соответствующий виджет находится внутри ListGroup, передается SubManager.

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import  Checkbox, ListGroup,  ManagedCheckbox, Radio, Row
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

def when_checked(data: dict, widget, manager: SubManager) -> bool:
    # manager for our case is already adapted for current ListGroup row
    # so `.find` returns widget adapted for current row
    # if you need to find widgets outside the row, use `.find_in_parent`
    check: ManagedCheckbox = manager.find("check")
    return check.is_checked()


async def data_getter(*args, **kwargs):
    return {
        "fruits": ["mango", "papaya", "kiwi"],
        "colors": ["blue", "pink"],
    }


dialog = Dialog(
        Window(
            Const(
            "Hello, please check you options for each item:",
            ),
            ListGroup(
                Checkbox(
                    Format("✓ {item}"),
                    Format("  {item}"),
                    id="check",
                ),
                Row(
                    Radio(
                        Format("? {item} ({data[item]})"),
                        Format("⚪️ {item} ({data[item]})"),
                        id="radio",
                        item_id_getter=str,
                        items=["black", "white"],
                        when=when_checked,
                    ),
                ),
                id="lg",
                item_id_getter=str,
                items=["apple", "orange", "pear"],                
            ),
            getter=data_getter,
            state=MySG.start,
        ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

ScrollingGroup

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

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import  Button, ScrollingGroup
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

def test_buttons_creator(btn_quantity):
    buttons = []
    for i in btn_quantity:
        i = str(i)
        buttons.append(Button(Const(i), id=i))
    return buttons


test_buttons = test_buttons_creator(range(0, 1000))


dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                ScrollingGroup(
                    *test_buttons,
                    id="numbers",
                    width=6,
                    height=6,
                ),
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Виджет Checkbox

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

Одним из таких виджетов является Checkbox (флажок). Он может находиться в состоянии отмеченного и неотмеченного, представленных двумя текстами. При каждом нажатии он инвертирует свое состояние.

Если диалог с флажком виден, вы можете проверить его состояние, вызвав метод is_checked, и изменить его, вызвав метод set_checked.

Так же, как у кнопки есть обратный вызов on_click, у флажка есть обратный вызов on_state_changed, который вызывается каждый раз при изменении состояния, независимо от причины.

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import  Checkbox, ManagedCheckbox
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

async def check_changed(event: ChatEvent, checkbox: ManagedCheckbox, manager: DialogManager):
    print("Check status changed:", checkbox.is_checked())



dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                Checkbox(
                    Const("✓  Checked"),
                    Const("Unchecked"),
                    id="check",
                    default=True,  # so it will be checked by default,
                    on_state_changed=check_changed,
                ),
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog aiogram-dialog

Виджет Select

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

Основные компоненты Select:

  • text: текст, который будет отображаться на кнопке выбора. Этот текст может быть динамическим и форматироваться с использованием данных элемента списка.
  • id: идентификатор виджета, который используется для определения, какой элемент был выбран.
  • item_id_getter: функция, которая извлекает идентификатор элемента из данных элемента списка. Этот идентификатор используется для идентификации выбранного элемента.
  • items: параметр, который указывает, откуда брать данные для элементов списка. Это может быть ключ в данных окна, статический список элементов, фильтр или функция, возвращающая список элементов.
  • on_click: функция обратного вызова, которая вызывается при выборе элемента. Она принимает параметры, такие как CallbackQuery, виджет и DialogManager.

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

from typing import Any
import operator
from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import  Select
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

# let's assume this is our window data getter
async def get_data(**kwargs):
    fruits = [
        ("Apple", '1'),
        ("Pear", '2'),
        ("Orange", '3'),
        ("Banana", '4'),
    ]
    return {
        "fruits": fruits,
        "count": len(fruits),
    }


async def on_fruit_selected(callback: CallbackQuery, widget: Any,
                            manager: DialogManager, item_id: str):
    print("Fruit selected: ", item_id)


dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                Select(
                    Format("{item[0]} ({pos}/{data[count]})"),  # E.g `✓ Apple (1/4)`
                    id="s_fruits",
                    item_id_getter=operator.itemgetter(1),
                    # each item is a tuple with id on a first position
                    items="fruits",  # we will use items from window data at a key `fruits`
                    on_click=on_fruit_selected,
                ),
                getter=get_data,
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

При кликах на кнопки Select будет выведено:

Fruit selected:  1
Fruit selected:  2
Fruit selected:  3
Fruit selected:  4
Fruit selected:  3
Fruit selected:  3
Fruit selected:  4
Fruit selected:  2
Fruit selected:  1

Таким образом, основная необходимая вещь — это items. У вас есть 4 варианта для указания этого параметра:

  1. Строка с ключом в данных вашего окна (например, items="fruits"). Значение по этому ключу должно быть коллекцией любых объектов.
  2. Статический список элементов (например, items=['apple', 'banana', 'orange']).
  3. Magic-фильтр (например, items=F["fruits"]). Фильтр должен возвращать коллекцию элементов.
  4. Функция с одним параметром (data: Dict), которая возвращает коллекцию элементов (например, items=lambda d: d["fruits"]).

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

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

Примечание: Select размещает все в одном ряду. Если вам это не подходит, просто оберните его в Group или Column.

Виджет Radio

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

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

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

Пример:

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import  Radio
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

# let's assume this is our window data getter
async def get_data(**kwargs):
    fruits = [
        ("Apple", '1'),
        ("Pear", '2'),
        ("Orange", '3'),
        ("Banana", '4'),
    ]
    return {
        "fruits": fruits,
        "count": len(fruits),
    }


dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                Radio(
                    Format("? {item[0]}"),  # Пример: `? Apple`
                    Format("⚪️ {item[0]}"),
                    id="r_fruits",
                    item_id_getter=operator.itemgetter(1),
                    items="fruits",
                ),
                getter=get_data,
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Полезные методы:

  • get_checked — возвращает id выбранного элемента
  • is_checked — возвращает, выбран ли определенный элемент
  • set_checked — устанавливает выбранный элемент по id

Multiselect

Multiselect — это еще один вид виджета выбора с состоянием. Он очень похож на Radio, но запоминает несколько выбранных элементов.

Как и для Radio, вы должны передать два текста (для выбранных и невыбранных элементов). Переданные данные такие же, как и для Select.

Пример:

import operator
from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import  Multiselect
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

# let's assume this is our window data getter
async def get_data(**kwargs):
    fruits = [
        ("Apple", '1'),
        ("Pear", '2'),
        ("Orange", '3'),
        ("Banana", '4'),
    ]
    return {
        "fruits": fruits,
        "count": len(fruits),
    }


dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                Multiselect(
                    Format("✓ {item[0]}"),  # Пример: `✓ Apple`
                    Format("{item[0]}"),
                    id="m_fruits",
                    item_id_getter=operator.itemgetter(1),
                    items="fruits",
                ),
                getter=get_data,
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

После нескольких кликов он будет выглядеть так:aiogram-dialog

Другие полезные параметры:

  • min_selected — ограничивает минимальное количество выбранных элементов, игнорируя клики, если это ограничение нарушается. Это не влияет на начальное состояние.
  • max_selected — ограничивает максимальное количество выбранных элементов
  • on_state_changed — функция обратного вызова. Вызывается, когда выбранный элемент меняет свое состояние.

Чтобы работать с выбором, вы можете использовать следующие методы:

  • get_checked — возвращает список идентификаторов всех выбранных элементов
  • is_checked — возвращает, выбран ли определенный элемент
  • set_checked — изменяет состояние выбора предоставленного идентификатора
  • reset_checked — сбрасывает все отмеченные элементы в состояние «не выбрано»

Виджет календаря

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

Пример использования:

from datetime import date
from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import  Calendar
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

async def on_date_selected(callback: CallbackQuery, widget, manager: DialogManager, selected_date: date):
    await callback.answer(str(selected_date))

dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                Calendar(id='calendar', on_click=on_date_selected),
                #getter=get_data,
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog aiogram-dialog aiogram-dialog

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

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

class CustomCalendar(Calendar):
    def _init_views(self) -> Dict[CalendarScope, CalendarScopeView]:
        return {
            CalendarScope.DAYS: CalendarDaysView(
                self._item_callback_data, self.config,
                today_text=Format("***"),
                header_text=Format("> {date: %B %Y} <"),
            ),
            CalendarScope.MONTHS: CalendarMonthView(
                self._item_callback_data, self.config,
            ),
            CalendarScope.YEARS: CalendarYearsView(
                self._item_callback_data, self.config,
            ),
        }

    async def _get_user_config(
            self,
            data: Dict,
            manager: DialogManager,
    ) -> CalendarUserConfig:
        return CalendarUserConfig(
            firstweekday=7,
        )

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

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

from typing import Dict
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import (
    Calendar, CalendarScope, CalendarUserConfig,
)
from aiogram_dialog.widgets.kbd.calendar_kbd import (
    CalendarDaysView, CalendarMonthView, CalendarScopeView, CalendarYearsView,
)
from aiogram_dialog.widgets.text import Const, Format

class CustomCalendar(Calendar):
    def _init_views(self) -> Dict[CalendarScope, CalendarScopeView]:
        return {
            CalendarScope.DAYS: CalendarDaysView(
                self._item_callback_data, self.config,
                today_text=Format("***"),
                header_text=Format("> {date: %B %Y} <"),
            ),
            CalendarScope.MONTHS: CalendarMonthView(
                self._item_callback_data, self.config,
            ),
            CalendarScope.YEARS: CalendarYearsView(
                self._item_callback_data, self.config,
            ),
        }

    async def _get_user_config(
            self,
            data: Dict,
            manager: DialogManager,
    ) -> CalendarUserConfig:
        return CalendarUserConfig(
            firstweekday=7,
        )

Счётчик

Виджет счетчика (Counter) представляет собой простой способ ввода числа с помощью кнопок «+» и «-«.

Пример использования:

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import   ManagedCounter, Counter
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs

storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

async def on_text_click(
        event: CallbackQuery,
        widget: ManagedCounter,
        dialog_manager: DialogManager
) -> None:
    await event.answer(f"Value: {widget.get_value()}")

dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                Counter(
                    id="someid",
                    default=0,
                    max_value=1000,
                    on_text_click=on_text_click
                ),
                #getter=get_data,
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

Классы:

aiogram_dialog.widgets.kbd.counter.OnCounterEvent:
Абстрактный класс для обработки событий счетчика.

aiogram_dialog.widgets.kbd.Counter:
Виджет счетчика. Используется для представления числа с кнопками инкремента/декремента.

Параметры:

  • id (str): Идентификатор виджета.
  • plus (Text | None): Текст для кнопки «+». Установите None, чтобы отключить.
  • minus (Text | None): Текст для кнопки «-«. Установите None, чтобы отключить.
  • text (Text | None): Текст для кнопки с текущим значением. Установите None, чтобы отключить.
  • min_value (float): Минимальное допустимое значение.
  • max_value (float): Максимальное допустимое значение.
  • increment (float): Шаг инкремента.
  • default (float): Значение по умолчанию.
  • cycle (bool): Флаг циклического перехода значений при достижении минимального или максимального значения.
  • on_click (OnCounterEvent | WidgetEventProcessor | None): Callback для обработки любого клика.
  • on_text_click (OnCounterEvent | WidgetEventProcessor | None): Callback для обработки клика по текстовой кнопке.
  • on_value_changed (OnCounterEvent | WidgetEventProcessor | None): Callback для обработки изменений значения, независимо от причины.
  • when (str | MagicFilter | Predicate | None): Условие отображения виджета.

aiogram_dialog.widgets.kbd.ManagedCounter:
Управляемый счетчик. Позволяет получить и установить текущее значение счетчика.

Методы:

  • get_value(): Получить текущее значение счетчика.
  • async set_value(value): Установить текущее значение счетчика.

Switch To

Виджет SwitchTo представляет собой кнопку, при нажатии на которую происходит переход к определенному состоянию в том же диалоге.

Пример использования с пользовательской функцией обработки события:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события
    await dialog_manager.switch_to(SOME_STATE)

button = Button(..., on_click=on_click)

Тот же пример, но с использованием класса SwitchTo:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import SwitchTo

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события

button = SwitchTo(..., state=SOME_STATE, on_click=on_click)

Параметры:

  • text (Text): Текст кнопки.
  • id (str): Идентификатор кнопки.
  • state (State): Состояние, к которому будет осуществлен переход.
  • on_click (Callable[[CallbackQuery, Button, DialogManager], Awaitable] | None): Функция, вызываемая при нажатии на кнопку.
  • when (str | MagicFilter | Predicate | None): Условие отображения кнопки.

Кнопки «Вперёд» и «Назад»

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

Пример использования с пользовательской функцией обработки события:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события
    await dialog_manager.next()  # или await dialog_manager.back()

button = Button(..., on_click=on_click)

Тот же пример, но с использованием классов Next и Back:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Next, Back

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события

button = Next(..., on_click=on_click)  # или button = Back(..., on_click=on_click)

Параметры:

  • text (Text): Текст кнопки.
  • id (str): Идентификатор кнопки.
  • on_click (Callable[[CallbackQuery, Button, DialogManager], Awaitable] | None): Функция, вызываемая при нажатии на кнопку.
  • when (str | MagicFilter | Predicate | None): Условие отображения кнопки.

Кнопка «Старт»

Виджет Start представляет собой кнопку, при нажатии на которую начинается новый диалог.

Пример использования с пользовательской функцией обработки события:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события
    await dialog_manager.start(SOME_STATE, SOME_DATA)

button = Button(..., on_click=on_click)

Тот же пример, но с использованием класса Start:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Start

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события

button = Start(..., state=SOME_STATE, data=SOME_DATA, on_click=on_click)

Параметры:

  • text (Text): Текст кнопки.
  • id (str): Идентификатор кнопки.
  • state (State): Состояние, с которого начнется новый диалог.
  • data (Dict | List | int | str | float | None): Данные, которые будут переданы в новый диалог. Может быть любым объектом, который поддерживает сериализацию и десериализацию в JSON.
  • on_click (Callable[[CallbackQuery, Button, DialogManager], Awaitable] | None): Функция, вызываемая при нажатии на кнопку.
  • mode (StartMode): Режим запуска нового диалога. По умолчанию используется StartMode.NORMAL.
  • when (str | MagicFilter | Predicate | None): Условие отображения кнопки.

Кнопка «Отмена»

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

Пример использования с пользовательской функцией обработки события:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Button

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события
    await dialog_manager.done()

button = Button(..., on_click=on_click)

Тот же пример, но с использованием класса Cancel:

from aiogram.types import CallbackQuery
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Cancel

async def on_click(
        cq: CallbackQuery,
        button: Button,
        dialog_manager: DialogManager
):
    ...  # ваш код обработки события

button = Cancel(..., on_click=on_click)

Параметры:

  • text (Text): Текст кнопки.
  • id (str): Идентификатор кнопки.
  • result (Any): Результат, который будет передан в вызывающий код при завершении диалога. Может быть любым объектом.
  • on_click (Callable[[CallbackQuery, Button, DialogManager], Awaitable] | None): Функция, вызываемая при нажатии на кнопку.
  • when (str | MagicFilter | Predicate | None): Условие отображения кнопки.

Виджеты для ввода текста

Два типа виджетов для ввода:

  1. MessageInput (ВводСообщения): Этот виджет используется для захвата сообщений, отправленных пользователем. Когда сообщение получено, он инициирует событие, которое позволяет обработать сообщение. Он особенно полезен, когда вам нужно захватить определенные типы сообщений (например, текстовые сообщения, фотографии, документы) и выполнить действия на основе содержания этих сообщений.
  2. TextInput (ТекстовыйВвод): Этот виджет позволяет пользователям вводить текст напрямую. Он предоставляет поле ввода, в котором пользователи могут вводить свой текст. Введенный текст сохраняется в контексте, что позволяет вам получить доступ к нему и использовать позже в ходе разговора. Этот виджет обычно используется для сценариев, где пользователям необходимо предоставить текстовую информацию, такую как ответы на вопросы, обратную связь или ввод команд.

TextInput

Класс TextInput (ТекстовыйВвод) представляет собой виджет для ввода текста пользователем. Вот его основные компоненты:

  • OnSuccess: Это абстрактный класс, который позволяет определить действия, которые будут выполнены при успешном вводе текста. Метод __call__ вызывается при успешном завершении ввода текста. Параметры метода включают сообщение (message), введенный текст (widget), менеджер диалогов (dialog_manager) и данные (data). Возвращаемое значение может быть любым.
  • OnError: Этот абстрактный класс позволяет определить действия, которые будут выполнены при возникновении ошибки во время ввода текста. Метод __call__ вызывается при возникновении ошибки. Параметры метода включают сообщение (message), виджет (widget), менеджер диалогов (dialog_manager) и объект ошибки (error). Возвращаемое значение может быть любым.
  • TextInput: Этот класс представляет сам виджет ввода текста. Он принимает несколько параметров:
    • id: Уникальный идентификатор виджета.
    • type_factory: Функция для преобразования вводимого текста в определенный тип данных.
    • on_success: Обработчик успешного завершения ввода текста. Может быть экземпляром класса OnSuccess, обработчиком событий виджета или None.
    • on_error: Обработчик ошибки во время ввода текста. Может быть экземпляром класса OnError, обработчиком событий виджета или None.
    • filter: Фильтр для вводимых данных, опциональный параметр.
  • ManagedTextInput: Этот класс обеспечивает управление виджетом TextInput. Он позволяет получить последние введенные данные с помощью метода get_value().

Виджеты для работы с медиа

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

1. StaticMedia (Статическое медиа)

StaticMedia представляет простой способ обмена медиафайлами по URL-адресу или пути к файлу.

2. DynamicMedia (Динамическое медиа)

DynamicMedia — это некоторое медиа-прикрепление, создаваемое динамически. Этот виджет позволяет динамически формировать медиа-вложения.

3. Другие источники медиа

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

StaticMedia (Статическое медиа)

StaticMedia позволяет обмениваться медиафайлами по их пути или URL-адресу. Хотя адрес поддерживает интерполяцию строк, как это может быть в виджете Text, другие параметры остаются статическими.

Вы можете использовать StaticMedia, предоставляя путь или URL-адрес к файлу, его тип содержимого (ContentType) и дополнительные параметры при необходимости. Также вам может потребоваться изменить тип медиа (например, type=ContentType.PHOTO) или предоставить любые дополнительные параметры, поддерживаемые aiogram, с помощью media_params.

Будьте осторожны при использовании относительных путей. Учитывайте рабочий каталог.

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import   ManagedCounter, Counter
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs
from aiogram.types import ContentType
from aiogram_dialog.widgets.media import StaticMedia


storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    start = State()

async def on_text_click(
        event: CallbackQuery,
        widget: ManagedCounter,
        dialog_manager: DialogManager
) -> None:
    await event.answer(f"Value: {widget.get_value()}")

dialog = Dialog(
            Window(
                Const(
                "Привет!",
                ),
                StaticMedia(
                    path=r"d:\Downloads\1646818457_69-sportishka-com-p-sovremennii-dom-u-ozera-turizm-krasivo-fot-69.jpg",
                    type=ContentType.PHOTO,
                ),
                #getter=get_data,
                state=MySG.start,
            ),
)

dp.include_router(dialog)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.start, mode=StartMode.RESET_STACK)

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

aiogram-dialog

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

Примечание:

  • Telegram позволяет отправлять файлы, используя идентификатор файла (file_id), вместо повторной загрузки того же файла. Это делает отправку медиафайлов намного быстрее. aiogram_dialog использует эту функцию и кэширует отправленные идентификаторы файлов в памяти.
  • Если вы хотите создать постоянный кэш file_id, реализуйте протокол MediaIdStorageProtocol и передайте экземпляр в реестр вашего диалога.

DynamicMedia (Динамическое медиа)

DynamicMedia позволяет обмениваться любыми поддерживаемыми медиафайлами. Просто верните объект MediaAttachment из функции-поставщика данных и установите селектор для имени поля. Другой вариант — передать вызываемый объект, возвращающий MediaAttachment, как селектор.

Пример кода:

from aiogram.enums import ContentType
from aiogram.fsm.state import StatesGroup, State
from aiogram_dialog import Dialog, Window
from aiogram_dialog.api.entities import MediaAttachment, MediaId
from aiogram_dialog.widgets.media import DynamicMedia

class Main(StatesGroup):
    menu = State()

async def get_data(**kwargs):
    # Ваш file_id
    image_id = "AgACAgIAAxkBAAICaGRBazvG-8X5riVWiz3vF9aW5LPqAAI8xjEbzg4ISoMkVbG_PhpbAQADAgADdwADLwQ"
    image = MediaAttachment(ContentType.PHOTO, file_id=MediaId(image_id))
    return {'photo': image}

dialog = Dialog(
    Window(
        DynamicMedia("photo"),
        state=Main.menu,
        getter=get_data,
    ),
)

Результат: Здесь будет изображение или другой медиафайл, возвращаемый из функции get_data.

Другие источники медиа

Иногда у вас есть пользовательские источники медиафайлов: ни файл в файловой системе, ни URL в интернете, ни существующий файл в Telegram. Это может быть какое-то внутреннее хранилище, такое как база данных или частное совместимое с S3, или даже объекты, созданные во время выполнения.

В этом случае рекомендуемые шаги для решения проблемы:

  1. Сгенерируйте какой-то пользовательский URI, идентифицирующий ваш медиафайл. Это может быть строка вроде «bot://1234» или что угодно еще, что вы хотите.
  2. Унаследуйтесь от класса MessageManager и переопределите метод get_media_source для загрузки данных, идентифицированных вашим URI, из пользовательского источника.
  3. Передайте экземпляр вашего менеджера сообщений при создании Registry.

С такой реализацией вы сможете сделать пользовательское получение медиафайлов и продолжать использовать существующие виджеты медиа и кэширование идентификаторов файлов.

Скрытие виджетов

На самом деле каждый виджет может быть скрыт, включая тексты, кнопки, группы и так далее. Это управляется атрибутом when. Он может быть ключом данных, предикатной функцией или F-фильтром (из magic-filter).

F-фильтр получает данные из геттера и другие данные диалога. Вы можете ссылаться на него, например, F[«extended»] или F[«dialog_data»][«user»][«name»].

Пример кода:

from typing import Dict

from aiogram.filters.state import StatesGroup, State
from magic_filter import F

from aiogram_dialog import Window, DialogManager
from aiogram_dialog.widgets.common import Whenable
from aiogram_dialog.widgets.kbd import Button, Row, Group
from aiogram_dialog.widgets.text import Const, Format, Multi


class MySG(StatesGroup):
    main = State()


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


def is_tishka17(data: Dict, widget: Whenable, manager: DialogManager):
    return data.get("name") == "Tishka17"


window = Window(
    Multi(
        Const("Hello"),
        Format("{name}", when="extended"),
        sep=" "
    ),
    Group(
        Row(
            Button(Const("Wait"), id="wait"),
            Button(Const("Ignore"), id="ignore"),
            when=F["extended"],
        ),
        Button(Const("Admin mode"), id="nothing", when=is_tishka17),
    ),
    state=MySG.main,
    getter=get_data,
)

Если вы измените данные, установив «extended»: True, окно будет выглядеть иначе.

Пользовательские виджеты

Если вас не удовлетворяют существующие виджеты, вы можете создать собственные.

Если вы создаете виджет клавиатуры, важно создавать экземпляры InlineKeyboardButton при каждой отрисовке, а не повторно использовать один и тот же экземпляр. Это связано с тем, что диалоги изменяют callback_data в нем.

SwitchInlineQueryCurrentChat

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

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

from typing import Dict, List

from aiogram.filters.state import StatesGroup, State
from aiogram.types import InlineKeyboardButton
from aiogram_dialog import Dialog, Window
from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import SwitchInlineQuery
from aiogram_dialog.widgets.text import Const


class SwitchInlineQueryCurrentChat(SwitchInlineQuery):
    async def _render_keyboard(
            self,
            data: Dict,
            manager: DialogManager,
    ) -> List[List[InlineKeyboardButton]]:
        return [
            [
                InlineKeyboardButton(
                    text=await self.text.render_text(data, manager),
                    switch_inline_query_current_chat=await self.switch_inline.render_text(
                        data, manager,
                    ),
                ),
            ],
        ]


class MySG(StatesGroup):
    main = State()


dialog = Dialog(
    Window(
        SwitchInlineQueryCurrentChat(
            Const("Some search"),  # Текст кнопки
            Const("query")  # Дополнительный запрос. Опционально
        ),
        state=MySG.main
    )
)

Переходы в диалогах

Типы переходов

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

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

stack_transitions

Стек задач

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

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

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

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

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

Переключение состояния

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

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

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

switchstate

Пример создания трех окон с кнопками и переходами:

from aiogram.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(callback: CallbackQuery, button: Button,
                    manager: DialogManager):
    await manager.switch_to(DialogSG.second)


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


async def go_next(callback: CallbackQuery, button: Button,
                  manager: DialogManager):
    await manager.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 при клике.

Тот же пример можно переписать, используя эти кнопки:

from aiogram.filters.state import StatesGroup, State

from aiogram_dialog import Dialog, Window
from aiogram_dialog.widgets.kbd import Back, Next, Row, SwitchTo
from aiogram_dialog.widgets.text import Const


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


dialog = Dialog(
    Window(
        Const("First"),
        SwitchTo(Const("To second"), id="sec", state=DialogSG.second),
        state=DialogSG.first,
    ),
    Window(
        Const("Second"),
        Row(
            Back(),
            Next(),
        ),
        state=DialogSG.second,
    ),
    Window(
        Const("Third"),
        Back(),
        state=DialogSG.third,
    )
)

Инструменты помощи (экспериментальные)

Диаграмма состояний

Вы можете создать изображение со своими состояниями и переходами.

Сначала вам нужно установить graphviz на свою систему. Проверьте инструкции по установке на официальном сайте.

Установите библиотеку с инструментами:

pip install aiogram_dialog[tools]

Импортируйте метод рендеринга:

from aiogram_dialog.tools import render_transitions

Вызовите его, передав свой экземпляр Dispatcher, Router или Dialog:

from aiogram.filters.state import StatesGroup, State
from aiogram.types import Message

from aiogram_dialog import Dialog, DialogManager, DialogProtocol, Window
from aiogram_dialog.tools import render_transitions
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Back, Next
from aiogram_dialog.widgets.text import Const


class MySG(StatesGroup):
    first = State()
    second = State()
    last = State()


async def on_input(
        message: Message, dialog: DialogProtocol, manager: DialogManager,
):
    manager.dialog_data["name"] = message.text
    await manager.next()


dialog = Dialog(
    Window(
        Const("1. First"),
        Next(),
        state=MySG.first,
    ),
    Window(
        Const("2. Second"),
        Back(),
        MessageInput(on_input),
        state=MySG.second,
    ),
    Window(
        Const("3. Last"),
        Back(),
        state=MySG.last,
    ),
)

# этот код рендерит диаграмму
render_transitions(dialog)

aiogram_dialog

 

Подсказки переходов состояний

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

Чтобы исправить это поведение, вы можете установить параметр preview_add_transitions окна:

from aiogram.filters.state import StatesGroup, State
from aiogram.types import Message

from aiogram_dialog import Dialog, DialogManager, DialogProtocol, Window
from aiogram_dialog.tools import render_transitions
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Next, Back
from aiogram_dialog.widgets.text import Const


class MySG(StatesGroup):
    first = State()
    second = State()
    last = State()


async def on_input(message: Message, dialog: DialogProtocol,
                   manager: DialogManager):
    manager.dialog_data["name"] = message.text
    await manager.next()  # инструмент рендеринга не может обнаружить этот вызов


dialog = Dialog(
    Window(
        Const("1. First"),
        Next(),
        state=MySG.first,
    ),
    Window(
        Const("2. Second"),
        Back(),
        MessageInput(on_input),
        state=MySG.second,
        preview_add_transitions=[Next()],  # это подсказка для инструмента рендеринга
    ),
    Window(
        Const("3. Last"),
        Back(),
        state=MySG.last,
    )
)

render_transitions(dialog)

aiogram_dialog

Предпросмотр диалогов

Импортируйте метод рендеринга:

from aiogram_dialog.tools import render_preview

Добавьте некоторые данные, которые должны отображаться на предварительном просмотре, используя параметр preview_data окна:

from aiogram import Bot, Dispatcher
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command
from aiogram.filters.state import StatesGroup, State
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram_dialog import DialogManager, StartMode, SubManager, ChatEvent
from aiogram_dialog import Dialog, Window, DialogProtocol
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Back, Next
from aiogram_dialog.widgets.text import Const, Format
from aiogram_dialog import setup_dialogs
from aiogram.types import ContentType
from aiogram_dialog.widgets.media import StaticMedia
from aiogram_dialog.tools import render_transitions,render_preview


storage = MemoryStorage()
bot = Bot(token=TOKEN)
dp = Dispatcher(storage=storage)

class MySG(StatesGroup):
    first = State()
    second = State()
    last = State()

async def on_input(
        message: Message, dialog: DialogProtocol, manager: DialogManager,
):
    manager.dialog_data["name"] = message.text
    await manager.next()

async def get_data(dialog_manager: DialogManager,**kwargs):
    return {"name": kwargs['event_from_user'].first_name,}


dialog = Dialog(
    Window(
        Format("description: {name}"),
        Next(),
        state=MySG.first,
        preview_data={"name": "Первый диалог"},
        getter=get_data,
    ),
    Window(
        Format("description: {name}"),
        Next(),
        Back(),
        MessageInput(on_input),
        state=MySG.second,
        preview_add_transitions=[Next()],  # это подсказка для инструмента рендеринга
        preview_data={"name": "Второй диалог"},
        getter=get_data,
    ),
    Window(
        Format("description: {name}"),
        Back(),
        state=MySG.last,
        preview_data={"name": "Последний диалог"},
        getter=get_data,        
    ),
    getter=get_data,
)



dp.include_router(dialog)
render_transitions(dp)

@dp.message(Command("start"))
async def start(message: Message, dialog_manager: DialogManager):
    # Важно: всегда устанавливайте `mode=StartMode.RESET_STACK`, чтобы не накапливать диалоги
    await dialog_manager.start(MySG.first, mode=StartMode.RESET_STACK)
    await render_preview(dialog, "preview.html")

setup_dialogs(dp)

if __name__ == '__main__':
    dp.run_polling(bot)

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

Предпросмотр диалогов

Веб-предпросмотр

Вместо создания файлов с предпросмотром вы можете предоставить их с помощью веб-браузера.

Просто запустите команду aiogram-dialog-preview, передав путь к экземпляру Dispatcher/Router/Dialog в форме path/to/dir/package.module:object_or_callable

aiogram-dialog-preview example/subdialog:dialog_router

Часто задаваемые вопросы (FAQ)

Как мне получить данные из состояний виджета (Checkbox, Multiselect и т. д.)?

Если у вас есть глобальная переменная с виджетом, вы можете использовать ее с помощью dialog_manager:

widget.get_checked(manager)

Другой вариант — использовать идентификатор виджета для получения адаптера, а затем вызвать его методы:

widget = dialog_manager.dialog().find('some_widget_id')
widget.get_checked()

Для чего предназначен current_context().widget_data?

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

Как я могу установить значение по умолчанию для Multiselect или Radio?

Лучший способ — это использовать метод on_start обратного вызова диалога.

Как мне показать виджет Select в нескольких строках? А как насчет пагинации?

Оберните его каким-либо макетным виджетом, таким как Group, Column или ScrollingGroup.

Как мне показать много кнопок, загруженных из моей базы данных, и разбить их по страницам?

Создайте виджет Select и оберните его ScrollingGroup. В этом случае элементы должны быть загружены в геттере окна.

Как мне показать фотографию по ее file_id?

Вам нужно создать пользовательский виджет. Используйте StaticMedia в качестве примера.

Как запросить местоположение или контакт пользователя?

Вам нужно каким-то образом отправить сообщение с клавиатурой ответа. Вы можете использовать MessageInput для обработки ответа внутри окна.

Как мне показать список с кнопками URL, аналогично виджету Select?

Создайте ListGroup и поместите туда виджет Url.

Как заставить библиотеку не отправлять новое сообщение, когда пользователь отправляет сообщение самостоятельно?

Это работает таким образом, потому что иначе диалог может находиться вне экрана пользователя, и он потеряет его. Если вы все еще хотите отключить эту функцию, вы можете добавить MessageInput, а затем установить dialog_manager.show_mode = ShowMode.EDIT внутри обработчика.

Как я могу получить доступ к данным middleware внутри обработчиков диалога или виджетов?

В геттере вы получите его как kwargs.

В обработчиках он доступен через dialog_manager.data.

Во время рендеринга (как в Format) он передается как middleware_data.

Как я могу найти текущего пользователя?

Получите его как dialog_manager.event.from_user.

Осторожно: в случае фоновых обновлений (выполняемых через BgManager) в нем может содержаться только идентификатор. Если это не подходит для вашего случая, установите load=True при создании bg manager.

Как мне передать данные между диалогами?

Ввод — передавайте через dialog_manager.start(…, data=»here»), читайте с помощью dialog_manager.start_data. Вывод — передавайте через dialog_manager.done(result=»here»), читайте как параметр в on_process_result родительского диалога.

Подробнее: Запуск диалога, Закрытие диалога.

Собственные виджеты и рендеринг

Базовая информация

В настоящее время существует 4 вида виджетов: текстовые, клавиатурные, ввода и медиа, и вы можете создавать свои собственные виджеты.

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

Клавиатурные виджеты представляют части InlineKeyboard.

Медиа-виджеты представляют собой медиа-вложения к сообщению.

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

Виджеты могут отображать статический (например, Const) и динамический (например, Format) контент. Чтобы использовать динамические данные, вы должны их установить. См. передачу данных.

Также существуют 2 общих типа:

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

Примечание:

Идентификатор виджета может содержать только буквы ASCII, цифры, подчеркивание и точку.

123, com.mysite.id, my_item — допустимые идентификаторы

hello world, my:item, птичка — недопустимые идентификаторы

Передача данных

Текстовые виджеты

  • Const: Отображает статический текст.
  • Format: Позволяет динамически форматировать текст с использованием данных.
  • Multi: Позволяет отображать несколько текстовых виджетов в одной строке.

Клавиатурные виджеты

  • Button: Создает кнопку с заданным текстом и ID.
  • Cancel: Представляет кнопку «Отмена».
  • Row: Группирует кнопки в одну строку.
  • Group: Группирует кнопки в группу.
  • Back: Представляет кнопку «Назад».
  • Next: Представляет кнопку «Далее».
  • SwitchTo: Переключает на указанное состояние при нажатии на кнопку.

Виджеты ввода

  • MessageInput: Ожидает входящее сообщение от пользователя.

Медиа-виджеты

  • StaticMedia: Позволяет отображать медиафайлы по их пути или URL.
  • DynamicMedia: Позволяет отображать динамически созданные медиафайлы.

Скрытие виджетов

Виджеты могут быть скрыты с помощью атрибута when. Он может быть ключом данных, предикатной функцией или F-фильтром (из magic-filter).

Предикат

Predicate — это функция, которая проверяет, должен ли виджет быть показан.

Пользовательские виджеты

Вы можете создавать свои собственные виджеты, если вы не удовлетворены существующими.

Примеры пользовательских виджетов:

  • Создание кастомной кнопки с определенной логикой.
  • Создание специфичного для вашего приложения виджета для ввода данных.

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

Примеры использования с объяснением

Примеры взяты из официального репозитория aiogram-dialog.

Пример 1. Работа с медиа и URL

Код
import asyncio
import logging
import os.path
from io import BytesIO
from typing import Union


from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import BufferedInputFile, ContentType, InputFile, Message
from PIL import Image, ImageDraw, ImageFont


from aiogram_dialog import (
    Dialog, DialogManager, setup_dialogs,
    StartMode, Window,
)
from aiogram_dialog.api.entities import MediaAttachment
from aiogram_dialog.manager.message_manager import MessageManager
from aiogram_dialog.widgets.kbd import Back, Next, Row
from aiogram_dialog.widgets.media import StaticMedia
from aiogram_dialog.widgets.text import Const


src_dir = os.path.normpath(os.path.join(__file__, os.path.pardir))

API_TOKEN = os.getenv("BOT_TOKEN")
CUSTOM_URL_PREFIX = "my://"


def draw(text) -> bytes:
    logging.info("Draw image")
    img = Image.new("RGB", (200, 100), 0x800000)
    draw = ImageDraw.Draw(img)

    fontsize = 40
    try:
        font = ImageFont.truetype("FreeSans.ttf", size=fontsize)
    except OSError:
        font = ImageFont.truetype("arial.ttf", size=fontsize)
    width = font.getlength(text)
    draw.text((100 - width / 2, 10), str(text), font=font)

    io = BytesIO()
    img.save(io, "PNG")
    io.seek(0)
    return io.read()


class DialogSG(StatesGroup):
    custom = State()
    custom2 = State()
    normal = State()


class CustomMessageManager(MessageManager):
    async def get_media_source(
        self, media: MediaAttachment, bot: Bot,
    ) -> Union[InputFile, str]:
        if media.file_id:
            return await super().get_media_source(media, bot)
        if media.url and media.url.startswith(CUSTOM_URL_PREFIX):
            text = media.url[len(CUSTOM_URL_PREFIX):]
            return BufferedInputFile(draw(text), f"{text}.png")
        return await super().get_media_source(media, bot)


dialog = Dialog(
    Window(
        Const("Custom image:"),
        StaticMedia(
            url="my://text",
            type=ContentType.PHOTO,
        ),
        Next(),
        state=DialogSG.custom,
    ),
    Window(
        Const("Another custom image:"),
        StaticMedia(
            url="my://another",
            type=ContentType.PHOTO,
        ),
        Row(Back(), Next()),
        state=DialogSG.custom2,
    ),
    Window(
        Const("Normal image:"),
        StaticMedia(
            path=os.path.join(src_dir, "python_logo.png"),
            type=ContentType.PHOTO,
        ),
        Back(),
        state=DialogSG.normal,
    ),
)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(DialogSG.custom, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    bot = Bot(token=API_TOKEN)

    dp = Dispatcher()
    dp.message.register(start, CommandStart())
    dp.include_router(dialog)
    setup_dialogs(dp, message_manager=CustomMessageManager())

    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Этот пример демонстрирует использование библиотеки aiogram-dialog для создания диалогового интерфейса в Telegram-боте с использованием изображений.

Что делает этот код?

  1. Создает изображения на лету с помощью библиотеки PIL (Python Imaging Library), когда пользователь запрашивает специальные изображения с текстом.
  2. Использует aiogram для создания Telegram-бота и регистрации обработчика команды /start.
  3. Использует aiogram-dialog для создания диалогового интерфейса, включающего в себя три различных окна:
    • Окно «Custom image:», позволяющее пользователю запросить изображение с текстом.
    • Окно «Another custom image:», также позволяющее пользователю запросить другое изображение с текстом.
    • Окно «Normal image:», предоставляющее обычное изображение, загруженное из файла.
  4. Определяет класс CustomMessageManager, который наследует от MessageManager из aiogram-dialog и переопределяет метод get_media_source. Этот метод позволяет пользователю передавать специальные URL для создания изображений с текстом.
  5. Запускает бота и настраивает обработчики команд.

Что делает каждая часть кода?

  • draw(text): Функция, которая создает изображение с заданным текстом с помощью библиотеки PIL.
  • CustomMessageManager: Класс, который расширяет функциональность стандартного MessageManager. Переопределенный метод get_media_source позволяет пользователю передавать специальные URL для создания изображений с текстом.
  • DialogSG: Определение состояний для диалога.
  • dialog: Создание диалога с тремя окнами: двумя для запроса изображений с текстом и одним для обычного изображения.
  • start(message, dialog_manager): Обработчик команды /start, который начинает диалог с пользователем, переходя к первому окну.
  • main(): Основная функция, которая настраивает и запускает бота.

Чем это полезно?

Этот код демонстрирует, как использовать aiogram-dialog для создания интерфейса с диалогами в Telegram-боте. Он позволяет боту взаимодействовать с пользователем, предлагая ему выбор изображений с текстом или просто отображение обычных изображений.

aiogram_dialog

Пример 2: Группировка медиафайлов с возможностью удаления

Этот код демонстрирует использование библиотеки aiogram-dialog для создания диалога, который позволяет пользователям отправлять медиафайлы (фотографии) и удалять их по запросу.

Код
import asyncio
import logging
import os

from aiogram import Bot, Dispatcher, F
from aiogram.enums import ContentType
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage, SimpleEventIsolation
from aiogram.types import CallbackQuery, Message

from aiogram_dialog import (
    Dialog, DialogManager, setup_dialogs, StartMode, Window,
)
from aiogram_dialog.api.entities import MediaAttachment, MediaId
from aiogram_dialog.widgets.common import ManagedScroll
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Button, Group, NumberedPager, StubScroll
from aiogram_dialog.widgets.media import DynamicMedia
from aiogram_dialog.widgets.text import Const, Format


class Medias(StatesGroup):
    start = State()


async def on_input_photo(
    message: Message,
    widget: MessageInput,
    dialog_manager: DialogManager,
):
    dialog_manager.dialog_data.setdefault("photos", []).append(
        (message.photo[-1].file_id, message.photo[-1].file_unique_id),
    )


async def on_delete(
        callback: CallbackQuery, widget: Button, dialog_manager: DialogManager,
):
    scroll: ManagedScroll = dialog_manager.find("pages")
    media_number = await scroll.get_page()
    photos = dialog_manager.dialog_data.get("photos", [])
    del photos[media_number]
    if media_number > 0:
        await scroll.set_page(media_number - 1)


async def getter(dialog_manager: DialogManager, **kwargs) -> dict:
    scroll: ManagedScroll = dialog_manager.find("pages")
    media_number = await scroll.get_page()
    photos = dialog_manager.dialog_data.get("photos", [])
    if photos:
        photo = photos[media_number]
        media = MediaAttachment(
            file_id=MediaId(*photo),
            type=ContentType.PHOTO,
        )
    else:
        media = MediaAttachment(
            url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/Image_not_available.png/800px-Image_not_available.png?20210219185637",  # noqa: E501
            type=ContentType.PHOTO,
        )
    return {
        "media_count": len(photos),
        "media_number": media_number + 1,
        "media": media,
    }


dialog = Dialog(Window(
    Const("Send media"),
    DynamicMedia(selector="media"),
    StubScroll(id="pages", pages="media_count"),
    Group(
        NumberedPager(scroll="pages", when=F["pages"] > 1),
        width=8,
    ),
    Button(
        Format("?️ Delete photo #{media_number}"),
        id="del",
        on_click=on_delete,
        when="media_count",
        # Alternative F['media_count']
    ),
    MessageInput(content_types=[ContentType.PHOTO], func=on_input_photo),
    getter=getter,
    state=Medias.start,
))


async def start(message: Message, dialog_manager: DialogManager):
    await dialog_manager.start(Medias.start, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    storage = MemoryStorage()
    bot = Bot(token=TOKEN)
    dp = Dispatcher(storage=storage, events_isolation=SimpleEventIsolation())
    dp.include_router(dialog)

    dp.message.register(start, CommandStart())
    setup_dialogs(dp)
    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Описание кода:

  1. Определение состояний:
    • Создается состояние start, которое будет использоваться для начала диалога.
  2. Определение обработчика ввода сообщений:
    • Функция on_input_photo вызывается при отправке пользователем фотографии. Она добавляет информацию о фотографии в данные диалога.
  3. Определение обработчика удаления фотографии:
    • Функция on_delete вызывается при нажатии на кнопку удаления фотографии. Она удаляет выбранную фотографию из данных диалога.
  4. Определение функции-геттера:
    • Функция getter используется для получения данных, которые будут использоваться для отображения виджетов в диалоге. В данном случае, она возвращает информацию о текущей фотографии, ее номере и общем количестве фотографий.
  5. Создание диалога:
    • Определяется диалог с одним окном, включающим в себя текстовое сообщение, динамическое отображение медиафайла, кнопку удаления фотографии, пейджер для навигации между фотографиями и возможностью добавления новых фотографий.
  6. Обработчик команды /start:
    • Функция start вызывается при получении команды /start и начинает диалог.
  7. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

Этот код полезен для создания интерфейса в Telegram-боте, который позволяет пользователям отправлять и управлять медиафайлами (например, фотографиями) с помощью кнопок и интерактивного ввода. Он демонстрирует, как использовать aiogram-dialog для управления динамическими данными и создания интерактивных диалоговых окон.

aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog

Пример 3: Управление режимами запуска диалогов

Этот код демонстрирует использование библиотеки aiogram-dialog для управления режимами запуска диалогов в Telegram-боте.

Код
import asyncio
import logging
import os

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.filters.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message

from aiogram_dialog import (
    Dialog, DialogManager, LaunchMode, setup_dialogs, StartMode, Window,
)
from aiogram_dialog.widgets.kbd import Cancel, Row, Start
from aiogram_dialog.widgets.text import Const, Format

API_TOKEN = os.getenv("BOT_TOKEN")


class BannerSG(StatesGroup):
    default = State()


class MainSG(StatesGroup):
    default = State()


class Product(StatesGroup):
    show = State()


banner = Dialog(
    Window(
        Const("BANNER IS HERE"),
        Start(Const("Try start"), id="start", state=MainSG.default),
        Cancel(),
        state=BannerSG.default,
    ),
    launch_mode=LaunchMode.EXCLUSIVE,
)
main_menu = Dialog(
    Window(
        Const("This is main menu"),
        Start(Const("Product"), id="product", state=Product.show),
        Cancel(),
        state=MainSG.default,
    ),
    # we do not worry about resetting stack
    # each time we start dialog with ROOT launch mode
    launch_mode=LaunchMode.ROOT,
)


async def product_getter(dialog_manager: DialogManager, **kwargs):
    return {
        "data": dialog_manager.current_context().id,
    }


product = Dialog(
    Window(
        Format("This is product: {data}"),
        Row(
            Start(Const("Main menu"), id="main", state=MainSG.default),
            Start(Const("Banner"), id="banner", state=BannerSG.default),
            Start(Const("Product"), id="product", state=Product.show),
        ),
        Cancel(),
        getter=product_getter,
        state=Product.show,
    ),
    # when this dialog is on top and tries to launch a copy
    # it just replaces himself with it
    launch_mode=LaunchMode.SINGLE_TOP,
)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(MainSG.default, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    storage = MemoryStorage()
    bot = Bot(token=API_TOKEN)
    dp = Dispatcher(storage=storage)
    dp.include_routers(banner, product, main_menu)

    dp.message.register(start, CommandStart())
    setup_dialogs(dp)
    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Описание кода:

  1. Определение состояний:
    • Создаются состояния для каждого диалога: BannerSG для диалога с баннером, MainSG для основного меню и Product для диалога с продуктом.
  2. Создание диалогов:
    • banner — содержит окно с баннером и кнопкой для запуска основного меню. Имеет режим запуска LaunchMode.EXCLUSIVE, что означает, что он будет запускаться только один раз и не будет совместно использоваться с другими диалогами.
    • main_menu — представляет собой основное меню с кнопкой для отображения продуктов. Имеет режим запуска LaunchMode.ROOT, что означает, что он будет запускаться как основной диалог, и каждый раз, когда он запускается, будет сбрасываться стек диалогов.
    • product — содержит информацию о продукте и кнопки для возврата в главное меню, отображения баннера или продукта. Имеет режим запуска LaunchMode.SINGLE_TOP, что означает, что он будет запускаться только один раз сверху и заменять собой предыдущий экземпляр.
  3. Определение обработчика команды /start:
    • Функция start вызывается при получении команды /start и начинает диалог с основным меню.
  4. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

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

aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog

Пример 4: Создание списка с возможностью выбора и радио-кнопками

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

Код
import asyncio
import logging
import os
from typing import Dict

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.filters.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message

from aiogram_dialog import (
    Dialog, DialogManager, LaunchMode,
    setup_dialogs, StartMode, SubManager,
    Window,
)
from aiogram_dialog.widgets.kbd import (
    Checkbox, ListGroup,
    ManagedCheckbox, Radio, Row,
)
from aiogram_dialog.widgets.text import Const, Format

API_TOKEN = os.getenv("BOT_TOKEN")


class DialogSG(StatesGroup):
    greeting = State()


def when_checked(data: Dict, widget, manager: SubManager) -> bool:
    # manager for our case is already adapted for current ListGroup row
    # so `.find` returns widget adapted for current row
    # if you need to find widgets outside the row, use `.find_in_parent`
    check: ManagedCheckbox = manager.find("check")
    return check.is_checked()


async def data_getter(*args, **kwargs):
    return {
        "fruits": ["mango", "papaya", "kiwi"],
        "colors": ["blue", "pink"],
    }


dialog = Dialog(
    Window(
        Const(
            "Hello, please check you options for each item:",
        ),
        ListGroup(
            Checkbox(
                Format("✓ {item}"),
                Format("  {item}"),
                id="check",
            ),
            Row(
                Radio(
                    Format("? {item} ({data[item]})"),
                    Format("⚪️ {item} ({data[item]})"),
                    id="radio",
                    item_id_getter=str,
                    items=["black", "white"],
                    # Alternatives:
                    # items=F["data"]["colors"],  # noqa: E800
                    # items=lambda d: d["data"]["colors"],  # noqa: E800
                    when=when_checked,
                ),
            ),
            id="lg",
            item_id_getter=str,
            items=["apple", "orange", "pear"],
            # Alternatives:
            # items=F["fruits"],  # noqa: E800
            # items=lambda d: d["fruits"],  # noqa: E800
        ),
        state=DialogSG.greeting,
        getter=data_getter,
    ),
    launch_mode=LaunchMode.SINGLE_TOP,
)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(DialogSG.greeting, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    storage = MemoryStorage()
    bot = Bot(token=API_TOKEN)
    dp = Dispatcher(storage=storage)
    dp.include_router(dialog)
    dp.message.register(start, CommandStart())
    setup_dialogs(dp)

    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Описание кода:

  1. Определение состояний:
    • Создается состояние greeting для диалога.
  2. Создание диалога:
    • dialog содержит окно с приветствием и списком ListGroup.
    • ListGroup содержит элементы списка с чекбоксом Checkbox и радио-кнопкой Radio.
    • Для радио-кнопки определена функция when_checked, которая проверяет, отмечен ли чекбокс для текущего элемента. Если чекбокс отмечен, радио-кнопка отображается, иначе скрыта.
    • В параметре getter указывается функция data_getter, которая предоставляет данные для отображения элементов списка.
  3. Определение обработчика команды /start:
    • Функция start вызывается при получении команды /start и начинает диалог с состоянием greeting.
  4. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

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

aiogram_dialog aiogram_dialog aiogram_dialog

Пример 5: Обновление прогресса в фоновом режиме

Этот код демонстрирует использование библиотеки aiogram-dialog для создания диалога, в котором отображается прогресс выполнения задачи в фоновом режиме.

Описание кода:

  1. Определение состояний:
    • Создается состояние progress для диалога, отображающего прогресс выполнения задачи.
  2. Диалог для отображения прогресса:
    • Создается диалог bg_dialog с окном, содержащим текст и виджет Progress, отображающий текущий прогресс.
    • В функции get_bg_data определяется getter для получения данных о прогрессе выполнения задачи.
  3. Главное меню:
    • Создается диалог main_menu с кнопкой «Start» для запуска фоновой задачи.
    • В функции start_bg определяется обработчик нажатия кнопки «Start», который запускает фоновую задачу.
  4. Фоновая задача:
    • Функция background выполняет фоновую задачу, обновляя прогресс в диалоге с помощью метода update менеджера диалога.
    • Прогресс обновляется каждую секунду.
  5. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

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

aiogram_dialog

Код
import asyncio
import logging
import os

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage, SimpleEventIsolation
from aiogram.types import CallbackQuery, Message

from aiogram_dialog import (
    BaseDialogManager, Dialog, DialogManager,
    setup_dialogs, StartMode, Window,
)
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const, Multi, Progress


# name input dialog

class Bg(StatesGroup):
    progress = State()


async def get_bg_data(dialog_manager: DialogManager, **kwargs):
    return {
        "progress": dialog_manager.dialog_data.get("progress", 0),
    }


bg_dialog = Dialog(
    Window(
        Multi(
            Const("Your click is processing, please wait..."),
            Progress("progress", 10),
        ),
        state=Bg.progress,
        getter=get_bg_data,
    ),
)


# main dialog
class MainSG(StatesGroup):
    main = State()


async def start_bg(
    callback: CallbackQuery,
    button: Button,
    manager: DialogManager,
):
    await manager.start(Bg.progress)
    asyncio.create_task(background(callback, manager.bg()))


async def background(callback: CallbackQuery, manager: BaseDialogManager):
    count = 10
    for i in range(1, count + 1):
        await asyncio.sleep(1)
        await manager.update({
            "progress": i * 100 / count,
        })
    await asyncio.sleep(1)
    await manager.done()


main_menu = Dialog(
    Window(
        Const("Press button to start processing"),
        Button(Const("Start"), id="start", on_click=start_bg),
        state=MainSG.main,
    ),
)


async def start(message: Message, dialog_manager: DialogManager):
    await dialog_manager.start(MainSG.main, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    logging.getLogger("aiogram_dialog").setLevel(logging.DEBUG)
    storage = MemoryStorage()
    bot = Bot(token=TOKEN)
    dp = Dispatcher(storage=storage, events_isolation=SimpleEventIsolation())
    dp.include_router(bg_dialog)
    dp.include_router(main_menu)

    dp.message.register(start, CommandStart())
    setup_dialogs(dp)
    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Пример 6: Множественные стеки диалогов

Этот код демонстрирует использование библиотеки aiogram-dialog для создания множественных стеков диалогов.

Описание кода:

  1. Определение состояний:
    • Создается состояние greeting для диалога.
  2. Функция получения данных:
    • Функция get_data используется для получения данных о текущем стеке, контексте, текущем времени, счетчике кликов и последнем введенном тексте.
    • Здесь также определяется список фруктов для мультиселекта.
  3. Обработчик ввода имени:
    • Функция name_handler используется для обработки введенного пользователем имени.
    • Введенное имя сохраняется в данных диалога.
  4. Обработчик нажатия кнопки:
    • Функция on_click увеличивает счетчик кликов при нажатии на кнопку.
  5. Диалог:
    • Создается диалог dialog с окном, отображающим информацию о текущем стеке, контексте, времени, счетчике кликов и последнем введенном тексте.
    • Добавляется мультиселект для выбора фруктов, кнопка для увеличения счетчика кликов и кнопка для ввода имени.
    • Определяется getter для получения данных.
  6. Функция старта:
    • Функция start используется для запуска диалога в новом стеке при получении команды /start.
  7. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

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

aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog

Код
import asyncio
import datetime
import logging
import operator
import os

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import CallbackQuery, Message

from aiogram_dialog import (
    Dialog, DialogManager,
    setup_dialogs, StartMode, Window,
)
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Button, Cancel, Multiselect, Start
from aiogram_dialog.widgets.text import Const, Format



class DialogSG(StatesGroup):
    greeting = State()


async def get_data(dialog_manager: DialogManager, **kwargs):
    return {
        "stack": dialog_manager.current_stack(),
        "context": dialog_manager.current_context(),
        "now": datetime.datetime.now(),
        "counter": dialog_manager.dialog_data.get("counter", 0),
        "last_text": dialog_manager.dialog_data.get("last_text", ""),
        "fruits": [
            ("Apple", 1),
            ("Pear", 2),
            ("Orange", 3),
            ("Banana", 4),
        ],
    }


async def name_handler(
        message: Message, message_input: MessageInput, manager: DialogManager,
):
    manager.dialog_data["last_text"] = message.text
    await message.answer(f"Nice to meet you, {message.text}")


async def on_click(
    callback: CallbackQuery,
    button: Button,
    manager: DialogManager,
):
    counter = manager.dialog_data.get("counter", 0)
    manager.dialog_data["counter"] = counter + 1


multi = Multiselect(
    Format("✓ {item[0]}"),  # E.g `✓ Apple`
    Format("{item[0]}"),
    id="check",
    item_id_getter=operator.itemgetter(1),
    items="fruits",
)

dialog = Dialog(
    Window(
        Format("Clicked: {counter}\n"),
        Format("Stack: {stack}\n"),
        Format("Context: {context}\n"),
        Format("Last text: {last_text}\n"),
        Format("{now}"),
        Button(Const("Click me!"), id="btn1", on_click=on_click),
        Start(
            Const("Start new stack"),
            mode=StartMode.NEW_STACK,
            state=DialogSG.greeting,
            id="s1",
        ),
        multi,
        Cancel(),
        # Inputs work only in default stack
        # or via reply to a message with buttons
        MessageInput(name_handler),
        state=DialogSG.greeting,
        getter=get_data,
    ),
)


async def start(message: Message, dialog_manager: DialogManager):
    await dialog_manager.start(DialogSG.greeting, mode=StartMode.NEW_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    storage = MemoryStorage()
    bot = Bot(token=TOKEN)
    dp = Dispatcher(storage=storage)
    dp.include_router(dialog)

    # register handler which resets stack and start dialogs on /start command
    dp.message.register(start, CommandStart())
    setup_dialogs(dp)
    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Пример 7: Прокрутка

Этот код демонстрирует использование различных виджетов прокрутки в диалогах aiogram-dialog.

Описание кода:

  1. Определение состояний:
    • Определяются состояния для каждого диалога: MAIN, DEFAULT_PAGER, PAGERS, LIST, TEXT и STUB.
  2. Функции-геттеры:
    • Функции-геттеры используются для получения данных для различных виджетов.
    • product_getter используется для получения списка продуктов для мультиселекта.
    • paging_getter используется для получения информации о страницах для стабильной прокрутки.
  3. Диалог:
    • Создается диалог dialog с различными окнами, демонстрирующими различные виды прокрутки:
      • Мультиселект с использованием стандартного пейджера.
      • Мультиселект с внешними элементами управления пейджером.
      • Прокручиваемый список текста с использованием пейджера.
      • Прокручиваемый текст с использованием пейджера.
      • Стабильная прокрутка с использованием функции-геттера.
  4. Функция старта:
    • Функция start используется для запуска диалога MAIN при получении команды /start.
  5. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

Этот код полезен для демонстрации различных видов прокрутки, которые можно использовать в диалогах aiogram-dialog. Он может быть использован для обучения или как отправная точка для создания более сложных диалогов с прокруткой.

aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog

Код
import asyncio
import calendar
import logging
import os.path
from operator import itemgetter

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Message

from aiogram_dialog import (
    Dialog, DialogManager,
    setup_dialogs, StartMode, Window,
)
from aiogram_dialog.widgets.kbd import (
    CurrentPage, FirstPage, LastPage,
    Multiselect, NextPage, NumberedPager,
    PrevPage, Row, ScrollingGroup,
    StubScroll, SwitchTo,
)
from aiogram_dialog.widgets.text import Const, Format, List, ScrollingText


class DialogSG(StatesGroup):
    MAIN = State()
    DEFAULT_PAGER = State()
    PAGERS = State()
    LIST = State()
    TEXT = State()
    STUB = State()


VERY_LONG_TEXT = """\
Lorem ipsum dolor sit amet. Ex dolores porro ut praesentium necessitatibus qui internos libero ut ipsa voluptatum eum blanditiis consequatur. Aut facere internos et nisi Quis eos omnis cumque. Et deleniti reprehenderit est aspernatur nulla ut impedit praesentium quo amet animi?

Id aperiam doloribus et quasi dolorem qui sunt consequatur non magni praesentium sed possimus omnis ut dolor natus est voluptatem dicta. In iure corporis non suscipit tempore qui veniam expedita et dolorem perferendis. Est autem blanditiis aut eius maxime in dolorem provident et veniam asperiores ea maxime rerum At nostrum temporibus! Aut nesciunt voluptatem ut eius autem eum Quis voluptatem ut iusto voluptas sed illo consequuntur vel molestiae quod.

Quo illo atque et pariatur magnam aut consectetur totam. Nam galisum neque ad laborum omnis 33 officia impedit ab odio officia est necessitatibus iste? Et ratione nulla non voluptas sunt vel dolores explicabo a quae enim et autem velit rem tempora nemo. Ut accusantium omnis vel iusto eius sit tempore illum aut aliquid vitae et accusamus repellat aut ducimus temporibus et omnis voluptates.

Et neque sint aut eius inventore et iste rerum a natus aliquid et natus obcaecati. Et quibusdam quos non ullam cupiditate ea molestiae tempore. Non quia ipsum qui velit quae eos molestias consequatur et quod autem et voluptas dolores et possimus pariatur in porro galisum.

Est molestiae laudantium non ullam facilis aut earum voluptatum ad omnis aliquid sed officia perferendis eum dignissimos debitis! Ea eaque architecto non voluptate possimus et dolores quos 33 rerum enim aut dolorem perferendis.

Eum minus impedit ut distinctio magni qui cupiditate minus qui distinctio beatae ut debitis velit rem accusantium accusantium et exercitationem nihil. Qui cupiditate porro ut sunt nemo et voluptatem ipsum. Et cumque dolorum eum quae earum aut impedit quasi nam fugit assumenda ex sapiente doloribus! A dolore Quis est labore nihil aut voluptatem rerum ut Quis voluptatem.

Rem culpa dolorum sed odio animi id quia inventore et molestiae quas. Qui suscipit placeat sit rerum alias eos esse aliquam est voluptas animi.

Ea illo recusandae et nisi possimus eos sunt corporis. Et dolor inventore aut nisi minus ut ipsam voluptas sed omnis ullam eos itaque galisum est omnis odit et cupiditate magnam.

Eos corporis quos aut magnam iure sed dicta galisum qui labore commodi rem nostrum ducimus vel nihil quia ut iste voluptate. Ut praesentium sint vel necessitatibus explicabo est aliquam rerum et dolorum aspernatur non neque modi est iste quos. Qui magnam voluptatem qui repellendus dolores ad voluptates nihil. Qui neque repellat et laboriosam officiis et officia eveniet qui iste porro et cupiditate error ea molestiae harum et dolorem modi.

Aut quae vitae nam ullam ratione sed blanditiis nihil aut nobis explicabo ab consequuntur quia ut unde quisquam qui impedit sunt. Ex dicta eligendi sed omnis facilis sed laudantium quas a corrupti iste! Quo vitae culpa eum saepe praesentium ad autem internos non Quis quidem qui vitae fuga. Et quis dolorem id voluptatibus dolor non quas incidunt in internos reprehenderit eos deserunt quas aut ipsum molestiae et rerum maiores?

Eos nulla eligendi rem galisum vero ut odio molestiae. Sed nulla iste quo deleniti possimus et doloremque sint vel sunt fuga sit laborum autem et doloribus laboriosam. Ab quae distinctio in magni quod est Quis explicabo ut tenetur magni in neque eius et ullam dolor.

Ea reprehenderit sunt aut voluptas vitae non iure consequatur. Aut repudiandae expedita et nulla sunt eum quae maiores non amet voluptas sed explicabo cumque ut totam laborum.

Eum odit tenetur eum galisum accusamus aut nulla iusto qui eaque illum non voluptatem magni. Ut placeat facere ea voluptatem voluptatem quo quia cumque aut provident cupiditate qui fuga voluptatem. Ad libero voluptatem rem aliquid deserunt est consequuntur pariatur et sequi asperiores et nostrum assumenda. Nam quia voluptatem aut quidem velit At fugit voluptas sit dicta dolores quo ratione delectus nam consectetur temporibus.

Id soluta voluptates a dolor amet est tempore modi et obcaecati dolor aut quae omnis. Qui nihil accusamus aut enim odit et ratione galisum cum assumenda sequi quo asperiores rerum et similique veniam non cumque ratione. Et nobis inventore aut facilis consequatur et commodi placeat eos quasi commodi non quis eligendi sit magnam consequatur et obcaecati Quis! Et expedita distinctio qui dolorum odio ut omnis tempore eos deserunt aspernatur vel sequi facilis.
"""  # noqa: E501


async def product_getter(**_kwargs):
    return {
        "products": [(f"Product {i}", i) for i in range(1, 30)],
    }


async def paging_getter(dialog_manager: DialogManager, **_kwargs):
    current_page = await dialog_manager.find("stub_scroll").get_page()
    return {
        "pages": 7,
        "current_page": current_page + 1,
        "day": calendar.day_name[current_page],
    }


MAIN_MENU_BTN = SwitchTo(Const("Main menu"), id="main", state=DialogSG.MAIN)

dialog = Dialog(
    Window(
        Const("Scrolling variant demo. Please, select an option:"),
        SwitchTo(
            Const("Default Pager"),
            state=DialogSG.DEFAULT_PAGER,
            id="default",
        ),
        SwitchTo(Const("Pager options"), id="pagers", state=DialogSG.PAGERS),
        SwitchTo(Const("Text list scroll"), id="list", state=DialogSG.LIST),
        SwitchTo(Const("Text scroll"), id="text", state=DialogSG.TEXT),
        SwitchTo(Const("Stub: getter-based"), id="stub", state=DialogSG.STUB),
        state=DialogSG.MAIN,
    ),
    Window(
        Const("Scrolling group with default pager (legacy mode)"),
        ScrollingGroup(
            Multiselect(
                Format("✓ {item[0]}"),
                Format("{item[0]}"),
                id="ms",
                items="products",
                item_id_getter=itemgetter(1),
            ),
            width=1,
            height=5,
            id="scroll_with_pager",
        ),
        MAIN_MENU_BTN,
        getter=product_getter,
        state=DialogSG.DEFAULT_PAGER,
    ),
    Window(
        Const("Scrolling group with external paging controls"),
        NumberedPager(
            scroll="scroll_no_pager",
            page_text=Format("{target_page1}\uFE0F\u20E3"),
            current_page_text=Format("{current_page1}"),
        ),
        NumberedPager(
            scroll="scroll_no_pager",
        ),
        ScrollingGroup(
            Multiselect(
                Format("✓ {item[0]}"),
                Format("{item[0]}"),
                id="ms",
                items="products",
                item_id_getter=itemgetter(1),
            ),
            width=1,
            height=5,
            hide_pager=True,
            id="scroll_no_pager",
        ),
        Row(

            FirstPage(
                scroll="scroll_no_pager", text=Format("⏮️ {target_page1}"),
            ),
            PrevPage(
                scroll="scroll_no_pager", text=Format("◀️"),
            ),
            CurrentPage(
                scroll="scroll_no_pager", text=Format("{current_page1}"),
            ),
            NextPage(
                scroll="scroll_no_pager", text=Format("▶️"),
            ),
            LastPage(
                scroll="scroll_no_pager", text=Format("{target_page1} ⏭️"),
            ),
        ),
        Row(
            PrevPage(scroll="scroll_no_pager"),
            NextPage(scroll="scroll_no_pager"),
            MAIN_MENU_BTN,
        ),
        getter=product_getter,
        state=DialogSG.PAGERS,
    ),
    Window(
        Const("Text list scrolling:\n"),
        List(
            Format("{pos}. {item[0]}"),
            items="products",
            id="list_scroll",
            page_size=10,
        ),
        NumberedPager(
            scroll="list_scroll",
        ),
        MAIN_MENU_BTN,
        getter=product_getter,
        state=DialogSG.LIST,
    ),
    Window(
        Const("Text scrolling:\n"),
        ScrollingText(
            text=Const(VERY_LONG_TEXT),
            id="text_scroll",
            page_size=1000,
        ),
        NumberedPager(
            scroll="text_scroll",
        ),
        MAIN_MENU_BTN,
        state=DialogSG.TEXT,
    ),
    Window(
        Const("Stub Scroll. Getter is used to paginate\n"),
        Format("You are at page {current_page} of {pages}"),
        Format("Day by number is {day}"),
        StubScroll(id="stub_scroll", pages="pages"),
        NumberedPager(
            scroll="stub_scroll",
        ),
        MAIN_MENU_BTN,
        state=DialogSG.STUB,
        getter=paging_getter,
    ),
)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(DialogSG.MAIN, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    bot = Bot(token=TOKEN)

    storage = MemoryStorage()
    dp = Dispatcher(storage=storage)
    dp.message.register(start, CommandStart())
    dp.include_router(dialog)
    setup_dialogs(dp)

    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Пример 8: Простой диалог

Этот пример демонстрирует создание простого диалога с использованием aiogram-dialog.

 

Обратите внимание! Этот код использует для хранения состояний БД Redis, а не оперативную память, как во всех предыдущих примерах. Использование Redis — хорошая практика в рабочих проектах. Для тестов, можно ограничиваться MemoryStorage.

Но, для того. чтобы этот пример кода запустился у вас, установите Redis server и библиотеку Redis для Python. Или поменяйте хранилище состояний на MemoryStorage

Описание кода:

  1. Определение состояний:
    • Определяются состояния для каждого этапа диалога: greeting, age и finish.
  2. Функции-геттеры:
    • Функция-геттер используется для получения данных о пользователе для диалога.
    • В данном случае используется функция get_data, которая возвращает имя и возраст пользователя.
  3. Обработчики сообщений:
    • name_handler обрабатывает ввод имени пользователя.
    • other_type_handler предназначен для обработки текста, но используется для демонстрации ожидания различных типов контента.
  4. Обработчики кнопок:
    • on_age_changed вызывается при выборе возрастной категории пользователем.
  5. Диалог:
    • Создается диалог dialog с тремя окнами:
      • Приветственное окно, где пользователю предлагается ввести свое имя.
      • Окно для ввода возраста.
      • Завершающее окно с благодарностью и возможностью перезапуска диалога.
  6. Функция старта:
    • Функция start используется для запуска диалога при получении команды /start.
  7. Обработка ошибок:
    • Обработчики on_unknown_intent и on_unknown_state используются для перезапуска диалога в случае возникновения ошибок UnknownIntent и UnknownState.
  8. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

Этот пример полезен для понимания основ создания диалогов с использованием aiogram-dialog. Он демонстрирует, как создавать простые интерактивные диалоги с пользователем, используя различные виджеты для ввода данных и отображения информации.

aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog

Код
import asyncio
import logging
import os.path
from typing import Any

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart, ExceptionTypeFilter
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.redis import DefaultKeyBuilder, RedisStorage
from aiogram.types import CallbackQuery, ContentType, Message
from redis.asyncio.client import Redis

from aiogram_dialog import (
    ChatEvent, Dialog, DialogManager,
    setup_dialogs, ShowMode,
    StartMode, Window,
)
from aiogram_dialog.api.exceptions import UnknownIntent, UnknownState
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import Back, Button, Row, Select, SwitchTo
from aiogram_dialog.widgets.media import StaticMedia
from aiogram_dialog.widgets.text import Const, Format, Multi

src_dir = os.path.normpath(os.path.join(__file__, os.path.pardir))


class DialogSG(StatesGroup):
    greeting = State()
    age = State()
    finish = State()


async def get_data(dialog_manager: DialogManager, **kwargs):
    age = dialog_manager.dialog_data.get("age", None)
    return {
        "name": dialog_manager.dialog_data.get("name", ""),
        "age": age,
        "can_smoke": age in ("18-25", "25-40", "40+"),
    }


async def name_handler(
    message: Message,
    message_input: MessageInput,
    manager: DialogManager,
):
    if manager.is_preview():
        await manager.next()
        return
    manager.dialog_data["name"] = message.text
    await message.answer(f"Рад тебя видеть, {message.text}")
    await manager.next()


async def other_type_handler(
    message: Message,
    message_input: MessageInput,
    manager: DialogManager,
):
    await message.answer("Text is expected")


async def on_finish(
    callback: CallbackQuery,
    button: Button,
    manager: DialogManager,
):
    if manager.is_preview():
        await manager.done()
        return
    await callback.message.answer("Спасибо. Для повторного запуска нажми /start")
    await manager.done()


async def on_age_changed(
    callback: ChatEvent,
    select: Any,
    manager: DialogManager,
    item_id: str,
):
    manager.dialog_data["age"] = item_id
    await manager.next()


dialog = Dialog(
    Window(
        Const("Добро пожаловать на Камчатку! Как тебя зовут?"),
        StaticMedia(
            path=os.path.join(src_dir, "camchatka.png"),
            type=ContentType.PHOTO,
        ),
        MessageInput(name_handler, content_types=[ContentType.TEXT]),
        MessageInput(other_type_handler),
        state=DialogSG.greeting,
    ),
    Window(
        Format("{name}! Сколько тебе лет?"),
        Select(
            Format("{item}"),
            items=["0-12", "12-18", "18-25", "25-40", "40+"],
            item_id_getter=lambda x: x,
            id="w_age",
            on_click=on_age_changed,
        ),
        state=DialogSG.age,
        getter=get_data,
        preview_data={"name": "Tishka17"},
    ),
    Window(
        Multi(
            Format("{name}! Спасибо за ответы."),
            Const("Надеюсь, ты не куришь...", when="can_smoke"),
            sep="\n\n",
        ),
        Row(
            Back(),
            SwitchTo(Const("Перезапуск"), id="restart", state=DialogSG.greeting),
            Button(Const("Завершить"), on_click=on_finish, id="finish"),
        ),
        getter=get_data,
        state=DialogSG.finish,
    ),
)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(DialogSG.greeting, mode=StartMode.RESET_STACK)


async def on_unknown_intent(event, dialog_manager: DialogManager):
    # Example of handling UnknownIntent Error and starting new dialog.
    logging.error("Restarting dialog: %s", event.exception)
    await dialog_manager.start(
        DialogSG.greeting, mode=StartMode.RESET_STACK, show_mode=ShowMode.SEND,
    )


async def on_unknown_state(event, dialog_manager: DialogManager):
    # Example of handling UnknownState Error and starting new dialog.
    logging.error("Restarting dialog: %s", event.exception)
    await dialog_manager.start(
        DialogSG.greeting, mode=StartMode.RESET_STACK, show_mode=ShowMode.SEND,
    )


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    bot = Bot(token=TOKEN)

    storage = RedisStorage(
        Redis(),
        # in case of redis you need to configure key builder
        key_builder=DefaultKeyBuilder(with_destiny=True),
    )
    dp = Dispatcher(storage=storage)
    dp.message.register(start, CommandStart())
    dp.errors.register(
        on_unknown_intent,
        ExceptionTypeFilter(UnknownIntent),
    )
    dp.errors.register(
        on_unknown_state,
        ExceptionTypeFilter(UnknownState),
    )
    dp.include_router(dialog)
    setup_dialogs(dp)

    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Пример 9: Поддиалоги

Этот пример демонстрирует использование поддиалогов в библиотеке aiogram-dialog.

Описание кода:

  1. Определение состояний:
    • Определяются состояния для каждого поддиалога: NameSG.input и NameSG.confirm для ввода имени пользователя и подтверждения имени соответственно, и MainSG.main для основного диалога.
  2. Обработчики сообщений:
    • name_handler обрабатывает ввод имени пользователя и переходит к следующему состоянию.
    • on_finish вызывается при подтверждении имени пользователя и завершает диалог.
  3. Функции-геттеры:
    • Функция-геттер используется для получения данных о пользователе для диалога.
    • В данном случае используется функция get_name_data, которая возвращает введенное имя пользователя.
  4. Определение диалогов:
    • Создается поддиалог name_dialog для ввода имени пользователя.
    • Создается основной диалог main_menu, который содержит элементы управления для основного меню и кнопку для перехода к поддиалогу ввода имени.
  5. Обработчик событий старта:
    • Функция start используется для запуска основного диалога при получении команды /start.
  6. Настройка маршрутизатора:
    • Создается маршрутизатор dialog_router, который включает в себя все созданные диалоги.
  7. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

Этот пример полезен для понимания того, как использовать поддиалоги в aiogram-dialog. Он демонстрирует, как создавать и встраивать дополнительные диалоги в основной поток диалога, что позволяет лучше организовывать и структурировать бота.

aiogram_dialog aiogram_dialog aiogram_dialog aiogram_dialog

Код
import asyncio
import logging
import os
from typing import Any

from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import CallbackQuery, Message

from aiogram_dialog import (
    Data, Dialog, DialogManager,
    setup_dialogs, StartMode, Window,
)
from aiogram_dialog.tools import render_preview, render_transitions
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import (
    Back, Button, Cancel,
    Group, Next, Row, Start,
)
from aiogram_dialog.widgets.text import Const, Format, Multi


# name input dialog
class NameSG(StatesGroup):
    input = State()
    confirm = State()


async def name_handler(
    message: Message,
    widget: MessageInput,
    manager: DialogManager,
):
    manager.dialog_data["name"] = message.text
    await manager.next()


async def get_name_data(dialog_manager: DialogManager, **kwargs):
    return {
        "name": dialog_manager.dialog_data.get("name"),
    }


async def on_finish(
    callback: CallbackQuery,
    button: Button,
    manager: DialogManager,
):
    await manager.done({"name": manager.dialog_data["name"]})


name_dialog = Dialog(
    Window(
        Const("Как тебя зовут?"),
        Cancel(),
        MessageInput(name_handler),
        state=NameSG.input,
        preview_add_transitions=[Next()],  # hint for graph rendering
    ),
    Window(
        Format("Ваше имя `{name}`, правильно?"),
        Row(
            Back(Const("Нет")),
            Button(Const("Да"), id="yes", on_click=on_finish),
        ),
        state=NameSG.confirm,
        getter=get_name_data,
        preview_add_transitions=[Cancel()],  # hint for graph rendering
        preview_data={"name": "John Doe"},  # for preview rendering
    ),
)


# main dialog
class MainSG(StatesGroup):
    main = State()


async def process_result(
    start_data: Data,
    result: Any,
    manager: DialogManager,
):
    if result:
        manager.dialog_data["name"] = result["name"]


async def get_main_data(dialog_manager: DialogManager, **kwargs):
    return {
        "name": dialog_manager.dialog_data.get("name"),
    }


async def on_reset_name(
    callback: CallbackQuery,
    button: Button,
    manager: DialogManager,
):
    del manager.dialog_data["name"]


main_menu = Dialog(
    Window(
        Multi(
            Format("Привет, {name}", when="name"),
            Const("Привет, неизвестный", when=~F["name"]),
        ),
        Group(
            Start(Const("Напиши имя"), id="set", state=NameSG.input),
            Button(
                Const("Сбросить имя"),
                id="reset",
                on_click=on_reset_name,
                when="name",
                # Alternative F['name']
            ),
        ),
        state=MainSG.main,
        getter=get_main_data,
        preview_data={"name": "John Doe"},  # for preview rendering
    ),
    on_process_result=process_result,
)

dialog_router = Router()
dialog_router.include_router(name_dialog)
dialog_router.include_router(main_menu)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(MainSG.main, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    storage = MemoryStorage()
    bot = Bot(token=TOKEN)
    dp = Dispatcher(storage=storage)
    dp.include_router(dialog_router)
    dp.message.register(start, CommandStart())

    # render graph with current transitions
    render_transitions(dp)
    # render windows preview
    await render_preview(dp, "preview.html")

    # setup dispatcher to use dialogs
    setup_dialogs(dp)

    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Пример 10: Мастер-диалог

Этот пример демонстрирует использование мастер-диалога в библиотеке aiogram-dialog.

Описание кода:

  1. Определение состояний:
    • Определяются состояния мастер-диалога: Wizard.title, Wizard.description, Wizard.options и Wizard.preview.
  2. Обработчики сообщений:
    • next_or_end обрабатывает ввод данных в каждом окне мастер-диалога и переходит к следующему окну или завершает диалог.
    • В примере используется кнопка CANCEL_EDIT, которая позволяет отменить редактирование, если диалог завершен.
  3. Функция-геттер:
    • Функция result_getter используется для получения результатов мастер-диалога после его завершения.
  4. Определение диалога:
    • Создается мастер-диалог dialog, который состоит из нескольких окон для ввода данных и окна предварительного просмотра.
    • Каждое окно содержит соответствующие виджеты для ввода данных или выбора опций.
  5. Обработчик событий старта:
    • Функция start используется для запуска мастер-диалога при получении команды /start.
  6. Основная функция:
    • Создает и запускает бота, настраивает обработчики команд и диалоги.

Зачем это нужно?

Этот пример полезен для понимания того, как использовать мастер-диалоги в aiogram-dialog. Мастер-диалог позволяет пользователю последовательно вводить или выбирать данные в нескольких окнах, а затем просматривать результаты и, при необходимости, редактировать их.

aiogram_dialog aiogram_dialog aiogram_dialog

Код
import asyncio
import logging
import os

from aiogram import Bot, Dispatcher, F
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message

from aiogram_dialog import (
    Dialog, DialogManager, setup_dialogs, StartMode, Window,
)
from aiogram_dialog.widgets.input import TextInput
from aiogram_dialog.widgets.kbd import Checkbox, Next, SwitchTo
from aiogram_dialog.widgets.text import Const, Jinja


class Wizard(StatesGroup):
    title = State()
    description = State()
    options = State()
    preview = State()


FINISHED_KEY = "finished"

CANCEL_EDIT = SwitchTo(
    Const("Отменить редактирование"),
    when=F["dialog_data"][FINISHED_KEY],
    id="cnl_edt",
    state=Wizard.preview,
)


async def next_or_end(event, widget, dialog_manager: DialogManager, *_):
    if dialog_manager.dialog_data.get(FINISHED_KEY):
        await dialog_manager.switch_to(Wizard.preview)
    else:
        await dialog_manager.next()


async def result_getter(dialog_manager: DialogManager, **kwargs):
    dialog_manager.dialog_data[FINISHED_KEY] = True
    options = []
    if dialog_manager.find("pink").is_checked():
        options.append("Розовые")
    if dialog_manager.find("glitter").is_checked():
        options.append("Блестки")
    if dialog_manager.find("bow").is_checked():
        options.append("С бантиком")
    return {
        "options": options,
        "title": dialog_manager.find("title").get_value(),
        "description": dialog_manager.find("description").get_value(),
    }


dialog = Dialog(
    Window(
        Const("Введите название"),
        TextInput(id="title", on_success=next_or_end),
        CANCEL_EDIT,
        state=Wizard.title,
    ),
    Window(
        Const("Введите описание"),
        TextInput(id="description", on_success=next_or_end),
        CANCEL_EDIT,
        state=Wizard.description,
    ),
    Window(
        Const("Выберите опции"),
        Checkbox(Const("✓ Розовый"), Const("Розовый"), id="pink"),
        Checkbox(Const("✓ Блестки"), Const("Блестки"), id="glitter"),
        Checkbox(Const("✓ С бантиком"), Const("С бантиком"), id="bow"),
        Next(Const("Далее")),
        CANCEL_EDIT,
        state=Wizard.options,
    ),
    Window(
        Jinja(
            "<u>Вы ввели</u>:\n\n"
            "<b>Название</b>: {{title}}\n"
            "<b>Описание</b>: {{description}}\n"
            "<b>Опции</b>: \n"
            "{% for item in options %}"
            "• {{item}}\n"
            "{% endfor %}",
        ),
        SwitchTo(
            Const("Изменить название"),
            state=Wizard.title, id="to_title",
        ),
        SwitchTo(
            Const("Изменить описание"),
            state=Wizard.description, id="to_desc",
        ),
        SwitchTo(
            Const("Изменить опции"),
            state=Wizard.options, id="to_opts",
        ),
        state=Wizard.preview,
        getter=result_getter,
        parse_mode="html",
    ),
)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(Wizard.title, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    bot = Bot(TOKEN)
    dp = Dispatcher()
    dp.include_router(dialog)
    dp.message.register(start, CommandStart())
    setup_dialogs(dp)

    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())

Пример 11. Локализация и мультиязычность в боте.

Этот проект демонстрирует локализацию (i18n) в библиотеке aiogram-dialog с использованием Fluent Localization.

Файлы в проекте:

  1. main.ftl (example/i18n/translations/en/main.ftl):
    • Файл перевода с приветствием и кнопкой «Close» на английском.
  2. bot.py (example/i18n/bot.py):
    • Главный файл бота, который настраивает маршрутизатор, регистрирует обработчики сообщений и диалоги.
    • Содержит определение состояний и обработчик старта.
    • Использует FluentLocalization для локализации сообщений.
    • Содержит определение функции make_i18n_middleware(), которая создает I18nMiddleware для локализации сообщений в различных языках.
  3. i18n_format.py (example/i18n/i18n_format.py):
    • Определяет класс I18NFormat, который позволяет использовать локализованные тексты в диалогах aiogram-dialog.
    • Реализует логику отрисовки текста с учетом локализации.
  4. i18n_middleware.py (example/i18n/i18n_middleware.py):
    • Реализует мидлвару I18nMiddleware, которая обрабатывает сообщения с учетом локализации.
    • Использует FluentLocalization для форматирования текста с учетом языка.

Зачем это нужно?

Этот проект демонстрирует, как добавить поддержку мультиязычности в бот, использующий aiogram-dialog. Локализация происходит с использованием Fluent Localization, что обеспечивает гибкость и мощный функционал для перевода сообщений.

Код example/i18n/translations/en/main.ftl
Hello-user = Hello and welcome, { $name }!
Cancel = Close
Demo-button = Click me
Код example/i18n/bot.py
"""
This is example of i18n with aiogram-dialog.

To use it you need to install `fluent.runtime` package
Translation files are located in `translations` directory
"""

import asyncio
import logging
import os.path

from aiogram import Bot, Dispatcher
from aiogram.filters import CommandStart
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message
from fluent.runtime import FluentLocalization, FluentResourceLoader
from i18n_format import I18NFormat
from i18n_middleware import I18nMiddleware

from aiogram_dialog import (
    Dialog, DialogManager, setup_dialogs, StartMode, Window,
)
from aiogram_dialog.widgets.kbd import Button, Cancel, Row


class DialogSG(StatesGroup):
    greeting = State()


async def get_data(dialog_manager: DialogManager, **kwargs):
    name = dialog_manager.event.from_user.full_name
    return {
        "name": name,
    }


dialog = Dialog(
    Window(
        I18NFormat("Hello-user"),
        Row(
            Button(I18NFormat("Demo-button"), id="demo"),
            Cancel(text=I18NFormat("Cancel")),
        ),
        getter=get_data,
        state=DialogSG.greeting,
    ),
)

DEFAULT_LOCALE = "en"
LOCALES = ["en"]


def make_i18n_middleware():
    loader = FluentResourceLoader(os.path.join(
        os.path.dirname(__file__),
        "translations",
        "{locale}",
    ))
    l10ns = {
        locale: FluentLocalization(
            [locale, DEFAULT_LOCALE], ["main.ftl"], loader,
        )
        for locale in LOCALES
    }
    return I18nMiddleware(l10ns, DEFAULT_LOCALE)


async def start(message: Message, dialog_manager: DialogManager):
    # it is important to reset stack because user wants to restart everything
    await dialog_manager.start(DialogSG.greeting, mode=StartMode.RESET_STACK)


async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    dp = Dispatcher()

    i18n_middleware = make_i18n_middleware()
    dp.message.middleware(i18n_middleware)
    dp.callback_query.middleware(i18n_middleware)

    dp.include_router(dialog)
    dp.message.register(start, CommandStart())
    setup_dialogs(dp)

    bot = Bot(token=os.getenv("BOT_TOKEN"))
    await dp.start_polling(bot)


if __name__ == '__main__':
    asyncio.run(main())
Код example/i18n/i18n_format.py
from typing import Any, Dict, Protocol

from aiogram_dialog.api.protocols import DialogManager
from aiogram_dialog.widgets.common import WhenCondition
from aiogram_dialog.widgets.text import Text

I18N_FORMAT_KEY = "aiogd_i18n_format"


class Values(Protocol):
    def __getitem__(self, item: Any) -> Any:
        raise NotImplementedError


def default_format_text(text: str, data: Values) -> str:
    return text.format_map(data)


class I18NFormat(Text):
    def __init__(self, text: str, when: WhenCondition = None):
        super().__init__(when)
        self.text = text

    async def _render_text(self, data: Dict, manager: DialogManager) -> str:
        format_text = manager.middleware_data.get(
            I18N_FORMAT_KEY, default_format_text,
        )
        return format_text(self.text, data)
Код example/i18n/i18n_middleware.py
from typing import Any, Awaitable, Callable, Dict, Union

from aiogram.dispatcher.middlewares.base import BaseMiddleware
from aiogram.types import CallbackQuery, Message
from fluent.runtime import FluentLocalization
from i18n_format import I18N_FORMAT_KEY


class I18nMiddleware(BaseMiddleware):
    def __init__(
            self,
            l10ns: Dict[str, FluentLocalization],
            default_lang: str,
    ):
        super().__init__()
        self.l10ns = l10ns
        self.default_lang = default_lang

    async def __call__(
            self,
            handler: Callable[
                [Union[Message, CallbackQuery], Dict[str, Any]],
                Awaitable[Any],
            ],
            event: Union[Message, CallbackQuery],
            data: Dict[str, Any],
    ) -> Any:
        # some language/locale retrieving logic
        if event.from_user:
            lang = event.from_user.language_code
        else:
            lang = self.default_lang
        if lang not in self.l10ns:
            lang = self.default_lang

        l10n = self.l10ns[lang]
        # we use fluent.runtime here, but you can create custom functions
        data[I18N_FORMAT_KEY] = l10n.format_value

        return await handler(event, data)

Мега пример с демонстрацией работы всех виджетов.

Это слайд-шоу требует JavaScript.

Проект представляет собой бота для мессенджера Telegram, написанного на основе фреймворков aiogram и aiogram-dialog. Он включает в себя несколько модулей, каждый из которых отвечает за определенную функциональность бота:

main.py (example/mega/bot_dialogs/main.py):

Этот модуль содержит описание главного диалога бота. В нем создается экземпляр Dialog, который представляет собой структуру для управления диалогом между ботом и пользователем. В главном окне диалога выводятся кнопки для доступа к различным опциям бота, таким как макеты виджетов, прокрутка, выбор, календарь и т. д. Каждая кнопка связана с определенным состоянием, которое определено в модуле states.

bot.py (example/mega/bot.py):

Этот модуль представляет собой основной скрипт бота. В нем создается экземпляр бота и диспетчера (Dispatcher) из библиотеки aiogram. Затем диспетчер настраивается, добавляются обработчики для команды /start и обработчик для неизвестных интентов (UnknownIntent). Также создается роутер для управления диалогами. В конце основной функции main() запускается цикл обработки сообщений бота.

Дополнительные модули:

  1. states.py: В этом модуле определены состояния (states) для управления диалогами бота. Каждый StatesGroup представляет собой группу состояний, которые могут использоваться для управления поведением бота в различных сценариях.
  2. switch.py: Этот модуль отвечает за диалоги, в которых пользователю предоставляется возможность пошагового ввода данных. Он включает три окна (Window), представляющих различные шаги ввода данных.
  3. select.py: Данный модуль содержит диалоги с различными видами клавиатур выбора: Select, Radio, Multiselect и Toggle.
  4. scrolls.py: В этом модуле реализованы диалоги с прокруткой контента, позволяющие пользователю просматривать большие объемы текста или списки элементов с возможностью прокрутки.
  5. multiwidget.py: Здесь содержатся диалоги, в которых на одном экране отображаются несколько виджетов для ввода данных: Checkbox, Radio, Multiselect и Counter.
  6. reply_buttons.py: Модуль, который предоставляет диалог с клавиатурой ответа (ReplyKeyboard), включающей в себя кнопки для отправки контакта, местоположения и чекбокс.
  7. layouts.py: В этом модуле реализованы диалоги с различными макетами (layouts), такими как Row, Column и Group, которые позволяют управлять расположением виджетов на экране.
  8. common.py: Файл содержит общие элементы для всех диалогов, такие как кнопка «Главное меню».
  9. counter.py: В этом модуле реализован диалог с виджетом счетчика (Counter), который позволяет пользователям выбирать числовые значения в определенном диапазоне.
  10. calendar.py: Модуль, который содержит диалоги для работы с календарем. Здесь могут быть реализованы различные функции, связанные с выбором даты и времени.

Проектные задания для тренировки

  1. Платформа для онлайн-обучения: Создание бота для онлайн-обучения, где пользователи могут просматривать курсы, проходить тесты и получать обратную связь. Использование aiogram-dialog для создания удобного интерфейса выбора курсов, прохождения тестов и взаимодействия с учебным материалом.
  2. Система управления задачами: Разработка бота для управления задачами и проектами, где пользователи могут создавать, редактировать и отслеживать задачи. Использование aiogram-dialog для создания интуитивного интерфейса для добавления новых задач, установки сроков, приоритетов и меток.
  3. Бот для заказа продуктов и услуг: Создание бота для заказа еды, товаров или услуг, где пользователи могут выбирать продукты из меню, указывать параметры заказа и оформлять заказы. Использование aiogram-dialog для создания удобного интерфейса выбора продуктов, указания количества и деталей заказа.
  4. Помощник по путешествиям: Разработка бота-помощника для путешественников, где пользователи могут получать информацию о достопримечательностях, бронировать отели и транспорт, а также составлять маршруты и планировать поездки. Использование aiogram-dialog для создания интерфейса выбора городов, даты поездки, типа развлечений и услуг.
  5. Система онлайн-консультаций: Создание бота для проведения онлайн-консультаций и обмена информацией между специалистами и клиентами. Использование aiogram-dialog для создания интерфейса выбора темы консультации, установки времени, ввода данных и обмена сообщениями.
Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.