Come creare un bot Discord: una panoramica e un tutorial
Pubblicato: 2022-03-11Discord è una piattaforma di messaggistica in tempo reale che si autodefinisce una "chat vocale e di testo all-in-one per i giocatori". Grazie alla sua interfaccia liscia, alla facilità d'uso e alle funzionalità estese, Discord ha registrato una rapida crescita e sta diventando sempre più popolare anche tra coloro che hanno scarso interesse per i videogiochi. Tra maggio 2017 e maggio 2018, la sua base di utenti è esplosa da 45 milioni a oltre 130 milioni, con più del doppio degli utenti giornalieri di Slack.
Una delle caratteristiche più interessanti di Discord dal punto di vista di uno sviluppatore di chatbot è il suo solido supporto per i robot programmabili che aiutano a integrare Discord con il mondo esterno e offrono agli utenti un'esperienza più coinvolgente. I bot sono onnipresenti su Discord e forniscono un'ampia gamma di servizi, tra cui assistenza per la moderazione, giochi, musica, ricerche su Internet, elaborazione dei pagamenti e altro ancora.
In questo tutorial sul bot Discord, inizieremo discutendo l'interfaccia utente di Discord e le sue API REST e WebSocket per i bot prima di passare a un tutorial in cui scriveremo un semplice bot Discord in JavaScript. Infine, ascolteremo lo sviluppatore di, in base a determinate metriche, il bot più popolare di Discord e le sue esperienze nello sviluppo e nel mantenimento della sua significativa infrastruttura e base di codice.
Interfaccia utente Discord
Prima di discutere i dettagli tecnici, è importante capire come un utente interagisce con Discord e come Discord si presenta agli utenti. Il modo in cui si presenta ai robot è concettualmente simile (ma ovviamente non visivo). In effetti, le applicazioni Discord ufficiali sono basate sulle stesse API utilizzate dai bot. È tecnicamente possibile eseguire un bot all'interno di un normale account utente con poche modifiche, ma ciò è vietato dai termini di servizio di Discord. I bot devono essere eseguiti negli account bot.
Diamo un'occhiata alla versione 1 del browser dell'applicazione Discord in esecuzione all'interno di Chrome.
1 L'interfaccia utente di Discord per l'applicazione desktop è praticamente la stessa dell'applicazione Web, inclusa nel pacchetto Electron. L'applicazione iOS è realizzata con React Native. L'applicazione Android è un codice Java Android nativo.
Analizziamolo.
1. Elenco dei server
Tutto a sinistra c'è l'elenco dei server di cui sono membro. Se hai familiarità con Slack, un server è analogo a un'area di lavoro Slack e rappresenta un gruppo di utenti che possono interagire tra loro all'interno di uno o più canali del server. Un server è gestito dal suo creatore e/o dal personale che seleziona e sceglie di delegare le responsabilità. Il creatore e/o lo staff definiscono le regole, la struttura dei canali nel server e gestiscono gli utenti.
Nel mio caso, il server Discord API è in cima all'elenco dei miei server. È un ottimo posto per ottenere aiuto e parlare con altri sviluppatori. Sotto c'è un server che ho creato chiamato Test . Testeremo il bot che creeremo più avanti lì. Sotto c'è un pulsante per creare un nuovo server. Chiunque può creare un server con pochi clic.
Nota che mentre il termine utilizzato nell'interfaccia utente di Discord è Server , il termine utilizzato nella documentazione per sviluppatori e nell'API è Guild . Una volta che si passerà a parlare di argomenti tecnici, si passerà a parlare di Gilde . I due termini sono intercambiabili.
2. Elenco canali
Appena a destra dell'elenco dei server c'è l'elenco dei canali per il server che sto attualmente visualizzando (in questo caso, il server Discord API). I canali possono essere suddivisi in un numero arbitrario di categorie. Nel server Discord API, le categorie includono INFORMAZIONI, GENERALI e LIBS, come mostrato. Ogni canale funziona come una chat room in cui gli utenti possono discutere qualsiasi argomento a cui il canale è dedicato. Il canale che stiamo visualizzando (info) ha uno sfondo più chiaro. I canali che hanno nuovi messaggi dall'ultima volta che li abbiamo visti hanno un colore di testo bianco.
3. Vista canale
Questa è la visualizzazione del canale in cui possiamo vedere di cosa hanno parlato gli utenti nel canale che stiamo attualmente visualizzando. Possiamo vedere un messaggio qui, solo parzialmente visibile. È un elenco di collegamenti per supportare i server per le singole librerie di bot Discord. Gli amministratori del server hanno configurato questo canale in modo che gli utenti regolari come me non possano inviare messaggi al suo interno. Gli amministratori utilizzano questo canale come bacheca per pubblicare alcune informazioni importanti in cui possono essere facilmente visualizzate e non saranno soffocate dalla chat.
4. Elenco utenti
Tutto a destra è un elenco degli utenti attualmente online in questo server. Gli utenti sono organizzati in diverse categorie e i loro nomi hanno colori diversi. Questo è il risultato dei ruoli che hanno. Un ruolo descrive in quale categoria (se presente) dovrebbe apparire l'utente, quale dovrebbe essere il colore del suo nome e quali autorizzazioni ha nel server. Un utente può avere più di un ruolo (e molto spesso lo fa) e c'è una matematica di precedenza che determina cosa succede in quel caso. Come minimo, ogni utente ha il ruolo @everyone. Altri ruoli vengono creati e assegnati dal personale del server.
5. Input di testo
Questo è l'input di testo in cui potrei digitare e inviare messaggi, se consentito. Dal momento che non ho l'autorizzazione per inviare messaggi in questo canale, non posso digitare qui.
6. Utente
Questo è l'utente corrente. Ho impostato il mio nome utente su "Io", per evitare di confondermi e perché sono pessimo nello scegliere i nomi. Sotto il mio nome utente c'è un numero (#9484) che è il mio discriminatore. Potrebbero esserci molti altri utenti chiamati "Me", ma io sono l'unico "Me#9484". È anche possibile per me impostare un nickname per me stesso in base al server, così posso essere conosciuto con nomi diversi su server diversi.
Queste sono le parti di base dell'interfaccia utente di Discord, ma c'è anche molto di più. È facile iniziare a utilizzare Discord anche senza creare un account, quindi sentiti libero di prendere un minuto per dare un'occhiata. Puoi accedere a Discord visitando la home page di Discord, facendo clic su "apri Discord in un browser", scegliendo un nome utente e possibilmente giocando uno o due round rinfrescanti di "fai clic sulle immagini dell'autobus".
L'API Discordia
L'API Discord è composta da due parti separate: WebSocket e API REST. In generale, l'API WebSocket viene utilizzata per ricevere eventi da Discord in tempo reale, mentre l'API REST viene utilizzata per eseguire azioni all'interno di Discord.
L'API WebSocket
L'API WebSocket viene utilizzata per ricevere eventi da Discord, inclusa la creazione di messaggi, l'eliminazione dei messaggi, gli eventi di kick/ban degli utenti, gli aggiornamenti delle autorizzazioni degli utenti e molti altri. La comunicazione da un bot all'API WebSocket è invece più limitata. Un bot utilizza l'API WebSocket per richiedere una connessione, identificarsi, eseguire il battito cardiaco, gestire le connessioni vocali e fare alcune altre cose fondamentali. Puoi leggere maggiori dettagli nella documentazione del gateway di Discord (una singola connessione all'API WebSocket viene definita gateway). Per eseguire altre azioni, viene utilizzata l'API REST.
Gli eventi dell'API WebSocket contengono un payload che include informazioni che dipendono dal tipo di evento. Ad esempio, tutti gli eventi di creazione del messaggio saranno accompagnati da un oggetto utente che rappresenta l'autore del messaggio. Tuttavia, l'oggetto utente da solo non contiene tutte le informazioni che c'è da sapere sull'utente. Ad esempio, non sono incluse informazioni sulle autorizzazioni dell'utente. Se hai bisogno di maggiori informazioni, puoi interrogare l'API REST per questo, ma per motivi spiegati più avanti nella sezione successiva, dovresti invece accedere generalmente alla cache che dovresti aver creato dai payload ricevuti da eventi precedenti. Esistono numerosi eventi che forniscono payload rilevanti per le autorizzazioni di un utente, inclusi, a titolo esemplificativo ma non esaustivo, Creazione gilda, Aggiornamento ruolo gilda e Aggiornamento canale .
Un bot può essere presente in un massimo di 2.500 gilde per connessione WebSocket. Per consentire a un bot di essere presente in più gilde, il bot deve implementare lo sharding e aprire diverse connessioni WebSocket separate a Discord. Se il tuo bot viene eseguito all'interno di un singolo processo su un singolo nodo, questa è solo una complessità aggiuntiva per te che potrebbe sembrare non necessaria. Ma se il tuo bot è molto popolare e deve avere il suo back-end distribuito su nodi separati, il supporto per lo sharding di Discord lo rende molto più semplice di quanto sarebbe altrimenti.
L'API REST
L'API Discord REST viene utilizzata dai bot per eseguire la maggior parte delle azioni, come l'invio di messaggi, il kick/banning degli utenti e l'aggiornamento delle autorizzazioni utente (sostanzialmente analogo agli eventi ricevuti dall'API WebSocket). L'API REST può essere utilizzata anche per richiedere informazioni; tuttavia, i bot si basano principalmente sugli eventi dell'API WebSocket e memorizzano nella cache le informazioni ricevute dagli eventi WebSocket.
Ci sono diverse ragioni per questo. L'esecuzione di query sull'API REST per ottenere informazioni sull'utente ogni volta che viene ricevuto un evento di creazione del messaggio , ad esempio, non viene ridimensionata a causa dei limiti di frequenza dell'API REST. È anche ridondante nella maggior parte dei casi, poiché l'API WebSocket fornisce le informazioni necessarie e dovresti averle nella cache.
Ci sono alcune eccezioni, tuttavia, e a volte potresti aver bisogno di informazioni che non sono presenti nella tua cache. Quando un bot si connette inizialmente a un gateway WebSocket, un evento Ready e un evento Guild Create per gilda in cui il bot è presente su quel frammento vengono inizialmente inviati al bot in modo che possa popolare la sua cache con lo stato corrente. Gli eventi Guild Create per le gilde densamente popolate includono solo informazioni sugli utenti online. Se il tuo bot ha bisogno di ottenere informazioni su un utente offline, le informazioni rilevanti potrebbero non essere presenti nella tua cache. In questo caso, ha senso fare una richiesta all'API REST. Oppure, se ti trovi spesso a dover ottenere informazioni sugli utenti offline, puoi invece scegliere di inviare un codice operativo Richiedi membri della gilda all'API WebSocket per richiedere membri della gilda offline.
Un'altra eccezione è se l'applicazione non è affatto connessa all'API WebSocket. Ad esempio, se il tuo bot ha una dashboard web a cui gli utenti possono accedere e modificare le impostazioni del bot nel loro server. Il dashboard Web potrebbe essere eseguito in un processo separato senza alcuna connessione all'API WebSocket e senza cache di dati da Discord. Potrebbe essere necessario effettuare solo occasionalmente alcune richieste API REST. In questo tipo di scenario, ha senso fare affidamento sull'API REST per ottenere le informazioni necessarie.
Wrapper API
Sebbene sia sempre una buona idea avere una certa comprensione di ogni livello del tuo stack tecnologico, l'utilizzo diretto di Discord WebSocket e delle API REST è dispendioso in termini di tempo, soggetto a errori, generalmente non necessario e di fatto pericoloso.
Discord fornisce un elenco curato di biblioteche ufficialmente controllate e avverte che:
L'utilizzo di implementazioni personalizzate o librerie non conformi che abusano dell'API o causano limiti di velocità eccessivi può comportare un divieto permanente.
Le librerie ufficialmente controllate da Discord sono generalmente mature, ben documentate e offrono una copertura completa dell'API Discord. La maggior parte degli sviluppatori di bot non avrà mai una buona ragione per sviluppare un'implementazione personalizzata, se non per curiosità o coraggio!
Al momento, le librerie ufficialmente controllate includono implementazioni per Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust e Swift. Potrebbero esserci due o più librerie diverse per la tua lingua preferita. Scegliere quale utilizzare può essere una decisione difficile. Oltre a controllare la rispettiva documentazione, potresti voler unirti al server dell'API Discord non ufficiale e avere un'idea del tipo di comunità dietro ogni libreria.
Come creare un bot Discord
Andiamo al sodo. Creeremo un bot Discord che si blocca nel nostro server e ascolta i webhook da Ko-fi. Ko-fi è un servizio che ti permette di accettare facilmente donazioni sul tuo conto PayPal. È molto semplice impostare webhook lì, al contrario di PayPal dove è necessario disporre di un account aziendale, quindi è ottimo per scopi dimostrativi o per l'elaborazione di donazioni su piccola scala.
Quando un utente dona $ 10 o più, il bot gli assegnerà un ruolo di Premium Member
che cambia il colore del suo nome e lo sposta in cima all'elenco degli utenti online. Per questo progetto, utilizzeremo Node.js e una libreria di API Discord chiamata Eris (link alla documentazione: https://abal.moe/Eris/). Eris non è l'unica libreria JavaScript. Potresti invece scegliere discord.js. Il codice che scriveremo sarebbe molto simile in entrambi i casi.
Per inciso, Patreon, un altro elaboratore di donazioni, fornisce un bot Discord ufficiale e supporta la configurazione dei ruoli Discord come vantaggi per i contributori. Implementeremo qualcosa di simile, ma ovviamente più basilare.
Il codice per ogni passaggio del tutorial è disponibile su GitHub (https://github.com/mistval/premium_bot). Alcuni dei passaggi mostrati in questo post omettono il codice invariato per brevità, quindi segui i collegamenti forniti a GitHub se pensi che potresti perderti qualcosa.
Creazione di un account bot
Prima di poter iniziare a scrivere codice, abbiamo bisogno di un account bot. Prima di poter creare un account bot, abbiamo bisogno di un account utente. Per creare un account utente, segui le istruzioni qui.
Quindi, per creare un account bot, noi:
1) Crea un'applicazione nel portale per sviluppatori.
2) Inserisci alcuni dettagli di base sull'applicazione (nota l'ID CLIENTE mostrato qui: ne avremo bisogno in seguito).
3) Aggiungere un utente bot connesso all'applicazione.
4) Disattiva l'interruttore PUBLIC BOT e prendi nota del token del bot mostrato (ci servirà anche in seguito). Se perdi il token del tuo bot, ad esempio pubblicandolo in un'immagine in un post del blog Toptal, è imperativo rigenerarlo immediatamente. Chiunque sia in possesso del token del tuo bot può controllare l'account del tuo bot e causare problemi potenzialmente seri e permanenti a te e ai tuoi utenti.
5) Aggiungi il bot alla tua gilda di prova. Per aggiungere un bot a una gilda, sostituisci il suo ID client (mostrato in precedenza) nel seguente URI e vai ad esso in un browser.
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
Dopo aver fatto clic su autorizza, il bot è ora nella mia gilda di test e posso vederlo nell'elenco degli utenti. È offline, ma lo sistemeremo presto.
Creazione del progetto
Supponendo che tu abbia installato Node.js, crea un progetto e installa Eris (la libreria di bot che useremo), Express (un framework di applicazioni web che useremo per creare un listener di webhook) e body-parser (per analizzare i corpi di webhook ).
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
Ottenere il bot online e reattivo
Cominciamo con i piccoli passi. Per prima cosa otterremo il bot online e ci risponderemo. Possiamo farlo in 10-20 righe di codice. All'interno di un nuovo file bot.js, dobbiamo creare un'istanza Eris Client, passarla al nostro token bot (acquisito quando abbiamo creato un'applicazione bot sopra), iscriverci ad alcuni eventi sull'istanza Client e dirgli di connettersi a Discord . A scopo dimostrativo, codificheremo il nostro token bot nel file bot.js, ma è buona norma creare un file di configurazione separato ed esentarlo dal controllo del codice sorgente.
(Link al codice 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();
Se tutto va bene, quando esegui questo codice con il tuo token bot, Connected and ready.
verrà stampato sulla console e vedrai il tuo bot online nel tuo server di prova. Puoi menzionare 2 il tuo bot facendo clic con il pulsante destro del mouse e selezionando "Menziona" oppure digitandone il nome preceduto da @. Il bot dovrebbe rispondere dicendo "Presente".
2 La menzione è un modo per attirare l'attenzione di un altro utente anche se non è presente. Un utente normale, quando menzionato, riceverà una notifica tramite notifica desktop, notifica push mobile e/o una piccola icona rossa che apparirà sopra l'icona di Discord nella barra delle applicazioni. Le modalità di notifica di un utente dipendono dalle sue impostazioni e dal suo stato online. I bot, invece, non ricevono alcun tipo di notifica speciale quando vengono menzionati. Ricevono un evento di creazione messaggio regolare come fanno per qualsiasi altro messaggio e possono controllare le menzioni allegate all'evento per determinare se sono stati menzionati.
Registra il comando di pagamento
Ora che sappiamo che possiamo ottenere un bot online, sbarazziamoci del nostro attuale gestore di eventi Message Create e creiamone uno nuovo che ci consenta di informare il bot che abbiamo ricevuto il pagamento da un utente.
Per informare il bot del pagamento, emetteremo un comando simile al seguente:
pb!addpayment @user_mention payment_amount
Ad esempio, pb!addpayment @Me 10.00
per registrare un pagamento di $ 10.00 effettuato da me.
Il p! parte è indicato come un prefisso di comando. È una buona convenzione scegliere un prefisso con cui devono iniziare tutti i comandi del tuo bot. Questo crea una misura dello spazio dei nomi per i bot e aiuta a evitare collisioni con altri bot. La maggior parte dei robot include un comando di aiuto, ma immagina il casino se avessi dieci robot nella tua gilda e tutti rispondessero per aiutare ! Usando pb! come prefisso non è una soluzione infallibile, poiché potrebbero esserci altri bot che utilizzano anche lo stesso prefisso. I bot più popolari consentono di configurare il loro prefisso in base alla gilda per aiutare a prevenire le collisioni. Un'altra opzione è usare la citazione del bot come prefisso, anche se questo rende l'emissione di comandi più prolissa.
(Link al codice 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();
Proviamolo.
Non solo abbiamo fatto in modo che il bot rispondesse al comando pb!addpayment
, ma abbiamo anche creato un modello generalizzato per la gestione dei comandi. Possiamo aggiungere più comandi semplicemente aggiungendo più gestori al dizionario commandHandlerForCommandName
. Abbiamo le caratteristiche di un semplice framework di comandi qui. La gestione dei comandi è una parte così fondamentale della creazione di un bot che molte persone hanno scritto e open source framework di comandi che potresti usare invece di scriverne di tuoi. I framework dei comandi spesso consentono di specificare tempi di attesa, autorizzazioni utente richieste, alias di comandi, descrizioni di comandi ed esempi di utilizzo (per un comando di aiuto generato automaticamente) e altro ancora. Eris viene fornito con un framework di comandi integrato.
A proposito di autorizzazioni, il nostro bot ha un piccolo problema di sicurezza. Chiunque può eseguire il comando addpayment
. Limitiamolo in modo che solo il proprietario del bot possa usarlo. Faremo il refactoring del dizionario commandHandlerForCommandName
e faremo in modo che contenga oggetti JavaScript come valori. Tali oggetti conterranno una proprietà di execute
con un gestore di comandi e una proprietà botOwnerOnly
con un valore booleano. Codificheremo anche il nostro ID utente nella sezione delle costanti del bot in modo che sappia chi è il suo proprietario. Puoi trovare il tuo ID utente abilitando la Modalità sviluppatore nelle impostazioni di Discord, quindi facendo clic con il pulsante destro del mouse sul tuo nome utente e selezionando Copia ID.

(Link al codice 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();
Ora il bot si rifiuterà con rabbia di eseguire il comando addpayment
se qualcuno diverso dal proprietario del bot tenta di eseguirlo.
Quindi facciamo in modo che il bot assegni un ruolo di Premium Member
a chiunque doni dieci dollari o più. Nella parte superiore del file bot.js:
(Link al codice 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), ]); }, };
Ora posso provare a dire pb!addpayment @Me 10.00
e il bot dovrebbe assegnarmi il ruolo di Premium Member
.
Spiacenti, nella console viene visualizzato un errore di autorizzazioni mancanti.
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
Il bot non dispone dell'autorizzazione Gestisci ruoli nella gilda di test, quindi non può creare o assegnare ruoli. Potremmo concedere al bot il privilegio di amministratore e non avremmo mai più questo tipo di problema, ma come con qualsiasi sistema, è meglio dare a un utente (o in questo caso un bot) solo i privilegi minimi di cui ha bisogno
Possiamo concedere al bot l'autorizzazione Gestisci ruoli creando un ruolo nelle impostazioni del server, abilitando l'autorizzazione Gestisci ruoli per quel ruolo e assegnando il ruolo al bot.
Ora, quando provo a eseguire di nuovo il comando, il ruolo viene creato e assegnato a me e ho un colore del nome di fantasia e una posizione speciale nell'elenco dei membri.
Nel gestore dei comandi, abbiamo un commento TODO che suggerisce che è necessario verificare la presenza di argomenti non validi. Prendiamocene cura ora.
(Link al codice 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), ]); }, };
Ecco il codice completo finora:
(Link al codice 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();
Questo dovrebbe darti una buona idea di base su come creare un bot Discord. Ora vedremo come integrare il bot con Ko-fi. Se lo desideri, puoi creare un webhook nella dashboard di Ko-fi, assicurarti che il tuo router sia configurato per inoltrare la porta 80 e inviare a te stesso webhook di prova dal vivo. Ma userò Postman per simulare le richieste.
I webhook di Ko-fi forniscono payload simili a questo:
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" }
Creiamo un nuovo file sorgente chiamato webhook_listener.js e usiamo Express per ascoltare i webhook. Avremo solo un percorso Express, e questo è a scopo dimostrativo, quindi non ci preoccuperemo troppo dell'uso di una struttura di directory idiomatica. Metteremo semplicemente tutta la logica del server web in un file.
(Link al codice 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;
Quindi richiediamo il nuovo file nella parte superiore di bot.js in modo che il listener si avvii quando eseguiamo bot.js.
(Link al codice GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
Dopo aver avviato il bot, dovresti vedere "Hello" quando accedi a http://localhost/kofi nel tuo browser.
Ora facciamo in modo che WebhookListener
elabori i dati dal webhook ed emetta un evento. E ora che abbiamo testato che il nostro browser può accedere al percorso, cambiamo il percorso in un percorso POST, poiché il webhook di Ko-fi sarà una richiesta POST.
(Link al codice 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
Prossimi passi
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.