O que o pacote de webhook faz
O pacote @spraxium/webhook dá a qualquer provider do bot uma forma limpa de enviar mensagens e embeds para webhooks do Discord. Em vez de montar requisições HTTP manualmente ou administrar clients de webhook por conta própria, você registra webhooks nomeados uma vez na configuração do plugin e depois se refere a eles pelo nome de qualquer service da aplicação.
O pacote oferece dois estilos de entrega. O estilo declarativo gira em torno do decorator @Send aplicado a um método de service; o Spraxium intercepta o valor retornado e faz o dispatch automaticamente de acordo com o tipo. O estilo imperativo injeta WebhookService no construtor e permite chamar métodos como send, sendEmbed, sendAll e formatAndSend diretamente. Os dois estilos compartilham o mesmo motor de entrega e o mesmo registro de webhooks nomeados, então você pode misturar os dois sem problema na mesma aplicação.
Quando escolher entrega declarativa ou imperativa
Prefira o estilo declarativo quando a função inteira do método for produzir uma mensagem de webhook. Isso mantém o service compacto e deixa a intenção muito óbvia durante leitura e revisão.
Já o estilo imperativo faz mais sentido quando a entrega depende de lógica condicional, múltiplos destinos, broadcast seletivo ou decisões tomadas em runtime. Nesses cenários, chamadas explícitas ao WebhookService costumam ficar mais fáceis de entender e depurar.
Setup
Instale o pacote:
pnpm add @spraxium/webhookDepois importe WebhookModule no módulo raiz para que WebhookService fique injetável em toda a aplicação:
import { Module } from '@spraxium/common';
import { WebhookModule } from '@spraxium/webhook';
@Module({
imports: [WebhookModule],
})
export class AppModule {}Configuração
A configuração de webhook deve ficar em um arquivo dedicado, como config/webhook.config.ts, exportado como constante nomeada e consumido por spraxium.config.ts via o array plugins.
import { defineWebhook } from '@spraxium/webhook';
export const webhookConfig = defineWebhook({
webhooks: {
alerts: process.env.WEBHOOK_ALERTS ?? '',
logs: process.env.WEBHOOK_LOGS ?? '',
reports: process.env.WEBHOOK_REPORTS ?? '',
},
globalUsername: 'MyBot',
globalAvatarUrl: 'https://example.com/avatar.png',
onError: (name, error) => {
console.error(`Webhook "${name}" failed: ${error.message}`);
},
});import { defineConfig } from '@spraxium/core';
import { webhookConfig } from './config/webhook.config';
export default defineConfig((env) => ({
plugins: [webhookConfig],
}));As chaves do mapa webhooks são os nomes usados no restante da aplicação, como em send('alerts', ...), @Send('reports'), sendAll() e assim por diante. Cada valor é a URL do webhook do Discord correspondente àquele destino. Já globalUsername e globalAvatarUrl definem a identidade padrão para toda mensagem de saída, mas ambos podem ser sobrescritos por chamada com SendOptions.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| webhooks | Record<string, string> | Sim | Mapa nome do webhook to URL do webhook do Discord. Os nomes são os identificadores usados em toda chamada de envio. |
| globalUsername | string | Não | Sobrescreve o nome exibido em toda mensagem enviada. Passa por cima do nome padrão do próprio webhook. |
| globalAvatarUrl | string | Não | Sobrescreve o avatar usado em toda mensagem enviada. |
| onError | (name: string, err: Error) => void | Não | Chamado quando o envio de um webhook falha, em vez de lançar erro. Se omitido, os erros sobem como promises rejeitadas. |
Armazene URLs de webhook em variáveis de ambiente via @spraxium/env e valide com @IsDiscordWebhookUrl(). Nunca deixe essas URLs hardcoded no código-fonte.
API declarativa: @Send
O decorator @Send é o jeito mais direto de disparar uma mensagem. Aplique-o a um método de uma classe decorada com @WebhookSender e retorne um entre três tipos: string para conteúdo puro, EmbedBuilder para um embed único ou MessageCreateOptions para um payload bruto completo. Depois que o método termina, o Spraxium intercepta esse retorno e faz o dispatch para o webhook nomeado.
@WebhookSender é obrigatório na classe. Ele aplica @Injectable automaticamente e registra a classe no pipeline de interceptação de webhooks, então você não precisa adicionar @Injectable separadamente.
import { Send, WebhookSender } from '@spraxium/webhook';
import { EmbedBuilder } from 'discord.js';
@WebhookSender()
export class ReportsService {
// A plain string sent as message content
@Send('logs')
async buildActivityLog(action: string, userId: string): Promise<string> {
return `Activity: \`${action}\` by <@${userId}> at ${new Date().toISOString()}`;
}
// An EmbedBuilder dispatched as a single embed
@Send('alerts')
async buildErrorReport(error: Error): Promise<EmbedBuilder> {
return new EmbedBuilder()
.setTitle('Error Report')
.setDescription(`\`\`\`\n${error.message}\n\`\`\``)
.setColor(0xed4245)
.setTimestamp();
}
// A MessageCreateOptions object sent as a raw payload
@Send('reports')
async buildDailySummary(): Promise<{ content: string }> {
return { content: `Daily summary for ${new Date().toDateString()}: all systems nominal.` };
}
}O tipo de retorno determina o comportamento do dispatch:
| Tipo de retorno | Comportamento de envio |
|---|---|
| string | Enviado como campo de conteúdo da mensagem. |
| EmbedBuilder | Enviado como embed único dentro do array de embeds. |
| MessageCreateOptions | Enviado como payload bruto do Discord, com controle total sobre cada campo. |
| undefined ou null | Nada é enviado. O método executa normalmente, mas o interceptor pula o dispatch. |
O estilo declarativo funciona melhor quando um método corresponde claramente a uma única mensagem de saída. Se o método começa a bifurcar, tomar decisões e montar várias chamadas de webhook, esse costuma ser o ponto em que a API imperativa fica mais sustentável.
Registre a classe sender no array providers de qualquer módulo de feature:
import { Module } from '@spraxium/common';
import { ReportsService } from './reports.service';
@Module({
providers: [ReportsService],
})
export class ReportsModule {}API imperativa: WebhookService
Injete WebhookService pelo construtor de qualquer provider quando você precisar de controle programático total. Esse estilo é ideal quando o conteúdo depende de branching, quando há broadcast condicional ou quando o disparo precisa acontecer em hooks de lifecycle como onReady.
import { Injectable } from '@spraxium/common';
import type { SpraxiumOnReady } from '@spraxium/common';
import { WebhookService } from '@spraxium/webhook';
import { EmbedBuilder } from 'discord.js';
@Injectable()
export class NotificationsService implements SpraxiumOnReady {
constructor(private readonly webhook: WebhookService) {}
async onReady(): Promise<void> {
await this.webhook.send('logs', 'Bot is now online and ready.');
const embed = new EmbedBuilder()
.setTitle('Bot Online')
.setDescription('All systems operational.')
.setColor(0x57f287)
.setTimestamp();
await this.webhook.sendEmbed('alerts', embed);
await this.webhook.sendAll('Startup complete.');
}
async notifyGuildJoin(guildName: string, memberCount: number): Promise<void> {
await this.webhook.formatAndSend(
'logs',
'Bot joined guild **{{guildName}}** ({{memberCount}} members).',
{ guildName, memberCount: String(memberCount) },
);
}
async broadcastAlert(message: string): Promise<void> {
await this.webhook.sendMany(['alerts', 'logs'], message);
}
async sendStatusReport(stats: { guilds: number; users: number; ping: number }): Promise<void> {
const guildsEmbed = new EmbedBuilder()
.setTitle('Guild Stats')
.addFields({ name: 'Total Guilds', value: String(stats.guilds), inline: true })
.setColor(0x5865f2);
const pingEmbed = new EmbedBuilder()
.setTitle('Performance')
.addFields(
{ name: 'Users', value: String(stats.users), inline: true },
{ name: 'WS Ping', value: `${stats.ping} ms`, inline: true },
)
.setColor(0xfee75c);
await this.webhook.sendEmbeds('reports', [guildsEmbed, pingEmbed]);
}
}Referência dos métodos
| Método | Descrição |
|---|---|
| send(name, content, options?) | Envia uma mensagem de texto para o webhook nomeado. |
| sendEmbed(name, embed, options?) | Envia um único EmbedBuilder para o webhook nomeado. |
| sendEmbeds(name, embeds[], options?) | Envia até 10 embeds em uma única mensagem para o webhook nomeado. |
| sendMessage(name, message, options?) | Envia um payload bruto de MessageCreateOptions para o webhook nomeado. |
| sendMany(names[], content, options?) | Faz broadcast de texto puro para um subconjunto específico de webhooks nomeados em paralelo. |
| sendAll(content, options?) | Faz broadcast de texto puro para todos os webhooks registrados ao mesmo tempo. |
| formatAndSend(name, template, vars, options?) | Substitui placeholders usando o objeto vars e depois envia a string resultante. |
| format(template, vars) | Substitui placeholders e devolve a string interpolada sem enviar nada. |
| get(name) | Retorna a WebhookEntry do webhook nomeado, ou undefined se ele não estiver registrado. |
| isRegistered(name) | Retorna true se existir um webhook registrado com esse nome. |
| registered() | Retorna um array com todos os nomes de webhook registrados. |
Opções por chamada
Todo método de envio aceita um objeto SendOptions opcional como último argumento. Valores definidos ali sobrescrevem globalUsername e globalAvatarUrl da configuração do plugin apenas naquela chamada específica.
| Opção | Tipo | Descrição |
|---|---|---|
| username | string | Sobrescreve o nome exibido pelo webhook apenas nesse envio. |
| avatarURL | string | Sobrescreve o avatar do webhook apenas nesse envio. |
| threadId | string | Envia a mensagem para uma thread específica de fórum ou texto dentro do canal do webhook. |
Use esses overrides por chamada com moderação. Se praticamente toda mensagem precisa de username ou avatar customizado, normalmente é sinal de que você deveria separar esse destino em vários webhooks nomeados, em vez de sobrescrever sempre o mesmo webhook.
await this.webhook.send('alerts', 'Maintenance window starting in 5 minutes.', {
username: 'Maintenance Bot',
avatarURL: 'https://example.com/maintenance.png',
threadId: '1234567890123456789',
});Verificando registro em runtime
Use isRegistered para proteger uma chamada quando um webhook é opcional em alguns ambientes, e registered para inspecionar a lista completa de nomes configurados no boot ou numa rota de health check.
if (this.webhook.isRegistered('alerts')) {
await this.webhook.send('alerts', 'Service is up.');
}
// List all configured webhook names
console.log(this.webhook.registered()); // ['alerts', 'logs', 'reports']Modelo de confiabilidade no modelo atual
No modelo atual, o comportamento de entrega de webhook ficou intencionalmente explícito. Algumas chamadas falham de forma direta, enquanto chamadas de broadcast priorizam sucesso parcial e continuam enviando para os destinos restantes.
A recomendação é modelar seu fluxo usando essa semântica, em vez de tratar todos os métodos como se tivessem o mesmo comportamento de erro.
| Método | Comportamento de falha | Uso típico |
|---|---|---|
| send / sendEmbed / sendEmbeds / sendMessage | Rejeita em caso de falha (exceto quando onError do plugin trata o erro). | Destino único e crítico, onde o caller decide retry/fallback. |
| sendMany | Faz fan-out paralelo, registra falhas individuais e não interrompe os outros envios. | Notificações best-effort para múltiplos destinos. |
| sendAll | Delega para sendMany; emite warning quando não há webhooks registrados. | Mensagens operacionais no estilo broadcast. |
onError global versus try/catch no ponto de chamada
O callback onError do plugin é excelente para telemetria centralizada, alertas e logs estruturados.
Mesmo assim, try/catch no ponto de chamada continua importante para decisões de negócio, por
exemplo: falhar o comando atual, trocar o canal de destino ou enfileirar retry.
import { defineWebhook } from '@spraxium/webhook';
import { logger } from '@spraxium/logger';
const log = logger.child('WebhookErrors');
export const webhookConfig = defineWebhook({
webhooks: {
alerts: process.env.WEBHOOK_ALERTS ?? '',
audit: process.env.WEBHOOK_AUDIT ?? '',
},
onError: (name, error) => {
log.error(`Webhook \"${name}\" failed: ${error.message}`);
},
});Padrão de arquitetura recomendado
Em bots médios e grandes, uma abordagem robusta é separar o tráfego por responsabilidade em vez de por módulo de feature. Exemplo de divisão:
alerts: incidentes e falhas críticas.audit: trilha de auditoria de segurança/moderação.ops: eventos de lifecycle e snapshots operacionais.
Essa separação deixa os canais downstream mais limpos e facilita políticas distintas de retenção e permissão.
import { Injectable } from '@spraxium/common';
import { WebhookService } from '@spraxium/webhook';
@Injectable()
export class OpsWebhookService {
constructor(private readonly webhook: WebhookService) {}
async reportIncident(summary: string): Promise<void> {
await this.webhook.send('alerts', `INCIDENT: ${summary}`);
}
async appendAudit(entry: string): Promise<void> {
await this.webhook.send('audit', entry);
}
async announceLifecycle(eventName: string): Promise<void> {
await this.webhook.send('ops', `Lifecycle event: ${eventName}`);
}
}Checklist de segurança e operação
Para projetos em produção, valide esta checklist antes de produção:
- Guarde URLs de webhook em variáveis de ambiente e nunca comite essas URLs.
- Valide os valores no boot (
@spraxium/env) para falhar cedo em caso de URL inválida. - Mantenha nomes de destino estáveis (
alerts,audit,ops) para evitar drift de strings. - Defina claramente onde best-effort é aceitável (
sendMany/sendAll) e onde não é. - Envie erros de
onErrorpara seu pipeline central de observabilidade. - Evite despejar markdown não tratado de usuário em canais privilegiados de auditoria.