Modais¶

Um modal é um formulário em janela. Um comando ou um componente responde com um, e o envio do usuário chega como uma nova interação, que uma rota de modal trata.
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) aceita um título de 1 a 45 caracteres e de um a cinco componentes: campos ui.label(...) e parágrafos ui.text(...). await ctx.modal(...) o abre; ele precisa ser a primeira resposta da interação, e nada mais pode responder a essa interação depois.
@bot.on_modal(template) roteia o envio como on_component roteia cliques: os mesmos modelos com marcadores {name} e {name:int}, o mesmo route.id(...), as mesmas configurações ephemeral, auto_defer e checks.
Campos¶
ui.label(label, component, description=None) coloca um rótulo de até 45 caracteres e uma descrição de até 100 acima de uma entrada:
| Entrada | Builder | Leia com |
|---|---|---|
| uma caixa de texto | ui.text_input(custom_id, style=TextInputStyle.SHORT) |
ctx.text(custom_id), uma str |
| um menu de seleção de opções | ui.string_select(custom_id, options) |
ctx.values(custom_id) |
| um menu de seleção de usuários, cargos, ambos, ou canais | ui.user_select, ui.role_select, ui.mentionable_select, ui.channel_select |
ctx.users, ctx.members, ctx.roles, ctx.channels ou ctx.ids |
| uploads de arquivo | ui.file_upload(custom_id) |
ctx.attachments(custom_id), os objetos Attachment enviados |
| uma escolha entre 2 e 10 | ui.radio_group(custom_id, options) |
ctx.choice(custom_id), o valor ou None |
| várias escolhas entre 1 e 10 | ui.checkbox_group(custom_id, options) |
ctx.values(custom_id) |
| uma caixa de sim ou não | ui.checkbox(custom_id, default=False) |
ctx.checked(custom_id), um bool |
Toda entrada, exceto a caixa de seleção, aceita required, True por padrão, e min_values=0 exige required=False. text_input aceita style=TextInputStyle.PARAGRAPH para várias linhas, placeholder, value para preenchê-la de antemão, min_length (de 0 a 4000) e max_length (de 1 a 4000). file_upload aceita min_values e max_values, até 10 arquivos, e file_types como ["image", ".pdf"]. As opções de botões de opção e de caixas de seleção são ui.group_option(label, value, description=None, default=False); um grupo de caixas de seleção sem max_values deixa o usuário marcar todas.
Os builders retornam ui.ModalDict, ui.LabelDict, ui.TextInputDict, ui.FileUploadDict, ui.RadioGroupDict, ui.CheckboxGroupDict, ui.CheckboxDict e ui.GroupOptionDict. Os leitores de ModalContext recebem o custom id da entrada. Um ID que o modal não tem levanta KeyError, e ler uma entrada de outro tipo, como ctx.text em uma caixa de seleção, levanta TypeError.
Atualizando a mensagem que abriu o modal¶
Um modal aberto a partir de um componente conhece a mensagem em que o componente está, e o envio dele pode editar essa mensagem:
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(...) e await ctx.defer_update() funcionam no envio como em um clique. Em um modal aberto a partir de um comando não há mensagem para atualizar, e eles levantam InteractionResponded; responda com respond no lugar disso. Um envio sem resposta é adiado depois de auto_defer segundos, com defer_update quando há uma mensagem.
Esperando o envio dentro de um handler¶
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) retorna o ModalContext do próximo envio cujo custom id é um de custom_ids, ou de qualquer modal quando nenhum é dado, e nenhuma rota roda para ele. Coloque algo único no custom id, como o ID da interação, para que dois usuários que rodam o comando ao mesmo tempo recebam cada um o seu próprio envio. Um usuário pode fechar um modal sem enviá-lo, e o Discord não avisa nada ao bot, então limite a espera com asyncio.timeout().