Spraxium logoSpraxium

Subcomandos e Grupos

Organize comandos complexos com subcomandos e grupos de subcomandos. Aprenda a hierarquia de três níveis, o padrão de composição com decorators e como os handlers são roteados para subcomandos específicos.

Hierarquia de comandos no Discord

O Discord suporta uma hierarquia de três níveis para slash commands: comando, grupo de subcomandos e subcomando. Um comando raiz como /ping fica no nível mais alto e aceita opções diretamente. Um comando como /config view usa subcomandos, em que config é o pai e view é o subcomando. Um comando como /config moderation automod usa um grupo de subcomandos, em que config é o pai, moderation é o grupo e automod é o subcomando dentro desse grupo.

Essa hierarquia é imposta pelo Discord no nível da API. Um comando que possui subcomandos não pode aceitar também opções no nível raiz, porque o Discord não permite misturar opções e subcomandos no mesmo nível. Da mesma forma, um grupo de subcomandos só pode conter subcomandos, não opções diretas. O Spraxium espelha essas restrições no sistema de decorators: @SlashSubcommand transforma métodos em subcomandos, @SlashSubcommandGroup transforma classes em contêineres de grupo e @SlashSubcommandGroups registra os grupos no comando pai. O framework valida essas relações no momento do registro e monta o JSON aninhado correto para a API do Discord.

Entender qual nível usar depende da complexidade do comando. Comandos simples, com poucas opções, usam opções no nível raiz. Comandos com de 2 a 5 ações distintas usam subcomandos. Comandos com muitas ações que podem ser agrupadas de forma lógica em categorias usam grupos de subcomandos.

O Discord impõe limites rígidos para subcomandos

Um comando pode ter no máximo 25 subcomandos no nível superior, e cada grupo de subcomandos também pode conter no máximo 25 subcomandos. Planeje a hierarquia do comando antes do registro para evitar ter de reconstruir o schema depois de atingir esses limites.

Subcomandos com @SlashSubcommand

Um subcomando é declarado ao decorar um método da classe @SlashCommand com @SlashSubcommand. Cada método decorado representa um subcomando no seletor do Discord. As opções do subcomando são declaradas nesse mesmo método com decorators @SlashOption, do mesmo jeito que opções de raiz são declaradas no método build().

commands/user.command.ts
import { SlashCommand, SlashOption, SlashSubcommand } from '@spraxium/common';

@SlashCommand({ name: 'user', description: 'User utilities.' })
export class UserCommand {
  @SlashOption.User('target', {
    description: 'The user to look up',
    required: true,
  })
  @SlashSubcommand({ name: 'info', description: 'Show user info' })
  info() {}

  @SlashOption.User('target', {
    description: 'The user to get the avatar of',
    required: false,
  })
  @SlashSubcommand({ name: 'avatar', description: 'Show user avatar' })
  avatar() {}
}

O decorator @SlashSubcommand aceita um objeto de metadados com name, description e chaves i18n opcionais. O nome do método na classe serve como identificador interno, usado pelo framework durante a coleta de metadados, mas a propriedade name na configuração do decorator é o que aparece no seletor de comandos do Discord. O corpo do método continua vazio, porque a lógica de execução vive nas classes de handler.

Cada subcomando recebe sua própria classe de handler. O handler usa a opção sub em @SlashCommandHandler para especificar qual subcomando ele trata.

import { SlashCommand, SlashOption, SlashSubcommand } from '@spraxium/common';

@SlashCommand({ name: 'user', description: 'User utilities.' })
export class UserCommand {
  @SlashOption.User('target', { description: 'The user to look up', required: true })
  @SlashSubcommand({ name: 'info', description: 'Show user info' })
  info() {}

  @SlashOption.User('target', { description: 'The user to get the avatar of', required: false })
  @SlashSubcommand({ name: 'avatar', description: 'Show user avatar' })
  avatar() {}
}

Grupos de subcomandos com @SlashSubcommandGroup

