Your first event handler
Run code when a discord.js event fires, with an event handler class. Covers registering an event, reading its payload, where handler files go, intents, running a handler once, and several handlers for one event.
A member joins, someone posts a message, someone edits it. Each one is a discord.js event. With plain discord.js you'd call client.on for each event in a startup file. That file grows with every listener you add. When an async listener throws, nothing catches the rejection unless you wrap every listener yourself.
seedcord gives each listener its own class. It finds those classes in your events folder, attaches one client.on per event, and catches a throw from each handler separately.
import { EventHandler, RegisterEvent } from '@seedcord/gateway';
import { Events } from 'discord.js';
@RegisterEvent([Events.GuildMemberAdd])
export class Welcome extends EventHandler<Events.GuildMemberAdd> {
public async execute(): Promise<void> {
const [member] = this.event;
await member.guild.systemChannel?.send(
`${member} just joined.`
);
}
}@RegisterEvent takes the event in a tuple, here Events.GuildMemberAdd. You also write the event in the generic on EventHandler. If the two differ, the class fails to compile.
If you pass Events.InteractionCreate in either place, it also fails to compile. Every interaction kind has its own handler base. Commands and Components cover them.
Gateway only
Discord delivers server events down the gateway websocket. An http bot only receives interactions, so EventHandler only ships on @seedcord/gateway.
Reading the payload
discord.js emits a tuple for each event. this.event is that tuple. guildMemberAdd emits one member. messageUpdate emits two, the message before the edit and the message after it.
@RegisterEvent([Events.MessageUpdate])
export class EditLog extends EventHandler<Events.MessageUpdate> {
public async execute(): Promise<void> {
const [before, after] = this.event;
if (before.content === after.content) return;
this.logger.info(
`${after.author.username} edited a message`
);
}
}If the content didn't change, EditLog returns early. discord.js also fires messageUpdate when an embed loads on a message.
EditLog only writes a log line. To respond in Discord, you use the objects in this.event, like after.reply(...) on a message. An event handler doesn't have this.reply, because Discord isn't waiting for a reply to an event.
The log line goes through this.logger, which writes to the events log channel. Tooling lists the channels and the levels.
Where to put handlers
seedcord scans the folder you set in bot.events.path at startup and registers every handler class it finds. The scan reaches every subfolder, so you can group handlers by feature.
import { resolve } from 'node:path';
import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';
export const seedcord = new Seedcord({
bot: {
clientOptions: {
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers
]
},
interactions: {
path: resolve(import.meta.dirname, './handlers')
},
commands: {
path: resolve(import.meta.dirname, './commands')
},
events: { path: resolve(import.meta.dirname, './events') }
},
subscribers: { path: null }
});If your bot has no event handlers, set events: { path: null }. seedcord then skips the scan.
Intents
Discord sends an event to your bot only if the bot asked for it with an intent. You list intents in clientOptions.intents. The bot.ts sample lists GuildMembers because the Welcome handler at the top of the page listens for member joins. Without that intent, Welcome never runs.
GuildMembers is a privileged intent, so it also needs its switch turned on in the developer portal.
If you created your bot with create-seedcord, it asked what your bot should react to. It then wrote the events folder and the matching intents into bot.ts.
Running once
Some work belongs to the first time an event fires, like logging which account your bot connected as. The tuple takes an options object as its second entry. Set frequency: 'once' to run the handler only the first time the event fires. It won't run again until your bot restarts.
@RegisterEvent([Events.ClientReady, { frequency: 'once' }])
export class Boot extends EventHandler<Events.ClientReady> {
public async execute(): Promise<void> {
const [client] = this.event;
this.logger.info(`connected as ${client.user.username}`);
}
}Without the options entry, frequency is 'on' and the handler runs every time.
Warning
If you forget @RegisterEvent on a handler class, that handler never runs. seedcord attaches listeners only for the events you registered, so nothing calls the class and nothing warns you at runtime. To catch it while you write the class, seedcord's event-handler-missing-register-event lint rule flags the missing decorator. If you scaffolded your project, that rule is already on.
Several handlers for one event
Several handlers can register the same event, so a welcome message and a join log can stay in separate files. They run one after another, in the order seedcord loaded their files. If one of them throws, the rest still run. Throwing covers what seedcord does with the error.
One event firing, with every handler registered for it, is an event dispatch. The event middleware for that event runs once per event dispatch, ahead of all of those handlers. Each event dispatch runs in this order.
an event fires
│
├─ 1. seedcord lists the event's handlers leaves out a once handler that already ran
├─ 2. each event middleware's execute() lowest priority first
├─ 3. for each handler, in load order
│ ├─ seedcord builds the handler your constructor runs here
│ ├─ the handler's gates checks, then commits
│ └─ the handler's execute()
└─ 4. each middleware's after() reverse of step 2, every time
if step 1 leaves no handlers, steps 2 to 4 don't run
a throw in 2 skips straight to 4
a throw in 3 stops that handler, and the next handler still runsAn interaction runs in a different order, with its one handler built before any middleware.