コンテンツにスキップ

スラッシュコマンド

吹き出しの中にスラッシュコマンドを書いている Bostie

スラッシュコマンドは、@bot.command でデコレートした非同期関数です。最初のパラメーターは Context を受け取り、それ以降のパラメーターはすべてコマンドのオプションになります。

from typing import Annotated

import bostie
from bostie import Attachment, Context, PartialChannel, ResolvedMember, Role

bot = bostie.Bot()


@bot.command
async def report(
    ctx: Context,
    member: Annotated[ResolvedMember, "Who to report"],
    reason: Annotated[str, "What happened"],
    channel: PartialChannel | None = None,
    proof: Attachment | None = None,
    notify: Role | None = None,
    urgent: bool = False,
) -> None:
    """Report a member to the moderators."""
    lines = [f"{member.mention}: {reason}"]
    if channel is not None:
        lines.append(f"In {channel.mention}")
    if proof is not None:
        lines.append(f"Proof: {proof.url}")
    if notify is not None:
        lines.append(f"Notify {notify.mention}")
    if urgent:
        lines.append("Urgent")
    await ctx.respond("\n".join(lines), ephemeral=True)

このコマンドは /report です。名前は関数名、説明は docstring の 1 行目になります。@bot.command(name=..., description=...) を使うと代わりにこれらを指定でき、どちらもないコマンドの説明は … になります。@bot.command と @bot.command(...) はどちらも使え、Bot にもルーターにも付けられます。

オプションの型

パラメーターの型ヒントでオプションの型が決まり、ハンドラーは変換済みの値を受け取ります。

型ヒント Discord のオプション ハンドラーが受け取るもの
str 文字列 テキスト
int 整数 int
float 数値 float(ユーザーが整数を入力した場合も)
bool 真偽値 bool
User ユーザー その User
ResolvedMember ユーザー そのメンバー(user 付き)。サーバー内のみ
Role ロール その Role
PartialChannel チャンネル インタラクションが解決したとおりのチャンネル
Attachment 添付ファイル アップロードされたファイルの Attachment
User \| Role または ResolvedMember \| Role メンション可能 ユーザーが選んだほう
Literal[...] または Enum 選択肢付きの文字列、整数、数値 そのリテラル、または列挙型のメンバー

表にない型ヒントを使うと、デコレーターの実行時に CommandDefinitionError が送出されます。チャンネルのオプションの型ヒントは Channel ではなく PartialChannel、メンバーは Member ではなく ResolvedMember です。インタラクションが運ぶのはこうした部分オブジェクトであり、エラーメッセージでもそのことが示されます。

必須と任意

既定値のないオプションは必須です。既定値を指定するか、型ヒントに | None を付けると任意になります。ユーザーが省略した任意のオプションには、その既定値または None が入ります。Discord は必須のオプションを先に並べることを求めるので、bostie はコマンドを組み立てる際に、パラメーターの順序にかかわらずその順に並べ替えます。

説明

Annotated 内の文字列がオプションの説明になります(Annotated[str, "What happened"])。代わりに docstring に説明を書くこともできます。

import bostie
from bostie import Context, Range

bot = bostie.Bot()


@bot.command
async def remind(ctx: Context, minutes: Range[int, 1, 1440], text: str) -> None:
    """Set a reminder.

    Args:
        minutes: How long to wait, up to a day.
        text: What to remind you of.
    """
    await ctx.respond(f"In {minutes} minutes: {text}", ephemeral=True)

bostie は、上の例のような Args: セクションと、ダッシュで下線を引いた Parameters セクションを読み取ります。説明は複数行にまたがってもかまいません。Annotated 内の文字列は Option(description=) より優先され、Option(description=) は docstring より優先されます。どこにも説明がないオプションの説明は … になります。

範囲とオプションの設定