Quando um comando tem muitos subcomandos, você pode organizá-los em grupos nomeados. Um grupo de subcomandos é declarado como uma classe separada decorada com @SlashSubcommandGroup. Dentro da classe do grupo, os subcomandos são declarados com @SlashSubcommand exatamente como são na classe do comando. Depois, a classe do grupo é registrada no comando pai usando @SlashSubcommandGroups.

A classe do grupo funciona como um namespace para subcomandos. No seletor do Discord, o usuário vê o nome do comando, depois o nome do grupo e depois o nome do subcomando: /config moderation automod. As opções são declaradas nos métodos de subcomando dentro da classe do grupo, não na própria classe do grupo. Assim como @SlashSubcommand, @SlashSubcommandGroup também aceita uma propriedade i18n opcional com a mesma estrutura de SlashI18nKeys, permitindo localizar o nome e a descrição do grupo por locale do Discord.

commands/config.command.ts
import {
  SlashCommand,
  SlashOption,
  SlashSubcommand,
  SlashSubcommandGroup,
  SlashSubcommandGroups,
} from '@spraxium/common';
import { ChannelType } from 'discord.js';

@SlashSubcommandGroup({
  name: 'moderation',
  description: 'Moderation settings',
})
export class ModerationGroup {
  @SlashOption.Boolean('enabled', { description: 'Enable or disable automod', required: true })
  @SlashOption.String('filter', { description: 'Filter profile to apply', required: false })
  @SlashSubcommand({ name: 'automod', description: 'Configure automod filters' })
  automod() {}

  @SlashOption.Channel('channel', {
    description: 'Channel to send moderation logs to',
    required: true,
    channelTypes: [ChannelType.GuildText],
  })
  @SlashSubcommand({ name: 'logging', description: 'Configure moderation logging channel' })
  logging() {}
}

@SlashSubcommandGroup({
  name: 'welcome',
  description: 'Welcome system settings',
})
export class WelcomeGroup {
  @SlashOption.Channel('channel', {
    description: 'Channel to send welcome messages to',
    required: true,
    channelTypes: [ChannelType.GuildText],
  })
  @SlashSubcommand({ name: 'channel', description: 'Set the welcome channel' })
  setChannel() {}

  @SlashOption.String('template', {
    description: 'Welcome message template',
    required: true,
    maxLength: 2000,
  })
  @SlashSubcommand({ name: 'message', description: 'Set the welcome message template' })
  message() {}
}

@SlashSubcommandGroups([ModerationGroup, WelcomeGroup])
@SlashCommand({
  name: 'config',
  description: 'Server configuration commands.',
  defaultMemberPermissions: 0x20n,
})
export class ConfigCommand {
  @SlashSubcommand({ name: 'view', description: 'View the current server configuration' })
  view() {}

  @SlashSubcommand({ name: 'reset', description: 'Reset configuration to defaults' })
  reset() {}
}

Misturando subcomandos de topo com grupos

Um mesmo comando pode ter subcomandos de topo, declarados na classe do comando, e grupos de subcomandos, registrados via @SlashSubcommandGroups. No exemplo acima, ConfigCommand tem dois subcomandos de topo (view e reset) mais dois grupos (moderation e welcome). O SlashRegistrar lida com isso automaticamente: primeiro aplica os subcomandos de topo da classe do comando, depois percorre as classes de grupo e adiciona cada grupo com seus respectivos subcomandos.

No seletor do Discord, o usuário vê os três níveis: /config view, /config reset, /config moderation automod, /config moderation logging, /config welcome channel, /config welcome message. Os subcomandos de topo aparecem sem prefixo de grupo.

Roteamento de handlers com group e sub

Handlers de subcomandos agrupados usam tanto a opção sub quanto a group no decorator @SlashCommandHandler. O framework associa handlers verificando nome do comando, nome do grupo e nome do subcomando como uma trinca. Um handler com { sub: 'automod', group: 'moderation' } só recebe interações para /config moderation automod, e nunca para /config view ou /config welcome channel.

import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { EmbedBuilder, type ChatInputCommandInteraction } from 'discord.js';
import { ConfigCommand } from '../commands/config.command';

