跳转至

斜杠命令

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。命令名就是函数名,描述是文档字符串的第一行;也可以用 @bot.command(name=..., description=...) 来指定,两者都没有的命令会以 … 作为描述。@bot.command 和 @bot.command(...) 两种写法都可以,在机器人上或在路由器上都能用。

选项类型

参数的类型提示决定选项类型,处理函数收到的是已经转换好的值:

类型提示 Discord 选项 处理函数收到的值
str 字符串 文本
int 整数 一个 int
float 数字 一个 float,即使用户输入的是整数
bool 布尔值 一个 bool
User 用户 该 User
ResolvedMember 用户 成员,带有其 user;仅限服务器内
Role 身份组 该 Role
PartialChannel 频道 交互解析出的频道
Attachment 附件 上传文件的 Attachment
User \| Role 或 ResolvedMember \| Role 可提及对象 用户所选的那一个
Literal[...] 或某个 Enum 带候选项的字符串、整数或数字 字面量或枚举成员

表中未列出的类型提示会在装饰器运行时引发 CommandDefinitionError。频道选项的类型提示是 PartialChannel 而不是 Channel,成员则是 ResolvedMember 而不是 Member:交互中携带的就是这些部分对象,错误信息里也会说明这一点。

必填与可选

没有默认值的选项是必填的。设置默认值,或在类型提示中加上 | None,就会让它变成可选;用户没有填写的可选选项会得到默认值,或者 None。Discord 要求必填选项排在前面,无论参数顺序如何,bostie 在构建命令时都会按这个顺序排好。

描述

Annotated 中的字符串就是选项的描述:Annotated[str, "What happened"]。也可以把描述写在文档字符串里:

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=),而后者又优先于文档字符串。哪里都没有描述的选项会得到 …。

取值范围与选项设置

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 附件 最多 10 个,取 image、video、audio 或 .pdf 这样的扩展名
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。一个选项最多 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 个字符的建议,每种情况都会输出一条警告。一个选项不能同时拥有 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(...) 在其中再添加一个子命令组。命令组只能嵌套一层,一个命令组最多包含 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 以外的命令设置,并对其下所有子命令生效:在 Discord 中,default_permissions、nsfw、guilds、contexts 和 integration_types 只存在于顶层命令上。嵌套的命令组接受两个本地化设置、checks 和 extra。子命令接受 name、description、两个本地化设置、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 全局命令可以在哪里使用:服务器、机器人的私信、私密频道
integration_types 哪些安装方式带有该命令:服务器安装、用户安装
checks 在处理函数之前运行的检查(检查)
extra 合并到命令 JSON 中的原始字段

ephemeral、auto_defer、default_permissions、nsfw、contexts 和 integration_types 也可以为某个路由器的所有命令统一设置,而 contexts、integration_types 和 auto_defer 还可以为机器人的所有命令统一设置。

限制

装饰器会检查 Discord 会拒绝的内容,并引发 CommandDefinitionError,指明出错的函数和参数:

  • 名称为 1 到 32 个字符,由字母、数字、- 和 _ 组成,不能有大写字母。用户命令和消息命令的名称还可以包含大写字母和空格。
  • 描述为 1 到 100 个字符。
  • 一个命令最多 25 个选项,一个选项最多 25 个候选项。
  • 不接受 *args、**kwargs、仅限位置的选项以及没有类型提示的选项。

当一个命令的名称、描述和候选项值加起来超过 Discord 允许的 8000 个字符时,会给出一条警告,而不是错误。当一个作用域中有超过 100 条斜杠命令、15 条用户命令或 15 条消息命令时,同步时会发出警告。

下一步