如何制作 Discord 机器人:概述和教程

已发表: 2022-03-11

Discord 是一个实时消息传递平台,自称为“游戏玩家的一体化语音和文本聊天”。 由于其流畅的界面、易用性和广泛的功能,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 CreateGuild Role UpdateChannel 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(我们将使用的 bot 库)、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.

Testing with Postman

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.

Two nice messages

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. 让我们收工。

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? 好吧,也许。 It covers the basics, but not much more. Here are some shortcomings you might want to think about first:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.

把它包起来

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.

相关: JS 最佳实践:使用 TypeScript 和依赖注入构建 Discord Bot