Discord
Escopo: JavaScript/Node.js. Guia baseado em discord.js v14.19 com Node.js 22. Conceitos fundamentais (webhook, polling, command routing, rate limit): shared/platform/bots.md. Primitivas Discord (Gateway Intents, Slash Commands, Embeds): shared/platform/bots-advanced.md.
discord.js é a biblioteca Node.js para interagir com a API do Discord. Um Client (instância do bot) conecta ao Gateway via WebSocket, recebe eventos e responde por meio da API REST. Eventos e flags são enumerados via objetos tipados (Events, GatewayIntentBits), nunca strings literais.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| Client (cliente do bot) | Instância principal do bot; gerencia conexão com o Gateway e registro de eventos |
| Events (enum de eventos) | Enum do discord.js com todos os nomes de eventos (Events.InteractionCreate, Events.ClientReady) |
| GatewayIntentBits (flags de intenção do Gateway) | Enum para declarar quais categorias de eventos o bot recebe; intents não declaradas não chegam |
| Interaction (interação do usuário) | Objeto recebido quando o usuário usa slash command, botão ou select menu |
| isChatInputCommand() (verificação de tipo) | Type guard que confirma que a Interaction é um slash command; obrigatório antes de acessar commandName |
| SlashCommandBuilder (construtor de comando barra) | Classe para definir o schema de um slash command antes de registrá-lo via REST |
| EmbedBuilder (construtor de mensagem rica) | Classe para construir mensagens ricas; enviada dentro do array embeds: [embed], nunca como objeto solto |
| REST (Representational State Transfer, cliente HTTP) | Cliente HTTP do discord.js configurado com version: '10'; usado para registrar commands via API |
Instalação
npm install discord.js
Setup do Client
Use o enum Events em todos os listeners. Strings literais como 'ready' ou 'interactionCreate' foram removidas no v14.
❌ Ruim: strings literais removidas no v14; sem type safety
client.on('ready', () => {
console.log('Bot pronto');
});
client.on('interactionCreate', async (interaction) => {
if (interaction.commandName === 'status') {
await interaction.reply('OK');
}
});
✅ Bom: Events enum; Client com intents declaradas; login no final
import { Client, Events, GatewayIntentBits } from 'discord.js';
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
client.once(Events.ClientReady, (readyClient) => {
console.log(`Bot conectado como ${readyClient.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);
Declare apenas as intents que o bot usa. MessageContent e GuildMembers são intents privilegiadas: precisam ser habilitadas no Discord Developer Portal para bots em mais de 100 servidores.
Registro de Slash Commands
O registro por servidor (applicationGuildCommands) é instantâneo, ideal para desenvolvimento. O registro global (applicationCommands) leva até 1 hora para propagar.
❌ Ruim: REST sem version; schema como objeto literal sem validação
import { REST, Routes } from 'discord.js';
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
await rest.put(Routes.applicationGuildCommands(appId, guildId), {
body: [{ name: 'status', description: 'Status do serviço' }],
});
✅ Bom: REST com version: '10'; schema validado via SlashCommandBuilder
import { REST, Routes, SlashCommandBuilder } from 'discord.js';
const commands = [
new SlashCommandBuilder()
.setName('status')
.setDescription('Retorna o status atual do serviço')
.toJSON(),
new SlashCommandBuilder()
.setName('order')
.setDescription('Consulta um pedido pelo ID')
.addStringOption((option) =>
option.setName('id').setDescription('ID do pedido').setRequired(true),
)
.toJSON(),
];
async function registerCommands() {
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
await rest.put(
Routes.applicationGuildCommands(process.env.APP_ID, process.env.GUILD_ID),
{ body: commands },
);
}
registerCommands();
Command Router
Use Events.InteractionCreate e o type guard isChatInputCommand(). Centralize o roteamento em um Strategy Map.
❌ Ruim: string literal no evento; sem type guard; lógica de negócio no router; sem deferReply
client.on('interactionCreate', async (interaction) => {
if (interaction.commandName === 'order') {
const orderId = interaction.options.getString('id');
const order = await db.findOrder(orderId);
await interaction.reply(`Status: ${order.status}`);
}
});
✅ Bom: Events enum; isChatInputCommand() guard; Strategy Map; router só delega
import { Events } from 'discord.js';
import { orderCommand } from './commands/order.js';
import { statusCommand } from './commands/status.js';
const COMMAND_MAP = {
order: orderCommand,
status: statusCommand,
};
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = COMMAND_MAP[interaction.commandName];
if (!command) {
await interaction.reply({ content: 'Comando não encontrado.', ephemeral: true });
return;
}
await command(interaction);
});
Implementando um Command
A resposta a uma Interaction deve ocorrer em até 3 segundos. Para operações assíncronas, chame deferReply antes e finalize com editReply.
❌ Ruim: reply direto em operação assíncrona; embed como objeto solto (sintaxe v13 removida)
export async function orderCommand(interaction) {
const orderId = interaction.options.getString('id');
const order = await db.findOrder(orderId);
await interaction.reply({ embed: buildOrderEmbed(order) });
}
✅ Bom: deferReply antes do await; embeds como array; orquestrador + helpers abaixo
import { EmbedBuilder } from 'discord.js';
export async function orderCommand(interaction) {
await interaction.deferReply();
const orderId = interaction.options.getString('id');
const order = await fetchOrder(orderId);
const embed = buildOrderEmbed(order);
const replyPayload = { embeds: [embed] };
await interaction.editReply(replyPayload);
}
async function fetchOrder(orderId) {
const order = await orderRepository.findById(orderId);
return order;
}
function buildOrderEmbed(order) {
const embed = new EmbedBuilder()
.setTitle(`Pedido #${order.id}`)
.setDescription(`Status: ${order.status}`)
.setColor(0x5865f2)
.addFields({ name: 'Cliente', value: order.customerName })
.setTimestamp();
return embed;
}
Eventos além de slash commands
❌ Ruim: string literal no evento; acesso a canal nulo sem guard; sem guard para bot
client.on('guildMemberAdd', async (member) => {
await member.guild.systemChannel.send(`Bem-vindo, ${member.user.username}!`);
});
client.on('messageReactionAdd', async (reaction, user) => {
await reaction.message.reply(`${user.username} confirmou`);
});
✅ Bom: Events enum; guard para canal nulo; guard para bot
client.on(Events.GuildMemberAdd, async (member) => {
const welcomeChannel = member.guild.systemChannel;
if (!welcomeChannel) return;
const welcomeMessage = `Bem-vindo ao servidor, ${member.user.username}!`;
await welcomeChannel.send(welcomeMessage);
});
client.on(Events.MessageReactionAdd, async (reaction, user) => {
if (user.bot) return;
if (reaction.emoji.name !== '✅') return;
const confirmMessage = `${user.username} confirmou com ✅`;
await reaction.message.reply(confirmMessage);
});
Veja também
- shared/platform/bots-advanced.md (Discord): Gateway Intents, Slash Commands, Embeds (conceitual)
- shared/platform/bots.md: webhook vs polling, command routing, rate limit
Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.