Range[int, 1, 10] は 1 から 10 まで(両端を含む)の int オプションです。Range は float も受け付け、str の場合は範囲が長さを表します(Range[str, 1, 2000])。それ以外の設定は Annotated 内の Option(...) で行い、型ヒントから導かれる設定よりも優先されます。

from typing import Annotated

import bostie
from bostie import Attachment, ChannelType, Context, Option, PartialChannel, Range

bot = bostie.Bot()


@bot.command
async def announce(
    ctx: Context,
    text: Range[str, 1, 2000],
    target: Annotated[
        PartialChannel,
        Option(
            name="channel",
            description="Where to post it",
            channel_types=[ChannelType.GUILD_TEXT, ChannelType.GUILD_ANNOUNCEMENT],
            description_localizations={"de": "Wo es erscheinen soll"},
        ),
    ],
    delay: Annotated[float, Option(min=0, max=3600, description="Seconds to wait")] = 0,
    image: Annotated[Attachment | None, Option(file_types=["image"])] = None,
) -> None:
    """Post an announcement."""
    extra = f" with {image.filename}" if image is not None else ""
    await ctx.respond(f"Posting in {target.mention} in {delay:g} s{extra}: {text}")
Option のフィールド 対象 意味
name すべて Discord 上でのオプション名。パラメーターは Python の名前のままです
description すべて 説明。Annotated に文字列がない場合に使われます
name_localizations、description_localizations すべて ロケールごとのテキスト({"fr": "salon"} など)
min、max 整数、数値 両端を含む範囲。±2⁵³ 以内
min_length、max_length 文字列 長さの範囲。それぞれ 0〜6000、1〜6000
channel_types チャンネル ユーザーが選べるチャンネルの種類
file_types 添付ファイル image、video、audio、または .pdf のような拡張子を最大 10 個
choices 文字列、整数、数値 固定の選択肢。下記を参照してください
autocomplete 文字列、整数、数値 ユーザーの入力に合わせて値を提案する関数
extra すべて オプションの JSON にマージされる生のフィールド。bostie がまだモデル化していないフィールド用です

対象外のオプションの型にフィールドを指定すると、CommandDefinitionError が送出されます。

選択肢

Literal や Enum は固定の選択肢のリストを提供します。Literal の選択肢は値がそのまま名前になります。Enum の選択肢はメンバー名が名前になり、送られるのはメンバーの値です。Option(choices=[...]) を使うと、同じ値を同じ順序で並べている限り、選択肢の名前を変えたりローカライズしたりできます。

from enum import Enum
from typing import Annotated, Literal

import bostie
from bostie import Choice, Context, Option

bot = bostie.Bot()


class Size(Enum):
    SMALL = 1
    MEDIUM = 2
    LARGE = 3


@bot.command
async def order(
    ctx: Context,
    drink: Literal["tea", "coffee", "water"],
    size: Size = Size.MEDIUM,
    milk: Annotated[
        Literal["none", "oat", "cow"],
        Option(
            choices=[
                Choice("No milk", "none"),
                Choice("Oat milk", "oat", name_localizations={"fr": "Lait d'avoine"}),
                Choice("Cow milk", "cow"),
            ]
        ),
    ] = "none",
) -> None:
    """Order a drink."""
    await ctx.respond(f"One {size.name.lower()} {drink}, milk: {milk}.")

/order のハンドラーは、drink には "tea"、"coffee"、"water" のいずれかを、size には Size のメンバーを、milk には選ばれた Choice の値を受け取ります。型ヒントが単なる str、int、float の場合は、Option(choices=[...]) だけで選択肢を定義します。各選択肢は Choice(name, value) か、値そのもの(名前は値と同じになります)です。選択肢の値はオプションの型と一致している必要があり、Literal や Enum の値はすべて str、すべて int、またはすべて float でなければなりません。1 つのオプションが持てる選択肢は最大 25 個で、選択肢の名前は 1〜100 文字、文字列の値は最大 100 文字です。

オートコンプリート

