Saltar a contenido

Registrar comandos

Discord muestra un comando solo después de que la aplicación lo registró. bostie lo hace por ti: cuando el bot arranca, compara los comandos que definiste con los que tiene Discord y los envía solo si algo es distinto.

La sincronización al arrancar

bot.start() y bot.run() sincronizan antes de que se conecte el gateway. Para cada ámbito, global o un servidor, bostie obtiene los comandos registrados con sus traducciones, los compara campo por campo y, cuando son distintos, reemplaza los comandos del ámbito en una sola solicitud. El log dice commands up to date (global) o commands synced (global): added [...], removed [...], changed [...].

Un comando que Discord tiene pero que ya no defines se elimina, salvo que su tipo sea uno que nunca defines: un bot sin @bot.entry_point conserva el comando de punto de entrada que Discord creó para su actividad.

Bot(auto_sync=False) omite la sincronización. await bot.sync_commands() reemplaza ya mismo los comandos de todos los ámbitos, hayan cambiado o no, y devuelve un SyncResult por ámbito con los nombres agregados (added), eliminados (removed) y modificados (changed), y los ids que Discord les dio a los comandos slash.

Ámbitos

Un comando sin guilds es global: funciona en cada servidor que instaló la aplicación y, según lo permitan sus contexts, en los MD. guilds=[...] lo registra solo en esos servidores.

Mientras trabajas en un bot, registra todo en un servidor de pruebas:

import os

import bostie
from bostie import Context, Permissions

dev_guild = int(os.environ.get("DEV_GUILD", "0"))
bot = bostie.Bot(dev_guilds=[dev_guild] if dev_guild else [])


@bot.command
async def about(ctx: Context) -> None:
    """Explain how to use the bot."""
    sync = await bot.command_mention("sync")
    await ctx.respond(f"Admins can run {sync} after an update.")


@bot.command(default_permissions=Permissions.ADMINISTRATOR, ephemeral=True)
async def sync(ctx: Context) -> None:
    """Send the commands to Discord again."""
    results = await bot.sync_commands()
    changed = sum(len(r.added) + len(r.removed) + len(r.changed) for r in results)
    await ctx.respond(f"{changed} commands changed.")

Bot(dev_guilds=[...]) registra en esos servidores, en lugar de hacerlo de forma global, cada comando que no tiene guilds propios. El comando de punto de entrada siempre es global, así que la sincronización lo deja afuera y advierte.

Un nombre en varios ámbitos

import bostie
from bostie import Context

bot = bostie.Bot()
EVENTS_GUILD = 1335172792320131073


@bot.command(name="rules")
async def rules(ctx: Context) -> None:
    """Show the rules of the bot."""
    await ctx.respond("Be kind and stay on topic.")


@bot.command(name="rules", guilds=[EVENTS_GUILD])
async def event_rules(ctx: Context) -> None:
    """Show the rules of this server's events."""
    await ctx.respond("Sign up before the event starts.")

Varios comandos pueden compartir un nombre cuando ningún servidor tiene dos de ellos: un /rules global y un /rules para un servidor, o un /rules por servidor. Cada uno se registra en su propio ámbito, y una interacción ejecuta el comando registrado en su servidor o, si no hay, el global. En ese servidor Discord muestra tanto el comando global como el del servidor. Con dev_guilds, el comando propio de un servidor ocupa el lugar del global en ese servidor. Dos comandos con el mismo nombre cuyos servidores se superponen, o dos globales, lanzan CommandDefinitionError cuando se registra el segundo. Los comandos de usuario y de mensaje siguen las mismas reglas.

La sincronización solo toca los ámbitos que tienen comandos. Cuando quitas dev_guilds, los comandos se quedan en el servidor de pruebas junto a los nuevos globales; await bot.rest.commands.bulk_overwrite_guild(bot.application_id, guild_id, []) los borra.

Dónde se pueden usar los comandos

contexts e integration_types se aplican a los comandos globales. Defínelos en un comando, un router o el bot:

  • contexts: InteractionContextType.GUILD, BOT_DM (los MD del bot) y PRIVATE_CHANNEL (otros MD y MD grupales, para una aplicación instalada por un usuario).
  • integration_types: ApplicationIntegrationType.GUILD_INSTALL y USER_INSTALL.

Bot(contexts=[...], integration_types=[...]) es el valor por defecto de cada comando global que no define el suyo. Si no se definen en ningún lado, bostie no envía ninguno de los dos campos y Discord aplica sus valores por defecto; para integration_types, son los contextos de instalación configurados para la aplicación. Quitar un ajuste que habías definido cuenta como un cambio, así que la siguiente sincronización le vuelve a dar al comando los valores por defecto de Discord.

Menciones de comandos

await bot.command_mention("roles color set") devuelve </roles color set:ID>, que Discord muestra como un comando en el que se puede hacer clic. Usa los ids de la última sincronización, o se los pide a Discord una vez por ámbito. Con guild_id=, un comando registrado en ese servidor tiene prioridad sobre uno global con el mismo nombre. Un nombre desconocido lanza LookupError, y llamarlo antes de que el bot arranque lanza StateError. fmt.command(name, command_id) construye el mismo texto a partir de un id que ya tienes.

Traducciones

name_localizations y description_localizations en un comando, un grupo, un subcomando, una Option o una Choice traducen un texto. Una función de traducción los traduce todos:

import bostie
from bostie import Context, Locale

TRANSLATIONS = {
    Locale.FRENCH: {"greet": "saluer", "Greet someone": "Saluer quelqu'un"},
    Locale.GERMAN: {"greet": "grüßen", "Greet someone": "Jemanden grüßen"},
}


def translate(text: str, locale: Locale) -> str | None:
    return TRANSLATIONS.get(locale, {}).get(text)


bot = bostie.Bot(translate=translate)


@bot.command
async def greet(ctx: Context) -> None:
    """Greet someone."""
    await ctx.respond(f"Hello! Your client is set to {ctx.locale.value}.")

Bot(translate=function) llama a function(text, locale) durante la sincronización para cada nombre y cada descripción de cada comando, grupo, subcomando y opción, y para cada nombre de choice, una vez por Locale. Si devuelve None, o el texto sin cambios, no se agrega nada para ese idioma, y una traducción que escribiste tú tiene prioridad sobre la función. El handler ve el idioma del usuario en ctx.locale y el del servidor en ctx.interaction.guild_locale.

Cuando Discord rechaza los comandos

Una sincronización que Discord rechaza con un 400 lanza CommandSyncError. Su mensaje tiene una línea por problema, con el comando, la ruta de la opción y el mensaje de Discord, y error.problems los contiene como objetos SyncProblem. La mayoría de los problemas se detectan antes: los decoradores comprueban nombres, descripciones y límites cuando se ejecutan.