Spraxium logoSpraxium

Integração com Exceções e Guards

Listeners seguem dois caminhos de execução, dependendo do formato do payload do evento. Entenda como guards e exceções funcionam dentro de listeners e em que situações eles realmente se aplicam.

Dois caminhos de execução

Do ponto de vista do framework, nem todo evento do Discord é igual. Alguns eventos trazem uma mensagem ou uma interação como primeiro argumento. Outros carregam tipos diferentes de payload, como um objeto Client, um GuildMember ou um canal. Essa distinção importa porque é com base nela que o framework decide como executar o método do listener e se deve ou não aplicar o pipeline de guards e exceções.

Quando o primeiro argumento do evento é um BaseInteraction ou um Message, o framework executa o método pelo pipeline completo. Isso significa que guards de classe e de método rodam primeiro e, se o método lançar uma exceção, o pipeline de exceção entra em ação com renderização de layout e comportamento opcional de reply. É exatamente o mesmo pipeline usado por slash commands e comandos prefixados, por isso guards e layouts de exceção funcionam sem atrito em listeners de mensagem e interação.

Quando o primeiro argumento é qualquer outra coisa, como o objeto Client em ClientReady ou um GuildMember em GuildMemberAdd, o framework chama o método diretamente, sem passar pelo pipeline de guards e exceções. Se o método lançar erro, o framework captura e registra isso como erro não tratado de listener, mas não há renderização de layout nem reply estruturado, porque esse tipo de payload não oferece resposta do mesmo jeito que mensagens e interações.

Tipo do primeiro argumentoCaminho de execuçãoTratamento de erro
BaseInteraction ou MessagePipeline completo com guards e handler de exceção

Layouts de exceção, comportamento de reply e tratamento estruturado

Qualquer outro tipoInvocação direta do métodoErro é capturado e logado, sem capacidade de reply

Esse detalhe precisa ficar claro quando você desenha seus listeners. Se você anexar guards a um listener de GuildMemberAdd, eles não vão rodar, porque o primeiro argumento é um GuildMember, não um BaseInteraction nem um Message. Guards só entram em ação quando o framework consegue construir um ExecutionContext a partir do payload, o que exige mensagem ou interação.

Usando guards em listeners

Nos eventos que passam pelo pipeline completo, você pode anexar guards no nível da classe ou do método com @UseGuards, exatamente como faria em um command handler. Os guards rodam antes do método do listener e, se algum negar a execução, o método é pulado e a negação segue pelo pipeline de exceções.

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.');
  }
}

Nesse exemplo, tanto o guard GuildOnly quanto o RoleGuard rodam antes de onMessage. Se a mensagem vier de DM, GuildOnly nega e o método nem executa. Se o membro não tiver o cargo “Verified”, RoleGuard nega. Só quando os dois passam o método é chamado.

Você também pode usar guards no nível do método para ter controle mais fino quando uma classe de listener possui vários métodos handler. Guards de classe valem para todos os métodos, enquanto guards de método acrescentam restrições extras apenas nos pontos específicos onde forem aplicados.

Lançando exceções a partir de listeners

Nos eventos que passam pelo pipeline completo, você pode lançar exceções de domínio dentro do listener, e a camada de exceções vai tratá-las do mesmo jeito que trata exceções de comandos. Isso significa que layouts, comportamento de reply e configuração de logs se aplicam a exceções lançadas por listeners sem nenhum setup 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;
  }
}

Já nos eventos que seguem pela invocação direta, e não pelo pipeline completo, erros lançados são capturados e logados, mas não passam pela camada de exceção. Isso significa que você não recebe renderização de layout nem replies estruturados nesses casos. Se precisar de tratamento confiável em eventos como GuildMemberAdd, trate o erro explicitamente no próprio método com try-catch e o seu logging.

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,
      );
    }
  }
}

Guards só funcionam em eventos de mensagem e interação

Anexar guards a um listener que trata eventos como ClientReady ou GuildMemberAdd não produz efeito, porque o framework não consegue montar um ExecutionContext a partir desses payloads. Nesses casos, faça as validações explicitamente dentro do método.