@SlashCommandHandler(ConfigCommand, { sub: 'view' })
export class ConfigViewHandler {
  async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
    const embed = new EmbedBuilder()
      .setColor(0x5865f2)
      .setTitle('Server Configuration')
      .addFields(
        { name: 'Prefix', value: '`!`', inline: true },
        { name: 'Language', value: 'en-US', inline: true },
      );

    await interaction.reply({ embeds: [embed] });

  }
}

Registro no módulo para comandos complexos

Quando um comando tem vários handlers de subcomando, todos os handlers precisam ser registrados no array handlers do módulo. O array commands só precisa da classe do comando pai, não das classes de grupo. Classes de grupo são descobertas por meio dos metadados @SlashSubcommandGroups na classe do comando.

modules/slash/slash.module.ts
import { Module } from '@spraxium/common';
import { ConfigCommand } from './commands/config.command';
import { ConfigAutomodHandler } from './handlers/config-automod.handler';
import { ConfigLoggingHandler } from './handlers/config-logging.handler';
import { ConfigResetHandler } from './handlers/config-reset.handler';
import { ConfigViewHandler } from './handlers/config-view.handler';
import { ConfigWelcomeChannelHandler } from './handlers/config-welcome-channel.handler';
import { ConfigWelcomeMessageHandler } from './handlers/config-welcome-message.handler';

@Module({
  commands: [ConfigCommand],
  handlers: [
    ConfigViewHandler,
    ConfigResetHandler,
    ConfigAutomodHandler,
    ConfigLoggingHandler,
    ConfigWelcomeChannelHandler,
    ConfigWelcomeMessageHandler,
  ],
})
export class SlashModule {}

O carregador de módulos instancia cada handler pelo container de DI e o conecta ao comando, ao subcomando e ao grupo para os quais ele foi declarado.

Estrutura de pastas para hierarquias complexas

project structure
config.command.ts
config-view.handler.ts
config-reset.handler.ts
config-automod.handler.ts
config-logging.handler.ts
config-welcome-channel.handler.ts
config-welcome-message.handler.ts
slash.module.ts

Classes de grupo como ModerationGroup e WelcomeGroup vivem no mesmo arquivo de comando que a classe do comando pai, porque fazem parte do schema do comando, não da lógica de execução. Arquivos de handler recebem nomes baseados no caminho completo do subcomando que tratam, o que facilita localizar o handler de qualquer rota de interação específica.

Checklist de design para subcomandos e grupos

Use subcomandos para ações distintas sob o mesmo namespace

Quando um comando agrupa operações relacionadas, mas diferentes, como ver, criar, excluir e atualizar, use subcomandos. Cada subcomando recebe sua própria classe de handler com lógica focada. Evite usar um único handler de raiz com ramificações condicionais baseadas no valor de uma opção.

Use grupos para categorizar conjuntos grandes de subcomandos

Quando um comando tiver mais de cinco subcomandos, considere organizá-los em grupos. Grupos adicionam um namespace visual no seletor do Discord e deixam o comando mais fácil de navegar. Limite os grupos a categorias realmente significativas, em vez de criar grupos com um único subcomando.

Mantenha classes de grupo no arquivo do comando

Classes de grupo decoradas com @SlashSubcommandGroup fazem parte da definição do schema do comando. Mantenha-as no mesmo arquivo da classe @SlashCommand para que a estrutura inteira do comando fique visível em um só lugar. Se o arquivo crescer demais, vale questionar se o comando está fazendo coisas demais.

Faça o roteamento do handler bater exatamente

Um handler com { sub: 'automod', group: 'moderation' } só corresponde a interações em que tanto o grupo quanto o subcomando batem. Esquecer a propriedade group ou escrevê-la diferente faz o handler nunca receber interações. Use go-to-definition da IDE para verificar se as strings sub e group batem com os metadados dos decorators na classe do comando.

Registre apenas o comando pai no módulo

O array commands em @Module deve conter apenas a classe @SlashCommand, não as classes de grupo. As classes de grupo são descobertas automaticamente pelos metadados de @SlashSubcommandGroups. Adicioná-las ao array commands não produz efeito e só gera confusão.