Option(autocomplete=function) を指定すると、ユーザーの入力中に function へ候補を問い合わせます。この関数は AutocompleteContext とそれまでに入力されたテキストを受け取り、値または Choice オブジェクトを返します。

from typing import Annotated

import bostie
from bostie import AutocompleteContext, Choice, Context, Option

bot = bostie.Bot()

CITIES = {
    "fr": ["Paris", "Lyon", "Marseille"],
    "jp": ["Tokyo", "Osaka", "Kyoto"],
}


async def countries(ctx: AutocompleteContext, typed: str) -> list[Choice]:
    return [
        Choice(code.upper(), code) for code in CITIES if code.startswith(typed.lower())
    ]


async def cities(ctx: AutocompleteContext, typed: str) -> list[str]:
    country = ctx.options.get("country", "")
    return [city for city in CITIES.get(country, []) if typed.lower() in city.lower()]


@bot.command
async def weather(
    ctx: Context,
    country: Annotated[str, Option(autocomplete=countries)],
    city: Annotated[str, Option(autocomplete=cities)],
) -> None:
    """Show the weather of a city."""
    await ctx.respond(f"Sunny in {city}, {country.upper()}.")

ctx.options には、それまでに入力されたオプションがオプション名ごとに入っており、変換できるものはハンドラーが受け取るのと同じ形に変換されています。入力中のオプションはテキストのままです。bostie は最初の 25 件の候補を送り、100 文字を超える名前は切り詰め、名前が空のもの、値の型が違うもの、文字列の値が 100 文字を超えるものは取り除きます。いずれの場合も、それぞれ警告が出ます。1 つのオプションに choices と autocomplete の両方を指定することはできません。関数が例外を送出すると、そのエラーはエラーフックに渡され、Discord には空のリストが送られます。

コンバーター

Annotated[T, Convert(function)] は、ハンドラーの実行前にオプションの値を T に変換します。function(ctx, value) は非同期でもかまいません。その value パラメーターの型ヒントで Discord のオプションの型が決まり、Range や Annotated の説明もそこから取られます。

from dataclasses import dataclass
from typing import Annotated

import bostie
from bostie import Context, Convert, Range

bot = bostie.Bot()


@dataclass(frozen=True)
class Color:
    red: int
    green: int
    blue: int


def parse_color(ctx: Context, text: Range[str, 7, 7]) -> Color:
    value = int(text.removeprefix("#"), 16)
    return Color(value >> 16, value >> 8 & 0xFF, value & 0xFF)


@bot.command
async def paint(
    ctx: Context,
    color: Annotated[Color, Convert(parse_color), "A hex color such as #5865F2"],
) -> None:
    """Paint with a color."""
    await ctx.respond(f"Red {color.red}, green {color.green}, blue {color.blue}.")

コンバーターが実行されるのは、ユーザーが指定したオプションに対してだけです。省略された任意のオプションには、変換されていない既定値が入ります。コンバーターからの例外は、ハンドラーからの例外と同じようにエラーフックに渡されます。

サブコマンドとグループ

bot.group(name, description) はサブコマンドからなるコマンドを登録し、group.group(...) はその中にサブコマンドのグループを追加します。グループの入れ子は 1 段までで、1 つのグループには最大 25 個まで入れられます。

import bostie
from bostie import Context, Permissions, ResolvedMember, Role

bot = bostie.Bot()

roles = bot.group("roles", "Manage roles", default_permissions=Permissions.MANAGE_ROLES)
colors = roles.group("color", "Color roles")


@roles.command
async def give(ctx: Context, member: ResolvedMember, role: Role) -> None:
    """Give a member a role."""
    await ctx.respond(f"/{ctx.command_name}: {member.mention} gets {role.mention}.")


@colors.command(name="set")
async def set_color(ctx: Context, member: ResolvedMember, hex_code: str) -> None:
    """Give a member a color role."""
    await ctx.respond(f"/{ctx.command_name}: {member.mention} is now {hex_code}.")

