Zum Inhalt

Modals

Bostie füllt ein Formular aus und schickt es ab

Ein Modal ist ein Popup-Formular. Ein Befehl oder eine Komponente antwortet mit einem, und das Absenden durch den Benutzer kommt als neue Interaktion an, die eine Modal-Route verarbeitet.

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) nimmt einen Titel von 1 bis 45 Zeichen und eine bis fünf Komponenten: Felder mit ui.label(...) und Absätze mit ui.text(...). await ctx.modal(...) öffnet es; es muss die erste Antwort der Interaktion sein, und danach kann nichts anderes mehr auf diese Interaktion antworten.

@bot.on_modal(template) leitet das Absenden weiter, so wie on_component Klicks weiterleitet: dieselben Vorlagen mit den Platzhaltern {name} und {name:int}, dasselbe route.id(...), dieselben Einstellungen ephemeral, auto_defer und checks.

Felder

ui.label(label, component, description=None) setzt ein Label von bis zu 45 Zeichen und eine Beschreibung von bis zu 100 über eine Eingabe:

Eingabe Builder Auslesen mit
ein Textfeld ui.text_input(custom_id, style=TextInputStyle.SHORT) ctx.text(custom_id), ein str
ein Auswahlmenü mit Optionen ui.string_select(custom_id, options) ctx.values(custom_id)
ein Auswahlmenü für Benutzer, Rollen, beides oder Kanäle ui.user_select, ui.role_select, ui.mentionable_select, ui.channel_select ctx.users, ctx.members, ctx.roles, ctx.channels oder ctx.ids
Datei-Uploads ui.file_upload(custom_id) ctx.attachments(custom_id), die hochgeladenen Attachment-Objekte
eine Wahl aus 2 bis 10 ui.radio_group(custom_id, options) ctx.choice(custom_id), der Wert oder None
mehrere Wahlen aus 1 bis 10 ui.checkbox_group(custom_id, options) ctx.values(custom_id)
ein Ja-oder-Nein-Kästchen ui.checkbox(custom_id, default=False) ctx.checked(custom_id), ein bool

Jede Eingabe außer der Checkbox nimmt required, standardmäßig True, und min_values=0 erfordert required=False. text_input nimmt style=TextInputStyle.PARAGRAPH für mehrere Zeilen, placeholder, value zum Vorausfüllen, min_length (0 bis 4000) und max_length (1 bis 4000). file_upload nimmt min_values und max_values, bis zu 10 Dateien, und file_types wie ["image", ".pdf"]. Optionen für Radio-Buttons und Checkboxen sind ui.group_option(label, value, description=None, default=False); eine Checkbox-Gruppe ohne max_values lässt den Benutzer alle ankreuzen.

Die Builder geben ui.ModalDict, ui.LabelDict, ui.TextInputDict, ui.FileUploadDict, ui.RadioGroupDict, ui.CheckboxGroupDict, ui.CheckboxDict und ui.GroupOptionDict zurück. Die Lesemethoden an ModalContext nehmen die Custom-ID der Eingabe. Eine ID, die das Modal nicht hat, löst KeyError aus, und das Lesen einer Eingabe anderer Art, etwa ctx.text bei einer Checkbox, löst TypeError aus.

Die Nachricht aktualisieren, die das Modal geöffnet hat

Ein Modal, das aus einer Komponente geöffnet wurde, kennt die Nachricht, an der die Komponente hängt, und sein Absenden kann diese Nachricht bearbeiten:

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(...) und await ctx.defer_update() funktionieren beim Absenden genauso wie bei einem Klick. Bei einem Modal, das aus einem Befehl geöffnet wurde, gibt es keine Nachricht zum Aktualisieren, und sie lösen InteractionResponded aus; antworte stattdessen mit respond. Ein unbeantwortetes Absenden wird nach auto_defer Sekunden aufgeschoben, mit defer_update, wenn es eine Nachricht gibt.

Innerhalb eines Handlers auf das Absenden warten

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) gibt den ModalContext des nächsten Absendens zurück, dessen Custom-ID eine der custom_ids ist, oder irgendeines Modals, wenn keine angegeben sind, und dafür läuft keine Route. Setz etwas Eindeutiges in die Custom-ID, etwa die ID der Interaktion, damit zwei Benutzer, die den Befehl gleichzeitig ausführen, jeweils ihr eigenes Absenden bekommen. Ein Benutzer kann ein Modal schließen, ohne es abzusenden, und Discord teilt dem Bot nichts mit; begrenze das Warten also mit asyncio.timeout().