Zum Inhalt

Slash-Befehle

Bostie schreibt einen Slash-Befehl in eine Sprechblase

Ein Slash-Befehl ist eine asynchrone Funktion mit dem Decorator @bot.command. Ihr erster Parameter nimmt den Context entgegen; jeder Parameter danach ist eine Option des Befehls.

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)

Der Befehl heißt /report. Sein Name ist der Name der Funktion, seine Beschreibung die erste Zeile des Docstrings; @bot.command(name=..., description=...) legt beide stattdessen fest, und ein Befehl ohne beides bekommt … als Beschreibung. @bot.command und @bot.command(...) funktionieren beide, am Bot wie an einem Router.

Optionstypen

Der Type Hint eines Parameters bestimmt den Optionstyp, und der Handler bekommt den Wert bereits umgewandelt:

Hint Discord-Option Der Handler bekommt
str Zeichenkette den Text
int Ganzzahl einen int
float Zahl einen float, auch wenn der Benutzer eine ganze Zahl eingegeben hat
bool Boolescher Wert einen bool
User Benutzer den User
ResolvedMember Benutzer das Mitglied, mit seinem user; nur auf Servern
Role Rolle die Role
PartialChannel Kanal den Kanal, wie die Interaktion ihn aufgelöst hat
Attachment Anhang das Attachment der hochgeladenen Datei
User \| Role oder ResolvedMember \| Role erwähnbar das, was der Benutzer ausgewählt hat
Literal[...] oder ein Enum Zeichenkette, Ganzzahl oder Zahl mit Auswahlmöglichkeiten das Literal oder das Enum-Mitglied

Ein Hint, der nicht in der Tabelle steht, löst beim Ausführen des Decorators CommandDefinitionError aus. Für eine Kanaloption lautet der Hint PartialChannel, nicht Channel, und für ein Mitglied ResolvedMember, nicht Member: Eine Interaktion enthält diese partiellen Objekte, und die Fehlermeldung sagt das auch.

Erforderlich und optional

Eine Option ohne Standardwert ist erforderlich. Ein Standardwert oder | None im Hint macht sie optional; eine optionale Option, die der Benutzer weggelassen hat, bekommt ihren Standardwert oder None. Discord will die erforderlichen Optionen zuerst, und bostie sortiert sie beim Bauen des Befehls entsprechend, egal in welcher Reihenfolge die Parameter stehen.

Beschreibungen

Ein String in Annotated ist die Beschreibung der Option: Annotated[str, "What happened"]. Stattdessen kann auch der Docstring die Beschreibungen enthalten:

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 liest einen Abschnitt Args: wie oben und einen mit Bindestrichen unterstrichenen Abschnitt Parameters; eine Beschreibung darf über mehrere Zeilen gehen. Ein String in Annotated hat Vorrang vor Option(description=), und das wiederum vor dem Docstring. Eine Option, die nirgends beschrieben ist, bekommt ….

Wertebereiche und Optionseinstellungen

Range[int, 1, 10] ist eine int-Option von 1 bis 10, beide Grenzen eingeschlossen. Range nimmt auch float, und bei str gelten die Grenzen für die Länge: Range[str, 1, 2000]. Option(...) in Annotated legt alles Übrige fest und hat Vorrang vor dem, was der Hint vorgibt:

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-Feld Gilt für Bedeutung
name alle der Name der Option bei Discord; der Parameter behält seinen Python-Namen
description alle die Beschreibung, wenn Annotated keinen String enthält
name_localizations, description_localizations alle Text pro Locale, etwa {"fr": "salon"}
min, max Ganzzahl, Zahl inklusive Grenzen, innerhalb von ±2⁵³
min_length, max_length Zeichenkette Längengrenzen, 0 bis 6000 und 1 bis 6000
channel_types Kanal die Kanaltypen, die der Benutzer auswählen darf
file_types Anhang bis zu 10 aus image, video, audio oder Dateiendungen wie .pdf
choices Zeichenkette, Ganzzahl, Zahl feste Auswahlmöglichkeiten; siehe unten
autocomplete Zeichenkette, Ganzzahl, Zahl eine Funktion, die Werte vorschlägt, während der Benutzer tippt
extra alle rohe Felder, die in das JSON der Option eingefügt werden, für ein Feld, das bostie noch nicht modelliert

Ein Feld an einem Optionstyp, für den es nicht gilt, löst CommandDefinitionError aus.

Auswahlmöglichkeiten

Ein Literal oder ein Enum bietet eine feste Liste an. Eine Literal-Auswahlmöglichkeit ist nach ihrem Wert benannt; eine Enum-Auswahlmöglichkeit ist nach dem Mitglied benannt und sendet dessen Wert. Option(choices=[...]) benennt sie um oder lokalisiert sie, solange es dieselben Werte in derselben Reihenfolge aufführt:

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}.")

Der Handler von /order bekommt für drink "tea", "coffee" oder "water", für size ein Size-Mitglied und für milk den Wert der ausgewählten Choice. Bei einem einfachen Hint str, int oder float legt Option(choices=[...]) die Auswahlmöglichkeiten allein fest; jede ist eine Choice(name, value) oder ein bloßer Wert, der nach sich selbst benannt ist. Die Werte der Auswahlmöglichkeiten müssen den Typ der Option haben, und die Werte eines Literal oder Enum müssen alle str, alle int oder alle float sein. Eine Option hat höchstens 25 Auswahlmöglichkeiten, ein Name ist 1 bis 100 Zeichen lang und ein String-Wert höchstens 100.

