Spraxium logoSpraxium

Subcomandos y Grupos

Organiza comandos complejos con subcomandos y grupos de subcomandos. Aprende la jerarquía de tres niveles, el patrón de composición con decoradores y cómo los handlers enrutan hacia subcomandos específicos.

Jerarquía de comandos en Discord

Discord soporta una jerarquía de tres niveles para slash commands: comando, grupo de subcomandos y subcomando. Un comando raíz como /ping está en el nivel superior y acepta opciones directamente. Un comando como /config view usa subcomandos donde config es el padre y view es el subcomando. Un comando como /config moderation automod usa un grupo de subcomandos donde config es el padre, moderation es el grupo y automod es el subcomando dentro de ese grupo.

Esta jerarquía es aplicada por Discord a nivel API. Un comando que tiene subcomandos no puede aceptar también opciones a nivel raíz, porque Discord no permite mezclar opciones y subcomandos en el mismo nivel. De forma similar, un grupo de subcomandos solo puede contener subcomandos, no opciones directas. Spraxium refleja estas restricciones mediante su sistema de decoradores: @SlashSubcommand convierte métodos en subcomandos, @SlashSubcommandGroup convierte clases en contenedores de grupo y @SlashSubcommandGroups registra grupos en el comando padre. El framework valida estas relaciones en tiempo de registro y construye el payload JSON anidado correcto para la API de Discord.

Entender qué nivel usar depende de la complejidad del comando. Los comandos simples con pocas opciones usan opciones a nivel raíz. Los comandos con 2-5 acciones distintas usan subcomandos. Los comandos con muchas acciones que pueden agruparse lógicamente en categorías usan grupos de subcomandos.

Discord aplica límites estrictos de subcomandos

Un comando puede tener un máximo de 25 subcomandos en el nivel superior, y cada grupo de subcomandos también puede contener un máximo de 25 subcomandos. Planifica la jerarquía de comandos antes de registrar para evitar reconstruir el esquema después de alcanzar estos límites.

Subcomandos con @SlashSubcommand

Un subcomando se declara decorando un método de la clase @SlashCommand con @SlashSubcommand. Cada método decorado representa un subcomando en el selector de Discord. Las opciones de un subcomando se declaran sobre el mismo método usando decoradores @SlashOption, igual que las opciones raíz se declaran sobre el 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() {}
}

El decorador @SlashSubcommand acepta un objeto de metadatos con name, description y claves opcionales i18n. El nombre del método en la clase sirve como identificador interno que el framework usa durante la recolección de metadatos, pero la propiedad name en la configuración del decorador es lo que aparece en el selector de comandos de Discord. El cuerpo del método permanece vacío porque la lógica de ejecución vive en clases handler.

Cada subcomando obtiene su propia clase handler. El handler usa la opción sub en @SlashCommandHandler para especificar qué subcomando maneja.

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 con @SlashSubcommandGroup

Cuando un comando tiene muchos subcomandos, puedes organizarlos en grupos con nombre. Un grupo de subcomandos se declara como una clase separada decorada con @SlashSubcommandGroup. Dentro de la clase de grupo, los subcomandos se declaran con @SlashSubcommand exactamente igual que en la clase de comando. Luego la clase de grupo se registra en el comando padre usando @SlashSubcommandGroups.

La clase de grupo funciona como namespace para subcomandos. En el selector de Discord, el usuario ve el nombre del comando, luego el nombre del grupo y luego el nombre del subcomando: /config moderation automod. Las opciones se declaran en los métodos de subcomando dentro de la clase de grupo, no en la propia clase de grupo. Igual que @SlashSubcommand, @SlashSubcommandGroup también acepta una propiedad opcional i18n con la misma estructura SlashI18nKeys, permitiendo localizar nombre y descripción del grupo por locale de 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() {}
}

Mezclar subcomandos de nivel superior con grupos

Un único comando puede tener tanto subcomandos de nivel superior (declarados en la clase de comando) como grupos de subcomandos (registrados vía @SlashSubcommandGroups). En el ejemplo anterior, ConfigCommand tiene dos subcomandos de nivel superior (view y reset) más dos grupos (moderation y welcome). El SlashRegistrar lo maneja automáticamente: primero aplica los subcomandos de nivel superior desde la clase de comando y luego itera sobre las clases de grupo agregando cada grupo con sus subcomandos.

En el selector de Discord, el usuario ve los tres niveles: /config view, /config reset, /config moderation automod, /config moderation logging, /config welcome channel, /config welcome message. Los subcomandos de nivel superior aparecen sin prefijo de grupo.

Enrutamiento de handlers con group y sub

Los handlers para subcomandos agrupados usan tanto la opción sub como group en el decorador @SlashCommandHandler. El framework hace match de handlers comprobando nombre de comando, nombre de grupo y nombre de subcomando como una tripleta. Un handler con { sub: 'automod', group: 'moderation' } solo recibe interacciones para /config moderation automod y nunca para /config view o /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 en módulos para comandos complejos

Cuando un comando tiene múltiples handlers de subcomando, todos los handlers deben registrarse en el arreglo handlers del módulo. El arreglo commands solo necesita la clase de comando padre, no las clases de grupo. Las clases de grupo se descubren mediante metadatos @SlashSubcommandGroups en la clase de 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 {}

El cargador de módulos instancia cada handler mediante el contenedor DI y lo conecta al comando, subcomando y grupo para los que fue declarado.

Estructura de carpetas para jerarquías complejas

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

Clases de grupo como ModerationGroup y WelcomeGroup viven en el mismo archivo de comando que la clase de comando padre porque forman parte del esquema del comando, no de la lógica de ejecución. Los archivos handler se nombran según la ruta completa de subcomando que manejan, facilitando encontrar el handler para cualquier ruta de interacción específica.

Checklist de diseño para subcomandos y grupos

Usa subcomandos para acciones distintas bajo un mismo namespace

Cuando un comando agrupa operaciones relacionadas pero distintas (view, create, delete, update), usa subcomandos. Cada subcomando obtiene su propia clase handler con lógica enfocada. Evita usar un único handler raíz con ramificación condicional basada en el valor de una opción.

Usa grupos para categorizar conjuntos grandes de subcomandos

Cuando un comando tenga más de cinco subcomandos, considera organizarlos en grupos. Los grupos agregan un namespace visual en el selector de Discord y hacen el comando más fácil de navegar. Limita los grupos a categorías significativas en lugar de crear grupos con un solo subcomando.

Mantén clases de grupo en el archivo de comando

Las clases de grupo decoradas con @SlashSubcommandGroup son parte de la definición del esquema del comando. Mantenlas en el mismo archivo que la clase @SlashCommand para que toda la estructura del comando sea visible en un solo lugar. Si el archivo crece demasiado, evalúa si el comando está haciendo demasiado.

Haz match exacto del enrutamiento de handler

Un handler con { sub: 'automod', group: 'moderation' } solo coincide con interacciones donde coinciden tanto el grupo COMO el subcomando. Olvidar la propiedad group o escribirla distinto provoca que el handler nunca reciba interacciones. Usa go-to-definition del IDE para verificar que los strings de sub y group coincidan con los metadatos del decorador en la clase de comando.

Registra solo el comando padre en el módulo

El arreglo commands en @Module debe contener solo la clase @SlashCommand, no las clases de grupo. Las clases de grupo se descubren automáticamente mediante metadatos @SlashSubcommandGroups. Agregar clases de grupo al arreglo commands no tiene efecto y añade confusión.