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_CHANNELno 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
},
})
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
type | Chega como | Observação |
|---|---|---|
string | string | padrão |
integer | number | recusa fracionário |
number | number | aceita fracionário |
boolean | boolean | sim/não, true/false, 1/0 |
user | User | null | resolve @usuario pelo cache de usuários |
channel | Channel | null | resolve #canal pelo nome |
rest | string | engole o resto da linha |
rest é o tipo para URL e fraseArgumentos 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
| Decorator | Injeta |
|---|---|
@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}`);
}
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 })
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:
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
| Membro | O que é |
|---|---|
ctx.client | O Client |
ctx.command | O SlashCommandMeta do decorator |
ctx.message | A mensagem que disparou |
ctx.channel, ctx.channelId | Onde foi chamado |
ctx.server, ctx.serverId | Nulos em DM |
ctx.author, ctx.authorId | Quem chamou |
ctx.member | O membro, com cargos e permissões |
ctx.args | Argumentos separados, respeitando aspas |
ctx.rest | A linha crua depois do nome |
ctx.options | Opções declaradas, convertidas |
ctx.interactionId | Preenchido 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çãoComo 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();