Autovervollständigung

Option(autocomplete=function) fragt function nach Vorschlägen, während der Benutzer tippt. Sie bekommt einen AutocompleteContext und den bisher eingegebenen Text und gibt Werte oder Choice-Objekte zurück:

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 enthält die bisher ausgefüllten Optionen nach Optionsnamen, umgewandelt, wie der Handler sie bekäme, sofern die Umwandlung gelingt; die Option, die gerade getippt wird, bleibt Text. bostie sendet die ersten 25 Vorschläge, kürzt Namen über 100 Zeichen und verwirft Vorschläge mit leerem Namen, mit einem Wert des falschen Typs oder mit einem String-Wert über 100 Zeichen, jeweils mit einer Warnung. Eine Option kann nicht gleichzeitig choices und autocomplete haben. Wirft die Funktion eine Exception, geht der Fehler an die Fehler-Hooks, und Discord bekommt eine leere Liste.

Konverter

Annotated[T, Convert(function)] wandelt den Wert der Option in ein T um, bevor der Handler läuft. function(ctx, value) darf asynchron sein, und der Type Hint ihres Parameters value bestimmt den Discord-Optionstyp, Range und Annotated-Beschreibungen eingeschlossen:

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}.")

Der Konverter läuft nur für eine Option, die der Benutzer angegeben hat; eine weggelassene optionale Option bekommt ihren Standardwert unverändert. Eine Exception aus dem Konverter geht an die Fehler-Hooks, genau wie eine Exception aus dem Handler.

Unterbefehle und Gruppen

bot.group(name, description) registriert einen Befehl, der aus Unterbefehlen besteht, und group.group(...) fügt darin eine Gruppe von Unterbefehlen hinzu. Gruppen lassen sich eine Ebene tief verschachteln, und eine Gruppe enthält höchstens 25 Einträge:

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}.")

Das definiert /roles give und /roles color set. @group.command(name=...) gibt einem Unterbefehl einen anderen Namen als seiner Funktion. ctx.command_name ist der vollständige Name, der ausgeführt wurde, etwa "roles color set", und command.qualified_name ist dasselbe für das Befehlsobjekt.

Eine Gruppe auf oberster Ebene nimmt die Einstellungen eines Befehls außer name, ephemeral und auto_defer, und sie gelten für alle ihre Unterbefehle: default_permissions, nsfw, guilds, contexts und integration_types gibt es bei Discord nur für Befehle auf oberster Ebene. Eine verschachtelte Gruppe nimmt die beiden Lokalisierungen, checks und extra. Ein Unterbefehl nimmt name, description, die beiden Lokalisierungen, ephemeral, auto_defer, checks und extra. Die Checks einer Gruppe laufen vor denen ihrer Unterbefehle.

Befehlseinstellungen

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.")
Einstellung Bedeutung
name, description der Name und die Beschreibung statt derer der Funktion
name_localizations, description_localizations Text pro Locale; eine Übersetzungsfunktion zeigt Befehle registrieren
ephemeral Antworten sind nur für den Benutzer sichtbar, sofern ein Aufruf nichts anderes sagt
auto_defer Sekunden, nach denen eine unbeantwortete Verwendung aufgeschoben wird; None schaltet das ab (Auf Interaktionen antworten)
default_permissions die Berechtigungen, die ein Mitglied braucht, um den Befehl zu sehen und zu verwenden; Server-Admins können ändern, wer ihn verwenden darf
nsfw nur in altersbeschränkten Kanälen angezeigt
guilds Server-IDs, auf denen der Befehl registriert wird, statt global
contexts wo ein globaler Befehl verwendet werden kann: auf Servern, in den DMs des Bots, in privaten Kanälen
integration_types welche Installationen den Befehl mitbringen: Guild Install, User Install
checks Checks, die vor dem Handler laufen (Checks)
extra rohe Felder, die in das JSON des Befehls eingefügt werden

ephemeral, auto_defer, default_permissions, nsfw, contexts und integration_types lassen sich auch für jeden Befehl eines Routers festlegen, und contexts, integration_types und auto_defer für jeden Befehl des Bots.

Limits

Der Decorator prüft, was Discord ablehnen würde, und löst CommandDefinitionError aus, mit der Funktion und dem Parameter im Text:

  • Namen sind 1 bis 32 Zeichen lang und bestehen aus Buchstaben, Ziffern, - und _, ohne Großbuchstaben. Namen von Benutzer- und Nachrichtenbefehlen dürfen auch Großbuchstaben und Leerzeichen enthalten.
  • Beschreibungen sind 1 bis 100 Zeichen lang.
  • Ein Befehl hat höchstens 25 Optionen und eine Option höchstens 25 Auswahlmöglichkeiten.
  • *args, **kwargs, rein positionelle Optionen und Optionen ohne Type Hint werden abgelehnt.

Eine Warnung, kein Fehler, meldet, wenn Namen, Beschreibungen und Auswahlwerte eines Befehls zusammen mehr als die 8000 Zeichen ergeben, die Discord erlaubt. Die Synchronisierung warnt, wenn ein Geltungsbereich mehr als 100 Slash-Befehle, 15 Benutzerbefehle oder 15 Nachrichtenbefehle enthält.

Weiter