Pular para o conteúdo principal

Comandos

O comando é uma mensagem​

O gateway do Concord não tem evento de interação. Não existe interactionCreate; o seletor que o cliente abre ao digitar / só completa o texto, não chama o seu bot por outro canal. O @SlashCommand do nescord é despachado de messageCreate: quando o texto começa com o prefixo configurado, o despachante acha o comando pelo nome, converte as opções e chama o seu método.

Isso tem consequências práticas, e é melhor saber delas agora:

  • Todo mundo vê o comando no chat. Não há resposta efêmera.
  • O bot precisa de VIEW_CHANNEL no canal — sem isso ele nunca vê a mensagem.
  • O prefixo é só convenção. /, !, . — o que você configurar.
NescordModule.forRoot({
token: process.env.CONCORD_TOKEN,
commands: {
prefix: '/', // padrão
allowMention: true, // também aceita "@meubot ping"
ignoreBots: true, // bot respondendo bot dá laço
caseInsensitive: true,
unknownCommandReply: null, // null cala a boca em comando inexistente
},
})
Nomes reservados pelo servidor

Com o prefixo /, estes nomes nunca chegam ao seu bot:

play tocar pause pausar resume continuar skip pular stop parar fila queue volume sair entrar ajuda

O servidor os consome antes de reemitir o chat (src/community/music.rs, função websocket, chamada de src/ws.rs), em qualquer canal e sem depender de configuração nenhuma — o bot de música nativo do Concord fica com eles. Um @SlashCommand({ name: 'tocar' }) compila, registra e não responde nunca; e um ctx.reply() que espere o eco desses textos estoura TimeoutError.

Escolha outros nomes (/toca, /lista, /proxima, /encerra, /pausa, /retoma, /vol, /desconecta) ou outro prefixo: a interceptação casa o token inteiro com a barra, então prefix: '!' deixa !tocar passar.

O mais simples possível​

@SlashCommand({ name: 'ping', description: 'Mede a ida e volta até o gateway' })
async ping(@Ctx() [ctx]: SlashCommandContext) {
await ctx.reply(`pong em ${ctx.client.ping} ms`);
}

No chat: /ping.

Opções​

As opções são declaradas no decorator e chegam convertidas.

@SlashCommand({
// `volume` está na lista reservada do aviso acima: `vol` chega, `volume` não.
name: 'vol',
description: 'Ajusta o volume da música',
options: [
{ name: 'nivel', description: 'De 0 a 200 por cento', type: 'integer', required: true },
],
})
async vol(@Ctx() [ctx]: SlashCommandContext, @Opt('nivel') nivel: number) {
await ctx.reply(`volume em ${nivel}%`);
}

Tipos​

typeChega comoObservação
stringstringpadrão
integernumberrecusa fracionário
numbernumberaceita fracionário
booleanbooleansim/não, true/false, 1/0
userUser | nullresolve @usuario pelo cache de usuários
channelChannel | nullresolve #canal pelo nome
reststringengole o resto da linha
rest é o tipo para URL e frase

Argumentos são separados por espaço, respeitando "entre aspas". Uma URL com parâmetros, um nome de música, uma frase — tudo isso vira vários argumentos sem querer. rest resolve: ele leva o resto da linha inteiro, e por isso só pode ser a última opção.

@SlashCommand({
name: 'toca',
description: 'Toca um áudio na sala de voz',
options: [{ name: 'fonte', description: 'URL ou caminho do arquivo', type: 'rest', required: true }],
})
async toca(@Ctx() [ctx]: SlashCommandContext, @Opt('fonte') fonte: string) { /* … */ }

choices​

options: [
{ name: 'modo', description: 'Como repetir', type: 'string', choices: ['nao', 'faixa', 'fila'] },
]

Valor fora da lista é recusado antes de o método rodar.

Os quatro decorators de parâmetro​

DecoratorInjeta
@Ctx()A tupla do contexto — [ctx] em comando, [args…] em evento
@Options()Todas as opções já convertidas, como objeto
@Opt('nome')Uma opção pelo nome
@Rest()A linha crua depois do nome do comando
@SlashCommand({
name: 'buscar',
description: 'Busca e mostra a primeira faixa',
options: [
{ name: 'termo', description: 'O que buscar', type: 'rest', required: true },
],
})
async buscar(
@Ctx() [ctx]: SlashCommandContext,
@Options() opcoes: Record<string, unknown>,
@Rest() linha: string,
) {
// `opcoes.termo` e `linha` coincidem aqui, porque a única opção é 'rest'.
await ctx.reply(`buscando: ${linha}`);
}
Por que o contexto é uma tupla

O @Ctx() entrega exatamente os argumentos que o Client emitiu, como no Necord. Em evento isso importa (messageUpdate emite dois argumentos); em comando a tupla tem um elemento só, e o [ctx] desestrutura. Manter a mesma forma nos dois casos evita duas convenções no mesmo arquivo.

Guardas do próprio decorator​

permissions​

Exige os bits de quem chamou, no servidor do canal:

@SlashCommand({
name: 'limpar',
description: 'Apaga as últimas mensagens do canal',
permissions: [PermissionFlags.MANAGE_MESSAGES],
})
async limpar(@Ctx() [ctx]: SlashCommandContext) { /* … */ }

Falhou, o comando não roda. Detalhes de como a checagem resolve cargos e dono estão em Permissões.

serverOnly e ownerOnly​

serverOnly é true por padrão: recusa o comando fora de canal de servidor. ownerOnly é false por padrão e, quando ligado, só o dono do servidor passa.

cooldown​

Segundos entre chamadas por pessoa:

@SlashCommand({ name: 'proxima', description: 'Pula a faixa', cooldown: 3 })
O servidor também tem limites

60 mensagens por segundo por usuário no gateway. Um comando que responde várias mensagens de uma vez chega perto desse teto rápido. Prefira uma mensagem com várias linhas a várias mensagens.

aliases​

// `p` e `musica` são livres; `tocar` seria engolido pelo servidor, como o aviso lá em cima diz.
@SlashCommand({ name: 'toca', description: 'Toca um áudio', aliases: ['p', 'musica'] })

Guards​

Para regra que não cabe num campo do decorator, há guard — a mesma ideia do NestJS, com o contexto do comando:

src/na-voz.guard.ts
import { Injectable } from '@nestjs/common';
import type { CommandContext, NescordGuard } from 'nescord';

@Injectable()
export class NaVozGuard implements NescordGuard {
canActivate(ctx: CommandContext): boolean {
// Comando de música só faz sentido para quem está numa sala de voz.
return ctx.member?.voiceChannel != null;
}
}
@UseNescordGuards(NaVozGuard)
@SlashCommand({ name: 'toca', description: 'Toca um áudio' })
async toca(@Ctx() [ctx]: SlashCommandContext) { /* … */ }

O decorator funciona na classe também, e aí vale para todos os comandos dela.

O contexto​

MembroO que é
ctx.clientO Client
ctx.commandO SlashCommandMeta do decorator
ctx.messageA mensagem que disparou
ctx.channel, ctx.channelIdOnde foi chamado
ctx.server, ctx.serverIdNulos em DM
ctx.author, ctx.authorIdQuem chamou
ctx.memberO membro, com cargos e permissões
ctx.argsArgumentos separados, respeitando aspas
ctx.restA linha crua depois do nome
ctx.optionsOpções declaradas, convertidas
ctx.interactionIdPreenchido só quando a origem foi a caixa REST

Os métodos:

await ctx.reply('citando a mensagem do comando');
await ctx.send('sem citar');
await ctx.react('✅'); // "recebi" barato, não gasta mensagem
const conn = await ctx.joinVoice(); // entra no canal de voz de quem chamou
if (!ctx.hasPermission(PermissionFlags.BAN)) return;
react como confirmação

Como não há resposta efêmera, reagir à mensagem do comando é a forma menos invasiva de dizer "entendi". ctx.react('▶️') depois de começar a tocar, por exemplo, em vez de mais uma mensagem no canal.

Registro e /help​

Os comandos encontrados pelo explorer ficam num registro injetável:

import { Injectable } from '@nestjs/common';
import { NescordCommandRegistry, SlashCommand, Ctx, type SlashCommandContext } from 'nescord';

@Injectable()
export class HelpHandler {
constructor(private readonly registry: NescordCommandRegistry) {}

@SlashCommand({ name: 'help', description: 'Lista os comandos' })
async help(@Ctx() [ctx]: SlashCommandContext) {
const linhas = [...this.registry.commands.values()]
.map((c) => `\`/${c.name}\` — ${c.description}`)
.join('\n');
// Uma mensagem com várias linhas, e não várias mensagens: o servidor
// limita a 60 mensagens por segundo por usuário.
await ctx.reply(linhas);
}
}

O seletor do chat​

Ao digitar / no começo da mensagem, o Concord lista os comandos das aplicações instaladas no servidor — nome, opções e descrição. Com token de bot, o nescord anuncia a lista sozinho ao subir, a partir dos @SlashCommand() encontrados: é o registry.publish() chamando PUT /api/applications/@me/commands (veja Aplicações). Num Concord que ainda não tem a rota o bot só avisa no log e segue. Para anunciar de novo depois de uma mudança em tempo de execução, chame registry.publish() você mesmo.

A lista é da aplicação, não de cada servidor: o que você anuncia aparece em todos os servidores onde o bot está instalado.

A caixa REST de interações​

Quem tem credencial de instalação pode ligar uma segunda fonte de comandos. Ela é polling — o gateway não empurra interação — e é opcional:

NescordModule.forRoot({
token: process.env.CONCORD_TOKEN,
installToken: process.env.CONCORD_INSTALL_TOKEN,
commands: {
interactionInbox: { enabled: true, intervalMs: 2000 },
},
})

Com ela ligada, o SDK consulta GET /api/bots/interactions, despacha cada item para o comando de mesmo nome e responde por POST /api/bots/interactions/{id}/reply. Nesses casos ctx.interactionId vem preenchido.

Para espelhar a lista de comandos na instalação:

// Anuncia à aplicação (token de bot) e, se houver `installToken`, à instalação. Não falha.
await registry.publish();