Comment créer un bot Discord : un aperçu et un tutoriel
Publié: 2022-03-11Discord est une plate-forme de messagerie en temps réel qui se présente comme un "chat vocal et textuel tout-en-un pour les joueurs". En raison de son interface élégante, de sa facilité d'utilisation et de ses fonctionnalités étendues, Discord a connu une croissance rapide et devient de plus en plus populaire, même parmi ceux qui s'intéressent peu aux jeux vidéo. Entre mai 2017 et mai 2018, sa base d'utilisateurs a explosé de 45 millions d'utilisateurs à plus de 130 millions, avec plus de deux fois plus d'utilisateurs quotidiens que Slack.
L'une des fonctionnalités les plus attrayantes de Discord du point de vue d'un développeur de chatbot est sa prise en charge robuste des robots programmables qui aident à intégrer Discord au monde extérieur et offrent aux utilisateurs une expérience plus attrayante. Les robots sont omniprésents sur Discord et fournissent une large gamme de services, notamment une assistance à la modération, des jeux, de la musique, des recherches sur Internet, le traitement des paiements, etc.
Dans ce tutoriel sur le bot Discord, nous commencerons par discuter de l'interface utilisateur Discord et de ses API REST et WebSocket pour les bots avant de passer à un tutoriel où nous écrirons un simple bot Discord en JavaScript. Enfin, nous entendrons le développeur de, selon certaines mesures, le bot le plus populaire de Discord et ses expériences de développement et de maintenance de son infrastructure et de sa base de code importantes.
Interface utilisateur Discord
Avant de discuter des détails techniques, il est important de comprendre comment un utilisateur interagit avec Discord et comment Discord se présente aux utilisateurs. La façon dont il se présente aux bots est conceptuellement similaire (mais bien sûr non visuelle). En fait, les applications Discord officielles sont construites sur les mêmes API que les bots utilisent. Il est techniquement possible d'exécuter un bot à l'intérieur d'un compte d'utilisateur régulier avec peu de modifications, mais cela est interdit par les conditions d'utilisation de Discord. Les bots doivent s'exécuter dans des comptes de bot.
Voici un aperçu de la version 1 du navigateur de l'application Discord exécutée dans Chrome.
1 L'interface utilisateur Discord pour l'application de bureau est pratiquement la même que l'application Web, fournie avec Electron. L'application iOS est construite avec React Native. L'application Android est un code Java Android natif.
Décomposons-le.
1. Liste des serveurs
Tout à gauche se trouve la liste des serveurs dont je suis membre. Si vous connaissez Slack, un serveur est analogue à un espace de travail Slack et représente un groupe d'utilisateurs qui peuvent interagir les uns avec les autres dans un ou plusieurs canaux du serveur. Un serveur est géré par son créateur et/ou le personnel qu'il sélectionne et choisit de déléguer des responsabilités. Le créateur et/ou le personnel définissent les règles, la structure des canaux dans le serveur et gèrent les utilisateurs.
Dans mon cas, le serveur Discord API est en haut de ma liste de serveurs. C'est un endroit idéal pour obtenir de l'aide et discuter avec d'autres développeurs. En dessous se trouve un serveur que j'ai créé appelé Test . Nous testerons le bot que nous créons plus tard. En dessous se trouve un bouton pour créer un nouveau serveur. N'importe qui peut créer un serveur en quelques clics.
Notez que si le terme utilisé dans l'interface utilisateur de Discord est Server , le terme utilisé dans la documentation du développeur et l'API est Guild . Une fois que nous passerons à parler de sujets techniques, nous passerons à parler de guildes . Les deux termes sont interchangeables.
2. Liste des chaînes
Juste à droite de la liste des serveurs se trouve la liste des chaînes du serveur que je consulte actuellement (dans ce cas, le serveur Discord API). Les canaux peuvent être divisés en un nombre arbitraire de catégories. Dans le serveur Discord API, les catégories incluent INFORMATION, GENERAL et LIBS, comme indiqué. Chaque chaîne fonctionne comme une salle de discussion où les utilisateurs peuvent discuter de n'importe quel sujet auquel la chaîne est dédiée. La chaîne que nous regardons actuellement (info) a un fond plus clair. Les chaînes qui ont de nouveaux messages depuis la dernière fois que nous les avons consultés ont une couleur de texte blanche.
3. Affichage des canaux
Il s'agit de la vue de la chaîne où nous pouvons voir de quoi les utilisateurs ont parlé dans la chaîne que nous regardons actuellement. Nous pouvons voir un message ici, seulement partiellement visible. Il s'agit d'une liste de liens pour prendre en charge les serveurs des bibliothèques de bot Discord individuelles. Les administrateurs du serveur ont configuré ce canal afin que les utilisateurs réguliers comme moi ne puissent pas y envoyer de messages. Les administrateurs utilisent ce canal comme un tableau d'affichage pour publier des informations importantes où elles peuvent être facilement vues et ne seront pas noyées par le chat.
4. Liste des utilisateurs
Tout à droite se trouve une liste des utilisateurs actuellement en ligne sur ce serveur. Les utilisateurs sont organisés en différentes catégories et leurs noms ont des couleurs différentes. C'est le résultat des rôles qu'ils ont. Un rôle décrit dans quelle catégorie (le cas échéant) l'utilisateur doit apparaître, la couleur de son nom et les autorisations dont il dispose sur le serveur. Un utilisateur peut avoir plus d'un rôle (et c'est très souvent le cas), et il existe des calculs de priorité qui déterminent ce qui se passe dans ce cas. Au minimum, chaque utilisateur a le rôle @tout le monde. D'autres rôles sont créés et attribués par le personnel du serveur.
5. Saisie de texte
C'est l'entrée de texte où je pourrais taper et envoyer des messages, si j'y étais autorisé. Comme je n'ai pas la permission d'envoyer des messages sur ce canal, je ne peux pas taper ici.
6. Utilisateur
Il s'agit de l'utilisateur actuel. J'ai défini mon nom d'utilisateur sur "Moi", pour m'empêcher de me perdre et parce que je ne sais pas choisir les noms. Sous mon nom d'utilisateur se trouve un numéro (#9484) qui est mon discriminateur. Il peut y avoir de nombreux autres utilisateurs nommés "Moi", mais je suis le seul "Moi # 9484". Il m'est également possible de me définir un surnom pour chaque serveur, afin que je puisse être connu sous différents noms sur différents serveurs.
Ce sont les éléments de base de l'interface utilisateur de Discord, mais il y a aussi beaucoup plus. Il est facile de commencer à utiliser Discord même sans créer de compte, alors n'hésitez pas à prendre une minute pour fouiner. Vous pouvez entrer dans Discord en visitant la page d'accueil de Discord, en cliquant sur "ouvrir Discord dans un navigateur", en choisissant un nom d'utilisateur et éventuellement en jouant une ou deux rondes rafraîchissantes de "cliquez sur les images du bus".
L'API Discord
L'API Discord se compose de deux éléments distincts : les API WebSocket et REST. D'une manière générale, l'API WebSocket est utilisée pour recevoir des événements de Discord en temps réel, tandis que l'API REST est utilisée pour effectuer des actions à l'intérieur de Discord.
L'API WebSocket
L'API WebSocket est utilisée pour recevoir des événements de Discord, y compris la création de messages, la suppression de messages, les événements de kick/ban d'utilisateur, les mises à jour des autorisations d'utilisateur, et bien d'autres. La communication d'un bot vers l'API WebSocket est en revanche plus limitée. Un bot utilise l'API WebSocket pour demander une connexion, s'identifier, battre, gérer les connexions vocales et faire quelques autres choses fondamentales. Vous pouvez lire plus de détails dans la documentation de la passerelle de Discord (une seule connexion à l'API WebSocket est appelée passerelle). Pour effectuer d'autres actions, l'API REST est utilisée.
Les événements de l'API WebSocket contiennent une charge utile comprenant des informations qui dépendent du type de l'événement. Par exemple, tous les événements Message Create seront accompagnés d'un objet utilisateur représentant l'auteur du message. Cependant, l'objet utilisateur ne contient pas à lui seul toutes les informations à connaître sur l'utilisateur. Par exemple, aucune information n'est incluse sur les autorisations de l'utilisateur. Si vous avez besoin de plus d'informations, vous pouvez interroger l'API REST, mais pour des raisons expliquées plus en détail dans la section suivante, vous devez généralement accéder au cache que vous auriez dû créer à partir des charges utiles reçues des événements précédents. Il existe un certain nombre d'événements qui fournissent des charges utiles relatives aux autorisations d'un utilisateur, y compris, mais sans s'y limiter, Guild Create , Guild Role Update et Channel Update .
Un bot peut être présent dans un maximum de 2 500 guildes par connexion WebSocket. Afin de permettre à un bot d'être présent dans plus de guildes, le bot doit implémenter le sharding et ouvrir plusieurs connexions WebSocket distinctes vers Discord. Si votre bot s'exécute à l'intérieur d'un seul processus sur un seul nœud, il s'agit simplement d'une complexité supplémentaire pour vous qui peut sembler inutile. Mais si votre bot est très populaire et doit avoir son back-end réparti sur des nœuds séparés, la prise en charge du partage de Discord rend cela beaucoup plus facile qu'il ne le serait autrement.
L'API REST
L'API REST Discord est utilisée par les bots pour effectuer la plupart des actions, telles que l'envoi de messages, l'expulsion/l'interdiction d'utilisateurs et la mise à jour des autorisations des utilisateurs (en gros analogues aux événements reçus de l'API WebSocket). L'API REST peut également être utilisée pour demander des informations ; cependant, les robots s'appuient principalement sur les événements de l'API WebSocket et mettent en cache les informations reçues des événements WebSocket.
Il y a plusieurs raisons à cela. Interroger l'API REST pour obtenir des informations sur l'utilisateur chaque fois qu'un événement de création de message est reçu, par exemple, n'évolue pas en raison des limites de débit de l'API REST. C'est également redondant dans la plupart des cas, car l'API WebSocket fournit les informations nécessaires et vous devriez les avoir dans votre cache.
Il existe cependant quelques exceptions et vous pouvez parfois avoir besoin d'informations qui ne sont pas présentes dans votre cache. Lorsqu'un bot se connecte initialement à une passerelle WebSocket, un événement Ready et un événement Guild Create par guilde dans laquelle le bot est présent sur ce fragment sont initialement envoyés au bot afin qu'il puisse remplir son cache avec l'état actuel. Les événements de création de guilde pour les guildes fortement peuplées incluent uniquement des informations sur les utilisateurs en ligne. Si votre bot a besoin d'obtenir des informations sur un utilisateur hors ligne, les informations pertinentes peuvent ne pas être présentes dans votre cache. Dans ce cas, il est logique de faire une demande à l'API REST. Ou, si vous avez fréquemment besoin d'obtenir des informations sur les utilisateurs hors ligne, vous pouvez à la place choisir d'envoyer un opcode Request Guild Members à l'API WebSocket pour demander des membres de guilde hors ligne.
Une autre exception est si votre application n'est pas du tout connectée à l'API WebSocket. Par exemple, si votre bot dispose d'un tableau de bord Web auquel les utilisateurs peuvent se connecter et modifier les paramètres du bot sur leur serveur. Le tableau de bord Web peut s'exécuter dans un processus séparé sans aucune connexion à l'API WebSocket et sans cache de données de Discord. Il se peut qu'il n'ait besoin de faire qu'occasionnellement quelques requêtes d'API REST. Dans ce type de scénario, il est logique de s'appuyer sur l'API REST pour obtenir les informations dont vous avez besoin.
Emballages d'API
Bien qu'il soit toujours judicieux de comprendre chaque niveau de votre pile technologique, l'utilisation directe des API Discord WebSocket et REST prend du temps, est sujette aux erreurs, généralement inutile et en fait dangereuse.
Discord fournit une liste organisée de bibliothèques officiellement contrôlées et avertit que :
L'utilisation d'implémentations personnalisées ou de bibliothèques non conformes qui abusent de l'API ou entraînent des limites de débit excessives peuvent entraîner une interdiction permanente.
Les bibliothèques officiellement approuvées par Discord sont généralement matures, bien documentées et offrent une couverture complète de l'API Discord. La plupart des développeurs de robots n'auront jamais de bonne raison de développer une implémentation personnalisée, sauf par curiosité ou par bravoure !
À l'heure actuelle, les bibliothèques officiellement approuvées incluent des implémentations pour Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust et Swift. Il peut y avoir deux ou plusieurs bibliothèques différentes pour la langue de votre choix. Choisir lequel utiliser peut être une décision difficile. En plus de consulter la documentation respective, vous souhaiterez peut-être rejoindre le serveur non officiel de l'API Discord et avoir une idée du type de communauté qui se cache derrière chaque bibliothèque.
Comment créer un bot Discord
Nous allons passer aux choses sérieuses. Nous allons créer un bot Discord qui traîne sur notre serveur et écoute les webhooks de Ko-fi. Ko-fi est un service qui vous permet d'accepter facilement des dons sur votre compte PayPal. Il est très simple d'y configurer des webhooks, contrairement à PayPal où vous devez avoir un compte professionnel, il est donc idéal à des fins de démonstration ou de traitement des dons à petite échelle.
Lorsqu'un utilisateur fait un don de 10 $ ou plus, le bot lui attribue un rôle de Premium Member
qui change la couleur de son nom et le place en haut de la liste des utilisateurs en ligne. Pour ce projet, nous allons utiliser Node.js et une bibliothèque d'API Discord appelée Eris (lien de la documentation : https://abal.moe/Eris/). Eris n'est pas la seule bibliothèque JavaScript. Vous pouvez choisir discord.js à la place. Le code que nous allons écrire serait très similaire de toute façon.
En passant, Patreon, un autre processeur de dons, fournit un bot Discord officiel et prend en charge la configuration des rôles Discord en tant qu'avantages des contributeurs. Nous allons implémenter quelque chose de similaire, mais bien sûr plus basique.
Le code de chaque étape du tutoriel est disponible sur GitHub (https://github.com/mistval/premium_bot). Certaines des étapes présentées dans cet article omettent le code inchangé par souci de brièveté, alors suivez les liens fournis vers GitHub si vous pensez qu'il vous manque quelque chose.
Création d'un compte de bot
Avant de pouvoir commencer à écrire du code, nous avons besoin d'un compte bot. Avant de pouvoir créer un compte bot, nous avons besoin d'un compte utilisateur. Pour créer un compte utilisateur, suivez les instructions ici.
Ensuite, pour créer un compte bot, nous :
1) Créez une application dans le portail des développeurs.
2) Remplissez quelques détails de base sur l'application (notez l'ID CLIENT indiqué ici - nous en aurons besoin plus tard).
3) Ajoutez un utilisateur bot connecté à l'application.
4) Éteignez l'interrupteur PUBLIC BOT et notez le jeton de bot affiché (nous en aurons également besoin plus tard). Si jamais vous divulguez votre jeton de bot, par exemple en le publiant dans une image dans un article du blog Toptal, il est impératif que vous le régénériez immédiatement. Toute personne en possession de votre jeton de bot peut contrôler le compte de votre bot et causer des problèmes potentiellement graves et permanents pour vous et vos utilisateurs.
5) Ajoutez le bot à votre guilde de test. Pour ajouter un bot à une guilde, remplacez son ID client (affiché précédemment) par l'URI suivant et accédez-y dans un navigateur.
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
Après avoir cliqué sur Autoriser, le bot est maintenant dans ma guilde de test et je peux le voir dans la liste des utilisateurs. Il est hors ligne, mais nous corrigerons cela bientôt.
Création du projet
En supposant que vous avez installé Node.js, créez un projet et installez Eris (la bibliothèque de robots que nous utiliserons), Express (un framework d'application Web que nous utiliserons pour créer un écouteur de webhook) et body-parser (pour analyser les corps de webhook ).
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
Mettre le bot en ligne et réactif
Commençons par les petits pas. Tout d'abord, nous allons simplement mettre le bot en ligne et nous répondre. Nous pouvons le faire en 10 à 20 lignes de code. Dans un nouveau fichier bot.js, nous devons créer une instance Eris Client, lui transmettre notre jeton de bot (acquis lorsque nous avons créé une application de bot ci-dessus), souscrire à certains événements sur l'instance Client et lui dire de se connecter à Discord . À des fins de démonstration, nous allons coder en dur notre jeton de bot dans le fichier bot.js, mais créer un fichier de configuration séparé et l'exempter du contrôle de code source est une bonne pratique.
(Lien du code GitHub : 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();
Si tout se passe bien, lorsque vous exécutez ce code avec votre propre jeton de bot, Connected and ready.
sera imprimé sur la console et vous verrez votre bot se connecter sur votre serveur de test. Vous pouvez mentionner 2 votre bot soit en faisant un clic droit dessus et en sélectionnant « Mentionner », soit en tapant son nom précédé de @. Le bot doit répondre en disant "Présent".
2 Mentionner est un moyen d'attirer l'attention d'un autre utilisateur même s'il n'est pas présent. Un utilisateur régulier, lorsqu'il est mentionné, sera averti par une notification de bureau, une notification push mobile et / ou une petite icône rouge apparaissant sur l'icône de Discord dans la barre d'état système. La ou les manières dont un utilisateur est averti dépendent de ses paramètres et de son état en ligne. Les robots, en revanche, ne reçoivent aucune sorte de notification spéciale lorsqu'ils sont mentionnés. Ils reçoivent un événement de création de message régulier comme ils le font pour tout autre message, et ils peuvent vérifier les mentions jointes à l'événement pour déterminer si elles ont été mentionnées.
Enregistrer la commande de paiement
Maintenant que nous savons que nous pouvons mettre un bot en ligne, débarrassons-nous de notre gestionnaire d'événements Message Create actuel et créons-en un nouveau qui nous permet d'informer le bot que nous avons reçu un paiement d'un utilisateur.
Pour informer le bot du paiement, nous émettrons une commande qui ressemble à ceci :
pb!addpayment @user_mention payment_amount
Par exemple, pb!addpayment @Me 10.00
pour enregistrer un paiement de 10,00 $ effectué par Moi.
Le pb ! partie est appelée préfixe de commande. C'est une bonne convention de choisir un préfixe par lequel toutes les commandes de votre bot doivent commencer. Cela crée une mesure d'espacement de noms pour les bots et permet d'éviter les collisions avec d'autres bots. La plupart des bots incluent une commande d'aide, mais imaginez le bordel si vous aviez dix bots dans votre guilde et qu'ils répondaient tous à l' aide ! Utilisation pb! comme préfixe n'est pas une solution infaillible, car il peut y avoir d'autres bots qui utilisent également le même préfixe. Les bots les plus populaires permettent de configurer leur préfixe par guilde pour éviter les collisions. Une autre option consiste à utiliser la propre mention du bot comme préfixe, bien que cela rende l'émission de commandes plus détaillée.
(Lien du code GitHub : 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();
Essayons.
Non seulement nous avons fait en sorte que le bot réponde à la commande pb!addpayment
, mais nous avons également créé un modèle généralisé pour gérer les commandes. Nous pouvons ajouter plus de commandes simplement en ajoutant plus de gestionnaires au dictionnaire commandHandlerForCommandName
. Nous avons ici l'étoffe d'un cadre de commande simple. La gestion des commandes est un élément tellement fondamental de la création d'un bot que de nombreuses personnes ont écrit des frameworks de commandes open source que vous pouvez utiliser au lieu d'écrire les vôtres. Les frameworks de commande vous permettent souvent de spécifier les temps de recharge, les autorisations utilisateur requises, les alias de commande, les descriptions de commande et les exemples d'utilisation (pour une commande d'aide générée automatiquement), et plus encore. Eris est livré avec un cadre de commande intégré.

En parlant d'autorisations, notre bot a un petit problème de sécurité. N'importe qui peut exécuter la commande addpayment
. Limitons-le afin que seul le propriétaire du bot puisse l'utiliser. Nous allons refactoriser le dictionnaire commandHandlerForCommandName
et lui faire contenir des objets JavaScript comme valeurs. Ces objets contiendront une propriété execute
avec un gestionnaire de commandes et une propriété botOwnerOnly
avec une valeur booléenne. Nous allons également coder en dur notre ID utilisateur dans la section des constantes du bot afin qu'il sache qui est son propriétaire. Vous pouvez trouver votre ID utilisateur en activant le mode développeur dans vos paramètres Discord, puis en cliquant avec le bouton droit sur votre nom d'utilisateur et en sélectionnant Copier l'ID.
(Lien du code GitHub : 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();
Maintenant, le bot refusera avec colère d'exécuter la commande addpayment
si quelqu'un d'autre que le propriétaire du bot essaie de l'exécuter.
Laissons ensuite le bot attribuer un rôle de Premium Member
à toute personne qui fait un don de dix dollars ou plus. Dans la partie supérieure du fichier bot.js :
(Lien du code GitHub : 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), ]); }, };
Maintenant, je peux essayer de dire pb!addpayment @Me 10.00
et le bot devrait m'attribuer le rôle Premium Member
.
Oups, une erreur d'autorisations manquantes apparaît dans la console.
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
Le bot n'a pas l'autorisation Gérer les rôles dans la guilde de test, il ne peut donc pas créer ou attribuer des rôles. Nous pourrions donner au bot le privilège administrateur et nous n'aurions plus jamais ce genre de problème, mais comme pour tout système, il est préférable de ne donner à un utilisateur (ou dans ce cas un bot) que les privilèges minimum dont il a besoin
Nous pouvons donner au bot l'autorisation Gérer les rôles en créant un rôle dans les paramètres du serveur, en activant l'autorisation Gérer les rôles pour ce rôle et en attribuant le rôle au bot.
Maintenant, lorsque j'essaie d'exécuter à nouveau la commande, le rôle est créé et m'est attribué et j'ai une couleur de nom fantaisiste et une position spéciale dans la liste des membres.
Dans le gestionnaire de commandes, nous avons un commentaire TODO suggérant que nous devons vérifier les arguments non valides. Occupons-nous de cela maintenant.
(Lien du code GitHub : 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), ]); }, };
Voici le code complet jusqu'à présent :
(Lien du code GitHub : 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();
Cela devrait vous donner une bonne idée de base sur la façon de créer un bot Discord. Nous allons maintenant voir comment intégrer le bot avec Ko-fi. Si vous le souhaitez, vous pouvez créer un webhook dans votre tableau de bord sur Ko-fi, vous assurer que votre routeur est configuré pour transférer le port 80 et vous envoyer de vrais webhooks de test en direct. Mais je vais juste utiliser Postman pour simuler des requêtes.
Les webhooks de Ko-fi fournissent des charges utiles qui ressemblent à ceci :
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" }
Créons un nouveau fichier source appelé webhook_listener.js et utilisons Express pour écouter les webhooks. Nous n'aurons qu'un seul itinéraire Express, et ceci à des fins de démonstration, nous ne nous soucierons donc pas trop de l'utilisation d'une structure de répertoires idiomatique. Nous allons simplement mettre toute la logique du serveur Web dans un seul fichier.
(Lien du code GitHub : 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;
Exigons ensuite le nouveau fichier en haut de bot.js afin que l'écouteur démarre lorsque nous exécutons bot.js.
(Lien du code GitHub : https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
Après avoir démarré le bot, vous devriez voir "Bonjour" lorsque vous accédez à http://localhost/kofi dans votre navigateur.
Laissons maintenant le WebhookListener
traiter les données du webhook et émettre un événement. Et maintenant que nous avons testé que notre navigateur peut accéder à la route, changeons la route en une route POST, car le webhook de Ko-fi sera une requête POST.
(Lien du code GitHub : 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
Prochaines étapes
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.