--- name: discord-bot-helper description: | Discord.js v14 for building Discord bots - slash commands, events, components, embeds, and permissions When user works with Discord bots, discord.js, slash commands, Discord API, or mentions discord.js patterns --- # Discord Bot Helper Agent ## What's New in Discord.js v14 (2024-2025) - **Node.js 18.17+** required (v14.14+), 22+ recommended - **PascalCase enums**: `ButtonStyle.Primary` instead of `'PRIMARY'` - **Renamed builders**: `EmbedBuilder` (was `MessageEmbed`), `AttachmentBuilder` (was `MessageAttachment`) - **Display components**: New layout and content elements beyond embeds - **Gateway v10**: Updated event handling and intents ## Installation ```bash # Install discord.js npm install discord.js # or bun add discord.js ``` ## Basic Bot Setup ### Main File (index.js) ```typescript import { Client, Events, GatewayIntentBits } from "discord.js"; const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, // Privileged intent ], }); client.once(Events.ClientReady, (readyClient) => { console.log(`Logged in as ${readyClient.user.tag}!`); }); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; if (interaction.commandName === "ping") { await interaction.reply("Pong!"); } }); client.login(process.env.DISCORD_TOKEN); ``` ## Gateway Intents Intents control which events your bot receives: ### Common Intents | Intent | Events Received | | ------------------ | ------------------------------------------------- | | `Guilds` | Guild create/update/delete, channels, roles | | `GuildMembers` | Member join/leave/update (privileged) | | `GuildMessages` | Message events in guilds | | `MessageContent` | Message content, attachments, embeds (privileged) | | `GuildVoiceStates` | Voice channel activity | | `GuildPresences` | Member presence updates (privileged) | | `DirectMessages` | DM message events | ### Privileged Intents Require manual enabling in Discord Developer Portal: - `GuildMembers` - `GuildPresences` - `MessageContent` ```typescript const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, // Privileged GatewayIntentBits.GuildPresences, // Privileged GatewayIntentBits.MessageContent, // Privileged ], }); ``` ## Slash Commands ### Command Definition ```typescript // commands/ping.js import { SlashCommandBuilder } from "discord.js"; export const data = new SlashCommandBuilder() .setName("ping") .setDescription("Replies with Pong!"); export async function execute(interaction) { await interaction.reply("Pong!"); } ``` ### Command with Options ```typescript import { SlashCommandBuilder } from "discord.js"; export const data = new SlashCommandBuilder() .setName("echo") .setDescription("Replies with your input") .addStringOption((option) => option .setName("message") .setDescription("The message to echo") .setRequired(true), ) .addUserOption((option) => option.setName("target").setDescription("User to mention"), ) .addIntegerOption((option) => option .setName("count") .setDescription("Number of times") .setMinValue(1) .setMaxValue(10), ); export async function execute(interaction) { const message = interaction.options.getString("message"); const target = interaction.options.getUser("target"); const count = interaction.options.getInteger("count") ?? 1; const reply = target ? `${target}, ${message.repeat(count)}` : message.repeat(count); await interaction.reply(reply); } ``` ### Command with Choices ```typescript export const data = new SlashCommandBuilder() .setName("gif") .setDescription("Sends a gif") .addStringOption((option) => option .setName("category") .setDescription("The gif category") .setRequired(true) .addChoices( { name: "Funny", value: "gif_funny" }, { name: "Meme", value: "gif_meme" }, { name: "Cute", value: "gif_cute" }, ), ); ``` ### Subcommands ```typescript export const data = new SlashCommandBuilder() .setName("user") .setDescription("User commands") .addSubcommand((subcommand) => subcommand .setName("info") .setDescription("Get user info") .addUserOption((option) => option.setName("target").setDescription("The user"), ), ) .addSubcommand((subcommand) => subcommand .setName("avatar") .setDescription("Get user avatar") .addUserOption((option) => option.setName("target").setDescription("The user"), ), ); export async function execute(interaction) { const subcommand = interaction.options.getSubcommand(); const target = interaction.options.getUser("target") ?? interaction.user; if (subcommand === "info") { await interaction.reply(`User: ${target.tag}\nID: ${target.id}`); } else if (subcommand === "avatar") { await interaction.reply(target.displayAvatarURL({ size: 256 })); } } ``` ### Registering Commands ```typescript // deploy-commands.js import { REST, Routes } from "discord.js"; import { commands } from "./commands/index.js"; const rest = new REST().setToken(process.env.DISCORD_TOKEN); // Guild commands (instant, for development) await rest.put(Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID), { body: commands.map((c) => c.data.toJSON()), }); // Global commands (takes up to 1 hour to propagate) await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands.map((c) => c.data.toJSON()), }); ``` ### Autocomplete ```typescript export const data = new SlashCommandBuilder() .setName("search") .setDescription("Search something") .addStringOption((option) => option .setName("query") .setDescription("Search query") .setAutocomplete(true), ); export async function autocomplete(interaction) { const focusedValue = interaction.options.getFocused(); const choices = ["apple", "banana", "cherry", "date", "elderberry"]; const filtered = choices.filter((c) => c.startsWith(focusedValue)); await interaction.respond( filtered.slice(0, 25).map((choice) => ({ name: choice, value: choice })), ); } export async function execute(interaction) { const query = interaction.options.getString("query"); await interaction.reply(`You searched for: ${query}`); } ``` ## Response Methods ### Basic Responses ```typescript // Simple reply await interaction.reply("Hello!"); // Ephemeral reply (only visible to user) await interaction.reply({ content: "Secret!", ephemeral: true }); // Deferred reply (for long operations) await interaction.deferReply(); // ... do work ... await interaction.editReply("Done!"); // Follow-up messages await interaction.reply("First message"); await interaction.followUp("Second message"); await interaction.followUp({ content: "Ephemeral followup", ephemeral: true }); ``` ### Fetching the Reply ```typescript const reply = await interaction.fetchReply(); console.log(reply.id); ``` ## Embeds ### Creating Embeds ```typescript import { EmbedBuilder } from "discord.js"; const embed = new EmbedBuilder() .setColor(0x0099ff) .setTitle("Embed Title") .setURL("https://discord.js.org/") .setAuthor({ name: "Author Name", iconURL: "https://example.com/icon.png", url: "https://example.com", }) .setDescription("This is the main description") .setThumbnail("https://example.com/thumbnail.png") .addFields( { name: "Field 1", value: "Value 1", inline: true }, { name: "Field 2", value: "Value 2", inline: true }, { name: "Field 3", value: "Value 3" }, ) .setImage("https://example.com/image.png") .setTimestamp() .setFooter({ text: "Footer text", iconURL: "https://example.com/footer.png", }); await interaction.reply({ embeds: [embed] }); ``` ### Multiple Embeds ```typescript const embed1 = new EmbedBuilder().setTitle("Embed 1").setColor(0xff0000); const embed2 = new EmbedBuilder().setTitle("Embed 2").setColor(0x00ff00); await interaction.reply({ embeds: [embed1, embed2] }); ``` ## Buttons ### Creating Buttons ```typescript import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("primary") .setLabel("Primary") .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("secondary") .setLabel("Secondary") .setStyle(ButtonStyle.Secondary), new ButtonBuilder() .setCustomId("success") .setLabel("Success") .setStyle(ButtonStyle.Success), new ButtonBuilder() .setCustomId("danger") .setLabel("Danger") .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setLabel("Link") .setURL("https://discord.js.org") .setStyle(ButtonStyle.Link), ); await interaction.reply({ content: "Click a button!", components: [row] }); ``` ### Handling Button Clicks ```typescript client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isButton()) return; if (interaction.customId === "primary") { await interaction.reply("You clicked the primary button!"); } }); ``` ### Disabling Buttons After Click ```typescript const row = ActionRowBuilder.from(interaction.message.components[0]); row.components.forEach((button) => button.setDisabled(true)); await interaction.update({ components: [row] }); ``` ## Select Menus ### String Select Menu ```typescript import { ActionRowBuilder, StringSelectMenuBuilder } from "discord.js"; const row = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder() .setCustomId("select") .setPlaceholder("Nothing selected") .addOptions( { label: "Option 1", description: "Description 1", value: "first" }, { label: "Option 2", description: "Description 2", value: "second" }, { label: "Option 3", description: "Description 3", value: "third" }, ), ); await interaction.reply({ content: "Select an option!", components: [row] }); ``` ### Handling Selection ```typescript client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isStringSelectMenu()) return; if (interaction.customId === "select") { const selected = interaction.values[0]; await interaction.reply(`You selected: ${selected}`); } }); ``` ### User/Role/Channel Select Menus ```typescript import { UserSelectMenuBuilder, RoleSelectMenuBuilder, ChannelSelectMenuBuilder, } from "discord.js"; const userSelect = new UserSelectMenuBuilder() .setCustomId("user-select") .setPlaceholder("Select a user") .setMinValues(1) .setMaxValues(3); const roleSelect = new RoleSelectMenuBuilder() .setCustomId("role-select") .setPlaceholder("Select a role"); const channelSelect = new ChannelSelectMenuBuilder() .setCustomId("channel-select") .setPlaceholder("Select a channel"); ``` ## Modals (Forms) ### Creating a Modal ```typescript import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, } from "discord.js"; const modal = new ModalBuilder() .setCustomId("feedback-modal") .setTitle("Feedback Form"); const titleInput = new TextInputBuilder() .setCustomId("title") .setLabel("Title") .setStyle(TextInputStyle.Short) .setPlaceholder("Enter a title") .setRequired(true) .setMaxLength(100); const descriptionInput = new TextInputBuilder() .setCustomId("description") .setLabel("Description") .setStyle(TextInputStyle.Paragraph) .setPlaceholder("Enter your feedback") .setRequired(true) .setMinLength(10) .setMaxLength(1000); modal.addComponents( new ActionRowBuilder().addComponents(titleInput), new ActionRowBuilder().addComponents(descriptionInput), ); // Show modal (must be first response) await interaction.showModal(modal); ``` ### Handling Modal Submission ```typescript client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isModalSubmit()) return; if (interaction.customId === "feedback-modal") { const title = interaction.fields.getTextInputValue("title"); const description = interaction.fields.getTextInputValue("description"); await interaction.reply( `Feedback received!\nTitle: ${title}\nDescription: ${description}`, ); } }); ``` ## Permissions ### Checking Permissions ```typescript import { PermissionFlagsBits } from "discord.js"; // Check if member has permission if (interaction.member.permissions.has(PermissionFlagsBits.Administrator)) { // Has admin } // Check multiple permissions if ( interaction.member.permissions.has([ PermissionFlagsBits.BanMembers, PermissionFlagsBits.KickMembers, ]) ) { // Has both } // Check bot permissions in channel const botPermissions = interaction.channel.permissionsFor( interaction.client.user, ); if (!botPermissions.has(PermissionFlagsBits.SendMessages)) { return interaction.reply({ content: "I cannot send messages here!", ephemeral: true, }); } ``` ### Setting Default Command Permissions ```typescript export const data = new SlashCommandBuilder() .setName("ban") .setDescription("Ban a user") .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers) .setDMPermission(false); // Disable in DMs ``` ### Common Permission Flags | Flag | Description | | ----------------- | ----------------------------- | | `Administrator` | All permissions | | `ManageGuild` | Manage server settings | | `ManageChannels` | Create/delete/modify channels | | `ManageRoles` | Manage roles below bot's role | | `ManageMessages` | Delete messages, pin, etc. | | `KickMembers` | Kick members | | `BanMembers` | Ban members | | `SendMessages` | Send messages in channels | | `EmbedLinks` | Embed links in messages | | `AttachFiles` | Upload files | | `MentionEveryone` | Mention @everyone/@here | ## Event Handling ### Event File Structure ```typescript // events/ready.js import { Events } from "discord.js"; export const name = Events.ClientReady; export const once = true; export function execute(client) { console.log(`Ready! Logged in as ${client.user.tag}`); } ``` ```typescript // events/interactionCreate.js import { Events } from "discord.js"; export const name = Events.InteractionCreate; export async function execute(interaction) { if (!interaction.isChatInputCommand()) return; const command = interaction.client.commands.get(interaction.commandName); if (!command) return; try { await command.execute(interaction); } catch (error) { console.error(error); const reply = { content: "There was an error!", ephemeral: true }; if (interaction.replied || interaction.deferred) { await interaction.followUp(reply); } else { await interaction.reply(reply); } } } ``` ### Loading Events Dynamically ```typescript import { readdirSync } from "fs"; import { join } from "path"; const eventsPath = join(__dirname, "events"); const eventFiles = readdirSync(eventsPath).filter((f) => f.endsWith(".js")); for (const file of eventFiles) { const event = await import(join(eventsPath, file)); if (event.once) { client.once(event.name, (...args) => event.execute(...args)); } else { client.on(event.name, (...args) => event.execute(...args)); } } ``` ## Sharding ### Basic Sharding Setup ```typescript // shard.js (entry point) import { ShardingManager } from "discord.js"; const manager = new ShardingManager("./bot.js", { token: process.env.DISCORD_TOKEN, totalShards: "auto", // Let Discord determine shard count }); manager.on("shardCreate", (shard) => { console.log(`Launched shard ${shard.id}`); }); manager.spawn(); ``` ### Fetching Data Across Shards ```typescript // Get guild count across all shards const results = await client.shard.fetchClientValues("guilds.cache.size"); const totalGuilds = results.reduce((acc, count) => acc + count, 0); ``` ## Best Practices Summary 1. **Use slash commands** over message-based commands for better UX 2. **Defer replies** for operations taking >3 seconds 3. **Use ephemeral messages** for sensitive or user-specific responses 4. **Check permissions** before performing privileged actions 5. **Handle errors gracefully** with try/catch in command handlers 6. **Use guild commands** for development, global for production 7. **Enable only needed intents** to reduce bandwidth 8. **Use collectors** for time-limited component interactions 9. **Shard your bot** when approaching 2,500 guilds 10. **Store tokens securely** in environment variables ## When to Ask for Help - Complex permission hierarchies and overwrites - Voice connection and audio streaming - Large-scale sharding across multiple processes/machines - Rate limit handling and optimization - OAuth2 flows and user authorization - Webhook management and integration