Modals¶

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().