A classe do handler
Uma classe de handler é uma classe independente decorada com @SlashCommandHandler que contém a lógica de execução de um slash command. O decorator recebe a classe de definição do comando como primeiro argumento, criando um vínculo de metadados entre o schema e a lógica. A classe do handler é instanciada pelo container de DI durante o carregamento do módulo, então os parâmetros do construtor são resolvidos automaticamente a partir dos providers do módulo.
O handler precisa expor um método handle. Quando chega uma interação que corresponde ao nome do comando, e opcionalmente ao subcomando e ao grupo, o SlashInvoker chama esse método depois que o pipeline de guards passa. A lista de parâmetros de handle é montada dinamicamente a partir de metadados: @Ctx() injeta a interação bruta, decorators de opção injetam valores de opção já resolvidos e qualquer parâmetro sem decorator recebe undefined.
import { Ctx, SlashCommandHandler, SlashStringOption } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { PingCommand } from '../commands/ping.command';
@SlashCommandHandler(PingCommand)
export class PingHandler {
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashStringOption('service') service: string | null,
): Promise<void> {
const ws = interaction.client.ws.ping;
if (service) {
await interaction.reply(`Service: ${service}, Latency: ${ws}ms`);
} else {
await interaction.reply(`Pong! ${ws}ms`);
}
}
}O Spraxium exporta um tipo SlashContext em @spraxium/common que é um alias direto de ChatInputCommandInteraction. Importar de @spraxium/common reduz o acoplamento direto com discord.js nos arquivos de handler. Os dois tipos são intercambiáveis; use o caminho que fizer mais sentido para a organização de imports da sua equipe.
O decorator @SlashCommandHandler
O decorator @SlashCommandHandler aceita um argumento obrigatório e um opcional. O primeiro argumento é a classe decorada com @SlashCommand à qual esse handler pertence. O segundo é um objeto de opções com propriedades sub e group para roteamento de subcomandos específicos.
| Argumento | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| command | Construtor de classe | Sim | A classe decorada com @SlashCommand que esse handler executa. |
| options.sub | string | Não | O nome do subcomando ao qual esse handler responde. Omita em handlers de comando raiz. |
| options.group | string | Não | O nome do grupo de subcomandos ao qual esse handler responde. Deve ser usado junto com sub. |
A resolução de roteamento segue uma regra estrita de correspondência. Quando o binder recebe uma interação, ele extrai o nome do comando, o subcomando opcional e o grupo de subcomandos opcional do objeto de interação do Discord. Depois, o registry encontra o handler em que os três valores batem exatamente. Isso significa que um handler raiz, sem sub nem group, nunca recebe interações de subcomando, e um handler de subcomando nunca recebe interações de raiz.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { InfoCommand } from '../commands/info.command';
@SlashCommandHandler(InfoCommand)
export class InfoHandler {
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const ws = interaction.client.ws.ping;
await interaction.reply(`Bot latency: ${ws}ms`);
}
}Injeção de parâmetros: @Ctx e decorators de opção
Os parâmetros do método handle são preenchidos pelo SlashInvoker em tempo de execução. @Ctx() injeta a interação bruta do Discord. Decorators de opção injetam o valor resolvido de cada opção nomeada da interação. As posições dos parâmetros são totalmente independentes, então você pode colocar @Ctx em qualquer índice e misturá-lo com decorators de opção em qualquer ordem.
@SlashOption (genérico)
@SlashOption('name') resolve o valor da opção consultando o tipo da opção na classe de definição do comando em tempo de execução e então chama o getter correto do discord.js. Use isso quando você quiser uma injeção rápida sem precisar especificar o tipo no próprio decorator, porque o tipo é inferido a partir dos metadados do comando.
import { Ctx, SlashCommandHandler, SlashOption } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { EchoCommand } from '../commands/echo.command';
@SlashCommandHandler(EchoCommand)
export class EchoHandler {
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashOption('message') message: string,
): Promise<void> {
await interaction.reply(message);
}
}Decorators de parâmetro tipados
Nove decorators tipados de parâmetro embutem o tipo da opção diretamente nos metadados, deixando o código do handler autoexplicativo. A consulta à classe de definição do comando é ignorada, e o dispatcher já conhece o tipo da opção do Discord pelo próprio decorator. Prefira esses decorators em vez de @SlashOption quando o tipo da opção já for conhecido no momento da escrita.
| Decorator | Tipo do Discord | Valor em runtime |
|---|---|---|
| @SlashStringOption(name) | STRING | string ou null |
| @SlashIntegerOption(name) | INTEGER | number ou null |
| @SlashNumberOption(name) | NUMBER | number ou null |
| @SlashBooleanOption(name) | BOOLEAN | boolean ou null |
| @SlashUserOption(name) | USER | User ou null |
| @SlashChannelOption(name) | CHANNEL | GuildBasedChannel ou null |
| @SlashRoleOption(name) | ROLE | Role ou null |
| @SlashMentionableOption(name) | MENTIONABLE | User ou Role ou null |
| @SlashAttachmentOption(name) | ATTACHMENT | Attachment ou null |
import {
Ctx,
SlashCommandHandler,
SlashIntegerOption,
SlashStringOption,
SlashUserOption,
} from '@spraxium/common';
import type { ChatInputCommandInteraction, User } from 'discord.js';
import { BanCommand } from '../commands/ban.command';
@SlashCommandHandler(BanCommand)
export class BanHandler {
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashUserOption('target') target: User | null,
@SlashStringOption('reason') reason: string | null,
@SlashIntegerOption('days') days: number | null,
): Promise<void> {
if (!target) {
await interaction.reply({ content: 'No target provided.', flags: 'Ephemeral' });
return;
}
await interaction.reply(`Banned **${target.tag}**, reason: ${reason ?? 'none'}`);
}
}Nomes de decorator precisam bater exatamente com os nomes das opções
A string passada para qualquer decorator de opção precisa ser idêntica ao nome da opção declarada na definição do comando. Um desencontro faz o invoker falhar na busca e injetar null. Revise os nomes das opções ao renomeá-las, ou importe a classe do comando como referência de tipo para perceber esses desvios durante a revisão de código.
@SlashOpt (deprecated)
@SlashOpt está marcado como @deprecated e será removido em uma futura major version. Ele continua funcionando do mesmo jeito, então o código existente não quebra. Migre para @SlashOption ou para o decorator tipado apropriado no seu tempo.
// Before (deprecated)
@SlashOpt('message') message: string | null
// After generic
@SlashOption('message') message: string
// After typed (preferred)
@SlashStringOption('message') message: string | nullInjeção de dependência em handlers
Classes de handler são instanciadas pelo SpraxiumContainer, então participam integralmente do sistema de DI do framework. Você pode declarar parâmetros de construtor para qualquer provider registrado na árvore de módulos. O container resolve dependências de forma recursiva, então um handler pode depender de serviços que, por sua vez, dependem de outros serviços.
import { Ctx, SlashCommandHandler, SlashStringOption } from '@spraxium/common';
import { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { LocaleCommand } from '../commands/locale.command';
@SlashCommandHandler(LocaleCommand)
export class LocaleHandler {
constructor(private readonly i18n: I18nService) {}
async handle(
@Ctx() interaction: ChatInputCommandInteraction,
@SlashStringOption('language') language: string | null,
): Promise<void> {
if (!language || !this.i18n.has(language)) {
const msg = await this.i18n.tUser(
interaction.user.id,
'commands.locale.invalid',
{ available: this.i18n.locales().join(', ') },
);
await interaction.reply({ content: msg, flags: 'Ephemeral' });
return;
}
await this.i18n.setUserLocale(interaction.user.id, language);
const msg = this.i18n.t('commands.locale.set', language, { locale: language });
await interaction.reply({ content: msg, flags: 'Ephemeral' });
}
}Padrões de handler
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import type { ChatInputCommandInteraction } from 'discord.js';
import { SecretCommand } from '../commands/secret.command';
@SlashCommandHandler(SecretCommand)
export class SecretHandler {
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
await interaction.reply({
content: 'This message is only visible to you.',
flags: 'Ephemeral',
});
}
}Checklist de design para handlers
Use decorators tipados de opção
Prefira @SlashStringOption, @SlashUserOption e similares em vez do genérico @SlashOption quando o tipo da opção já for conhecido. Decorators tipados deixam o handler autoexplicativo e evitam a busca em runtime na classe de definição do comando.
Trate null em parâmetros opcionais
Quando uma opção do comando tiver required: false, o parâmetro do handler
será null se o usuário omitir a opção. Sempre tipifique parâmetros
opcionais como Type | null e verifique null antes de usar o valor.
Responda em até três segundos, ou use @Defer / @AutoDefer
O Discord exige uma resposta em até três segundos. Em handlers lentos, use
@Defer, que sempre faz defer, ou @AutoDefer, que só faz defer se o
limite for ultrapassado, em vez de chamar deferReply() manualmente. Veja
Respostas diferidas.
Mantenha classes de handler focadas
Cada handler implementa exatamente uma operação lógica. Quando um comando tiver subcomandos, use classes de handler separadas para cada subcomando em vez de ramificar tudo dentro de um único handler.
Use injeção pelo construtor para lógica compartilhada
Quando vários handlers compartilham lógica, como validação, formatação ou acesso ao banco, extraia isso para uma classe de serviço e injete-a pelo construtor. O container de DI cuida do ciclo de vida.