A powerful Node.js library for interacting with the Discord API, enabling developers to create Discord bots and applications with full API coverage
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Comprehensive event system for Discord gateway events and collectors for gathering user interactions, messages, and reactions with filtering and time-based collection.
Core events emitted by the Discord client for real-time Discord activity.
/**
* Events emitted by the Discord client
*/
interface ClientEvents {
/** Emitted when the client becomes ready to start working */
ready: [client: Client<true>];
/** Emitted when a message is created */
messageCreate: [message: Message];
/** Emitted when a message is updated */
messageUpdate: [oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage];
/** Emitted when a message is deleted */
messageDelete: [message: Message | PartialMessage];
/** Emitted when messages are bulk deleted */
messageDeleteBulk: [messages: Collection<Snowflake, Message | PartialMessage>, channel: GuildTextBasedChannel];
/** Emitted when a reaction is added to a message */
messageReactionAdd: [reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser];
/** Emitted when a reaction is removed from a message */
messageReactionRemove: [reaction: MessageReaction | PartialMessageReaction, user: User | PartialUser];
/** Emitted when all reactions are removed from a message */
messageReactionRemoveAll: [message: Message | PartialMessage, reactions: Collection<string | Snowflake, MessageReaction>];
/** Emitted when reactions of a specific emoji are removed from a message */
messageReactionRemoveEmoji: [reaction: MessageReaction | PartialMessageReaction];
/** Emitted when an interaction is created */
interactionCreate: [interaction: Interaction];
/** Emitted when a guild becomes available */
guildCreate: [guild: Guild];
/** Emitted when a guild is updated */
guildUpdate: [oldGuild: Guild, newGuild: Guild];
/** Emitted when a guild becomes unavailable or the client leaves */
guildDelete: [guild: Guild];
/** Emitted when a user joins a guild */
guildMemberAdd: [member: GuildMember];
/** Emitted when a guild member is updated */
guildMemberUpdate: [oldMember: GuildMember | PartialGuildMember, newMember: GuildMember];
/** Emitted when a user leaves a guild */
guildMemberRemove: [member: GuildMember | PartialGuildMember];
/** Emitted when a guild role is created */
roleCreate: [role: Role];
/** Emitted when a guild role is updated */
roleUpdate: [oldRole: Role, newRole: Role];
/** Emitted when a guild role is deleted */
roleDelete: [role: Role];
/** Emitted when a channel is created */
channelCreate: [channel: NonThreadGuildBasedChannel];
/** Emitted when a channel is updated */
channelUpdate: [oldChannel: DMChannel | NonThreadGuildBasedChannel, newChannel: DMChannel | NonThreadGuildBasedChannel];
/** Emitted when a channel is deleted */
channelDelete: [channel: DMChannel | NonThreadGuildBasedChannel];
/** Emitted when a thread is created */
threadCreate: [thread: ThreadChannel, newlyCreated: boolean];
/** Emitted when a thread is updated */
threadUpdate: [oldThread: ThreadChannel, newThread: ThreadChannel];
/** Emitted when a thread is deleted */
threadDelete: [thread: ThreadChannel];
/** Emitted when a user starts typing */
typingStart: [typing: Typing];
/** Emitted when a user's presence is updated */
presenceUpdate: [oldPresence: Presence | null, newPresence: Presence];
/** Emitted when a voice state is updated */
voiceStateUpdate: [oldState: VoiceState, newState: VoiceState];
/** Emitted when an invite is created */
inviteCreate: [invite: Invite];
/** Emitted when an invite is deleted */
inviteDelete: [invite: Invite];
/** Emitted when a guild ban is added */
guildBanAdd: [ban: GuildBan];
/** Emitted when a guild ban is removed */
guildBanRemove: [ban: GuildBan];
/** Emitted when an emoji is created */
emojiCreate: [emoji: GuildEmoji];
/** Emitted when an emoji is updated */
emojiUpdate: [oldEmoji: GuildEmoji, newEmoji: GuildEmoji];
/** Emitted when an emoji is deleted */
emojiDelete: [emoji: GuildEmoji];
/** Emitted when a sticker is created */
stickerCreate: [sticker: Sticker];
/** Emitted when a sticker is updated */
stickerUpdate: [oldSticker: Sticker, newSticker: Sticker];
/** Emitted when a sticker is deleted */
stickerDelete: [sticker: Sticker];
/** Emitted when a scheduled event is created */
guildScheduledEventCreate: [guildScheduledEvent: GuildScheduledEvent];
/** Emitted when a scheduled event is updated */
guildScheduledEventUpdate: [oldGuildScheduledEvent: GuildScheduledEvent | null, newGuildScheduledEvent: GuildScheduledEvent];
/** Emitted when a scheduled event is deleted */
guildScheduledEventDelete: [guildScheduledEvent: GuildScheduledEvent];
/** Emitted when a user subscribes to a scheduled event */
guildScheduledEventUserAdd: [guildScheduledEvent: GuildScheduledEvent, user: User];
/** Emitted when a user unsubscribes from a scheduled event */
guildScheduledEventUserRemove: [guildScheduledEvent: GuildScheduledEvent, user: User];
/** Emitted for debug information */
debug: [message: string];
/** Emitted for warnings */
warn: [message: string];
/** Emitted when an error occurs */
error: [error: Error];
}
/**
* Event name constants
*/
const Events: {
Ready: 'ready';
MessageCreate: 'messageCreate';
MessageUpdate: 'messageUpdate';
MessageDelete: 'messageDelete';
MessageDeleteBulk: 'messageDeleteBulk';
MessageReactionAdd: 'messageReactionAdd';
MessageReactionRemove: 'messageReactionRemove';
MessageReactionRemoveAll: 'messageReactionRemoveAll';
MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji';
InteractionCreate: 'interactionCreate';
GuildCreate: 'guildCreate';
GuildUpdate: 'guildUpdate';
GuildDelete: 'guildDelete';
GuildMemberAdd: 'guildMemberAdd';
GuildMemberUpdate: 'guildMemberUpdate';
GuildMemberRemove: 'guildMemberRemove';
RoleCreate: 'roleCreate';
RoleUpdate: 'roleUpdate';
RoleDelete: 'roleDelete';
ChannelCreate: 'channelCreate';
ChannelUpdate: 'channelUpdate';
ChannelDelete: 'channelDelete';
ThreadCreate: 'threadCreate';
ThreadUpdate: 'threadUpdate';
ThreadDelete: 'threadDelete';
TypingStart: 'typingStart';
PresenceUpdate: 'presenceUpdate';
VoiceStateUpdate: 'voiceStateUpdate';
InviteCreate: 'inviteCreate';
InviteDelete: 'inviteDelete';
GuildBanAdd: 'guildBanAdd';
GuildBanRemove: 'guildBanRemove';
EmojiCreate: 'emojiCreate';
EmojiUpdate: 'emojiUpdate';
EmojiDelete: 'emojiDelete';
StickerCreate: 'stickerCreate';
StickerUpdate: 'stickerUpdate';
StickerDelete: 'stickerDelete';
GuildScheduledEventCreate: 'guildScheduledEventCreate';
GuildScheduledEventUpdate: 'guildScheduledEventUpdate';
GuildScheduledEventDelete: 'guildScheduledEventDelete';
GuildScheduledEventUserAdd: 'guildScheduledEventUserAdd';
GuildScheduledEventUserRemove: 'guildScheduledEventUserRemove';
Debug: 'debug';
Warn: 'warn';
Error: 'error';
};Usage Examples:
import { Client, Events, GatewayIntentBits } from 'discord.js';
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
// Basic event handling
client.on(Events.Ready, (client) => {
console.log(`Logged in as ${client.user.tag}!`);
});
client.on(Events.MessageCreate, (message) => {
if (message.author.bot) return;
if (message.content === '!ping') {
message.reply('Pong!');
}
});
// Guild member events
client.on(Events.GuildMemberAdd, (member) => {
const channel = member.guild.systemChannel;
if (channel) {
channel.send(`Welcome ${member} to ${member.guild.name}!`);
}
});
client.on(Events.GuildMemberRemove, (member) => {
console.log(`${member.user.tag} left ${member.guild.name}`);
});
// Reaction events
client.on(Events.MessageReactionAdd, async (reaction, user) => {
if (reaction.partial) {
try {
await reaction.fetch();
} catch (error) {
console.error('Something went wrong when fetching the message:', error);
return;
}
}
if (reaction.emoji.name === '⭐' && reaction.count >= 5) {
console.log('Message starred!');
}
});Base collector functionality for gathering events over time with filtering.
/**
* Abstract base class for collectors
* @template Key The key type for collected items
* @template Value The value type for collected items
* @template Args The arguments passed to event handlers
*/
abstract class Collector<Key, Value, Args extends unknown[] = []> extends EventEmitter {
constructor(client: Client, options?: CollectorOptions<[Key, Value, ...Args]>);
readonly client: Client;
readonly collected: Collection<Key, Value>;
readonly ended: boolean;
readonly endReason: string | null;
readonly filter: CollectorFilter<Args>;
readonly options: CollectorOptions<[Key, Value, ...Args]>;
// Event handlers
on(event: 'collect', listener: (value: Value, ...args: Args) => Awaitable<void>): this;
on(event: 'dispose', listener: (value: Value, ...args: Args) => Awaitable<void>): this;
on(event: 'end', listener: (collected: Collection<Key, Value>, reason: string) => Awaitable<void>): this;
// Collector control
stop(reason?: string): void;
resetTimer(options?: CollectorResetTimerOptions): void;
checkEnd(): boolean;
// Abstract methods
abstract collect(...args: Args): Key | Promise<Key> | null;
abstract dispose(...args: Args): Key | Promise<Key> | null;
// Static methods
static key(...args: unknown[]): unknown;
}
interface CollectorOptions<T extends unknown[]> {
filter?: CollectorFilter<T>;
time?: number;
idle?: number;
max?: number;
maxProcessed?: number;
dispose?: boolean;
}
type CollectorFilter<T extends unknown[]> = (...args: T) => boolean | Promise<boolean>;
interface CollectorResetTimerOptions {
time?: number;
idle?: number;
}
interface CollectorEvents<Key, Value> {
collect: [value: Value];
dispose: [value: Value];
end: [collected: Collection<Key, Value>, reason: string];
}Collect messages from a channel with filtering and time limits.
/**
* Collects messages from a channel
* @extends Collector
*/
class MessageCollector extends Collector<Snowflake, Message, [Collection<Snowflake, Message>]> {
constructor(channel: TextBasedChannel, options?: MessageCollectorOptions);
readonly channel: TextBasedChannel;
readonly received: number;
collect(message: Message): Snowflake | null;
dispose(message: Message): Snowflake | null;
static key(message: Message): Snowflake;
}
interface MessageCollectorOptions extends CollectorOptions<[Message]> {
max?: number;
maxProcessed?: number;
}
/**
* Await messages utility for simple message collection
*/
interface AwaitMessagesOptions extends MessageCollectorOptions {
errors?: string[];
}Usage Examples:
// Basic message collector
const filter = (message: Message) => message.author.id === interaction.user.id;
const collector = channel.createMessageCollector({
filter,
time: 15000, // 15 seconds
max: 1 // Collect only 1 message
});
collector.on('collect', (message) => {
console.log(`Collected: ${message.content}`);
});
collector.on('end', (collected, reason) => {
if (reason === 'time') {
channel.send('Time ran out!');
} else {
channel.send(`Collection ended: ${reason}`);
}
});
// Await messages (promise-based)
try {
const collected = await channel.awaitMessages({
filter: (msg) => msg.author.id === user.id,
max: 1,
time: 30000,
errors: ['time']
});
const message = collected.first();
await channel.send(`You said: ${message?.content}`);
} catch (error) {
await channel.send('No response received in time.');
}
// Advanced message collector with multiple filters
const advancedCollector = channel.createMessageCollector({
filter: (message) => {
// Only collect messages from non-bots
if (message.author.bot) return false;
// Only collect messages that start with specific prefix
if (!message.content.startsWith('!vote')) return false;
// Only collect from specific users
const allowedUsers = ['123456789012345678', '876543210987654321'];
return allowedUsers.includes(message.author.id);
},
time: 60000, // 1 minute
max: 10, // Maximum 10 messages
idle: 10000 // Stop if no messages for 10 seconds
});
advancedCollector.on('collect', (message) => {
console.log(`Vote collected from ${message.author.tag}: ${message.content}`);
});Collect interactions (buttons, select menus) from messages.
/**
* Collects interactions from message components
* @extends Collector
*/
class InteractionCollector extends Collector<Snowflake, Interaction, [Collection<Snowflake, Interaction>]> {
constructor(client: Client, options?: InteractionCollectorOptions);
readonly message: Message | null;
readonly messageId: Snowflake | null;
readonly messageInteractionId: Snowflake | null;
readonly channelId: Snowflake | null;
readonly guildId: Snowflake | null;
readonly interactionType: InteractionType | null;
readonly componentType: ComponentType | null;
readonly total: number;
readonly users: Collection<Snowflake, User>;
collect(interaction: Interaction): Snowflake | null;
dispose(interaction: Interaction): Snowflake | null;
empty(): void;
static key(interaction: Interaction): Snowflake;
}
interface InteractionCollectorOptions extends CollectorOptions<[Interaction]> {
message?: Message;
messageId?: Snowflake;
messageInteractionId?: Snowflake;
channel?: TextBasedChannel;
channelId?: Snowflake;
guild?: Guild;
guildId?: Snowflake;
interactionType?: InteractionType | InteractionType[];
componentType?: ComponentType | ComponentType[];
}
/**
* Await interactions utility for simple interaction collection
*/
interface AwaitInteractionsOptions extends InteractionCollectorOptions {
errors?: string[];
}Usage Examples:
// Button interaction collector
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder()
.setCustomId('yes')
.setLabel('Yes')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId('no')
.setLabel('No')
.setStyle(ButtonStyle.Danger)
);
const message = await channel.send({
content: 'Do you agree?',
components: [row]
});
const collector = message.createInteractionCollector({
componentType: ComponentType.Button,
time: 15000 // 15 seconds
});
collector.on('collect', async (interaction) => {
if (interaction.customId === 'yes') {
await interaction.reply('You agreed!');
} else if (interaction.customId === 'no') {
await interaction.reply('You disagreed!');
}
collector.stop('answered');
});
collector.on('end', async (collected, reason) => {
// Disable all buttons when collector ends
const disabledRow = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
ButtonBuilder.from(row.components[0]).setDisabled(true),
ButtonBuilder.from(row.components[1]).setDisabled(true)
);
await message.edit({ components: [disabledRow] });
if (reason === 'time') {
await message.followUp('Time ran out! No response received.');
}
});
// Select menu interaction collector
const selectCollector = message.createInteractionCollector({
componentType: ComponentType.StringSelect,
filter: (interaction) => interaction.user.id === targetUserId,
max: 1
});
selectCollector.on('collect', async (interaction) => {
const selectedValues = interaction.values;
await interaction.reply(`You selected: ${selectedValues.join(', ')}`);
});Collect reactions added to messages with emoji filtering.
/**
* Collects reactions on messages
* @extends Collector
*/
class ReactionCollector extends Collector<string | Snowflake, MessageReaction, [User]> {
constructor(message: Message, options?: ReactionCollectorOptions);
readonly message: Message;
readonly users: Collection<Snowflake, User>;
readonly total: number;
collect(reaction: MessageReaction, user: User): string | Snowflake | null;
dispose(reaction: MessageReaction, user: User): string | Snowflake | null;
empty(): void;
static key(reaction: MessageReaction): string | Snowflake;
}
interface ReactionCollectorOptions extends CollectorOptions<[MessageReaction, User]> {
max?: number;
maxEmojis?: number;
maxUsers?: number;
}
/**
* Await reactions utility for simple reaction collection
*/
interface AwaitReactionsOptions extends ReactionCollectorOptions {
errors?: string[];
}Usage Examples:
// Reaction-based poll
const pollMessage = await channel.send('React with 👍 for yes, 👎 for no');
await pollMessage.react('👍');
await pollMessage.react('👎');
const collector = pollMessage.createReactionCollector({
filter: (reaction, user) => {
return ['👍', '👎'].includes(reaction.emoji.name!) && !user.bot;
},
time: 60000 // 1 minute
});
collector.on('collect', (reaction, user) => {
console.log(`${user.tag} reacted with ${reaction.emoji.name}`);
});
collector.on('end', (collected) => {
const yesReaction = collected.get('👍');
const noReaction = collected.get('👎');
const yesCount = yesReaction ? yesReaction.count - 1 : 0; // -1 to exclude bot's reaction
const noCount = noReaction ? noReaction.count - 1 : 0;
channel.send(`Poll ended! Yes: ${yesCount}, No: ${noCount}`);
});
// Reaction role system
const reactionRoleMessage = await channel.send({
embeds: [
new EmbedBuilder()
.setTitle('Reaction Roles')
.setDescription('React to get roles!\n🎮 - Gamer\n🎵 - Music Lover\n📚 - Reader')
]
});
await reactionRoleMessage.react('🎮');
await reactionRoleMessage.react('🎵');
await reactionRoleMessage.react('📚');
const reactionRoleCollector = reactionRoleMessage.createReactionCollector({
dispose: true // Also listen for reaction removals
});
const roleMap = new Map([
['🎮', 'gamer-role-id'],
['🎵', 'music-role-id'],
['📚', 'reader-role-id']
]);
reactionRoleCollector.on('collect', async (reaction, user) => {
if (user.bot) return;
const roleId = roleMap.get(reaction.emoji.name!);
if (!roleId) return;
const member = await reaction.message.guild?.members.fetch(user.id);
const role = reaction.message.guild?.roles.cache.get(roleId);
if (member && role) {
await member.roles.add(role);
console.log(`Added ${role.name} role to ${user.tag}`);
}
});
reactionRoleCollector.on('dispose', async (reaction, user) => {
if (user.bot) return;
const roleId = roleMap.get(reaction.emoji.name!);
if (!roleId) return;
const member = await reaction.message.guild?.members.fetch(user.id);
const role = reaction.message.guild?.roles.cache.get(roleId);
if (member && role) {
await member.roles.remove(role);
console.log(`Removed ${role.name} role from ${user.tag}`);
}
});Complex event handling patterns and utilities for sophisticated bot behavior.
/**
* Event handling utilities and patterns
*/
interface EventHandlingPatterns {
// Event listener management
once<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaitable<void>): Client;
off<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaitable<void>): Client;
removeAllListeners<K extends keyof ClientEvents>(event?: K): Client;
// Event emitter utilities
listenerCount<K extends keyof ClientEvents>(event: K): number;
listeners<K extends keyof ClientEvents>(event: K): Function[];
rawListeners<K extends keyof ClientEvents>(event: K): Function[];
// Custom event handling
prependListener<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaitable<void>): Client;
prependOnceListener<K extends keyof ClientEvents>(event: K, listener: (...args: ClientEvents[K]) => Awaitable<void>): Client;
}
/**
* WebSocket shard events for advanced shard management
*/
const WebSocketShardEvents: {
Close: 'close';
Debug: 'debug';
Error: 'error';
Hello: 'hello';
Open: 'open';
Ready: 'ready';
Resumed: 'resumed';
};Advanced Usage Examples:
// Event-driven command cooldown system
const cooldowns = new Map<string, Map<string, number>>();
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName, user } = interaction;
const cooldownTime = 5000; // 5 seconds
if (!cooldowns.has(commandName)) {
cooldowns.set(commandName, new Map());
}
const timestamps = cooldowns.get(commandName)!;
const now = Date.now();
if (timestamps.has(user.id)) {
const expirationTime = timestamps.get(user.id)! + cooldownTime;
if (now < expirationTime) {
const timeLeft = (expirationTime - now) / 1000;
return interaction.reply({
content: `Please wait ${timeLeft.toFixed(1)} more second(s) before reusing the \`${commandName}\` command.`,
ephemeral: true
});
}
}
timestamps.set(user.id, now);
setTimeout(() => timestamps.delete(user.id), cooldownTime);
// Execute command...
});
// Multi-step interaction flow
const multiStepFlows = new Map<string, { step: number; data: any }>();
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isButton()) return;
const flowId = `${interaction.user.id}_setup`;
if (interaction.customId === 'start_setup') {
multiStepFlows.set(flowId, { step: 1, data: {} });
// Continue with step 1...
const modal = new ModalBuilder()
.setCustomId('setup_step1')
.setTitle('Setup - Step 1')
.addComponents(/* ... */);
await interaction.showModal(modal);
}
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isModalSubmit()) return;
const flowId = `${interaction.user.id}_setup`;
const flow = multiStepFlows.get(flowId);
if (!flow) return;
if (interaction.customId === 'setup_step1') {
flow.data.step1 = interaction.fields.getTextInputValue('input1');
flow.step = 2;
// Continue to step 2...
}
});
// Event-based auto-moderation
client.on(Events.MessageCreate, async (message) => {
if (message.author.bot || !message.guild) return;
const badWords = ['spam', 'inappropriate']; // Example list
const containsBadWords = badWords.some(word =>
message.content.toLowerCase().includes(word)
);
if (containsBadWords) {
await message.delete();
const warningMessage = await message.channel.send({
content: `${message.author}, your message was removed for inappropriate content.`,
allowedMentions: { users: [message.author.id] }
});
// Auto-delete warning after 5 seconds
setTimeout(() => warningMessage.delete().catch(() => {}), 5000);
// Log to moderation channel
const modChannel = message.guild.channels.cache.find(
ch => ch.name === 'mod-logs'
) as TextChannel;
if (modChannel) {
await modChannel.send({
embeds: [
new EmbedBuilder()
.setTitle('Message Deleted')
.setDescription(`Message from ${message.author} deleted for inappropriate content`)
.addFields(
{ name: 'Channel', value: message.channel.toString() },
{ name: 'Content', value: message.content || 'No content' }
)
.setTimestamp()
.setColor(Colors.Red)
]
});
}
}
});Install with Tessl CLI
npx tessl i tessl/npm-discord-js