Comandos slash¶

Un comando slash es una función asíncrona decorada con @bot.command. Su primer parámetro recibe el Context; cada parámetro que va después es una opción del comando.
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)
El comando es /report. Su nombre es el nombre de la función y su descripción es la primera línea del docstring; @bot.command(name=..., description=...) los define en su lugar, y un comando sin ninguno de los dos recibe … como descripción. @bot.command y @bot.command(...) funcionan los dos, en el bot o en un router.
Tipos de opción¶
La anotación de tipo de un parámetro elige el tipo de opción, y el handler recibe el valor ya convertido:
| Anotación | Opción de Discord | El handler recibe |
|---|---|---|
str |
cadena | el texto |
int |
entero | un int |
float |
número | un float, también cuando el usuario escribió un número entero |
bool |
booleano | un bool |
User |
usuario | el User |
ResolvedMember |
usuario | el miembro, con su user; solo en servidores |
Role |
rol | el Role |
PartialChannel |
canal | el canal tal como lo resolvió la interacción |
Attachment |
archivo adjunto | el Attachment del archivo subido |
User \| Role o ResolvedMember \| Role |
mencionable | lo que haya elegido el usuario |
Literal[...] o un Enum |
cadena, entero o número con choices | el literal o el miembro del enum |
Una anotación que no aparece en la tabla lanza CommandDefinitionError cuando se ejecuta el decorador. Para una opción de canal la anotación es PartialChannel, no Channel, y para un miembro es ResolvedMember, no Member: una interacción trae esos objetos parciales, y el mensaje de error lo dice.
Obligatorias y opcionales¶
Una opción sin valor por defecto es obligatoria. Un valor por defecto, o | None en la anotación, la vuelve opcional; una opción opcional que el usuario no completó recibe su valor por defecto, o None. Discord quiere las opciones obligatorias primero, y bostie las ordena así al construir el comando, sin importar el orden de los parámetros.
Descripciones¶
Una cadena dentro de Annotated es la descripción de la opción: Annotated[str, "What happened"]. El docstring también puede contener las descripciones:
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 lee una sección Args:, como la de arriba, y una sección Parameters subrayada con guiones; una descripción puede ocupar varias líneas. Una cadena en Annotated tiene prioridad sobre Option(description=), que a su vez tiene prioridad sobre el docstring. Una opción que no se describe en ningún lado recibe ….
Rangos y ajustes de opción¶
Range[int, 1, 10] es una opción int de 1 a 10, ambos incluidos. Range también acepta float, y para str los límites son la longitud: Range[str, 1, 2000]. Option(...) dentro de Annotated define todo lo demás, y tiene prioridad sobre lo que implica la anotación:
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}")
Campo de Option |
Se aplica a | Significado |
|---|---|---|
name |
todas | el nombre de la opción en Discord; el parámetro conserva su nombre en Python |
description |
todas | la descripción, cuando Annotated no contiene ninguna cadena |
name_localizations, description_localizations |
todas | texto por idioma, como {"fr": "salon"} |
min, max |
entero, número | límites incluidos, dentro de ±2⁵³ |
min_length, max_length |
cadena | límites de longitud, de 0 a 6000 y de 1 a 6000 |
channel_types |
canal | los tipos de canal que el usuario puede elegir |
file_types |
archivo adjunto | hasta 10 entre image, video, audio o extensiones como .pdf |
choices |
cadena, entero, número | choices fijas; ver más abajo |
autocomplete |
cadena, entero, número | una función que sugiere valores mientras el usuario escribe |
extra |
todas | campos sin procesar que se combinan con el JSON de la opción, para un campo que bostie todavía no modela |
Un campo en un tipo de opción al que no se aplica lanza CommandDefinitionError.
Choices¶
Un Literal o un Enum ofrece una lista fija. Una choice de un Literal se llama como su valor; una choice de un Enum se llama como el miembro y envía el valor del miembro. Option(choices=[...]) les cambia el nombre o las traduce, siempre que enumere los mismos valores en el mismo orden:
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}.")
El handler de /order recibe "tea", "coffee" o "water" para drink, un miembro de Size para size, y el valor de la Choice elegida para milk. Con una anotación simple str, int o float, Option(choices=[...]) por sí sola define las choices; cada una es una Choice(name, value) o un valor suelto, que se llama como él mismo. Los valores de las choices deben tener el tipo de la opción, y los valores de un Literal o de un Enum deben ser todos str, todos int o todos float. Una opción tiene como máximo 25 choices, el nombre de una choice tiene de 1 a 100 caracteres y un valor de cadena, como máximo 100.
Autocompletado¶
Option(autocomplete=function) le pide sugerencias a function mientras el usuario escribe. Recibe un AutocompleteContext y el texto escrito hasta el momento, y devuelve valores u objetos 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 contiene las opciones completadas hasta el momento, por nombre de opción, convertidas como las recibiría el handler cuando se pueden convertir; la opción que se está escribiendo queda como texto. bostie envía las primeras 25 sugerencias, recorta los nombres de más de 100 caracteres y descarta las sugerencias con un nombre vacío, un valor del tipo incorrecto o un valor de cadena de más de 100 caracteres, con una advertencia para cada caso. Una opción no puede tener a la vez choices y autocomplete. Si la función lanza una excepción, el error va a los hooks de errores y Discord recibe una lista vacía.
Conversores¶
Annotated[T, Convert(function)] convierte el valor de la opción en un T antes de que se ejecute el handler. function(ctx, value) puede ser asíncrona, y la anotación de tipo de su parámetro value decide el tipo de opción de Discord, incluidos Range y las descripciones de 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}.")
El conversor solo se ejecuta para una opción que el usuario completó; una opción opcional que no se completó recibe su valor por defecto sin convertir. Una excepción del conversor va a los hooks de errores igual que una excepción del handler.
Subcomandos y grupos¶
bot.group(name, description) registra un comando formado por subcomandos, y group.group(...) agrega dentro de él un grupo de subcomandos. Los grupos se anidan un solo nivel, y un grupo contiene como máximo 25 entradas:
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}.")
Esto define /roles give y /roles color set. @group.command(name=...) le da a un subcomando un nombre distinto del de su función. ctx.command_name es el nombre completo que se ejecutó, como "roles color set", y command.qualified_name es lo mismo para el objeto del comando.
Un grupo de nivel superior acepta los ajustes de un comando salvo name, ephemeral y auto_defer, y se aplican a todos sus subcomandos: en Discord, default_permissions, nsfw, guilds, contexts e integration_types solo existen en los comandos de nivel superior. Un grupo anidado acepta las dos traducciones, checks y extra. Un subcomando acepta name, description, las dos traducciones, ephemeral, auto_defer, checks y extra. Los checks de un grupo se ejecutan antes que los de sus subcomandos.
Ajustes del comando¶
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.")
| Ajuste | Significado |
|---|---|
name, description |
el nombre y la descripción, en lugar de los de la función |
name_localizations, description_localizations |
texto por idioma; ver Registrar comandos para una función de traducción |
ephemeral |
las respuestas solo las ve el usuario, salvo que una llamada indique otra cosa |
auto_defer |
segundos tras los cuales un uso sin responder se difiere; None lo desactiva (Responder a interacciones) |
default_permissions |
los permisos que un miembro necesita para ver y usar el comando; los administradores del servidor pueden cambiar quién puede usarlo |
nsfw |
solo se muestra en canales restringidos por edad |
guilds |
ids de servidores donde registrar el comando en lugar de hacerlo de forma global |
contexts |
dónde se puede usar un comando global: servidores, los MD del bot, canales privados |
integration_types |
qué instalaciones incluyen el comando: instalación en servidor, instalación de usuario |
checks |
checks que se ejecutan antes del handler (Checks) |
extra |
campos sin procesar que se combinan con el JSON del comando |
ephemeral, auto_defer, default_permissions, nsfw, contexts e integration_types también se pueden definir para todos los comandos de un router, y contexts, integration_types y auto_defer para todos los comandos del bot.
Límites¶
El decorador comprueba lo que Discord rechazaría y lanza CommandDefinitionError indicando la función y el parámetro:
- Los nombres tienen de 1 a 32 caracteres entre letras, dígitos,
-y_, sin mayúsculas. Los nombres de los comandos de usuario y de mensaje también pueden tener mayúsculas y espacios. - Las descripciones tienen de 1 a 100 caracteres.
- Un comando tiene como máximo 25 opciones y una opción, como máximo 25 choices.
- Se rechazan
*args,**kwargs, las opciones solo posicionales y las opciones sin anotación de tipo.
Una advertencia, no un error, avisa cuando los nombres, las descripciones y los valores de las choices de un comando suman más de los 8000 caracteres que permite Discord. La sincronización advierte cuando un ámbito contiene más de 100 comandos slash, 15 comandos de usuario o 15 comandos de mensaje.
Siguiente paso¶
- Comandos de usuario, de mensaje y de punto de entrada
- Registrar comandos en Discord, en un servidor o de forma global, y traducirlos
- Responder a interacciones