Spraxium logoSpraxium

Excepciones e integración de guards

Los listeners tienen dos rutas de ejecución según la forma del payload del evento. Entiende cómo funcionan guards y excepciones dentro de listeners y cuándo aplican.

Dos rutas de ejecución

No todos los eventos de Discord son iguales desde la perspectiva del framework. Algunos eventos llevan un mensaje o una interacción como su primer argumento, y otros eventos llevan otros tipos de payload como un objeto Client, un GuildMember o un canal. Esta distinción importa porque el framework la usa para decidir cómo ejecutar tu método listener y si aplicar el pipeline de guards y excepciones.

Cuando el primer argumento del evento es un BaseInteraction o un Message, el framework ejecuta el método a través del pipeline completo. Esto significa que los guards a nivel de clase y método se ejecutan primero y, si el método lanza una excepción, el pipeline de excepciones la maneja con renderizado de layout y comportamiento de respuesta opcional. Este es el mismo pipeline que usan slash y prefix commands, por eso los guards y layouts de excepción funcionan sin fricción en listeners de mensajes e interacciones.

Cuando el primer argumento es cualquier otra cosa, como el objeto Client para ClientReady o un GuildMember para GuildMemberAdd, el framework llama al método directamente sin el pipeline de guards ni de excepciones. Si el método lanza un error, el framework lo captura y lo registra como error no manejado del listener, pero no hay renderizado de layout ni comportamiento de respuesta porque ese tipo de payload no permite responder como lo hacen mensajes e interacciones.

Tipo del primer argumentoRuta de ejecuciónManejo de errores
BaseInteraction o MessagePipeline completo con guards y manejador de excepciones

Layouts de excepción, comportamiento de respuesta y manejo estructurado

Cualquier otro tipoInvocación directa del métodoError capturado y registrado, sin capacidad de respuesta

Este detalle es importante cuando diseñas tus listeners. Si adjuntas guards a un listener que maneja GuildMemberAdd, esos guards no se ejecutarán porque el primer argumento es un GuildMember, no un BaseInteraction o Message. Los guards solo se activan cuando el framework puede construir un ExecutionContext desde el payload del evento, lo que requiere una interacción o un mensaje.

Uso de guards en listeners

Para eventos que pasan por el pipeline completo, puedes adjuntar guards a nivel de clase o método con @UseGuards, exactamente igual que en un command handler. Los guards se ejecutan antes de que se ejecute el método listener y, si alguno deniega, el método se omite y la denegación pasa por el pipeline de excepciones.

protected-message.listener.ts
import { Events, Listener, On, UseGuards, withOptions } from '@spraxium/common';
import { GuildOnly, RoleGuard } from '@spraxium/core';
import type { Message } from 'discord.js';

@Listener(Events.MessageCreate)
@UseGuards(GuildOnly, withOptions(RoleGuard, { roles: ['Verified'] }))
export class ProtectedMessageListener {
  @On()
  async onMessage(message: Message): Promise<void> {
    if (message.author.bot) return;
    await message.reply('You are verified and in a server.');
  }
}

En este ejemplo, tanto GuildOnly como RoleGuard se ejecutan antes de onMessage. Si el mensaje viene de un DM, GuildOnly deniega y el método nunca se ejecuta. Si el miembro no tiene el rol "Verified", RoleGuard deniega. Solo cuando ambos guards pasan se ejecuta el método.

También puedes usar guards a nivel de método para control más fino cuando una clase listener tiene múltiples métodos handler. Los guards de clase aplican a todos los métodos, y los guards de método agregan restricciones extra solo a métodos concretos.

Lanzar excepciones desde listeners

Para eventos en el pipeline completo, puedes lanzar excepciones de dominio desde métodos listener y la capa de excepciones las maneja igual que las excepciones de comandos. Esto significa que tus layouts, comportamiento de respuesta y configuración de logs aplican a excepciones de listeners sin configuración adicional.

account-policy.listener.ts
import { Events, Listener, On } from '@spraxium/common';
import type { Message } from 'discord.js';
import { SpraxiumException } from '@spraxium/core';

class AccountSuspendedException extends SpraxiumException {
  constructor(props: { reason: string; userId: string }) {
    super({
      code: 'ACCOUNT_SUSPENDED',
      message: 'Your account has been suspended: {{reason}}.',
      props,
    });
  }
}

@Listener(Events.MessageCreate)
export class AccountPolicyListener {
  @On()
  async onMessage(message: Message): Promise<void> {
    if (message.author.bot) return;

    const isSuspended = await this.checkSuspension(message.author.id);
    if (isSuspended) {
      throw new AccountSuspendedException({
        reason: 'Policy violation',
        userId: message.author.id,
      });
    }
  }

  private async checkSuspension(userId: string): Promise<boolean> {
    // replace with actual database lookup
    return false;
  }
}

Para eventos que pasan por invocación directa en lugar de pipeline completo, los errores lanzados se capturan y registran, pero no se procesan mediante la capa de excepciones. Eso significa que no tienes renderizado de layouts ni respuestas estructuradas para esos eventos. Si necesitas manejo de errores confiable en eventos de invocación directa como GuildMemberAdd, maneja errores explícitamente dentro del método con try-catch y logging propio.

member-join.listener.ts
import { Events, Listener, On } from '@spraxium/common';
import { Logger } from '@spraxium/core';
import type { GuildMember } from 'discord.js';

@Listener(Events.GuildMemberAdd)
export class MemberJoinListener {
  private readonly logger = new Logger(MemberJoinListener.name);

  @On()
  async onJoin(member: GuildMember): Promise<void> {
    try {
      const channel = await member.guild.channels.fetch('1234567890');
      if (channel?.isTextBased()) {
        await channel.send(`Welcome to the server, ${member}!`);
      }
    } catch (error) {
      this.logger.error(
        `Failed to send welcome message for ${member.user.username}`,
        error,
      );
    }
  }
}

Los guards solo funcionan en eventos de mensaje e interacción

Adjuntar guards a un listener que maneja eventos como ClientReady o GuildMemberAdd no tiene efecto porque el framework no puede construir un ExecutionContext desde esos payloads de evento. Usa checks explícitos dentro del método en su lugar.

Página siguiente

Continúa con Descripción general de comandos prefix.