Aller au contenu

Enregistrer les commandes

Discord n'affiche une commande qu'une fois que l'application l'a enregistrée. bostie s'en charge pour vous : au démarrage du bot, il compare les commandes que vous avez définies avec celles que Discord détient et ne les envoie que si quelque chose diffère.

La synchronisation au démarrage

bot.start() et bot.run() synchronisent avant que la gateway se connecte. Pour chaque portée, globale ou un serveur, bostie récupère les commandes enregistrées avec leurs localisations, les compare champ par champ, et remplace les commandes de la portée en une seule requête quand elles diffèrent. Le journal affiche commands up to date (global) ou commands synced (global): added [...], removed [...], changed [...].

Une commande que Discord détient mais que vous ne définissez plus est supprimée, sauf si son type est un type que vous ne définissez jamais : un bot sans @bot.entry_point garde la commande de point d'entrée que Discord a créée pour son Activité.

Bot(auto_sync=False) saute la synchronisation. await bot.sync_commands() remplace tout de suite les commandes de chaque portée, modifiées ou non, et renvoie un SyncResult par portée avec les noms added, removed et changed, et les ids que Discord a attribués aux commandes slash.

Portées

Une commande sans guilds est globale : elle fonctionne sur chaque serveur qui a installé l'application et, si ses contexts le permettent, en messages privés. guilds=[...] ne l'enregistre que sur ces serveurs.

Pendant que vous travaillez sur un bot, enregistrez tout sur un serveur de test :

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=[...]) enregistre chaque commande qui n'a pas ses propres guilds sur ces serveurs au lieu de l'enregistrer globalement. La commande de point d'entrée est toujours globale, donc la synchronisation l'omet et émet un avertissement.

Un même nom dans plusieurs portées

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

Des commandes peuvent partager un nom tant qu'aucun serveur n'en a deux : un /rules global et un /rules pour un serveur, ou un /rules par serveur. Chacune est enregistrée dans sa propre portée, et une interaction exécute la commande enregistrée sur son serveur, sinon la commande globale. Sur ce serveur, Discord liste à la fois la commande globale et celle du serveur. Avec dev_guilds, la commande propre à un serveur prend la place de la commande globale sur ce serveur. Deux commandes du même nom dont les serveurs se recoupent, ou deux commandes globales, lèvent CommandDefinitionError à l'enregistrement de la seconde. Les commandes utilisateur et de message suivent les mêmes règles.

La synchronisation ne touche que les portées qui ont des commandes. Quand vous retirez dev_guilds, les commandes restent sur le serveur de test à côté des nouvelles commandes globales ; await bot.rest.commands.bulk_overwrite_guild(bot.application_id, guild_id, []) les efface.

Où les commandes peuvent être utilisées

contexts et integration_types s'appliquent aux commandes globales. Réglez-les sur une commande, un routeur ou le bot :

  • contexts : InteractionContextType.GUILD, BOT_DM (les messages privés du bot) et PRIVATE_CHANNEL (les autres messages privés et les groupes privés, pour une application installée par l'utilisateur).
  • integration_types : ApplicationIntegrationType.GUILD_INSTALL et USER_INSTALL.

Bot(contexts=[...], integration_types=[...]) est la valeur par défaut de chaque commande globale qui ne règle pas la sienne. S'ils ne sont réglés nulle part, bostie n'envoie aucun des deux champs et Discord applique ses valeurs par défaut ; pour integration_types, ce sont les contextes d'installation configurés pour l'application. Retirer un réglage que vous aviez défini compte comme une modification, donc la synchronisation suivante redonne à la commande les valeurs par défaut de Discord.

Mentions de commandes

await bot.command_mention("roles color set") renvoie </roles color set:ID>, que Discord affiche comme une commande cliquable. Elle utilise les ids de la dernière synchronisation, ou interroge Discord une fois par portée. Avec guild_id=, une commande enregistrée sur ce serveur l'emporte sur une commande globale du même nom. Un nom inconnu lève LookupError, et un appel avant le démarrage du bot lève StateError. fmt.command(name, command_id) construit le même texte à partir d'un id que vous avez déjà.

Traductions

name_localizations et description_localizations sur une commande, un groupe, une sous-commande, une Option ou un Choice traduisent un texte. Une fonction de traduction les traduit tous :

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) appelle function(text, locale) pendant la synchronisation pour chaque nom et chaque description de chaque commande, groupe, sous-commande et option, et pour chaque nom de choix, une fois par Locale. Un retour None, ou le texte inchangé, n'ajoute rien pour cette locale, et une localisation que vous avez écrite vous-même l'emporte sur la fonction. Le gestionnaire voit la langue de l'utilisateur dans ctx.locale et celle du serveur dans ctx.interaction.guild_locale.

Quand Discord refuse les commandes

Une synchronisation que Discord rejette avec un 400 lève CommandSyncError. Son message a une ligne par problème, qui nomme la commande, le chemin de l'option et le message de Discord, et error.problems les contient sous forme d'objets SyncProblem. La plupart des problèmes sont détectés plus tôt : les décorateurs vérifient les noms, les descriptions et les limites à leur exécution.