如何製作 Discord 機器人:概述和教程
已發表: 2022-03-11Discord 是一個實時消息傳遞平台,自稱為“遊戲玩家的一體化語音和文本聊天”。 由於其流暢的界面、易用性和廣泛的功能,Discord 經歷了快速增長,甚至在那些對視頻遊戲不感興趣的人中也越來越受歡迎。 2017 年 5 月至 2018 年 5 月期間,其用戶群從 4500 萬激增至超過 1.3 億,每日用戶數量是 Slack 的兩倍多。
從聊天機器人開發人員的角度來看,Discord 最具吸引力的功能之一是它對可編程機器人的強大支持,有助於將 Discord 與外部世界集成並為用戶提供更具吸引力的體驗。 機器人在 Discord 上無處不在,並提供廣泛的服務,包括審核協助、遊戲、音樂、互聯網搜索、支付處理等。
在本 Discord 機器人教程中,我們將首先討論 Discord 用戶界面及其用於機器人的 REST 和 WebSocket API,然後繼續學習使用 JavaScript 編寫簡單 Discord 機器人的教程。 最後,我們將聽取 Discord 最受歡迎機器人的開發人員的意見,以及他開發和維護重要基礎設施和代碼庫的經驗。
不和諧的用戶界面
在我們討論技術細節之前,了解用戶如何與 Discord 交互以及 Discord 如何向用戶展示自己非常重要。 它向機器人展示自己的方式在概念上是相似的(但當然是非視覺的)。 事實上,官方的 Discord 應用程序建立在機器人使用的相同 API 之上。 從技術上講,無需修改即可在普通用戶帳戶內運行機器人,但 Discord 的服務條款禁止這樣做。 機器人需要在機器人帳戶中運行。
下面是在 Chrome 中運行的 Discord 應用程序的瀏覽器版本1 。
1桌面應用程序的 Discord UI 與使用 Electron 打包的 Web 應用程序幾乎相同。 iOS 應用程序是使用 React Native 構建的。 Android 應用程序是原生 Android Java 代碼。
讓我們分解一下。
1.服務器列表
一直到左邊是我所屬的服務器列表。 如果您熟悉 Slack,則服務器類似於 Slack 工作區,代表一組用戶,他們可以在服務器中的一個或多個通道內相互交互。 服務器由其創建者和/或他們選擇並選擇委派職責的任何員工管理。 創建者和/或工作人員定義規則、服務器中的頻道結構並管理用戶。
就我而言, Discord API服務器位於我的服務器列表的頂部。 這是獲得幫助和與其他開發人員交談的好地方。 下面是我創建的名為Test的服務器。 我們將在那裡測試我們稍後創建的機器人。 下面是一個用於創建新服務器的按鈕。 任何人都可以通過點擊幾下創建服務器。
請注意,雖然 Discord 用戶界面中使用的術語是Server ,但開發人員文檔和 API 中使用的術語是Guild 。 一旦我們繼續談論技術話題,我們將轉向談論公會。 這兩個術語可以互換。
2.頻道列表
服務器列表右側是我當前正在查看的服務器的頻道列表(在本例中為 Discord API 服務器)。 頻道可以分為任意數量的類別。 在 Discord API 服務器中,類別包括 INFORMATION、GENERAL 和 LIBS,如圖所示。 每個頻道都充當聊天室,用戶可以在其中討論頻道專用的任何主題。 我們當前正在觀看的頻道(信息)背景較淺。 自我們上次查看以來有新消息的頻道具有白色文本顏色。
3.頻道視圖
這是頻道視圖,我們可以在其中看到用戶在我們當前正在查看的頻道中談論的內容。 我們可以在這裡看到一條消息,只是部分可見。 它是單個 Discord 機器人庫的支持服務器的鏈接列表。 服務器管理員已配置此通道,因此像我這樣的普通用戶無法在其中發送消息。 管理員將此頻道用作公告板,將一些重要信息發佈在易於看到且不會被聊天淹沒的地方。
4. 用戶列表
一直到右邊是這個服務器當前在線的用戶列表。 用戶被組織成不同的類別,他們的名字有不同的顏色。 這是他們所擁有的角色的結果。 角色描述了用戶應該出現在哪個類別(如果有)、他們的名字顏色應該是什麼以及他們在服務器中擁有什麼權限。 一個用戶可以擁有多個角色(並且經常這樣做),並且有一些優先級數學可以確定在這種情況下會發生什麼。 至少,每個用戶都有@everyone 角色。 其他角色由服務器人員創建和分配。
5.文本輸入
如果允許的話,這是我可以輸入和發送消息的文本輸入。 由於我無權在此頻道中發送消息,因此我無法在此處輸入。
6. 用戶
這是當前用戶。 我將我的用戶名設置為“我”,以幫助我避免混淆,因為我不擅長選擇名字。 在我的用戶名下方是一個數字(#9484),它是我的鑑別器。 可能有很多其他用戶名為“我”,但我是唯一的“我#9484”。 我也可以在每個服務器的基礎上為自己設置一個暱稱,這樣我就可以在不同的服務器上以不同的名字被認識。
這些是 Discord 用戶界面的基本部分,但還有更多。 即使不創建帳戶也可以輕鬆開始使用 Discord,因此請隨意花點時間瀏覽一下。 您可以通過訪問 Discord 主頁,單擊“在瀏覽器中打開 Discord”,選擇用戶名,並可能玩一兩輪“單擊巴士圖片”來刷新 Discord。
不和諧 API
Discord API 由兩個獨立的部分組成:WebSocket 和 REST API。 從廣義上講,WebSocket API 用於實時接收來自 Discord 的事件,而 REST API 用於在 Discord 內部執行操作。
WebSocket API
WebSocket API 用於接收來自 Discord 的事件,包括消息創建、消息刪除、用戶踢/禁止事件、用戶權限更新等等。 另一方面,從機器人到 WebSocket API 的通信受到更多限制。 機器人使用 WebSocket API 來請求連接、識別自身、檢測信號、管理語音連接以及做一些更基本的事情。 您可以在 Discord 的網關文檔中閱讀更多詳細信息(到 WebSocket API 的單個連接稱為網關)。 為了執行其他操作,使用了 REST API。
來自 WebSocket API 的事件包含一個有效負載,其中包含取決於事件類型的信息。 例如,所有Message Create事件都將伴隨一個代表消息作者的用戶對象。 然而,單獨的用戶對象並不包含所有需要了解的關於用戶的信息。 例如,沒有包含有關用戶權限的信息。 如果您需要更多信息,您可以查詢 REST API 以獲取它,但出於下一節中進一步解釋的原因,您通常應該訪問您應該使用從先前事件接收的有效負載構建的緩存。 有許多事件會傳遞與用戶權限相關的有效負載,包括但不限於Guild Create 、 Guild Role Update和Channel Update 。
每個 WebSocket 連接最多可以存在 2,500 個公會中的機器人。 為了允許機器人出現在更多的公會中,機器人必須實現分片並打開幾個單獨的 WebSocket 連接到 Discord。 如果您的機器人在單個節點上的單個進程內運行,這對您來說只是增加了複雜性,這似乎是不必要的。 但是,如果您的機器人非常受歡迎並且需要將其後端分佈在不同的節點上,那麼 Discord 的分片支持使這比其他方式更容易。
REST API
機器人使用 Discord REST API 來執行大多數操作,例如發送消息、踢/禁止用戶和更新用戶權限(大致類似於從 WebSocket API 接收的事件)。 REST API 也可用於查詢信息; 但是,機器人主要依賴來自 WebSocket API 的事件,並緩存從 WebSocket 事件接收到的信息。
有幾個原因。 例如,每次收到消息創建事件時查詢 REST API 以獲取用戶信息,由於 REST API 的速率限制,無法擴展。 在大多數情況下,它也是多餘的,因為 WebSocket API 提供了必要的信息,您應該將其保存在緩存中。
但是,也有一些例外情況,您有時可能需要緩存中不存在的信息。 當機器人最初連接到 WebSocket 網關時,該分片上存在該機器人的每個公會的Ready事件和一個Guild Create事件最初被發送到該機器人,以便它可以使用當前狀態填充其緩存。 公會人口密集的公會創建事件僅包含有關在線用戶的信息。 如果您的機器人需要獲取有關離線用戶的信息,則相關信息可能不會出現在您的緩存中。 在這種情況下,向 REST API 發出請求是有意義的。 或者,如果您發現自己經常需要獲取有關離線用戶的信息,您可以選擇向 WebSocket API 發送請求公會成員操作碼以請求離線公會成員。
另一個例外是如果您的應用程序根本沒有連接到 WebSocket API。 例如,如果您的機器人有一個 Web 儀表板,用戶可以登錄並更改其服務器中的機器人設置。 Web 儀表板可以在一個單獨的進程中運行,而無需任何與 WebSocket API 的連接,也沒有來自 Discord 的數據緩存。 它可能只需要偶爾發出一些 REST API 請求。 在這種情況下,依靠 REST API 來獲取您需要的信息是有意義的。
API 包裝器
雖然對技術堆棧的每個級別都有一些了解總是一個好主意,但直接使用 Discord WebSocket 和 REST API 非常耗時、容易出錯、通常是不必要的,而且實際上是危險的。
Discord 提供了一份經過官方審查的庫的精選列表,並警告說:
使用濫用 API 或導致過高速率限制的自定義實現或不合規庫可能會導致永久禁止。
Discord 官方審查的庫通常是成熟的、有據可查的,並且完全覆蓋了 Discord API。 大多數機器人開發人員永遠不會有充分的理由開發自定義實現,除非出於好奇或勇敢!
目前,經過官方審查的庫包括 Crystal、C#、D、Go、Java、JavaScript、Lua、Nim、PHP、Python、Ruby、Rust 和 Swift 的實現。 您選擇的語言可能有兩個或更多不同的庫。 選擇使用哪個可能是一個艱難的決定。 除了查看各自的文檔之外,您可能還想加入非官方的 Discord API 服務器,並了解每個庫背後的社區類型。
如何製作 Discord 機器人
我們開始談正事吧。 我們將創建一個 Discord 機器人,它掛在我們的服務器中並監聽來自 Ko-fi 的 webhook。 Ko-fi 是一項服務,可讓您輕鬆接受對 PayPal 帳戶的捐款。 在那裡設置 webhook 非常簡單,與 PayPal 相比,您需要有一個企業帳戶,因此非常適合演示目的或小規模捐贈處理。
當用戶捐贈 10 美元或更多時,機器人將為他們分配一個Premium Member
角色,該角色會更改他們的姓名顏色並將他們移動到在線用戶列表的頂部。 對於這個項目,我們將使用 Node.js 和一個名為 Eris 的 Discord API 庫(文檔鏈接:https://abal.moe/Eris/)。 Eris 不是唯一的 JavaScript 庫。 你可以選擇 discord.js。 無論哪種方式,我們將編寫的代碼都非常相似。
順便說一句,另一個捐贈處理器 Patreon 提供了一個官方的 Discord 機器人,並支持將 Discord 角色配置為貢獻者福利。 我們將實現類似的東西,但當然更基本。
本教程每一步的代碼都可以在 GitHub (https://github.com/mistval/premium_bot) 上找到。 為簡潔起見,本文中顯示的某些步驟省略了未更改的代碼,因此如果您認為自己可能遺漏了什麼,請點擊提供的 GitHub 鏈接。
創建機器人帳戶
在我們開始編寫代碼之前,我們需要一個機器人帳戶。 在我們可以創建一個機器人帳戶之前,我們需要一個用戶帳戶。 要創建用戶帳戶,請按照此處的說明進行操作。
然後,要創建一個機器人帳戶,我們:
1) 在開發者門戶中創建一個應用程序。
2) 填寫有關應用程序的一些基本詳細信息(注意此處顯示的 CLIENT ID——我們稍後會用到它)。
3) 添加連接到應用程序的機器人用戶。
4) 關閉 PUBLIC BOT 開關並註意顯示的機器人令牌(我們稍後也需要它)。 如果您洩露了您的機器人令牌,例如通過在 Toptal 博客文章的圖片中發布它,您必須立即重新生成它。 擁有您的機器人令牌的任何人都可以控制您的機器人帳戶,並給您和您的用戶帶來潛在的嚴重和永久性麻煩。
5) 將機器人添加到您的測試公會。 要將機器人添加到公會,請將其客戶端 ID(如前所示)替換為以下 URI 並在瀏覽器中導航到它。
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
單擊授權後,該機器人現在在我的測試公會中,我可以在用戶列表中看到它。 它處於離線狀態,但我們會盡快解決。
創建項目
假設您安裝了 Node.js,創建一個項目並安裝 Eris(我們將使用的機器人庫)、Express(我們將用於創建 webhook 偵聽器的 Web 應用程序框架)和 body-parser(用於解析 webhook 主體)。
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
讓機器人在線並響應
讓我們從嬰兒步開始。 首先,我們將讓機器人上線並回复我們。 我們可以用 10-20 行代碼做到這一點。 在一個新的 bot.js 文件中,我們需要創建一個 Eris 客戶端實例,將我們的機器人令牌(在我們上面創建機器人應用程序時獲得)傳遞給它,訂閱客戶端實例上的一些事件,並告訴它連接到 Discord . 出於演示目的,我們將我們的 bot 令牌硬編碼到 bot.js 文件中,但是創建一個單獨的配置文件並將其從源代碼控制中排除是一種很好的做法。
(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();
如果一切順利,當您使用自己的機器人令牌運行此代碼時, Connected and ready.
將打印到控制台,您將看到您的機器人在您的測試服務器中聯機。 您可以通過右鍵單擊它並選擇“提及”來提及您的2 個機器人,也可以通過在其名稱前加上 @ 來提及它。 機器人應該回答說“現在”。
2提及是一種引起其他用戶注意的方式,即使他們不在場。 當提到普通用戶時,將通過桌面通知、移動推送通知和/或出現在系統托盤中 Discord 圖標上方的紅色小圖標來通知。 通知用戶的方式取決於他們的設置和在線狀態。 另一方面,機器人在被提及時不會收到任何特殊通知。 他們會像接收任何其他消息一樣接收常規消息創建事件,並且他們可以檢查附加到事件的提及以確定它們是否被提及。
記錄支付命令
現在我們知道我們可以讓機器人在線,讓我們擺脫當前的Message Create事件處理程序並創建一個新的事件處理程序,讓我們通知機器人我們已收到用戶的付款。
為了通知機器人付款,我們將發出如下命令:
pb!addpayment @user_mention payment_amount
例如, pb!addpayment @Me 10.00
記錄我支付的 10.00 美元。
鉛! 部分稱為命令前綴。 選擇一個前綴是一個很好的約定,你的機器人的所有命令都必須以該前綴開頭。 這為機器人創建了命名空間的度量,並有助於避免與其他機器人發生衝突。 大多數機器人都包含一個幫助命令,但想像一下,如果你的公會中有十個機器人並且他們都響應了幫助,那會是怎樣的一團糟! 使用鉛! 作為前綴不是萬無一失的解決方案,因為可能還有其他機器人也使用相同的前綴。 大多數流行的機器人允許在每個公會的基礎上配置它們的前綴,以幫助防止衝突。 另一種選擇是使用機器人自己的提及作為其前綴,儘管這會使發出命令更加冗長。
(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();
讓我們試試看。
我們不僅讓機器人響應pb!addpayment
命令,而且我們創建了一個通用模式來處理命令。 我們可以通過向commandHandlerForCommandName
字典添加更多處理程序來添加更多命令。 我們這裡有一個簡單的命令框架。 處理命令是製作機器人的一個基本部分,許多人已經編寫了開源命令框架,您可以使用這些框架而不是自己編寫。 命令框架通常允許您指定冷卻時間、所需的用戶權限、命令別名、命令描述和使用示例(用於自動生成的幫助命令)等等。 Eris 帶有一個內置的命令框架。
說到權限,我們的機器人有點安全問題。 任何人都可以執行addpayment
命令。 讓我們限制它,以便只有機器人的所有者可以使用它。 我們將重構commandHandlerForCommandName
字典並讓它包含 JavaScript 對像作為它的值。 這些對象將包含一個帶有命令處理程序的execute
屬性,以及一個帶有布爾值的botOwnerOnly
屬性。 我們還將我們的用戶 ID 硬編碼到機器人的常量部分,以便它知道它的所有者是誰。 您可以通過在 Discord 設置中啟用開發人員模式,然後右鍵單擊您的用戶名並選擇複製 ID 來找到您的用戶 ID。
(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();
現在,如果機器人所有者以外的任何人試圖執行addpayment
命令,機器人將憤怒地拒絕執行它。
接下來,讓機器人將Premium Member
角色分配給任何捐贈 10 美元或更多的人。 在 bot.js 文件的頂部:
(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), ]); }, };
現在我可以嘗試說pb!addpayment @Me 10.00
,機器人應該為我分配Premium Member
角色。
糟糕,控制台中出現 Missing Permissions 錯誤。
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
該機器人在測試公會中沒有“管理角色”權限,因此無法創建或分配角色。 我們可以給機器人管理員權限,我們再也不會遇到這種問題,但是對於任何系統,最好只給用戶(或者在這種情況下是機器人)他們需要的最低權限
我們可以通過在服務器設置中創建角色、為該角色啟用管理角色權限並將角色分配給機器人來為機器人提供管理角色權限。
現在,當我再次嘗試執行命令時,角色被創建並分配給我,我有一個花哨的名字顏色和成員列表中的一個特殊位置。
在命令處理程序中,我們有一條 TODO 註釋建議我們需要檢查無效參數。 現在讓我們處理一下。
(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), ]); }, };
到目前為止,這是完整的代碼:
(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();
這應該讓您對如何創建 Discord 機器人有一個很好的基本概念。 現在我們將了解如何將機器人與 Ko-fi 集成。 如果您願意,您可以在 Ko-fi 的儀表板中創建一個 webhook,確保您的路由器配置為轉發端口 80,然後將真實的實時測試 webhook 發送給您自己。 但我只是打算使用 Postman 來模擬請求。

Ko-fi 的 Webhook 提供如下所示的有效負載:
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" }
讓我們創建一個名為 webhook_listener.js 的新源文件並使用 Express 來監聽 webhook。 我們只有一個 Express 路由,這是出於演示目的,所以我們不會太擔心使用慣用的目錄結構。 我們將把所有的 Web 服務器邏輯放在一個文件中。
(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;
然後讓我們在 bot.js 的頂部 require 新文件,以便在我們運行 bot.js 時啟動偵聽器。
(GitHub代碼鏈接:https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
啟動機器人後,當您在瀏覽器中導航到 http://localhost/kofi 時,您應該會看到“Hello”。
現在讓WebhookListener
處理來自 webhook 的數據並發出一個事件。 現在我們已經測試了我們的瀏覽器可以訪問路由,讓我們將路由更改為 POST 路由,因為來自 Ko-fi 的 webhook 將是一個 POST 請求。
(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
下一步
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.