Перейти к содержанию

Модальные окна

Бости заполняет форму и отправляет её

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

import bostie
from bostie import Context, ModalContext, TextInputStyle, ui

bot = bostie.Bot()


@bot.command
async def apply(ctx: Context) -> None:
    """Apply to join the staff."""
    await ctx.modal(
        ui.modal(
            submitted.id(),
            "Staff application",
            ui.text("Answers are seen by the admins only."),
            ui.label("Why do you want to join?", ui.text_input("why")),
            ui.label(
                "Tell us about yourself",
                ui.text_input("about", style=TextInputStyle.PARAGRAPH, max_length=1000),
                description="Experience, time zone, languages",
            ),
            ui.label(
                "Role",
                ui.radio_group(
                    "role",
                    [
                        ui.group_option("Moderator", "mod"),
                        ui.group_option("Event host", "events"),
                    ],
                ),
            ),
            ui.label("CV or portfolio", ui.file_upload("cv", required=False)),
        )
    )


@bot.on_modal("staff:apply")
async def submitted(ctx: ModalContext) -> None:
    files = ctx.attachments("cv")
    await ctx.respond(
        f"Thanks! You applied as {ctx.choice('role')} because: {ctx.text('why')}"
        f" ({len(files)} file(s) attached)",
        ephemeral=True,
    )

ui.modal(custom_id, title, *components) принимает заголовок от 1 до 45 символов и от одного до пяти компонентов: поля ui.label(...) и абзацы ui.text(...). await ctx.modal(...) открывает его; это должен быть первый ответ на взаимодействие, и после него на это взаимодействие уже ничем не ответить.

@bot.on_modal(template) направляет отправку так же, как on_component направляет нажатия: те же шаблоны с плейсхолдерами {name} и {name:int}, тот же route.id(...), те же настройки ephemeral, auto_defer и checks.

Поля

ui.label(label, component, description=None) ставит над одним полем ввода подпись длиной до 45 символов и описание длиной до 100:

Поле ввода Конструктор Чем читать
текстовое поле ui.text_input(custom_id, style=TextInputStyle.SHORT) ctx.text(custom_id), str
меню выбора из вариантов ui.string_select(custom_id, options) ctx.values(custom_id)
меню выбора пользователей, ролей, тех и других или каналов ui.user_select, ui.role_select, ui.mentionable_select, ui.channel_select ctx.users, ctx.members, ctx.roles, ctx.channels или ctx.ids
загрузка файлов ui.file_upload(custom_id) ctx.attachments(custom_id), загруженные объекты Attachment
один выбор из 2–10 ui.radio_group(custom_id, options) ctx.choice(custom_id), значение или None
несколько выборов из 1–10 ui.checkbox_group(custom_id, options) ctx.values(custom_id)
один флажок «да или нет» ui.checkbox(custom_id, default=False) ctx.checked(custom_id), bool

Каждое поле ввода, кроме флажка, принимает required, по умолчанию True, а min_values=0 требует required=False. text_input принимает style=TextInputStyle.PARAGRAPH для нескольких строк, placeholder, value для заранее заполненного значения, min_length (от 0 до 4000) и max_length (от 1 до 4000). file_upload принимает min_values и max_values, до 10 файлов, и file_types, например ["image", ".pdf"]. Варианты радиокнопок и флажков — это ui.group_option(label, value, description=None, default=False); группа флажков без max_values позволяет пользователю отметить все.

Конструкторы возвращают ui.ModalDict, ui.LabelDict, ui.TextInputDict, ui.FileUploadDict, ui.RadioGroupDict, ui.CheckboxGroupDict, ui.CheckboxDict и ui.GroupOptionDict. Методы чтения в ModalContext принимают custom id поля ввода. ID, которого нет в модальном окне, вызывает KeyError, а чтение поля другого вида, например ctx.text для флажка, вызывает TypeError.

Изменение сообщения, которое открыло модальное окно

Модальное окно, открытое из компонента, знает сообщение, на котором находится компонент, и его отправка может изменить это сообщение:

import bostie
from bostie import ComponentContext, Context, ModalContext, ui

bot = bostie.Bot()


@bot.command
async def notifications(ctx: Context) -> None:
    """Change what you are notified about."""
    button = ui.button("Edit", custom_id=edit.id())
    await ctx.respond(components=[ui.text("Notifications: none"), ui.row(button)])


@bot.on_component("notifications:edit")
async def edit(ctx: ComponentContext) -> None:
    topics = [
        ui.group_option("Releases", "releases"),
        ui.group_option("Events", "events", default=True),
        ui.group_option("Polls", "polls"),
    ]
    await ctx.modal(
        ui.modal(
            saved.id(),
            "Notifications",
            ui.label(
                "Topics",
                ui.checkbox_group("topics", topics, min_values=0, required=False),
            ),
            ui.label("Also by DM", ui.checkbox("dm")),
            ui.label("Who may ping you", ui.user_select("pingers", max_values=5)),
        )
    )


@bot.on_modal("notifications:saved")
async def saved(ctx: ModalContext) -> None:
    topics = ", ".join(ctx.values("topics")) or "none"
    by_dm = " by DM too" if ctx.checked("dm") else ""
    pingers = " ".join(user.mention for user in ctx.users("pingers"))
    await ctx.update(
        components=[ui.text(f"Notifications: {topics}{by_dm}. Pings from {pingers}.")]
    )

await ctx.update(...) и await ctx.defer_update() работают при отправке так же, как при нажатии. У модального окна, открытого из команды, нет сообщения, которое можно изменить, и они вызывают InteractionResponded; отвечайте вместо этого через respond. На отправку без ответа ответ откладывается через auto_defer секунд — через defer_update, если есть сообщение.

Ожидание отправки внутри обработчика

import asyncio

import bostie
from bostie import Context, ui

bot = bostie.Bot()


@bot.command
async def rename(ctx: Context) -> None:
    """Pick a new nickname."""
    form = f"rename:{ctx.interaction.id}"
    field = ui.text_input("nick", min_length=1, max_length=32, value=ctx.user.username)
    await ctx.modal(ui.modal(form, "New nickname", ui.label("Nickname", field)))
    try:
        async with asyncio.timeout(300):
            sent = await bot.wait_for_modal(form)
    except TimeoutError:
        return
    await sent.respond(f"You will be called {sent.text('nick')}.", ephemeral=True)

await bot.wait_for_modal(*custom_ids, check=None) возвращает ModalContext следующей отправки, чей custom id входит в custom_ids, или любого модального окна, если они не указаны, и никакой маршрут для неё не запускается. Добавьте в custom id что-нибудь уникальное, например ID взаимодействия, чтобы два пользователя, одновременно запустившие команду, получили каждый свою отправку. Пользователь может закрыть модальное окно, не отправив его, и Discord ничего не сообщит боту, поэтому ограничьте ожидание с помощью asyncio.timeout().