Your first event handler
Run code when discord.js emits an event, like a member joining a server, with an event handler class that reads its payload.
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.
Instead, 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 emits 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, and a handler that never runs
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 you receive an event, 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 for the first occurrence. 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, for example, can stay in separate files. They run at the same time, and if one of them throws, the rest still run. Don't make one handler depend on another's result, since nothing guarantees the order. Throwing covers what seedcord does with the error.
Processing one event 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.
discord.js emits an event
│
├─ 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. every handler at the same time
│ ├─ 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 others still runAn interaction runs in a different order, with its one handler built before any middleware.