Registrando comandos¶
O Discord só mostra um comando depois que o app o registrou. O bostie faz isso por você: quando o bot inicia, ele compara os comandos que você definiu com os que o Discord tem e só os envia se algo for diferente.
A sincronização na inicialização¶
bot.start() e bot.run() sincronizam antes de o gateway conectar. Para cada escopo, global ou um servidor, o bostie busca os comandos registrados com as suas traduções, compara-os campo a campo e substitui os comandos do escopo em uma única requisição quando eles diferem. O log diz commands up to date (global) ou commands synced (global): added [...], removed [...], changed [...].
Um comando que o Discord tem mas que você não define mais é removido, a menos que o tipo dele seja um que você nunca define: um bot sem @bot.entry_point mantém o comando de ponto de entrada que o Discord criou para a Atividade dele.
Bot(auto_sync=False) pula a sincronização. await bot.sync_commands() substitui agora os comandos de todos os escopos, tenham mudado ou não, e retorna um SyncResult por escopo com os nomes que ele adicionou (added), removeu (removed) e alterou (changed), e os ids que o Discord deu aos comandos de barra.
Escopos¶
Um comando sem guilds é global: ele funciona em todo servidor que instalou o app e, conforme os seus contexts permitirem, em mensagens diretas. guilds=[...] o registra só nesses servidores.
Enquanto você trabalha em um bot, registre tudo em um servidor de teste:
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 nesses servidores, em vez de globalmente, todo comando que não tem guilds próprio. O comando de ponto de entrada é sempre global, então a sincronização o deixa de fora e avisa.
Um nome em vários escopos¶
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.")
Comandos podem compartilhar um nome quando nenhum servidor tem dois deles: um /rules global e um /rules para um servidor, ou um /rules por servidor. Cada um é registrado no seu próprio escopo, e uma interação roda o comando registrado no servidor dela; se não houver, o global. Nesse servidor, o Discord lista tanto o comando global quanto o do servidor. Com dev_guilds, o comando próprio de um servidor toma o lugar do global nesse servidor. Dois comandos com o mesmo nome cujos servidores se sobrepõem, ou dois globais, levantam CommandDefinitionError quando o segundo é registrado. Comandos de usuário e de mensagem seguem as mesmas regras.
A sincronização só mexe em escopos que têm comandos. Quando você remove dev_guilds, os comandos continuam no servidor de teste ao lado dos novos globais; await bot.rest.commands.bulk_overwrite_guild(bot.application_id, guild_id, []) os apaga.
Onde os comandos podem ser usados¶
contexts e integration_types valem para comandos globais. Defina-os em um comando, em um router ou no bot:
contexts:InteractionContextType.GUILD,BOT_DM(as mensagens diretas do bot) ePRIVATE_CHANNEL(outras mensagens diretas e mensagens diretas em grupo, para um app instalado no usuário).integration_types:ApplicationIntegrationType.GUILD_INSTALLeUSER_INSTALL.
Bot(contexts=[...], integration_types=[...]) é o padrão para todo comando global que não define os seus. Se não forem definidos em lugar nenhum, o bostie não envia nenhum dos dois campos e o Discord aplica os seus padrões; para integration_types, isso são os contextos de instalação configurados para o app. Remover uma configuração que você tinha definido conta como mudança, então a próxima sincronização devolve ao comando os padrões do Discord.
Menções a comandos¶
await bot.command_mention("roles color set") retorna </roles color set:ID>, que o Discord mostra como um comando clicável. Ele usa os IDs da última sincronização, ou pergunta ao Discord uma vez por escopo. Com guild_id=, um comando registrado nesse servidor tem prioridade sobre um global de mesmo nome. Um nome desconhecido levanta LookupError, e chamá-lo antes de o bot iniciar levanta StateError. fmt.command(name, command_id) monta o mesmo texto a partir de um ID que você já tem.
Traduções¶
name_localizations e description_localizations em um comando, um grupo, um subcomando, uma Option ou um Choice traduzem um texto. Uma função de tradução traduz todos eles:
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) chama function(text, locale) durante a sincronização para cada nome e descrição de cada comando, grupo, subcomando e opção, e para cada nome de escolha, uma vez por Locale. Um retorno None, ou o texto sem mudança, não adiciona nada para esse idioma, e uma tradução que você mesmo escreveu tem prioridade sobre a função. O handler vê o idioma do usuário em ctx.locale e o do servidor em ctx.interaction.guild_locale.
Quando o Discord recusa os comandos¶
Uma sincronização que o Discord rejeita com 400 levanta CommandSyncError. A mensagem dela tem uma linha por problema, nomeando o comando, o caminho da opção e a mensagem do Discord, e error.problems os guarda como objetos SyncProblem. A maioria dos problemas é pega antes: os decoradores verificam nomes, descrições e limites quando rodam.