この例は /roles give と /roles color set を定義します。@group.command(name=...) を使うと、サブコマンドに関数とは別の名前を付けられます。ctx.command_name は実行されたコマンドの完全な名前("roles color set" など)で、コマンドオブジェクトでは command.qualified_name が同じものを表します。

トップレベルのグループは、name、ephemeral、auto_defer を除くコマンドの設定を受け付け、それらはすべてのサブコマンドに適用されます。default_permissions、nsfw、guilds、contexts、integration_types は、Discord ではトップレベルのコマンドにしか存在しないためです。入れ子のグループが受け付けるのは、2 つのローカライズ、checks、extra です。サブコマンドが受け付けるのは、name、description、2 つのローカライズ、ephemeral、auto_defer、checks、extra です。グループのチェックは、そのサブコマンドのチェックより先に実行されます。

コマンドの設定

import bostie
from bostie import (
    ApplicationIntegrationType,
    Context,
    InteractionContextType,
    Permissions,
)

bot = bostie.Bot()


@bot.command(
    name="purge",
    description="Delete recent messages",
    name_localizations={"es-ES": "purgar"},
    description_localizations={"es-ES": "Borra mensajes recientes"},
    default_permissions=Permissions.MANAGE_MESSAGES,
    contexts=[InteractionContextType.GUILD],
    integration_types=[ApplicationIntegrationType.GUILD_INSTALL],
    ephemeral=True,
    auto_defer=1.0,
)
async def purge_messages(ctx: Context, count: int = 10) -> None:
    await ctx.respond(f"Deleting {count} messages.")


@bot.command(guilds=[1335172792320131073], nsfw=True)
async def staff(ctx: Context) -> None:
    """Registered in one guild only, and shown in age-restricted channels only."""
    await ctx.respond("Staff only.")
設定 意味
name、description 関数のものの代わりに使う名前と説明
name_localizations、description_localizations ロケールごとのテキスト。翻訳関数についてはコマンドの登録を参照してください
ephemeral 呼び出し側で別の指定をしない限り、応答はそのユーザーにだけ表示されます
auto_defer 応答のない使用を遅延応答にするまでの秒数。None で無効になります(インタラクションへの応答)
default_permissions メンバーがコマンドを表示して使うのに必要な権限。誰が使えるかはサーバーの管理者が変更できます
nsfw 年齢制限のあるチャンネルにだけ表示されます
guilds コマンドをグローバルではなく登録するサーバーの ID
contexts グローバルコマンドを使える場所(サーバー、Bot との DM、プライベートチャンネル)
integration_types コマンドを持つインストール(サーバーインストール、ユーザーインストール)
checks ハンドラーの前に実行されるチェック(チェック)
extra コマンドの JSON にマージされる生のフィールド

ephemeral、auto_defer、default_permissions、nsfw、contexts、integration_types はルーターのすべてのコマンドに対してまとめて設定することもでき、contexts、integration_types、auto_defer は Bot のすべてのコマンドに対して設定できます。

上限

デコレーターは Discord が拒否する内容をチェックし、関数名とパラメーター名を示した CommandDefinitionError を送出します。

  • 名前は英字、数字、-、_ からなる 1〜32 文字で、大文字は使えません。ユーザーコマンドとメッセージコマンドの名前には、大文字と空白も使えます。
  • 説明は 1〜100 文字です。
  • 1 つのコマンドが持てるオプションは最大 25 個、1 つのオプションが持てる選択肢は最大 25 個です。
  • *args、**kwargs、位置専用のオプション、型ヒントのないオプションは受け付けません。

コマンドの名前、説明、選択肢の値の合計が、Discord が許す 8000 文字を超えると、エラーではなく警告が出ます。同期では、1 つのスコープにスラッシュコマンドが 100 個、ユーザーコマンドが 15 個、メッセージコマンドが 15 個を超えると警告が出ます。

次のステップ