Saltar a contenido

Modales

Bostie completa un formulario y lo envía

Un modal es un formulario emergente. Un comando o un componente responde con uno, y el envío del usuario llega como una interacción nueva que maneja una ruta de modal.

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) acepta un título de 1 a 45 caracteres y de uno a cinco componentes: campos ui.label(...) y párrafos ui.text(...). await ctx.modal(...) lo abre; tiene que ser la primera respuesta de la interacción, y después nada más puede responder esa interacción.

@bot.on_modal(template) enruta el envío igual que on_component enruta los clics: las mismas plantillas con marcadores de posición {name} y {name:int}, el mismo route.id(...), los mismos ajustes ephemeral, auto_defer y checks.

Campos

ui.label(label, component, description=None) pone una etiqueta de hasta 45 caracteres y una descripción de hasta 100 encima de una entrada:

Entrada Builder Se lee con
un cuadro de texto ui.text_input(custom_id, style=TextInputStyle.SHORT) ctx.text(custom_id), un str
un menú de selección de opciones ui.string_select(custom_id, options) ctx.values(custom_id)
un menú de selección de usuarios, roles, ambos o canales ui.user_select, ui.role_select, ui.mentionable_select, ui.channel_select ctx.users, ctx.members, ctx.roles, ctx.channels o ctx.ids
archivos subidos ui.file_upload(custom_id) ctx.attachments(custom_id), los objetos Attachment subidos
una opción entre 2 y 10 ui.radio_group(custom_id, options) ctx.choice(custom_id), el valor o None
varias opciones entre 1 y 10 ui.checkbox_group(custom_id, options) ctx.values(custom_id)
una casilla de sí o no ui.checkbox(custom_id, default=False) ctx.checked(custom_id), un bool

Todas las entradas salvo la casilla aceptan required, True por defecto, y min_values=0 necesita required=False. text_input acepta style=TextInputStyle.PARAGRAPH para varias líneas, placeholder, value para completarlo de antemano, min_length (de 0 a 4000) y max_length (de 1 a 4000). file_upload acepta min_values y max_values, hasta 10 archivos, y file_types como ["image", ".pdf"]. Las opciones de los grupos de botones de opción y de casillas son ui.group_option(label, value, description=None, default=False); un grupo de casillas sin max_values permite que el usuario las marque todas.

Los builders devuelven ui.ModalDict, ui.LabelDict, ui.TextInputDict, ui.FileUploadDict, ui.RadioGroupDict, ui.CheckboxGroupDict, ui.CheckboxDict y ui.GroupOptionDict. Los métodos de lectura de ModalContext aceptan el custom id de la entrada. Un id que el modal no tiene lanza KeyError, y leer una entrada de otro tipo, como ctx.text en una casilla, lanza TypeError.

Actualizar el mensaje que abrió el modal

Un modal abierto desde un componente conoce el mensaje en el que está el componente, y su envío puede editar ese mensaje:

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(...) y await ctx.defer_update() funcionan en el envío igual que en un clic. En un modal abierto desde un comando no hay ningún mensaje que actualizar, y lanzan InteractionResponded; responde con respond en su lugar. Un envío sin respuesta se difiere después de auto_defer segundos, con defer_update cuando hay un mensaje.

Esperar el envío dentro de un 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) devuelve el ModalContext del siguiente envío cuyo custom id sea uno de custom_ids, o de cualquier modal si no se indica ninguno, y no se ejecuta ninguna ruta para él. Pon algo único en el custom id, como el id de la interacción, para que dos usuarios que ejecutan el comando a la vez reciban cada uno su propio envío. Un usuario puede cerrar un modal sin enviarlo, y Discord no le avisa nada al bot, así que ponle un límite a la espera con asyncio.timeout().