Slash-Befehle¶

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¶
- Benutzer-, Nachrichten- und Einstiegspunkt-Befehle
- Befehle bei Discord registrieren, auf einem Server oder global, und sie übersetzen
- Auf Interaktionen antworten