So erstellen Sie einen Discord-Bot: eine Übersicht und ein Tutorial
Veröffentlicht: 2022-03-11Discord ist eine Echtzeit-Messaging-Plattform, die sich selbst als „All-in-One-Voice- und Text-Chat für Gamer“ bezeichnet. Aufgrund seiner glatten Benutzeroberfläche, Benutzerfreundlichkeit und umfangreichen Funktionen hat Discord ein schnelles Wachstum erfahren und wird selbst bei Personen mit geringem Interesse an Videospielen immer beliebter. Zwischen Mai 2017 und Mai 2018 explodierte die Benutzerbasis von 45 Millionen Benutzern auf mehr als 130 Millionen, mit mehr als doppelt so vielen täglichen Benutzern wie Slack.
Eines der attraktivsten Merkmale von Discord aus Sicht eines Chatbot-Entwicklers ist seine robuste Unterstützung für programmierbare Bots, die dazu beitragen, Discord mit der Außenwelt zu integrieren und den Benutzern ein ansprechenderes Erlebnis zu bieten. Bots sind auf Discord allgegenwärtig und bieten eine breite Palette von Diensten, darunter Moderationsunterstützung, Spiele, Musik, Internetsuchen, Zahlungsabwicklung und mehr.
In diesem Discord-Bot-Tutorial beginnen wir mit der Erörterung der Discord-Benutzeroberfläche und ihrer REST- und WebSocket-APIs für Bots, bevor wir zu einem Tutorial übergehen, in dem wir einen einfachen Discord-Bot in JavaScript schreiben. Schließlich werden wir vom Entwickler des nach bestimmten Metriken beliebtesten Bots von Discord und seinen Erfahrungen mit der Entwicklung und Wartung seiner bedeutenden Infrastruktur und Codebasis hören.
Discord-Benutzeroberfläche
Bevor wir technische Details besprechen, ist es wichtig zu verstehen, wie ein Benutzer mit Discord interagiert und wie sich Discord den Benutzern präsentiert. Die Art und Weise, wie es sich Bots präsentiert, ist konzeptionell ähnlich (aber natürlich nicht visuell). Tatsächlich basieren die offiziellen Discord-Anwendungen auf denselben APIs, die Bots verwenden. Es ist technisch möglich, einen Bot mit geringfügigen Änderungen innerhalb eines regulären Benutzerkontos auszuführen, dies ist jedoch durch die Nutzungsbedingungen von Discord verboten. Bots müssen in Bot-Konten ausgeführt werden.
Hier ist ein Blick auf die Browserversion 1 der Discord-Anwendung, die in Chrome ausgeführt wird.
1 Die Discord-Benutzeroberfläche für die Desktop-Anwendung ist praktisch identisch mit der Webanwendung, die mit Electron verpackt ist. Die iOS-Anwendung wird mit React Native erstellt. Die Android-Anwendung ist nativer Android-Java-Code.
Lass es uns aufschlüsseln.
1. Serverliste
Ganz links ist die Liste der Server, bei denen ich Mitglied bin. Wenn Sie mit Slack vertraut sind, entspricht ein Server einem Slack-Workspace und stellt eine Gruppe von Benutzern dar, die in einem oder mehreren Kanälen auf dem Server miteinander interagieren können. Ein Server wird von seinem Ersteller und/oder dem Personal verwaltet, das sie auswählen und an das sie Verantwortlichkeiten delegieren. Der Ersteller und/oder das Personal definieren die Regeln, die Struktur der Kanäle auf dem Server und verwalten die Benutzer.
In meinem Fall steht der Discord-API -Server ganz oben auf meiner Serverliste. Es ist ein großartiger Ort, um Hilfe zu erhalten und mit anderen Entwicklern zu sprechen. Darunter befindet sich ein von mir erstellter Server namens Test . Wir werden den von uns erstellten Bot später dort testen. Darunter befindet sich eine Schaltfläche zum Erstellen eines neuen Servers. Jeder kann mit wenigen Klicks einen Server erstellen.
Beachten Sie, dass der Begriff, der in der Benutzeroberfläche von Discord verwendet wird, Server lautet, der Begriff, der in der Entwicklerdokumentation und der API verwendet wird, Guild lautet. Sobald wir über technische Themen sprechen, werden wir auf Gilden übergehen . Die beiden Begriffe sind austauschbar.
2. Kanalliste
Direkt rechts neben der Serverliste befindet sich die Liste der Kanäle für den Server, den ich gerade ansehe (in diesem Fall der Discord-API-Server). Kanäle können in eine beliebige Anzahl von Kategorien unterteilt werden. Auf dem Discord-API-Server umfassen die Kategorien INFORMATIONEN, ALLGEMEINES und LIBS, wie gezeigt. Jeder Kanal fungiert als Chatroom, in dem Benutzer diskutieren können, welchem Thema der Kanal gewidmet ist. Der Kanal, den wir gerade ansehen (Info), hat einen helleren Hintergrund. Kanäle mit neuen Nachrichten, seit wir sie zuletzt angesehen haben, haben eine weiße Textfarbe.
3. Kanalansicht
Dies ist die Kanalansicht, in der wir sehen können, worüber Benutzer in dem Kanal gesprochen haben, den wir gerade ansehen. Wir können hier eine Nachricht sehen, die nur teilweise sichtbar ist. Es ist eine Liste mit Links zu Support-Servern für einzelne Discord-Bot-Bibliotheken. Die Serveradministratoren haben diesen Kanal so konfiguriert, dass normale Benutzer wie ich darin keine Nachrichten senden können. Die Administratoren verwenden diesen Kanal als schwarzes Brett, um einige wichtige Informationen dort zu veröffentlichen, wo sie leicht zu sehen sind und nicht vom Chat übertönt werden.
4. Benutzerliste
Ganz rechts ist eine Liste der Benutzer, die derzeit auf diesem Server online sind. Die Benutzer sind in verschiedene Kategorien eingeteilt und ihre Namen haben unterschiedliche Farben. Dies ist ein Ergebnis der Rollen , die sie haben. Eine Rolle beschreibt, unter welcher Kategorie (falls vorhanden) der Benutzer angezeigt werden soll, welche Farbe sein Name haben soll und welche Berechtigungen er auf dem Server hat. Ein Benutzer kann mehr als eine Rolle haben (was sehr oft der Fall ist), und es gibt eine Vorrang-Mathematik, die bestimmt, was in diesem Fall passiert. Jeder Benutzer hat mindestens die @everyone-Rolle. Andere Rollen werden von Servermitarbeitern erstellt und zugewiesen.
5. Texteingabe
Dies ist die Texteingabe, in die ich Nachrichten eingeben und senden könnte, wenn ich dazu berechtigt wäre. Da ich keine Berechtigung zum Senden von Nachrichten in diesem Kanal habe, kann ich hier nichts eingeben.
6. Benutzer
Dies ist der aktuelle Benutzer. Ich habe meinen Benutzernamen auf „Ich“ gesetzt, damit ich nicht verwirrt werde und weil ich schlecht darin bin, Namen zu wählen. Unter meinem Benutzernamen ist eine Zahl (#9484), die mein Unterscheidungsmerkmal ist. Es mag viele andere Benutzer mit dem Namen „Me“ geben, aber ich bin der einzige „Me#9484“. Es ist mir auch möglich, pro Server einen Spitznamen für mich festzulegen, sodass ich auf verschiedenen Servern unter verschiedenen Namen bekannt bin.
Dies sind die grundlegenden Teile der Discord-Benutzeroberfläche, aber es gibt noch viel mehr. Es ist einfach, Discord zu verwenden, auch ohne ein Konto zu erstellen, also nimm dir ruhig eine Minute Zeit, um herumzustöbern. Sie können Discord betreten, indem Sie die Discord-Homepage besuchen, auf „Discord in einem Browser öffnen“ klicken, einen Benutzernamen auswählen und möglicherweise eine oder zwei erfrischende Runden von „Klicken Sie auf die Busbilder“ spielen.
Die Discord-API
Die Discord-API besteht aus zwei separaten Teilen: der WebSocket- und der REST-API. Im Großen und Ganzen wird die WebSocket-API verwendet, um Ereignisse von Discord in Echtzeit zu empfangen, während die REST-API verwendet wird, um Aktionen innerhalb von Discord auszuführen.
Die WebSocket-API
Die WebSocket-API wird verwendet, um Ereignisse von Discord zu empfangen, einschließlich Nachrichtenerstellung, Nachrichtenlöschung, Benutzer-Kick/Ban-Ereignisse, Benutzerberechtigungsaktualisierungen und vieles mehr. Die Kommunikation von einem Bot zur WebSocket-API ist dagegen eingeschränkter. Ein Bot verwendet die WebSocket-API, um eine Verbindung anzufordern, sich selbst zu identifizieren, Heartbeats durchzuführen, Sprachverbindungen zu verwalten und einige weitere grundlegende Dinge zu tun. Weitere Einzelheiten finden Sie in der Gateway-Dokumentation von Discord (eine einzelne Verbindung zur WebSocket-API wird als Gateway bezeichnet). Zum Ausführen anderer Aktionen wird die REST-API verwendet.
Ereignisse aus der WebSocket-API enthalten eine Nutzlast mit Informationen, die vom Typ des Ereignisses abhängen. Beispielsweise werden alle Message Create- Ereignisse von einem Benutzerobjekt begleitet, das den Autor der Nachricht darstellt. Das Benutzerobjekt allein enthält jedoch nicht alle Informationen, die es über den Benutzer zu wissen gibt. Beispielsweise sind keine Informationen über die Berechtigungen des Benutzers enthalten. Wenn Sie weitere Informationen benötigen, können Sie die REST-API danach fragen, aber aus Gründen, die im nächsten Abschnitt näher erläutert werden, sollten Sie im Allgemeinen stattdessen auf den Cache zugreifen, den Sie aus den von früheren Ereignissen erhaltenen Nutzlasten erstellt haben sollten. Es gibt eine Reihe von Ereignissen, die Payloads liefern, die für die Berechtigungen eines Benutzers relevant sind, einschließlich, aber nicht beschränkt auf Gildenerstellung , Gildenrollenaktualisierung und Kanalaktualisierung .
Ein Bot kann pro WebSocket-Verbindung in maximal 2.500 Gilden präsent sein. Damit ein Bot in mehr Gilden präsent sein kann, muss der Bot Sharding implementieren und mehrere separate WebSocket-Verbindungen zu Discord öffnen. Wenn Ihr Bot innerhalb eines einzelnen Prozesses auf einem einzelnen Knoten ausgeführt wird, ist dies nur eine zusätzliche Komplexität für Sie, die unnötig erscheinen mag. Aber wenn Ihr Bot sehr beliebt ist und sein Backend auf separate Knoten verteilt werden muss, macht die Sharding-Unterstützung von Discord dies viel einfacher als es sonst der Fall wäre.
Die REST-API
Die Discord-REST-API wird von Bots verwendet, um die meisten Aktionen auszuführen, z. B. das Senden von Nachrichten, das Kicken/Sperren von Benutzern und das Aktualisieren von Benutzerberechtigungen (weitgehend analog zu den von der WebSocket-API empfangenen Ereignissen). Die REST-API kann auch zum Abfragen von Informationen verwendet werden; Bots verlassen sich jedoch hauptsächlich auf Ereignisse von der WebSocket-API und speichern die von den WebSocket-Ereignissen erhaltenen Informationen zwischen.
Dafür gibt es mehrere Gründe. Das Abfragen der REST-API zum Abrufen von Benutzerinformationen jedes Mal, wenn ein Nachrichtenerstellungsereignis empfangen wird, wird beispielsweise aufgrund der Ratenbegrenzungen der REST-API nicht skaliert. Es ist in den meisten Fällen auch redundant, da die WebSocket-API die erforderlichen Informationen liefert und Sie sie in Ihrem Cache haben sollten.
Es gibt jedoch einige Ausnahmen, und Sie benötigen möglicherweise manchmal Informationen, die nicht in Ihrem Cache vorhanden sind. Wenn sich ein Bot zum ersten Mal mit einem WebSocket-Gateway verbindet, werden zunächst ein Bereit -Ereignis und ein Gildenerstellungsereignis pro Gilde, in der der Bot auf diesem Shard vorhanden ist, an den Bot gesendet, damit er seinen Cache mit dem aktuellen Status füllen kann. Die Gildenerstellungsereignisse für stark besetzte Gilden enthalten nur Informationen über Online-Benutzer. Wenn Ihr Bot Informationen über einen Offline-Benutzer abrufen muss, sind die relevanten Informationen möglicherweise nicht in Ihrem Cache vorhanden. In diesem Fall ist es sinnvoll, eine Anfrage an die REST-API zu stellen. Wenn Sie häufig Informationen über Offline-Benutzer benötigen, können Sie stattdessen einen Opcode „ Gildenmitglieder anfordern“ an die WebSocket-API senden, um Offline-Gildenmitglieder anzufordern.
Eine weitere Ausnahme ist, wenn Ihre Anwendung überhaupt nicht mit der WebSocket-API verbunden ist. Wenn Ihr Bot beispielsweise über ein Web-Dashboard verfügt, bei dem sich Benutzer anmelden und die Einstellungen des Bots auf ihrem Server ändern können. Das Web-Dashboard könnte in einem separaten Prozess ohne Verbindungen zur WebSocket-API und ohne Cache von Daten aus Discord ausgeführt werden. Es muss möglicherweise nur gelegentlich ein paar REST-API-Anforderungen stellen. In einem solchen Szenario ist es sinnvoll, sich auf die REST-API zu verlassen, um die benötigten Informationen zu erhalten.
API-Wrapper
Obwohl es immer eine gute Idee ist, ein gewisses Verständnis für jede Ebene Ihres Technologie-Stacks zu haben, ist die direkte Verwendung des Discord WebSocket und der REST-APIs zeitaufwändig, fehleranfällig, im Allgemeinen unnötig und in der Tat gefährlich.
Discord bietet eine kuratierte Liste offiziell geprüfter Bibliotheken und warnt davor:
Die Verwendung benutzerdefinierter Implementierungen oder nicht konformer Bibliotheken, die die API missbrauchen oder übermäßige Ratenbegrenzungen verursachen, kann zu einer dauerhaften Sperrung führen.
Die von Discord offiziell geprüften Bibliotheken sind im Allgemeinen ausgereift, gut dokumentiert und bieten eine vollständige Abdeckung der Discord-API. Die meisten Bot-Entwickler werden nie einen guten Grund haben, eine benutzerdefinierte Implementierung zu entwickeln, außer aus Neugier oder Mut!
Derzeit umfassen die offiziell geprüften Bibliotheken Implementierungen für Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust und Swift. Es kann zwei oder mehr verschiedene Bibliotheken für die Sprache Ihrer Wahl geben. Die Wahl, welche Sie verwenden möchten, kann eine schwierige Entscheidung sein. Zusätzlich zum Lesen der jeweiligen Dokumentation möchten Sie vielleicht dem inoffiziellen Discord-API-Server beitreten und ein Gefühl dafür bekommen, welche Art von Community hinter jeder Bibliothek steckt.
So erstellen Sie einen Discord-Bot
Kommen wir zur Sache. Wir werden einen Discord-Bot erstellen, der auf unserem Server rumhängt und auf Webhooks von Ko-fi lauscht. Ko-fi ist ein Service, mit dem Sie ganz einfach Spenden auf Ihr PayPal-Konto annehmen können. Es ist sehr einfach, dort Webhooks einzurichten, im Gegensatz zu PayPal, wo Sie ein Geschäftskonto benötigen, sodass es sich hervorragend für Demonstrationszwecke oder die Verarbeitung von Spenden in kleinem Umfang eignet.
Wenn ein Benutzer 10 $ oder mehr spendet, weist ihm der Bot die Rolle eines Premium Member
zu, die seine Namensfarbe ändert und ihn an die Spitze der Liste der Online-Benutzer verschiebt. Für dieses Projekt verwenden wir Node.js und eine Discord-API-Bibliothek namens Eris (Dokumentationslink: https://abal.moe/Eris/). Eris ist nicht die einzige JavaScript-Bibliothek. Sie können stattdessen discord.js wählen. Der Code, den wir schreiben werden, wäre in beiden Fällen sehr ähnlich.
Nebenbei stellt Patreon, ein weiterer Spendenverarbeiter, einen offiziellen Discord-Bot bereit und unterstützt die Konfiguration von Discord-Rollen als Beitragsvorteile. Wir werden etwas Ähnliches implementieren, aber natürlich einfacher.
Der Code für jeden Schritt des Tutorials ist auf GitHub (https://github.com/mistval/premium_bot) verfügbar. Bei einigen der in diesem Beitrag gezeigten Schritte wird aus Gründen der Kürze unveränderter Code weggelassen. Folgen Sie daher den bereitgestellten Links zu GitHub, wenn Sie der Meinung sind, dass Ihnen etwas fehlt.
Erstellen eines Bot-Kontos
Bevor wir mit dem Schreiben von Code beginnen können, benötigen wir ein Bot-Konto. Bevor wir ein Bot-Konto erstellen können, benötigen wir ein Benutzerkonto. Um ein Benutzerkonto zu erstellen, folgen Sie den Anweisungen hier.
Um dann ein Bot-Konto zu erstellen, gehen wir wie folgt vor:
1) Erstellen Sie eine Anwendung im Entwicklerportal.
2) Geben Sie einige grundlegende Details zur Anwendung ein (notieren Sie die hier angezeigte CLIENT-ID – wir benötigen sie später).
3) Fügen Sie einen Bot-Benutzer hinzu, der mit der Anwendung verbunden ist.
4) Schalten Sie den PUBLIC BOT-Schalter aus und notieren Sie sich das angezeigte Bot-Token (dieses benötigen wir später ebenfalls). Wenn Sie jemals Ihr Bot-Token verlieren, indem Sie es beispielsweise in einem Bild in einem Toptal-Blog-Beitrag veröffentlichen, müssen Sie es unbedingt sofort neu generieren. Jeder, der im Besitz Ihres Bot-Tokens ist, kann das Konto Ihres Bots kontrollieren und möglicherweise ernsthafte und dauerhafte Probleme für Sie und Ihre Benutzer verursachen.
5) Füge den Bot zu deiner Testgilde hinzu. Um einen Bot zu einer Gilde hinzuzufügen, ersetzen Sie seine Client-ID (zuvor gezeigt) durch den folgenden URI und navigieren Sie in einem Browser zu ihm.
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
Nachdem ich auf Autorisieren geklickt habe, befindet sich der Bot jetzt in meiner Testgilde und ich kann ihn in der Benutzerliste sehen. Es ist offline, aber wir werden das bald beheben.
Erstellen des Projekts
Angenommen, Sie haben Node.js installiert, erstellen Sie ein Projekt und installieren Sie Eris (die von uns verwendete Bot-Bibliothek), Express (ein Webanwendungs-Framework, das wir zum Erstellen eines Webhook-Listeners verwenden) und body-parser (zum Analysieren von Webhook-Texten). ).
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
Den Bot online und reaktionsfähig machen
Beginnen wir mit kleinen Schritten. Zuerst werden wir den Bot einfach online bringen und uns antworten. Wir können dies in 10-20 Codezeilen tun. Innerhalb einer neuen bot.js-Datei müssen wir eine Eris-Client-Instanz erstellen, ihr unser Bot-Token übergeben (erworben, als wir oben eine Bot-Anwendung erstellt haben), einige Ereignisse auf der Client-Instanz abonnieren und ihr mitteilen, dass sie sich mit Discord verbinden soll . Zu Demonstrationszwecken codieren wir unser Bot-Token fest in die bot.js-Datei, aber es empfiehlt sich, eine separate Konfigurationsdatei zu erstellen und sie von der Quellcodeverwaltung auszunehmen.
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step1.js)
const eris = require('eris'); // Create a Client instance with our bot token. const bot = new eris.Client('my_token'); // When the bot is connected and ready, log to console. bot.on('ready', () => { console.log('Connected and ready.'); }); // Every time a message is sent anywhere the bot is present, // this event will fire and we will check if the bot was mentioned. // If it was, the bot will attempt to respond with "Present". bot.on('messageCreate', async (msg) => { const botWasMentioned = msg.mentions.find( mentionedUser => mentionedUser.id === bot.user.id, ); if (botWasMentioned) { try { await msg.channel.createMessage('Present'); } catch (err) { // There are various reasons why sending a message may fail. // The API might time out or choke and return a 5xx status, // or the bot may not have permission to send the // message (403 status). console.warn('Failed to respond to mention.'); console.warn(err); } } }); bot.on('error', err => { console.warn(err); }); bot.connect();
Wenn alles gut geht, wenn Sie diesen Code mit Ihrem eigenen Bot-Token ausführen, Connected and ready.
wird auf der Konsole gedruckt und Sie sehen, wie Ihr Bot auf Ihrem Testserver online geht. Sie können Ihren Bot erwähnen, indem Sie entweder mit der rechten Maustaste darauf klicken und „Erwähnen“ auswählen, oder indem Sie seinen Namen mit vorangestelltem @ eingeben. Der Bot sollte antworten, indem er „Present“ sagt.
2 Erwähnung ist eine Möglichkeit, die Aufmerksamkeit eines anderen Benutzers zu erlangen, auch wenn er nicht anwesend ist. Ein normaler Benutzer wird, wenn erwähnt, per Desktop-Benachrichtigung, mobiler Push-Benachrichtigung und/oder einem kleinen roten Symbol, das über dem Symbol von Discord in der Taskleiste erscheint, benachrichtigt. Die Art und Weise, wie ein Benutzer benachrichtigt wird, hängt von seinen Einstellungen und seinem Online-Status ab. Bots hingegen erhalten keinerlei besondere Benachrichtigung, wenn sie erwähnt werden. Sie erhalten ein regelmäßiges Nachrichtenerstellungsereignis, wie sie es für jede andere Nachricht tun, und sie können die an das Ereignis angehängten Erwähnungen überprüfen, um festzustellen, ob sie erwähnt wurden.
Zahlungsbefehl aufzeichnen
Da wir nun wissen, dass wir einen Bot online stellen können, entfernen wir unseren aktuellen Message Create -Ereignishandler und erstellen einen neuen, mit dem wir den Bot darüber informieren können, dass wir eine Zahlung von einem Benutzer erhalten haben.
Um den Bot über die Zahlung zu informieren, geben wir einen Befehl aus, der so aussieht:
pb!addpayment @user_mention payment_amount
Beispiel: pb!addpayment @Me 10.00
, um eine von mir getätigte Zahlung in Höhe von 10,00 $ zu erfassen.
Die pb! Teil wird als Befehlspräfix bezeichnet. Es ist eine gute Konvention, ein Präfix zu wählen, mit dem alle Befehle an Ihren Bot beginnen müssen. Dies schafft ein gewisses Maß an Namensraum für Bots und hilft, Kollisionen mit anderen Bots zu vermeiden. Die meisten Bots enthalten einen Hilfebefehl, aber stellen Sie sich das Chaos vor, wenn Sie zehn Bots in Ihrer Gilde hätten und alle auf Ihre Hilfe antworten würden! Mit pb! als Präfix ist keine narrensichere Lösung, da es möglicherweise andere Bots gibt, die dasselbe Präfix verwenden. Bei den meisten beliebten Bots kann ihr Präfix pro Gilde konfiguriert werden, um Kollisionen zu vermeiden. Eine andere Möglichkeit besteht darin, die eigene Erwähnung des Bots als Präfix zu verwenden, obwohl dies die Ausgabe von Befehlen ausführlicher macht.
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step2.js)
const eris = require('eris'); const PREFIX = 'pb!'; const bot = new eris.Client('my_token'); const commandHandlerForCommandName = {}; commandHandlerForCommandName['addpayment'] = (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }; bot.on('messageCreate', async (msg) => { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts of the command and the command name const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the appropriate handler for the command, if there is one. const commandHandler = commandHandlerForCommandName[commandName]; if (!commandHandler) { return; } // Separate the command arguments from the command prefix and command name. const args = parts.slice(1); try { // Execute the command. await commandHandler(msg, args); } catch (err) { console.warn('Error handling command'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
Lass es uns versuchen.
Wir haben den Bot nicht nur dazu gebracht, auf den Befehl pb!addpayment
zu reagieren, sondern wir haben auch ein allgemeines Muster für die Verarbeitung von Befehlen erstellt. Wir können weitere Befehle hinzufügen, indem wir einfach weitere Handler zum Wörterbuch commandHandlerForCommandName
. Wir haben hier das Zeug zu einem einfachen Befehlsframework. Die Handhabung von Befehlen ist ein so grundlegender Bestandteil der Erstellung eines Bots, dass viele Leute geschriebene und Open-Source-Befehlsframeworks haben, die Sie verwenden könnten, anstatt Ihre eigenen zu schreiben. Mit Befehlsframeworks können Sie häufig Cooldowns, erforderliche Benutzerberechtigungen, Befehlsaliase, Befehlsbeschreibungen und Verwendungsbeispiele (für einen automatisch generierten Hilfebefehl) und mehr angeben. Eris wird mit einem integrierten Befehlsframework geliefert.
Apropos Berechtigungen, unser Bot hat ein kleines Sicherheitsproblem. Jeder kann den addpayment
Befehl ausführen. Beschränken wir es so, dass nur der Besitzer des Bots es verwenden kann. Wir werden das Wörterbuch commandHandlerForCommandName
und es JavaScript-Objekte als Werte enthalten lassen. Diese Objekte enthalten eine Eigenschaft „ execute
“ mit einem Befehlshandler und eine Eigenschaft botOwnerOnly
mit einem booleschen Wert. Wir codieren auch unsere Benutzer-ID fest in den Konstantenabschnitt des Bots, damit er weiß, wer sein Besitzer ist. Sie finden Ihre Benutzer-ID, indem Sie den Entwicklermodus in Ihren Discord-Einstellungen aktivieren, dann mit der rechten Maustaste auf Ihren Benutzernamen klicken und ID kopieren auswählen.

(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step3.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const bot = new eris.Client('my_token'); const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
Jetzt weigert sich der Bot wütend, den addpayment
Befehl auszuführen, wenn jemand anderes als der Bot-Eigentümer versucht, ihn auszuführen.
Lassen Sie uns als Nächstes den Bot jedem, der zehn Dollar oder mehr spendet, die Rolle eines Premium Member
zuweisen. Im oberen Teil der bot.js-Datei:
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step4.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };
Jetzt kann ich versuchen, pb!addpayment @Me 10.00
und der Bot sollte mir die Premium Member
zuweisen.
Hoppla, in der Konsole wird ein Fehler wegen fehlender Berechtigungen angezeigt.
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
Der Bot hat in der Testgilde nicht die Berechtigung „Rollen verwalten“, daher kann er keine Rollen erstellen oder zuweisen. Wir könnten dem Bot Administratorrechte geben und hätten diese Art von Problemen nie wieder, aber wie bei jedem System ist es am besten, einem Benutzer (oder in diesem Fall einem Bot) nur die minimalen Rechte zu geben, die er benötigt
Wir können dem Bot die Berechtigung „Rollen verwalten“ erteilen, indem wir eine Rolle in den Servereinstellungen erstellen, die Berechtigung „Rollen verwalten“ für diese Rolle aktivieren und die Rolle dem Bot zuweisen.
Wenn ich nun versuche, den Befehl erneut auszuführen, wird die Rolle erstellt und mir zugewiesen und ich habe eine schicke Namensfarbe und eine besondere Position in der Mitgliederliste.
Im Befehlshandler haben wir einen TODO-Kommentar, der darauf hinweist, dass wir nach ungültigen Argumenten suchen müssen. Kümmern wir uns jetzt darum.
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)
const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };
Hier ist der vollständige Code bisher:
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)
const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();
Dies sollte Ihnen eine gute grundlegende Vorstellung davon geben, wie Sie einen Discord-Bot erstellen. Jetzt werden wir sehen, wie man den Bot mit Ko-Fi integriert. Wenn Sie möchten, können Sie in Ihrem Dashboard bei Ko-fi einen Webhook erstellen, sicherstellen, dass Ihr Router für die Weiterleitung von Port 80 konfiguriert ist, und echte Live-Test-Webhooks an sich selbst senden. Aber ich werde Postman nur verwenden, um Anfragen zu simulieren.
Webhooks von Ko-fi liefern Payloads, die wie folgt aussehen:
data: { "message_id":"3a1fac0c-f960-4506-a60e-824979a74e74", "timestamp":"2017-08-21T13:04:30.7296166Z", "type":"Donation","from_name":"John Smith", "message":"Good luck with the integration!", "amount":"3.00", "url":"https://ko-fi.com" }
Lassen Sie uns eine neue Quelldatei mit dem Namen webhook_listener.js erstellen und Express verwenden, um auf Webhooks zu lauschen. Wir haben nur eine Express-Route, und dies dient zu Demonstrationszwecken, sodass wir uns nicht zu viele Gedanken über die Verwendung einer idiomatischen Verzeichnisstruktur machen müssen. Wir packen einfach die gesamte Webserver-Logik in eine Datei.
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/webhook_listener_step6.js)
const express = require('express'); const app = express(); const PORT = process.env.PORT || 80; class WebhookListener { listen() { app.get('/kofi', (req, res) => { res.send('Hello'); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;
Lassen Sie uns dann die neue Datei oben in bot.js anfordern, damit der Listener startet, wenn wir bot.js ausführen.
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
Nach dem Start des Bots sollten Sie „Hello“ sehen, wenn Sie in Ihrem Browser zu http://localhost/kofi navigieren.
Lassen Sie uns nun den WebhookListener
die Daten vom Webhook verarbeiten und ein Ereignis ausgeben. Und jetzt, da wir getestet haben, dass unser Browser auf die Route zugreifen kann, ändern wir die Route in eine POST-Route, da der Webhook von Ko-fi eine POST-Anforderung sein wird.
(GitHub-Code-Link: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)
const express = require('express'); const bodyParser = require('body-parser'); const EventEmitter = require('events'); const PORT = process.env.PORT || 80; const app = express(); app.use(bodyParser.json()); class WebhookListener extends EventEmitter { listen() { app.post('/kofi', (req, res) => { const data = req.body.data; const { message, timestamp } = data; const amount = parseFloat(data.amount); const senderName = data.from_name; const paymentId = data.message_id; const paymentSource = 'Ko-fi'; // The OK is just for us to see in Postman. Ko-fi doesn't care // about the response body, it just wants a 200. res.send({ status: 'OK' }); this.emit( 'donation', paymentSource, paymentId, timestamp, amount, senderName, message, ); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;
Next we need to have the bot listen for the event, decide which user donated, and assign them a role. To decide which user donated, we'll try to find a user whose username is a substring of the message received from Ko-fi. Donors must be instructed to provide their username (with the discriminator) in the message than they write when they make their donation.
At the bottom of bot.js:
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)
function findUserInString(str) { const lowercaseStr = str.toLowerCase(); // Look for a matching username in the form of username#discriminator. const user = bot.users.find( user => lowercaseStr.indexOf(`${user.username.toLowerCase()}#${user.discriminator}`) !== -1, ); return user; } async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await updateMemberRoleForDonation(guild, guildMember, amount); } catch (err) { console.warn('Error handling donation event.'); console.warn(err); } } webhookListener.on('donation', onDonation); bot.connect();
In the onDonation
function, we see two representations of a user: as a User, and as a Member. These both represent the same person, but the Member object contains guild-specific information about the User, such as their roles in the guild and their nickname. Since we want to add a role, we need to use the Member representation of the user. Each User in Discord has one Member representation for each guild that they are in.
Now I can use Postman to test the code.
I receive a 200 status code, and I get the role granted to me in the server.
If the message from Ko-fi does not contain a valid username; however, nothing happens. The donor doesn't get a role, and we are not aware that we received an orphaned donation. Let's add a log for logging donations, including donations that can't be attributed to a guild member.
First we need to create a log channel in Discord and get its channel ID. The channel ID can be found using the developer tools, which can be enabled in Discord's settings. Then you can right-click any channel and click “Copy ID.”
The log channel ID should be added to the constants section of bot.js.
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)
const LOG_CHANNEL_;
And then we can write a logDonation
function.
(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)
function logDonation(member, donationAmount, paymentSource, paymentId, senderName, message, timestamp) { const isKnownMember = !!member; const memberName = isKnownMember ? `${member.username}#${member.discriminator}` : 'Unknown'; const embedColor = isKnownMember ? 0x00ff00 : 0xff0000; const logMessage = { embed: { title: 'Donation received', color: embedColor, timestamp: timestamp, fields: [ { name: 'Payment Source', value: paymentSource, inline: true }, { name: 'Payment ID', value: paymentId, inline: true }, { name: 'Sender', value: senderName, inline: true }, { name: 'Donor Discord name', value: memberName, inline: true }, { name: 'Donation amount', value: donationAmount.toString(), inline: true }, { name: 'Message', value: message, inline: true }, ], } } bot.createMessage(LOG_CHANNEL_ID, logMessage); }
Now we can update onDonation
to call the log function:
async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await Promise.all([ updateMemberRoleForDonation(guild, guildMember, amount), logDonation(guildMember, amount, paymentSource, paymentId, senderName, message, timestamp), ]); } catch (err) { console.warn('Error updating donor role and logging donation'); console.warn(err); } }
Now I can invoke the webhook again, first with a valid username, and then without one, and I get two nice log messages in the log channel.
Previously, we were just sending strings to Discord to display as messages. The more complex JavaScript object that we create and send to Discord in the new logDonation
function is a special type of message referred to as a rich embed. An embed gives you some scaffolding for making attractive messages like those shown. Only bots can create embeds, users cannot.
Now we are being notified of donations, logging them, and rewarding our supporters. We can also add donations manually with the addpayment command in case a user forgets to specify their username when they donate. Let's call it a day.
The completed code for this tutorial is available on GitHub here https://github.com/mistval/premium_bot
Nächste Schritte
We've successfully created a bot that can help us track donations. Is this something we can actually use? Well, perhaps. It covers the basics, but not much more. Here are some shortcomings you might want to think about first:
- If a user leaves our guild (or if they weren't even in our guild in the first place), they will lose their
Premium Member
role, and if they rejoin, they won't get it back. We should store payments by user ID in a database, so if a premium member rejoins, we can give them their role back and maybe send them a nice welcome-back message if we were so inclined. - Paying in installments won't work. If a user sends $5 and then later sends another $5, they won't get a premium role. Similar to the above issue, storing payments in a database and issuing the
Premium Member
role when the total payments from a user reaches $10 would help here. - It's possible to receive the same webhook more than once, and this bot will record the payment multiple times. If Ko-fi doesn't receive or doesn't properly acknowledge a code 200 response from the webhook listener, it will try to send the webhook again later. Keeping track of payments in a database and ignoring webhooks with the same ID as previously received ones would help here.
- Our webhook listener isn't very secure. Anyone could forge a webhook and get a
Premium Member
role for free. Ko-fi doesn't seem to sign webhooks, so you'll have to rely on either no one knowing your webhook address (bad), or IP whitelisting (a bit better). - The bot is designed to be used in one guild only.
Interview: When a Bot Gets Big
There are over a dozen websites for listing Discord bots and making them available to the public at large, including DiscordBots.org and Discord.Bots.gg. Although Discord bots are mostly the foray of small-time hobbyists, some bots experience tremendous popularity and maintaining them evolves into a complex and demanding job.
By guild-count, Rythm is currently the most widespread bot on Discord. Rythm is a music bot whose specialty is connecting to voice channels in Discord and playing music requested by users. Rythm is currently present in over 2,850,000 guilds containing a sum population of around 90 million users, and at its peak plays audio for around 100,000 simultaneous users in 20,000 separate guilds. Rythm's creator and main developer, ImBursting, kindly agreed to answer a few questions about what it's like to develop and maintain a large-scale bot like Rythm.
Interviewer: Can you tell us a bit about Rythm's high level architecture and how it's hosted?
ImBursting: Rythm is scaled across 9 physical servers, each have 32 cores, 96GB of RAM and a 10gbps connection. These servers are collocated at a data center with help from a small hosting company, GalaxyGate.
I imagine that when you started working on Rythm, you didn't design it to scale anywhere near as much as it has. Can you tell us about about how Rythm started, and its technical evolution over time?
Rythm's first evolution was written in Python, which isn't a very performant language, so around the time we hit 10,000 servers (after many scaling attempts) I realised this was the biggest roadblock and so I began recoding the bot to Java, the reason being Java's audio libraries were a lot more optimised and it was generally a better suited language for such a huge application. After re-coding, performance improved tenfold and kept the issues at bay for a while. And then we hit the 300,000 servers milestone when issues started surfacing again, at which point I realised that more scaling was required since one JVM just wasn't able to handle all that. So we slowly started implementing improvements and major changes like tuning the garbage collector and splitting voice connections onto separate microservices using an open source server called Lavalink. This improved performance quite a bit but the final round of infrastructure was when we split this into 9 seperate clusters to run on 9 physical servers, and made custom gateway and stats microservices to make sure everything ran smoothly like it would on one machine.
I noticed that Rythm has a canary version and you get some help from other developers and staff. I imagine you and your team must put a lot of effort into making sure things are done right. Can you tell us about what processes are involved in updating Rythm?
Rythm canary is the alpha bot we use to test freshly made features and performance improvements before usually deploying them to Rythm 2 to test on a wider scale and then production Rythm. The biggest issue we encounter is really long reboot times due to Discord rate limits, and is the reason I try my best to make sure an update is ready before deciding to push it.
I do get a lot of help from volunteer developers and people who genuinely want to help the community, I want to make sure everything is done correctly and that people will always get their questions answered and get the best support possible which means im constantly on the lookout for new opportunities.
Wrapping It Up
Discord's days of being a new kid on the block are past, and it is now one of the largest real-time communication platforms in the world. While Discord bots are largely the foray of small-time hobbyists, we may well see commercial opportunities increase as the population of the service continues to increase. Some companies, like the aforementioned Patreon, have already waded in.
In this article, we saw a high-level overview of Discord's user interface, a high-level overview of its APIs, a complete lesson in Discord bot programming, and we got to hear about what it's like to operate a bot at enterprise scale. I hope you come away interested in the technology and feeling like you understand the fundamentals of how it works.
Chatbots are generally fun, except when their responses to your intricate queries have the intellectual the depth of a cup of water. To ensure a great UX for your users see The Chat Crash - When a Chatbot Fails by the Toptal Design Blog for 5 design problems to avoid.