Listeners
Event listeners allow your module to react to Discord events in real time. The module system automatically activates or deactivates listeners based on whether the module is enabled on the relevant guild.
Creating a Listener
// src/modules/greeter/listeners/message-create.listener.ts
import { declareEventListener } from "#lib/listener.js";
export default declareEventListener({
eventType: "messageCreate",
async execute(message, config) {
if (message.author.bot) return;
await message.reply("Hello!");
},
});Listener Interface
interface EventListener {
eventType: keyof ClientEvents; // Discord.js event name
execute: (...args) => Promise<void>;
}The execute function receives the standard Discord.js event arguments plus a ConfigProvider | undefined as the last parameter. The config is available when the event originates from a guild context; it's undefined for DM events.
Registering in the Module
// src/modules/greeter/greeter.module.ts
import messageListener from "./listeners/message-create.listener.js";
export default defineModule({
onLoad(_client, registry) {
registry.register(messageListener);
},
});Auto-Activation System
When you register a listener, the loader (listener-loader.ts) wraps it with automatic activation logic:
- The wrapper extracts
guildIdfrom the event arguments - It looks up the module's activation state in the database for that guild
- If the module is disabled, the listener returns immediately (no-op)
- If the module is enabled, it fetches the module's config and calls
execute()with the config as the last argument
This means you don't need to check module state or fetch config manually in most cases.
DM Edge Case
For events that can occur outside a guild context (e.g., messageCreate in DMs), there is no guildId to look up. In this case, the wrapper cannot determine activation state, so the listener is executed with config: undefined. Your listener should handle this:
async execute(message, config) {
// In DMs, config is undefined — handle gracefully
if (!config) {
if (!message.guild) return; // Ignore DMs
// Or handle DM case separately
}
}Discord Intents
Your module must declare the required gateway intents in defineModule(). These are aggregated at startup and passed to the Discord client.
export default defineModule({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});Additionally, privileged intents (GuildMembers, GuildPresences, MessageContent) must be enabled in the Discord Developer Portal under Bot > Privileged Gateway Intents. A module whose privileged intent is not granted is skipped at startup (see Intent Management).
Localization
Listener responses can use config.t() for localized strings. Provide translations in your module's i18n/ files:
async execute(message, config) {
if (!config) return;
await message.reply(config.t("welcomeMessage", { user: message.author.username }));
}The guild's locale is configured via /config core > locale. See Configuration → Localization for details.
Best Practices
- Filter early — check for bots, DMs, or irrelevant channels at the top of
execute() - Use services for complex event handling logic
- Be mindful of performance — high-volume events like
messageCreaterun on every message - Handle errors — uncaught errors in listeners are caught and logged by the loader
- Don't assume config is available — handle the
undefinedcase for DM events
Next Steps
- Configuration for managing module settings
- Database for persistent data storage
- Services for organizing business logic