Wir schreiben einen Bot mit der Bibliothek discord.js und zwei Slash-Befehlen: /ping und /pozdrav („grüßen“) mit einem Parameter. Wir erklären jede Zeile und betreiben den Bot am Ende bei StormHost.
Was Sie brauchen
- Node.js LTS auf Ihrem Computer (Download auf nodejs.org (öffnet sich in einem neuen Fenster)) und ein Code-Editor, zum Beispiel Visual Studio Code
- Eine Anwendung im Discord Developer Portal, ein Token und der mit dem Scope
applications.commandsauf Ihren Testserver eingeladene Bot (Anleitung) - Ein Discord-Bot bei StormHost für den Dauerbetrieb
- Etwa 30 Minuten
So funktioniert es#
Ein Bot mit Slash-Befehlen besteht aus drei Teilen:
- Befehlsdefinitionen – Name, Beschreibung und Parameter des Befehls. Sie teilen sie Discord einmal mit, über ein separates Skript über die REST-API (Registrierung).
- Der laufende Bot – verbindet sich mit Discord (dem Gateway) und wartet auf Ereignisse.
- Interaktionsverarbeitung – wenn jemand einen Befehl verwendet, sendet Discord dem Bot eine Interaktion, und der Bot muss innerhalb von 3 Sekunden antworten. Andernfalls sieht der Nutzer, dass die Anwendung nicht reagiert hat.
Registrierung und Betrieb des Bots sind getrennt: Sie müssen die Befehle nur dann neu registrieren, wenn Sie ihre Definition ändern (Name, Beschreibung, Parameter). Änderungen am Antwortcode brauchen keine neue Registrierung.
Projekt anlegen#
Erstellen Sie einen Ordner
muj-botund öffnen Sie darin ein Terminal (in VS Code Strg+`).Legen Sie das Projekt an und installieren Sie die Bibliotheken:
bash npm init -y npm install discord.js dotenvnpm init -y– erstelltpackage.jsonmit Standardwerten (-y= beantwortet jede Frage mit „ja“).npm install discord.js dotenv– installiert discord.js und dotenv (lädt die Datei.env) und trägt sie independenciesein.
Setzen Sie in
package.jsondie Felder"main": "index.js"und"type": "commonjs". Die Beispiele verwendenrequire(). Die Felder erklärt die Anleitung Hochladen und Starten des Bots.Erstellen Sie eine Datei
.env:.envini DISCORD_TOKEN=<BOT_TOKEN> CLIENT_ID=<APPLICATION_ID> GUILD_ID=<SERVER_ID>Ersetzen Sie
<BOT_TOKEN>durch Ihren Bot-Token,<APPLICATION_ID>durch die Application ID von der Seite General Information im Developer Portal und<SERVER_ID>durch die ID Ihres Test-Discord-Servers. Wo Sie diese finden, beschreibt die Anleitung Token und Intents.Erstellen Sie eine Datei
.gitignoremit zwei Zeilen:node_modulesund.env.
Die resultierende Struktur:
muj-bot/
├── .env
├── .gitignore
├── package.json
├── package-lock.json
├── index.js
├── deploy-commands.js
└── commands/
└── utility/
├── ping.js
└── pozdrav.jsBefehle#
Jeder Befehl hat eine eigene Datei mit zwei Teilen: data (die Definition für Discord) und execute (was passiert, wenn jemand ihn verwendet).
const { SlashCommandBuilder, MessageFlags } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Antwortet mit Pong! und zeigt die Latenz des Bots.'),
async execute(interaction) {
await interaction.reply({
content: `Pong! Latenz: ${interaction.client.ws.ping} ms`,
flags: MessageFlags.Ephemeral,
});
},
};| Zeile | Was sie tut |
|---|---|
require('discord.js') | Lädt die Klasse SlashCommandBuilder (baut die Befehlsdefinition) und MessageFlags (Nachrichten-Flags) aus der Bibliothek. |
.setName('ping') | Der Befehlsname, den der Nutzer nach dem Schrägstrich tippt. 1–32 Zeichen, nur Kleinbuchstaben, Ziffern, - und _, keine Leerzeichen. |
.setDescription('…') | Die Beschreibung im Befehlsmenü. Pflichtfeld, 1–100 Zeichen. |
async execute(interaction) | Die Funktion, die der Bot aufruft, wenn der Befehl verwendet wird. interaction enthält alles dazu (wer, wo, Parameter). |
interaction.reply({ … }) | Die Antwort auf den Befehl. Sie muss innerhalb von 3 Sekunden ankommen. |
interaction. | Die Latenz der Verbindung des Bots zu Discord in Millisekunden („Latenz“ in der Antwort). Direkt nach dem Start kann sie -1 sein, bis sie gemessen wurde. |
flags: MessageFlags.Ephemeral | Nur die Person, die den Befehl verwendet hat, sieht die Antwort. Andere im Kanal nicht. |
Ein Befehl mit Parameter:
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('pozdrav')
.setDescription('Grüßt die angegebene Person.')
.addStringOption((option) =>
option.setName('jmeno').setDescription('Wen grüßen').setRequired(true),
),
async execute(interaction) {
const jmeno = interaction.options.getString('jmeno');
await interaction.reply(`Hallo, ${jmeno}! ${interaction.user.username} lässt grüßen.`);
},
};.addStringOption(…)– fügt einen Textparameter hinzu (jmeno= Name). Discord bietet ihn dem Nutzer als Feldjmeno:an..setRequired(true)– der Parameter ist erforderlich; ohne ihn lässt sich der Befehl nicht absenden.interaction.– liest den Wert des Parameters.options. getString( 'jmeno') interaction.– der Benutzername der Person, die den Befehl verwendet hat.user. username interaction.reply('…')– eine öffentliche Antwort im Kanal.
Weitere Parametertypen: addIntegerOption (ganze Zahl), addBooleanOption (ja/nein), addUserOption (Servermitglied), addChannelOption (Kanal), addRoleOption (Rolle).
Befehle registrieren#
Das Skript deploy-commands.js lädt die Definitionen aller Befehle und sendet sie an Discord. Sie führen es von Hand aus, nur wenn Sie einen Befehl hinzufügen oder seine Definition ändern.
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
const { REST, Routes } = require('discord.js');
const commands = [];
const foldersPath = path.join(__dirname, 'commands');
for (const folder of fs.readdirSync(foldersPath)) {
const commandsPath = path.join(foldersPath, folder);
for (const file of fs.readdirSync(commandsPath).filter((f) => f.endsWith('.js'))) {
const command = require(path.join(commandsPath, file));
if ('data' in command && 'execute' in command) {
commands.push(command.data.toJSON());
} else {
console.log(`[WARNUNG] Der Datei ${file} fehlt "data" oder "execute".`);
}
}
}
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
(async () => {
try {
console.log(`Befehle werden registriert: ${commands.length}`);
const data = await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands },
);
console.log(`Fertig, registrierte Befehle: ${data.length}`);
} catch (error) {
console.error(error);
}
})();| Teil | Was er tut |
|---|---|
fs. | Geht die Unterordner von commands durch (zum Beispiel utility) und die .js-Dateien darin. |
command.data.toJSON() | Wandelt die Befehlsdefinition in das Format um, das die Discord-API erwartet. |
new REST().setToken(…) | Ein Discord-REST-API-Client, der mit dem Bot-Token authentifiziert ist. Er verbindet sich nicht als Bot, er sendet nur die Anfrage. |
Routes. | Die Route für die Befehle eines einzelnen Servers (Guild). |
rest.put(…, { body: commands }) | Ersetzt alle Befehle auf dem Server durch diese Liste. Ein Befehl, der in der Liste fehlt, verschwindet aus Discord. |
Führen Sie es auf Ihrem Computer im Projektordner aus:
node deploy-commands.jsBefehle werden registriert: 2
Fertig, registrierte Befehle: 2Server- oder globale Befehle?#
| Typ | Route | Wo sie funktionieren | Wann verwenden |
|---|---|---|---|
| Server (Guild) | Routes. | Nur auf dem Server GUILD_ID. | Entwicklung und Tests, ein Bot für einen Server. |
| Global | Routes. | Auf jedem Server, auf den der Bot mit dem Scope applications.commands eingeladen ist, und standardmäßig auch in Direktnachrichten. | Ein fertiger Bot für mehrere Server. |
Wenn Sie denselben Befehl sowohl für den Server als auch global registriert haben, sehen Sie ihn auf Ihrem Testserver doppelt. Löschen Sie vor dem Wechsel zur globalen Registrierung die Serverbefehle: Führen Sie die Serverregistrierung mit einer leeren Liste aus (body: []).
Discord begrenzt das Anlegen von Befehlen auf 200 pro Tag und Server. Führen Sie das Registrierungsskript deshalb nicht bei jedem Start des Bots aus.
Die Hauptdatei des Bots#
const fs = require('node:fs');
const path = require('node:path');
require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
const { Client, Collection, Events, GatewayIntentBits, MessageFlags } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// Befehle aus dem Ordner commands laden
client.commands = new Collection();
const foldersPath = path.join(__dirname, 'commands');
for (const folder of fs.readdirSync(foldersPath)) {
const commandsPath = path.join(foldersPath, folder);
for (const file of fs.readdirSync(commandsPath).filter((f) => f.endsWith('.js'))) {
const command = require(path.join(commandsPath, file));
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
}
}
}
client.once(Events.ClientReady, (readyClient) => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
console.error(`Der Befehl ${interaction.commandName} existiert nicht.`);
return;
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
const odpoved = { content: 'Beim Ausführen dieses Befehls ist ein Fehler aufgetreten.', flags: MessageFlags.Ephemeral };
if (interaction.replied || interaction.deferred) {
await interaction.followUp(odpoved);
} else {
await interaction.reply(odpoved);
}
}
});
client.login(process.env.DISCORD_TOKEN);| Teil | Was er tut |
|---|---|
new Client({ intents: [GatewayIntentBits. | Erstellt den Bot. Der Intent Guilds reicht für Slash-Befehle und ist nicht privilegiert. Das Lesen gewöhnlicher Nachrichten bräuchte GuildMessages und MessageContent. |
client.commands = new Collection() | Eine Liste der Befehle nach Namen. Collection ist eine erweiterte Map aus discord.js. |
client. | Speichert den Befehl unter seinem Namen, zum Beispiel ping. |
client. | Läuft einmal nach erfolgreicher Anmeldung. readyClient.user.tag ist der Name des Bots. |
client. | Läuft bei jeder Interaktion (Befehl, Schaltfläche, Formular). |
interaction. | Lässt nur Slash-Befehle durch und ignoriert andere Interaktionen. |
client. | Findet die Befehlsdatei anhand des Namens, den der Nutzer verwendet hat. |
try { … } catch (error) { … } | Ein Fehler in einem Befehl darf den Bot nicht abstürzen lassen. Der Nutzer erhält eine Fehlerantwort, und der Fehler wird in der Konsole ausgegeben. |
interaction.replied / interaction.deferred | Wenn der Bot bereits geantwortet oder die Antwort aufgeschoben hat, muss er jede weitere Nachricht mit followUp senden. |
client. | Meldet den Bot mit dem Token aus .env an. |
Probieren Sie den Bot auf Ihrem Computer aus:
node index.jsReady! Logged in as MujBot#1234Tippen Sie auf Discord /ping und wählen Sie den Befehl Ihres Bots.
Antwort auf einen langsamen Befehl#
Wenn ein Befehl mehr als 3 Sekunden braucht (eine Datenbankabfrage, ein Aufruf einer anderen API), schieben Sie die Antwort zuerst auf und senden Sie das Ergebnis später:
async execute(interaction) {
await interaction.deferReply();
const vysledek = await nejakaDlouhaPrace();
await interaction.editReply(`Fertig: ${vysledek}`);
}interaction.deferReply()– bestätigt die Interaktion sofort. Der Nutzer sieht, dass der Bot nachdenkt. Für eine Antwort, die nur er sieht, verwenden SiedeferReply({ flags: MessageFlags.Ephemeral }).interaction.editReply(…)– ersetzt „denkt nach“ durch die eigentliche Antwort. Eine aufgeschobene Antwort müssen Sie innerhalb von 15 Minuten senden.nejakaDlouhaPrace()– Ihre eigene Funktion; der Name ist nur ein Beispiel.
Antwort mit einem Embed#
Ein Embed ist eine Nachricht mit farbigem Streifen, Titel und Feldern. Er eignet sich, um Informationen übersichtlich darzustellen. Der Befehl /server zeigt Details zum Server:
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('server')
.setDescription('Informationen über diesen Discord-Server.')
.setContexts(InteractionContextType.Guild),
async execute(interaction) {
const guild = interaction.guild;
const embed = new EmbedBuilder()
.setColor(0xe11d48)
.setTitle(guild.name)
.addFields(
{ name: 'Mitglieder', value: String(guild.memberCount), inline: true },
{ name: 'Erstellt', value: guild.createdAt.toLocaleDateString('de-DE'), inline: true },
)
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};| Teil | Was er tut |
|---|---|
.setContexts( | Der Befehl kann nur auf einem Server verwendet werden, nicht in Direktnachrichten (wo interaction.guild nicht existieren würde). |
new EmbedBuilder() | Beginnt den Aufbau eines Embeds. |
.setColor(0xe11d48) | Die Streifenfarbe als Hexadezimalzahl (hier Rot). |
.setTitle(guild.name) | Der Titel, hier der Servername. |
.addFields({ name, value, inline }) | Felder mit Beschriftung und Wert. value muss Text sein, daher String(…). inline: true stellt die Felder nebeneinander. |
guild.memberCount | Die Mitgliederzahl des Servers. Funktioniert mit dem Intent Guilds, kein privilegierter Intent nötig. |
.setTimestamp() | Fügt die aktuelle Zeit in die Fußzeile ein. |
interaction.reply({ embeds: [embed] }) | Sendet den Embed. Sie können mehrere Embeds als Array in eine Nachricht packen. |
Führen Sie nach dem Hinzufügen der Datei erneut node deploy-commands.js aus; der neue Befehl muss registriert werden.
Wer einen Befehl verwenden darf#
Die Standardberechtigungen legen Sie direkt in der Befehlsdefinition fest:
const { SlashCommandBuilder, PermissionFlagsBits, InteractionContextType, MessageFlags } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('smazat')
.setDescription('Löscht die neuesten Nachrichten im Kanal.')
.addIntegerOption((o) =>
o.setName('pocet').setDescription('Wie viele Nachrichten (1–100)').setMinValue(1).setMaxValue(100).setRequired(true),
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
.setContexts(InteractionContextType.Guild),
async execute(interaction) {
const pocet = interaction.options.getInteger('pocet');
const smazane = await interaction.channel.bulkDelete(pocet, true);
await interaction.reply({ content: `Gelöschte Nachrichten: ${smazane.size}`, flags: MessageFlags.Ephemeral });
},
};.setDefaultMemberPermissions(– standardmäßig sehen nur Mitglieder mit der Berechtigung Nachrichten verwalten den Befehl. Ein Serveradministrator kann das in den Servereinstellungen ändern (Integrationen).PermissionFlagsBits. ManageMessages) .setMinValue(1)und.setMaxValue(100)– Discord akzeptiert keine Zahl außerhalb des Bereichs.interaction.– löscht Nachrichten massenhaft.channel. bulkDelete( pocet, true) trueüberspringt Nachrichten, die älter als 14 Tage sind und nicht massenhaft gelöscht werden können.- Der Bot selbst braucht im Kanal die Berechtigung Nachrichten verwalten, sonst schlägt er mit
DiscordAPIError[50013]: Missing Permissionsfehl.
Keine Befehlseinstellung hält Mitglieder mit der Berechtigung Administrator auf. Registrieren Sie Befehle, die nur für Sie gedacht sind (zum Beispiel eval), deshalb nur auf einem privaten Testserver.
Ereignis: ein neues Mitglied begrüßen#
Neben Befehlen reagiert ein Bot auf Ereignisse. Das Begrüßen eines neuen Mitglieds braucht den privilegierten Server Members Intent: Schalten Sie ihn im Developer Portal auf der Seite Bot ein und fügen Sie ihn in Ihren Code ein.
const { Events } = require('discord.js');
module.exports = {
name: Events.GuildMemberAdd,
once: false,
async execute(member) {
const kanal = member.guild.systemChannel;
if (!kanal) return;
await kanal.send(`Willkommen auf dem Server, ${member}!`);
},
};Aktualisieren Sie in index.js die Intents und fügen Sie das Laden der Ereignisse hinzu (nach dem Laden der Befehle):
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
// … Laden der Befehle …
const eventsPath = path.join(__dirname, 'events');
for (const file of fs.readdirSync(eventsPath).filter((f) => f.endsWith('.js'))) {
const event = require(path.join(eventsPath, file));
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute(...args));
}
}GatewayIntentBits.– Discord sendet dann Ereignisse, wenn Mitglieder beitreten und den Server verlassen. Wenn der Intent im Developer Portal nicht aktiviert ist, meldet der BotGuildMembers Used disallowed intents.member.– der Systemkanal des Servers (in dem Discord Willkommensnachrichten postet). Wenn der Server keinen hat, sendet der Bot nichts.guild. systemChannel ${member}– fügt eine Erwähnung des neuen Mitglieds ein (@name).client.once/client.on– ein Ereignis mitonce: truewird nur einmal verarbeitet (Start des Bots), die anderen jedes Mal.
Struktur eines größeren Bots#
- Befehle nach Kategorien in Ordnern wie
commands/moderace,commands/zabava… Der Ladecode oben geht sie alle durch. - Ereignisse im Ordner
events, jedes in einer eigenen Datei mitname,onceundexecute(siehe oben). Auch die HandlerClientReadyundInteractionCreatekönnen Sie nacheventsverschieben. - Konfiguration (Kanal- und Rollen-IDs) in
config.json, Geheimnisse in.env. - Daten (Punkte, Verwarnungen) in einer Datenbank, nicht in Variablen im Speicher, die bei einem Neustart verloren gehen.
Betrieb bei StormHost#
- Führen Sie die Befehlsregistrierung (
node deploy-commands.js) auf Ihrem Computer aus. Auf dem Server müssen Sie sie nicht ausführen. - Stoppen Sie den Bot auf Ihrem Computer (Strg+C). Zwei laufende Kopien mit demselben Token stehlen sich gegenseitig die Interaktionen.
- Laden Sie
index.js,package.json,package-lock.jsonund den Ordnercommandsmit allen Unterordnern in der Registerkarte Dateien hoch. Wenn Sie die Begrüßung neuer Mitglieder hinzugefügt haben, laden Sie auch den Ordnereventshoch. Ohne ihn stürzt der Bot beim Start mitENOENT: no such file or directoryab. Legen Sie.envdirekt auf dem Server an. Schritte: Hochladen und Starten des Bots. - Prüfen Sie in der Registerkarte Einstellungen, dass die Hauptdatei
index.jsist. - Klicken Sie in der Registerkarte Konsole auf Start.
Überprüfung#
- Die Konsole zeigt
Ready! Logged in as …, und der Bot ist auf Discord online. - Wenn Sie auf Ihrem Testserver
/tippen, zeigt das Menüpingundpozdravmit dem Symbol Ihres Bots. /pingantwortet mit einer Nachricht, die nur Sie sehen,/pozdrav jmeno:Janmit einer öffentlichen Nachricht.
Fehlerbehebung#
Befehle erscheinen nicht im Menü#
- Die Registrierung ist nicht gelaufen: Führen Sie
node deploy-commands.jsaus und achten Sie auf die AusgabeFertig, registrierte Befehle: …. GUILD_IDist die ID eines anderen Servers als dem, auf dem Sie testen.- Der Bot wurde ohne den Scope
applications.commandseingeladen. Laden Sie ihn erneut mit einem Link ein, der sowohlbotals auchapplications.commandsenthält. - Discord cacht das Menü. Laden Sie die Discord-App neu (Strg+R).
Die Registrierung schlägt mit DiscordAPIError[50001]: Missing Access fehl#
Der Bot ist nicht auf dem Server GUILD_ID oder hat dort nicht den Scope applications.commands. Prüfen Sie die Server-ID und laden Sie den Bot erneut ein.
Discord meldet „Die Anwendung hat nicht reagiert“#
Der Bot hat nicht innerhalb von 3 Sekunden geantwortet. Entweder läuft er nicht (prüfen Sie die Konsole), der Befehl dauert lange und deferReply() fehlt, oder im Befehl ist vor der Antwort ein Fehler aufgetreten (Sie finden ihn in der Konsole).
DiscordAPIError[10062]: Unknown interaction#
Die Antwort kam zu spät (nach 3 Sekunden), oder zwei Kopien des Bots laufen mit demselben Token und die andere hat die Interaktion bereits verarbeitet. Verwenden Sie deferReply() und lassen Sie nur eine Kopie laufen.
DiscordAPIError[40060]: Interaction has already been acknowledged#
Sie haben reply() bei einer Interaktion zweimal aufgerufen. Senden Sie die zweite Nachricht mit followUp() oder verwenden Sie editReply() nach deferReply().
[WARNUNG] Der Datei … fehlt "data" oder "execute".#
Eine Datei im Ordner commands exportiert nicht beide Teile. Prüfen Sie module.exports = { data, execute } und suchen Sie nach Tippfehlern.
require is not defined in ES module scope oder Cannot use import statement outside a module#
Sie mischen CommonJS und ES-Module. Die Beispiele gelten für CommonJS: package.json muss "type": "commonjs" enthalten (oder gar kein Feld type). Details in der Anleitung Logs und Fehler.
Zusammenfassung
- Ein Slash-Befehl = eine Definition (
SlashCommandBuilder), eine Registrierung (deploy-commands.jsüber REST) und eine Verarbeitung (Events.InteractionCreate). - Führen Sie die Registrierung nur aus, wenn sich eine Definition ändert. Registrieren Sie während der Entwicklung pro Server (
applicationGuildCommands); einen fertigen Bot registrieren Sie global (applicationCommands). - Antworten Sie auf eine Interaktion innerhalb von 3 Sekunden. Beginnen Sie längere Arbeit mit
deferReply()und schließen Sie sie miteditReply()ab. - Fangen Sie Fehler in Befehlen mit
try/catchab, damit ein Fehler den Bot nicht abstürzen lässt. Lassen Sie nie zwei Kopien mit demselben Token laufen.