コンテンツにスキップ

モーダル

フォームに記入して送信する Bostie

モーダルとは、ポップアップで表示されるフォームです。コマンドやコンポーネントがモーダルで応答し、ユーザーの送信は新しいインタラクションとして届いて、モーダルのルートがそれを処理します。

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 文字のタイトルと、1〜5 個のコンポーネントを受け取ります。コンポーネントは 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) は、1 つの入力欄の上に、最大 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 個から 1 つを選ぶ ui.radio_group(custom_id, options) ctx.choice(custom_id)(値または None)
1〜10 個から複数を選ぶ ui.checkbox_group(custom_id, options) ctx.values(custom_id)
はい・いいえのチェックボックス 1 つ 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 ファイル)、["image", ".pdf"] のような file_types を受け取ります。ラジオボタンとチェックボックスの選択肢は 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 の読み取りメソッドは、入力欄のカスタム 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) は、カスタム ID が custom_ids のいずれかである次の送信(何も指定しなければ、任意のモーダルの次の送信)の ModalContext を返し、その送信に対してはルートを実行しません。2 人のユーザーが同時にコマンドを実行してもそれぞれ自分の送信を受け取れるよう、カスタム ID にはインタラクション ID のような一意な値を入れてください。ユーザーは送信せずにモーダルを閉じることができ、そのとき Discord は Bot に何も知らせないので、asyncio.timeout() で待機時間を区切ってください。