Slash-Commands: die große Umstellung
Fünfundvierzig Befehle, zwei Aufrufwege, ein halbes Jahr Übergangszeit. Was bei der Umstellung auf Anwendungsbefehle wirklich Arbeit war — und was überraschend einfach ging.
1039 Wörter · 5 Min. Lesezeit
- yurna
- discord
- migration
Im Januar habe ich angefangen, den Bot auf Anwendungsbefehle umzustellen. Ich hatte im Herbst zwei Abende damit verbracht, die Dokumentation zu lesen und eine Testanwendung zu bauen, und war entsprechend überzeugt, dass es schnell gehen würde.
Es ging nicht schnell. Aber es ging aus anderen Gründen langsam als erwartet.
Was überraschend einfach war
Die Befehlslogik selbst. Fast jeder meiner Befehle bestand aus: Eingabe holen, Prüfung, Arbeit, Antwort. Nur der erste und der letzte Schritt hängen am Aufrufweg. Weil ich die Befehle im Vorjahr in einzelne Dateien mit klarer Struktur getrennt hatte, konnte ich für die meisten eine dünne Zwischenschicht bauen:
// Aus einer Interaktion wird dasselbe, was ein Textbefehl bekommt
function kontextAusInteraktion(interaktion) {
return {
gilde: interaktion.guild,
mitglied: interaktion.member,
kanal: interaktion.channel,
antworten: (inhalt) => interaktion.reply(inhalt),
nachreichen: (inhalt) => interaktion.editReply(inhalt),
};
}Damit liefen zwei Drittel der Befehle über beide Wege, ohne dass ich die Logik angefasst habe. Das war der Moment, in dem sich die Aufräumarbeit vom Vorjahr bezahlt gemacht hat — und zwar deutlich sichtbar, an einem einzigen Nachmittag.
Die Rechteprüfung. Anwendungsbefehle können auf Discord-Ebene mit Standardberechtigungen versehen werden. Ein Befehl, der ManageGuild verlangt, taucht bei anderen gar nicht erst auf. Das ersetzt keine serverseitige Prüfung — die bleibt Pflicht —, aber es verhindert die häufigste Frustration: einen Befehl zu sehen, ihn zu benutzen und dann zu erfahren, dass man nicht darf.
Was schwierig war
Die Anmeldung der Befehle. Anwendungsbefehle existieren nicht einfach, weil sie im Code stehen. Sie müssen bei Discord registriert werden, und dabei gibt es zwei Wege mit sehr unterschiedlichem Verhalten: global oder pro Server.
// pro Server: sofort sichtbar, ideal zum Testen
await rest.put(Routes.applicationGuildCommands(clientId, testGuildId), { body: befehle });
// global: gilt überall, braucht aber Zeit bis zur Verbreitung
await rest.put(Routes.applicationCommands(clientId), { body: befehle });Der Fehler, den ich zweimal gemacht habe: Befehle sowohl global als auch für den Testserver registriert. Ergebnis — jeder Befehl erscheint dort doppelt. Das sieht nach einem Fehler im Code aus und ist keiner.
Die Drei-Sekunden-Regel. Auf eine Interaktion muss man innerhalb von drei Sekunden antworten. Bei allem, was länger dauert — ein Bild zeichnen, eine externe Schnittstelle abfragen, eine große Datenbankabfrage —, muss man vorher melden, dass man dran ist:
await interaktion.deferReply();
const bild = await karteErzeugen(daten);
await interaktion.editReply({ files: [bild] });Das klingt nach einer Kleinigkeit. In der Praxis heißt es, dass man jeden einzelnen Befehl daraufhin ansehen muss, ob er es schaffen kann. Und die Antwort ist häufiger „nein", als man denkt: Eine Datenbankabfrage, die lokal drei Millisekunden braucht, kann unter Last plötzlich dreihundert brauchen.
Die Parameterdefinition. Bei Textbefehlen hat man Argumente zerlegt und interpretiert. Bei Anwendungsbefehlen muss man vorher deklarieren, was es gibt, welchen Typ es hat und ob es Pflicht ist. Das ist besser — aber es ist Arbeit, und zwar für jeden Befehl einzeln:
new SlashCommandBuilder()
.setName("warn")
.setDescription("Verwarnt ein Mitglied.")
.addUserOption((o) => o.setName("nutzer").setDescription("Wer?").setRequired(true))
.addStringOption((o) => o.setName("grund").setDescription("Warum?").setRequired(true))
.addIntegerOption((o) => o.setName("tage").setDescription("Verfällt nach … Tagen"));Der Teil, den ich unterschätzt habe: die Übergangszeit
Man kann nicht an einem Tag umstellen, wenn Leute den Bot benutzen. Also laufen beide Wege parallel, und zwar länger, als man will. Bei mir waren es gut sechs Monate.
Diese Zeit hat ihre eigenen Probleme:
- Jede Änderung muss in beiden Wegen ankommen. Ich habe zweimal einen Fehler nur im Slash-Weg behoben und mich gewundert, warum sich Leute weiter beschweren.
- Die Hilfe muss beides erklären, ohne Leute zu verwirren.
- Man muss irgendwann abschalten. Ich habe die Textbefehle mit einem Hinweis versehen („Dieser Befehl heißt jetzt
/warn"), das drei Monate laufen lassen und sie dann entfernt. Ohne Ankündigung wäre das unfreundlich gewesen, mit Ankündigung hat sich niemand beschwert.
Tipp
Bei Umstellungen dieser Art hilft ein Zähler mehr als jede Diskussion. Ich habe mitgeschrieben, wie oft ein Befehl über den alten und über den neuen Weg aufgerufen wurde. Als der alte Weg unter fünf Prozent lag, war die Entscheidung zum Abschalten keine Meinungsfrage mehr.
Was besser wurde, als ich dachte
Die Fehlerquote der Nutzer ist eingebrochen. Vorher war ein spürbarer Teil aller Befehlsaufrufe schlicht falsch getippt: fehlende Argumente, vertauschte Reihenfolge, falsche Erwähnung. Danach war das praktisch weg, weil die Plattform vorher prüft.
Und der Support wurde ruhiger. Nicht weil der Bot besser wurde, sondern weil sich Befehle jetzt selbst erklären. Man tippt einen Schrägstrich und sieht, was es gibt. Das ersetzt die Hälfte aller Fragen, die vorher an mich gingen.
Die Sache mit den Antwortarten
Ein Bereich, in dem Anwendungsbefehle deutlich mehr können als Textbefehle und den ich zunächst gar nicht genutzt habe: Wer sieht die Antwort?
Bei einer Textantwort sehen alle im Kanal mit. Bei einer Interaktion kann man wählen — sichtbar für alle oder nur für den Aufrufenden.
Das klingt nach Kosmetik und verändert die Bedienung erheblich:
- Eine Fehlermeldung sollte nur der sehen, der den Fehler gemacht hat. Vorher stand in jedem Kanal „Falsche Verwendung", was den Chat vollmüllte und die Person unnötig vorführte.
- Eine Einstellungsübersicht interessiert nur den, der sie abfragt.
- Ein Ergebnis, über das man reden will — eine Verlosung, eine Rangliste — gehört dagegen ausdrücklich in den Kanal.
Ich habe nach dieser Unterscheidung sämtliche Befehle einmal durchgesehen, und ungefähr die Hälfte hat den Sichtbarkeitsstatus gewechselt. Das war eine der wirkungsvollsten Änderungen des Jahres, obwohl sie am Verhalten nichts geändert hat.
Der Fehler, der mich am längsten beschäftigt hat
Ein Problem, das ich lange nicht verstanden habe: Manche Befehle antworteten sporadisch mit einem Fehler, obwohl sie funktionierten.
Die Ursache war die Drei-Sekunden-Regel, aber nicht in der offensichtlichen Form. Der Befehl selbst war schnell. Was gelegentlich langsam war, war die erste Datenbankabfrage nach einer Ruhephase — der Verbindungsaufbau, ein kalter Zwischenspeicher, ein Betriebssystem, das die Datei nicht mehr im Speicher hatte.
Im Normalbetrieb lag der Befehl bei vierzig Millisekunden. Einmal am Morgen bei viertausend.
Die Lösung war nicht, schneller zu werden, sondern die Antwort grundsätzlich aufzuschieben: erst melden, dass man dran ist, dann arbeiten, dann nachliefern. Seitdem gilt bei mir die Regel: Wenn ein Vorgang eine externe Abhängigkeit hat, wird die Antwort aufgeschoben — unabhängig davon, wie schnell er im Normalfall ist. Der Normalfall ist nicht das Problem.
Was ich als Muster mitgenommen habe
Diese Umstellung ist mein Standardbeispiel geworden, wenn es darum geht, warum sich Struktur auszahlt, die man vorher nicht braucht.
Als ich 2020 die Befehle in einzelne Dateien getrennt habe, hatte ich keinen konkreten Nutzen. Es war „sauberer", mehr nicht. Ein Jahr später hat genau diese Trennung die Umstellung von einem Neuschreiben zu einer Zwischenschicht gemacht.
Ich glaube trotzdem nicht, dass man auf Vorrat Struktur bauen sollte — das führt zu Abstraktionen, die nie gebraucht werden. Was funktioniert hat, war etwas anderes: Ich habe getrennt, was ich beim Arbeiten dauernd als getrennt empfunden habe. Der Aufrufweg hat mich immer gestört, wenn ich an der Logik arbeiten wollte. Diese Reibung war das Signal, nicht eine Vorhersage der Zukunft.