Commandes slash¶

Une commande slash est une fonction async décorée avec @bot.command. Son premier paramètre reçoit le Context ; chaque paramètre suivant est une option de la commande.
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)
La commande est /report. Son nom est celui de la fonction et sa description est la première ligne de la docstring ; @bot.command(name=..., description=...) les définit à la place, et une commande qui n'a ni l'un ni l'autre reçoit … comme description. @bot.command et @bot.command(...) fonctionnent tous les deux, sur le bot comme sur un routeur.
Types d'options¶
L'annotation de type d'un paramètre choisit le type de l'option, et le gestionnaire reçoit la valeur déjà convertie :
| Annotation | Option Discord | Le gestionnaire reçoit |
|---|---|---|
str |
chaîne | le texte |
int |
entier | un int |
float |
nombre | un float, même quand l'utilisateur a tapé un nombre entier |
bool |
booléen | un bool |
User |
utilisateur | le User |
ResolvedMember |
utilisateur | le membre, avec son user ; uniquement sur un serveur |
Role |
rôle | le Role |
PartialChannel |
salon | le salon tel que l'interaction l'a résolu |
Attachment |
pièce jointe | l'Attachment du fichier envoyé |
User \| Role ou ResolvedMember \| Role |
mentionnable | celui que l'utilisateur a choisi |
Literal[...] ou un Enum |
chaîne, entier ou nombre avec des choix | le littéral ou le membre de l'enum |
Une annotation absente du tableau lève CommandDefinitionError à l'exécution du décorateur. Pour une option de salon, l'annotation est PartialChannel, pas Channel, et pour un membre, c'est ResolvedMember, pas Member : une interaction transporte ces objets partiels, et le message d'erreur le précise.
Obligatoires et facultatives¶
Une option sans valeur par défaut est obligatoire. Une valeur par défaut, ou | None dans l'annotation, la rend facultative ; une option facultative que l'utilisateur a omise reçoit sa valeur par défaut, ou None. Discord veut les options obligatoires en premier, et bostie les trie ainsi quand il construit la commande, quel que soit l'ordre des paramètres.
Descriptions¶
Une chaîne dans Annotated est la description de l'option : Annotated[str, "What happened"]. La docstring peut contenir les descriptions à la place :
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 lit une section Args:, comme ci-dessus, et une section Parameters soulignée de tirets ; une description peut s'étendre sur plusieurs lignes. Une chaîne dans Annotated l'emporte sur Option(description=), qui l'emporte sur la docstring. Une option décrite nulle part reçoit ….
Plages et réglages d'option¶
Range[int, 1, 10] est une option int de 1 à 10 inclus. Range accepte aussi float, et pour str les bornes portent sur la longueur : Range[str, 1, 2000]. Option(...) dans Annotated règle tout le reste, et l'emporte sur ce que l'annotation implique :
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}")
Champ de Option |
S'applique à | Signification |
|---|---|---|
name |
tous | le nom de l'option sur Discord ; le paramètre garde son nom Python |
description |
tous | la description, quand Annotated ne contient pas de chaîne |
name_localizations, description_localizations |
tous | le texte par locale, par exemple {"fr": "salon"} |
min, max |
entier, nombre | bornes incluses, dans ±2⁵³ |
min_length, max_length |
chaîne | bornes de longueur, de 0 à 6000 et de 1 à 6000 |
channel_types |
salon | les types de salons que l'utilisateur peut choisir |
file_types |
pièce jointe | jusqu'à 10 valeurs parmi image, video, audio ou des extensions comme .pdf |
choices |
chaîne, entier, nombre | des choix fixes ; voir plus bas |
autocomplete |
chaîne, entier, nombre | une fonction qui suggère des valeurs pendant que l'utilisateur tape |
extra |
tous | des champs bruts fusionnés dans le JSON de l'option, pour un champ que bostie ne modélise pas encore |
Un champ placé sur un type d'option auquel il ne s'applique pas lève CommandDefinitionError.
Choix¶
Un Literal ou un Enum propose une liste fixe. Un choix de Literal porte le nom de sa valeur ; un choix d'Enum porte le nom du membre et envoie la valeur du membre. Option(choices=[...]) les renomme ou les localise, à condition de lister les mêmes valeurs dans le même ordre :
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}.")
Le gestionnaire de /order reçoit "tea", "coffee" ou "water" pour drink, un membre de Size pour size, et la valeur du Choice choisi pour milk. Avec une simple annotation str, int ou float, Option(choices=[...]) définit à lui seul les choix ; chacun est un Choice(name, value) ou une valeur nue, qui porte son propre nom. Les valeurs des choix doivent avoir le type de l'option, et les valeurs d'un Literal ou d'un Enum doivent être toutes des str, toutes des int ou toutes des float. Une option a au plus 25 choix, un nom de choix fait de 1 à 100 caractères et une valeur de type chaîne au plus 100.
Autocomplétion¶
Option(autocomplete=function) demande des suggestions à function pendant que l'utilisateur tape. Elle reçoit un AutocompleteContext et le texte tapé jusque-là, et renvoie des valeurs ou des objets 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 contient les options déjà remplies, par nom d'option, converties comme le gestionnaire les recevrait quand elles se convertissent ; l'option en cours de saisie reste du texte. bostie envoie les 25 premières suggestions, tronque les noms de plus de 100 caractères, et écarte les suggestions au nom vide, à la valeur du mauvais type ou à la valeur de type chaîne de plus de 100 caractères, avec un avertissement pour chaque cas. Une option ne peut pas avoir à la fois choices et autocomplete. Si la fonction lève une exception, l'erreur part vers les hooks d'erreur et Discord reçoit une liste vide.
Convertisseurs¶
Annotated[T, Convert(function)] transforme la valeur de l'option en T avant l'exécution du gestionnaire. function(ctx, value) peut être async, et l'annotation de type de son paramètre value décide du type d'option Discord, descriptions Range et Annotated comprises :
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}.")
Le convertisseur ne s'exécute que pour une option que l'utilisateur a fournie ; une option facultative omise reçoit sa valeur par défaut sans conversion. Une exception levée par le convertisseur part vers les hooks d'erreur comme une exception du gestionnaire.
Sous-commandes et groupes¶
bot.group(name, description) enregistre une commande faite de sous-commandes, et group.group(...) y ajoute un groupe de sous-commandes. Les groupes s'imbriquent sur un seul niveau, et un groupe contient au plus 25 entrées :
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}.")
Ce code définit /roles give et /roles color set. @group.command(name=...) donne à une sous-commande un autre nom que celui de sa fonction. ctx.command_name est le nom complet qui s'est exécuté, par exemple "roles color set", et command.qualified_name donne la même chose pour l'objet commande.
Un groupe de premier niveau accepte les réglages d'une commande sauf name, ephemeral et auto_defer, et ils s'appliquent à toutes ses sous-commandes : default_permissions, nsfw, guilds, contexts et integration_types n'existent que sur les commandes de premier niveau chez Discord. Un groupe imbriqué accepte les deux localisations, checks et extra. Une sous-commande accepte name, description, les deux localisations, ephemeral, auto_defer, checks et extra. Les vérifications d'un groupe s'exécutent avant celles de ses sous-commandes.
Réglages de commande¶
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.")
| Réglage | Signification |
|---|---|
name, description |
le nom et la description, à la place de ceux de la fonction |
name_localizations, description_localizations |
le texte par locale ; voir Enregistrer les commandes pour une fonction de traduction |
ephemeral |
les réponses ne sont visibles que par l'utilisateur, sauf si un appel en décide autrement |
auto_defer |
le nombre de secondes après lequel une utilisation sans réponse est différée ; None le désactive (Répondre aux interactions) |
default_permissions |
les permissions dont un membre a besoin pour voir et utiliser la commande ; les administrateurs du serveur peuvent changer qui peut l'utiliser |
nsfw |
affichée uniquement dans les salons avec restriction d'âge |
guilds |
les ids des serveurs où enregistrer la commande au lieu de l'enregistrer globalement |
contexts |
où une commande globale peut être utilisée : serveurs, messages privés du bot, salons privés |
integration_types |
quelles installations portent la commande : installation sur un serveur, installation utilisateur |
checks |
les vérifications qui s'exécutent avant le gestionnaire (Vérifications) |
extra |
des champs bruts fusionnés dans le JSON de la commande |
ephemeral, auto_defer, default_permissions, nsfw, contexts et integration_types peuvent aussi être réglés pour toutes les commandes d'un routeur, et contexts, integration_types et auto_defer pour toutes les commandes du bot.
Limites¶
Le décorateur vérifie ce que Discord refuserait et lève CommandDefinitionError en nommant la fonction et le paramètre :
- Les noms font de 1 à 32 caractères parmi les lettres, les chiffres,
-et_, sans majuscule. Les noms de commandes utilisateur et de commandes de message peuvent aussi contenir des majuscules et des espaces. - Les descriptions font de 1 à 100 caractères.
- Une commande a au plus 25 options et une option au plus 25 choix.
*args,**kwargs, les options positionnelles uniquement et les options sans annotation de type sont refusés.
Un avertissement, et non une erreur, signale quand les noms, descriptions et valeurs de choix d'une commande dépassent au total les 8000 caractères que Discord autorise. La synchronisation avertit quand une même portée contient plus de 100 commandes slash, 15 commandes utilisateur ou 15 commandes de message.
Ensuite¶
- Commandes utilisateur, de message et de point d'entrée
- Enregistrer les commandes auprès de Discord, sur un serveur ou globalement, et les traduire
- Répondre aux interactions