Mein erster Discord-Bot war 400 Zeilen in einer Datei
Eine index.js, eine lange if-Kette, ein Präfix und viel zu viel Stolz. Wie mein Einstieg in discord.js aussah — und welche drei Sachen ich sofort falsch gemacht habe.
1123 Wörter · 6 Min. Lesezeit
- discord
- javascript
- anfaenge
Auf unserem Minecraft-Server gab es einen Discord-Server, auf dem Discord-Server gab es einen Bot, und dieser Bot konnte genau eine Sache: auf !ping mit Pong! antworten. Er war meiner, und ich war unfassbar stolz auf ihn.
Der Weg dorthin war kürzer, als ich erwartet hatte. Man legt im Entwicklerportal von Discord eine Anwendung an, klickt auf „Bot", bekommt einen Token, lädt den Bot über eine URL auf den eigenen Server — und dann steht er da, grau, offline, und wartet darauf, dass irgendwo ein Programm läuft, das sich mit seinem Token anmeldet.
Der erste Code
Das war er, ungefähr wortgleich, discord.js in der damals aktuellen Version 11:
const Discord = require("discord.js");
const client = new Discord.Client();
const config = require("./config.json");
client.on("ready", () => {
console.log(`Eingeloggt als ${client.user.tag}`);
});
client.on("message", (message) => {
if (message.author.bot) return;
if (!message.content.startsWith("!")) return;
if (message.content === "!ping") {
message.channel.send("Pong!");
}
});
client.login(config.token);Zwei Dinge daran haben mich damals fasziniert. Erstens: Der Bot bekommt jede Nachricht mit, die auf dem Server geschrieben wird. Das ist ein Ereignisstrom, kein Frage-Antwort-Spiel. Zweitens: Diese Zeile if (message.author.bot) return; habe ich aus einem Tutorial übernommen, ohne sie zu verstehen — bis zwei Wochen später ein zweiter Bot dazukam und die beiden anfingen, sich gegenseitig endlos zu antworten. Seitdem verstehe ich sie sehr genau.
Wie aus zehn Zeilen vierhundert wurden
Der Bot wuchs, wie solche Bots wachsen: durch Zurufe im Sprachchat. „Kannst du machen, dass er würfelt?" Klar. „Kannst du machen, dass er sagt, wer online ist?" Klar. „Kannst du eine Umfrage einbauen?" Klar.
Nach zwei Monaten sah die message-Funktion so aus:
client.on("message", (message) => {
if (message.author.bot) return;
if (!message.content.startsWith(config.prefix)) return;
const args = message.content.slice(config.prefix.length).trim().split(/ +/);
const befehl = args.shift().toLowerCase();
if (befehl === "ping") { ... }
else if (befehl === "würfel") { ... }
else if (befehl === "umfrage") { ... }
else if (befehl === "online") { ... }
else if (befehl === "kick") { ... }
else if (befehl === "ban") { ... }
// ... und so weiter, 23 Mal
});Vierhundert Zeilen, eine einzige Datei, eine einzige Funktion. Jede Änderung an einem Befehl bedeutete, in diesem Block zu scrollen und zu hoffen, dass man die richtige Klammer erwischt. Einmal habe ich beim Löschen eines Befehls eine schließende Klammer zu viel entfernt, und der Bot startete drei Tage nicht mehr, weil ich den Fehler nicht fand.
Tipp
Wenn du gerade an derselben Stelle stehst: Es ist völlig in Ordnung, so anzufangen. Die eine Datei ist kein Fehler, sie ist eine Phase. Der Fehler wäre, in ihr zu bleiben, nachdem man gemerkt hat, dass sie nicht mehr trägt. Bei mir hat das ungefähr ein halbes Jahr gedauert — der Umbau kommt in einem eigenen Eintrag.
Drei Dinge, die ich sofort falsch gemacht habe
Der Token lag im Repository. Dazu gibt es einen eigenen Eintrag, weil die Geschichte einen eigenen verdient. Kurzfassung: config.json mit dem Token drin, git add ., GitHub, öffentlich. Discord hat den Token noch am selben Tag automatisch für ungültig erklärt.
Ich habe alles synchron gedacht. Wenn ein Befehl eine Datei las, tat ich das mit fs.readFileSync. Bei einem Bot mit fünf Nutzern merkt man das nicht. Was ich nicht verstanden hatte: Node.js arbeitet die Ereignisse nacheinander ab. Solange eine Datei gelesen wird, tut der Bot gar nichts anderes — keine andere Nachricht, kein anderer Befehl. Das ist die Sorte Fehler, die erst zum Problem wird, wenn es gut läuft.
Ich habe Daten in Variablen gehalten. Die Punktestände meines Würfelspiels lagen in einem Objekt im Speicher. Bei jedem Neustart war alles weg. Meine erste Lösung war, das Objekt regelmäßig in eine JSON-Datei zu schreiben. Das hat erstaunlich lange funktioniert und ist genau bis zu dem Tag gut gegangen, an dem der Bot mitten im Schreiben abgestürzt ist und eine halbe Datei zurückließ, die kein Parser mehr lesen wollte.
Was mich damals wirklich gepackt hat
Nicht die Technik. Es war die Rückmeldung.
Bei einem Minecraft-Server merkst du, ob etwas funktioniert, wenn du dich einloggst. Bei einem Discord-Bot merkst du es sofort und öffentlich: Du änderst eine Zeile, startest neu, tippst den Befehl, und in einem Chat, in dem sechs Leute sitzen, passiert etwas. Wenn es klappt, schreibt jemand „nice". Wenn es nicht klappt, schreibt jemand „kaputt". Diese Schleife ist kurz, und für jemanden, der gerade anfängt, ist eine kurze Schleife das Wertvollste überhaupt.
Ich glaube, das ist der eigentliche Grund, warum aus dem Bot später ein größeres Projekt geworden ist und nicht aus etwas anderem. Es war nicht die spannendste Technik, die mir 2019 begegnet ist. Aber es war die, bei der ich am schnellsten sehen konnte, ob ich etwas verstanden hatte.
Die Sache mit den Berechtigungen
Ein Teil, den ich beim ersten Mal komplett übersehen habe: Ein Bot hat eigene Rechte auf einem Server, und die sind unabhängig davon, was er im Code versucht.
Beim Hinzufügen gibt man an, welche Berechtigungen er bekommen soll. Meine erste Einladungs-URL hatte — weil ich es aus einem Tutorial kopiert hatte — den Wert für Administrator. Das ist bequem, weil danach alles funktioniert, und es ist der schlechteste Standardzustand, den man wählen kann.
Ein Bot mit Administratorrechten darf alles: Kanäle löschen, Rollen vergeben, Mitglieder ausschließen, den Server umbenennen. Wenn sein Token in falsche Hände gerät, ist der Schaden nicht auf das begrenzt, was der Bot können sollte, sondern auf alles, was auf dem Server möglich ist.
Ich habe das ein halbes Jahr später korrigiert und dabei gemerkt, wie wenig ein Bot tatsächlich braucht. Für alles, was meiner damals konnte, reichten: Nachrichten lesen, Nachrichten senden, Reaktionen hinzufügen, Nachrichtenverlauf lesen. Vier Rechte statt aller.
Seitdem gehe ich bei jedem Dienst, der Rechte anfordert, nach demselben Muster vor: mit der kleinsten Menge anfangen und ergänzen, wenn etwas fehlschlägt. Das dauert beim Einrichten zwanzig Minuten länger und erspart die Frage, was jemand alles anrichten könnte.
Was aus den vierhundert Zeilen geworden ist
Der Bot von damals existiert nicht mehr als Programm, aber einige seiner Befehle schon.
Der Würfelbefehl ist über mehrere Umbauten, zwei Bibliotheksversionen und eine komplette Umstellung der Aufrufart hinweg im Kern derselbe geblieben: eine Zahl prüfen, eine Zufallszahl ziehen, antworten. Er ist heute besser abgesichert — die Obergrenze verhindert, dass jemand eine Million Seiten anfragt und eine unlesbare Antwort erzeugt —, aber die Logik dazwischen sind immer noch fünf Zeilen.
Ich finde das erstaunlich und beruhigend zugleich. Es gibt in jedem gewachsenen System einen Kern, der so einfach bleibt, wie er am ersten Tag war. Was sich ändert, ist alles drumherum: wie er aufgerufen wird, wie Fehler behandelt werden, wo die Texte herkommen, wer ihn benutzen darf.
Wenn ich heute einen neuen Befehl schreibe, versuche ich diesen Kern zuerst zu finden — die fünf Zeilen, die die eigentliche Sache tun. Alles andere ist Umgebung, und Umgebung ändert sich.
Was davon heute noch stimmt
Zwei Dinge aus diesem Code haben die Jahre überlebt und stehen sinngemäß bis heute in jedem Bot, den ich schreibe:
- Ignoriere andere Bots, immer, als erste Zeile. Sonst baust du irgendwann versehentlich eine Endlosschleife, die einen ganzen Kanal flutet.
- Nimm die Eingabe auseinander, bevor du sie prüfst. Diese vier Zeilen, die aus einer Nachricht Befehl und Argumente machen, sind der Kern jedes Befehlssystems. Der Rest ist Organisation.
Alles andere aus dieser Datei ist inzwischen ersetzt, umgeschrieben, weggeworfen. Der Bot heißt heute anders, läuft auf einem gemieteten Server, spricht mit einer Datenbank und kennt über hundert Befehle. Aber if (message.author.bot) return; steht sinngemäß immer noch drin.