个性化阅读
专注于IT技术分析

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

本文概述

Discord是一个实时消息传递平台, 自称为”游戏玩家的多合一语音和文本聊天”。由于其光滑的界面, 易用性和广泛的功能, Discord经历了快速的增长, 甚至在对视频游戏不感兴趣的人中也越来越受欢迎。在2017年5月至2018年5月期间, 其用户群从4500万用户激增至1.3亿以上, 日均用户数量是Slack的两倍以上。

从聊天机器人开发人员的角度来看, Discord最具吸引力的功能之一是其对可编程bot的强大支持, 有助于将Discord与外界集成, 并为用户提供更引人入胜的体验。僵尸网络在Discord上无处不在, 并提供广泛的服务, 包括审核协助, 游戏, 音乐, 互联网搜索, 付款处理等。

在本Discord机器人教程中, 我们将首先讨论Discord用户界面及其针对机器人的REST和WebSocket API, 然后再转向教程, 在此我们将用JavaScript编写一个简单的Discord机器人。最后, 我们将从一定程度上从Discord最受欢迎的bot的开发人员那里获得经验, 以及他在开发和维护其重要基础架构和代码库方面的经验。

Discord用户界面

在讨论技术细节之前, 重要的是要了解用户如何与Discord进行交互以及Discord如何向用户展示自己。它向机器人展示的方式在概念上是相似的(但当然不是视觉的)。实际上, 正式的Discord应用程序是基于漫游器使用的相同API构建的。从技术上讲, 可以在常规用户帐户中运行机器人, 而无需进行任何修改, 但这是Discord的服务条款所禁止的。机器人程序必须在机器人帐户中运行。

这是在Chrome中运行的Discord应用程序的浏览器版本1。

Discord Web UI

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 bot库的支持服务器的链接列表。服务器管理员已配置了此通道, 以便像我这样的普通用户无法在其中发送消息。管理员使用此频道作为布告栏, 以发布一些重要信息, 以便轻松查看并且不会被聊天淹没。

4.用户列表

一直在右边列出该服务器中当前在线的用户。用户分为不同的类别, 其名称具有不同的颜色。这是他们所扮演角色的结果。角色描述用户应显示在哪个类别下(如果有), 其名称颜色应该是什么以及他们在服务器中具有什么权限。一个用户可以有多个角色(并且经常有), 并且有一些优先级数学可以确定在这种情况下会发生什么。至少每个用户都具有@everyone角色。其他角色由服务器人员创建和分配。

5.文字输入

如果允许的话, 这是我可以输入和发送消息的文本输入。由于我无权在此频道中发送消息, 因此无法在此处输入。

6.用户

这是当前用户。我将用户名设置为”我”, 以免让我感到困惑, 并且因为我在选择姓名时感到很糟糕。我的用户名下方是一个数字(#9484), 这是我的区分符。可能还有许多其他用户名为” Me”, 但我是唯一的” Me#9484″。对于每个服务器, 我也可以为自己设置一个昵称, 因此可以在不同的服务器中以不同的名称来认识我。

这些是Discord用户界面的基本组成部分, 但还有更多内容。即使不创建帐户, 也可以轻松开始使用Discord, 因此, 请花一点时间浏览一下。你可以通过访问Discord主页, 单击”在浏览器中打开Discord”, 选择一个用户名, 然后可能会播放一两回合的”单击总线图片”来输入Discord。

Discord API

Discord API由两部分组成:WebSocket和REST API。广义地说, WebSocket API用于实时接收来自Discord的事件, 而REST API用于执行Discord内部的操作。

如何使Discord机器人通信回路

WebSocket API

WebSocket API用于接收来自Discord的事件, 包括消息创建, 消息删除, 用户踢/禁事件, 用户权限更新等。另一方面, 从漫游器到WebSocket API的通信更为有限。机器人使用WebSocket API来请求连接, 识别自身, 心跳, 管理语音连接以及执行其他一些基本操作。你可以在Discord的网关文档中阅读更多详细信息(与WebSocket API的单个连接称为网关)。为了执行其他操作, 使用了REST API。

WebSocket API中的事件包含有效负载, 其中包含取决于事件类型的信息。例如, 所有消息创建事件都将伴随一个代表消息作者的用户对象。但是, 仅用户对象并不包含要了解的有关用户的所有信息。例如, 不包括有关用户权限的信息。如果需要更多信息, 则可以查询REST API, 但是出于下一节中进一步说明的原因, 通常应该访问应该从先前事件接收的有效负载中构建的缓存。有许多事件会传递与用户权限相关的有效负载, 包括但不限于公会创建, 公会角色更新和频道更新。

每个WebSocket连接最多可以容纳2500个行业协会。为了使漫游器可以出现在更多的行会中, 漫游器必须实现分片并打开与Discord的几个单独的WebSocket连接。如果你的漫游器在单个节点上的单个进程内运行, 那么这对于你来说就增加了复杂性, 这似乎是不必要的。但是, 如果你的机器人非常流行, 并且需要将其后端分布在单独的节点上, 那么Discord的分片支持将使其变得比以前更加容易。

REST API

僵尸程序使用Discord REST API来执行大多数操作, 例如发送消息, 踢/禁止用户和更新用户权限(与从WebSocket API接收到的事件大致类似)。 REST API也可以用于查询信息。但是, 机器人主要依赖于WebSocket API中的事件, 并缓存从WebSocket事件中接收到的信息。

有几个原因。例如, 由于REST API的速率限制, 每次接收到Message Create事件时, 查询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的网络响声。 Ko-fi是一项服务, 可让你轻松接受对你的PayPal帐户的捐款。在这里设置网络钩子非常简单, 而在贝宝那里你需要拥有一个企业帐户, 因此非常简单, 因此非常适合演示或小规模捐赠处理。

当用户捐款10美元或更多时, 机器人会为他们分配一个高级会员角色, 该角色将更改其名称颜色并将其移至在线用户列表的顶部。对于本项目, 我们将使用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开关, 并注意显示的机器人令牌(我们稍后也会用到)。如果你曾经泄漏过你的机器人令牌, 例如通过将其发布在srcmini Blog帖子中的图像中, 则必须立即重新生成它。拥有你的漫游器令牌的任何人都可以控制你的漫游器帐户, 并给你和你的用户带来潜在的严重且永久性的麻烦。

"出现了一个野生机器人"的屏幕截图

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

使Bot在线响应

让我们从婴儿步开始。首先, 我们将使该Bot在线并响应我们。我们可以用10-20行代码来实现。在一个新的bot.js文件中, 我们需要创建一个Eris Client实例, 将其bot令牌(在上面创建bot应用程序时获取)传递给它, 订阅Client实例上的一些事件, 并告诉它连接到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();

如果一切顺利, 当你使用自己的机器人令牌运行此代码时, 请准备就绪。将被打印到控制台, 你将看到你的机器人在测试服务器中联机。你可以通过右键单击它并选择” Mention”来提及你的机器人, 也可以通过键入其名称前面的@来提及它。机器人应该通过说” Present”来回应。

你的机器人在场

2提及是一种吸引其他用户注意的方法, 即使他们不在场也是如此。提及普通用户时, 系统会通过桌面通知, 移动推送通知和/或系统托盘中Discord图标上出现的红色小图标来通知该用户。通知用户的方式取决于他们的设置和他们的在线状态。另一方面, 提及机器人时, 它们不会收到任何特殊通知。他们像接收其他任何消息一样会收到一个常规的Message Create事件, 并且他们可以检查该事件附带的提及, 以确定是否被提及。

记录付款命令

既然我们知道我们可以在线上获得一个僵尸程序, 那么让我们摆脱当前的Message Create事件处理程序, 并创建一个新的程序, 使我们可以通知该僵尸程序我们已经从用户处收到付款。

为了通知机器人, 我们将发出如下命令:

pb!addpayment @user_mention payment_amount

例如, pb!addpayment @Me 10.00记录了Me支付的$ 10.00。

pb!该部分称为命令前缀。最好选择一个前缀, 该前缀是你的bot的所有命令都必须以该前缀开头。这为机器人定义了命名空间, 并有助于避免与其他机器人冲突。大多数机器人都包含一个帮助命令, 但是如果你的公会中有十个机器人, 并且所有人都响应了帮助, 请想象一下混乱!用pb!作为前缀不是万无一失的解决方案, 因为可能还有其他一些使用相同前缀的漫游器。多数流行的bot允许在每个行业中配置其前缀, 以帮助防止冲突。另一种选择是使用机器人自己的提及作为前缀, 尽管这会使发出命令更加冗长。

(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_ID = '123456789';

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命令, 则该机器人将愤怒地拒绝执行该命令。

接下来, 让该机器人为捐赠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_ID = '523407722880827415';
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, 并且该漫游器应将我分配为高级会员角色。

糟糕, 控制台中会出现”缺少权限”错误。

DiscordRESTError: DiscordRESTError [50013]: Missing Permissions
index.js:85
code:50013

该机器人在测试行会中没有”管理角色”权限, 因此无法创建或分配角色。我们可以为机器人赋予管理员权限, 而我们再也不会遇到这种问题, 但是与任何系统一样, 最好仅向用户(或在这种情况下为机器人)授予他们所需的最低权限

我们可以通过在服务器设置中创建角色, 为该角色启用”管理角色”权限并将该角色分配给机器人, 来向bot授予”管理角色”权限。

开始管理角色

现在, 当我尝试再次执行命令时, 将创建角色并将其分配给我, 并且我的名字颜色很漂亮, 并且在成员列表中有特殊的位置。

创建一个新角色
分配了新角色

在命令处理程序中, 我们有一个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_ID = '123456789';
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的Webhooks提供的有效负载如下所示:

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侦听webhooks。我们只有一条Express路线, 这只是出于演示目的, 因此我们不必担心使用惯用的目录结构。我们将所有网络服务器逻辑都放在一个文件中。

(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顶部添加新文件, 以便在我们运行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的网络钩子将是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;

接下来, 我们需要让漫游器监听事件, 确定捐赠的用户, 并为其分配角色。为了决定捐赠哪个用户, 我们将尝试找到一个用户名, 该用户名是从Ko-fi收到的邮件的子字符串。必须指示捐赠者在消息中提供他们的用户名(带有区分符), 而不是捐赠时写的。

在bot.js的底部:

(GitHub代码链接: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();

在onDonation函数中, 我们看到用户的两种表示形式:作为用户和作为成员。它们都代表同一个人, 但是Member对象包含有关该行会的特定于行会的信息, 例如, 他们在行会中的角色及其昵称。由于我们要添加角色, 因此需要使用用户的成员表示。 Discord中的每个用户对其所在的每个公会都有一个Member表示。

现在, 我可以使用Postman来测试代码。

与邮递员测试

我收到200状态代码, 并且在服务器中获得授予我的角色。

如果来自Ko-fi的邮件不包含有效的用户名;但是, 什么也没有发生。捐赠者没有任何作用, 我们也不知道我们收到了孤儿捐款。让我们添加一个用于记录捐赠的日志, 包括不能归因于公会成员的捐赠。

首先, 我们需要在Discord中创建一个日志通道并获取其通道ID。可以使用开发者工具找到频道ID, 该工具可以在Discord的设置中启用。然后, 你可以右键单击任何频道, 然后单击”复制ID”。

日志通道ID应该添加到bot.js的常量部分。

(GitHub代码链接:https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)

const LOG_CHANNEL_ID = '526653321109438474';

然后我们可以编写一个logDonation函数。

(GitHub代码链接: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);
}

现在我们可以更新onDonation来调用log函数:

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);
   }
}

现在, 我可以再次调用webhook, 首先使用有效的用户名, 然后不使用它, 然后在日志通道中收到两条不错的日志消息。

两个好消息

以前, 我们只是将字符串发送到Discord以显示为消息。我们在新的logDonation函数中创建并发送给Discord的更为复杂的JavaScript对象是一种特殊类型的消息, 称为丰富嵌入。嵌入可为你提供一些脚手架, 以制作出类似所示的诱人信息。只有漫游器才能创建嵌入, 用户则不能。

现在, 我们将收到有关捐赠的通知, 将其记录下来, 并奖励我们的支持者。如果用户在捐赠时忘记指定用户名, 我们还可以使用addpayment命令手动添加捐赠。让我们收工。

本教程的完整代码可在GitHub上找到, 网址为https://github.com/mistval/premium_bot

下一步

我们已经成功创建了一个机器人, 可以帮助我们跟踪捐款。这是我们实际上可以使用的东西吗?好吧, 也许吧。它涵盖了基础知识, 但没有更多内容。以下是你可能首先要考虑的一些缺点:

  1. 如果用户离开我们的行会(或者甚至一开始他们甚至都没有加入我们的行会), 他们将失去其高级会员角色, 并且如果重新加入, 他们也将无法取回。我们应该按用户ID将付款存储在数据库中, 因此, 如果高级会员重新加入, 我们可以退还他们的角色, 如果我们愿意的话, 还可以向他们发送一个很好的欢迎信息。
  2. 分期付款无法使用。如果用户发送了$ 5, 然后又发送了另外的$ 5, 则他们将不会获得高级职位。与上述问题类似, 将付款存储在数据库中并在用户的总付款达到10美元时发出高级会员角色将对此有所帮助。
  3. 可能会多次收到同一个Webhook, 并且该漫游器会多次记录付款。如果Ko-fi没有收到或未正确确认来自Webhook侦听器的代码200响应, 它将稍后尝试再次发送Webhook。在数据库中跟踪付款并忽略具有与先前收到的ID相同的ID的webhooks, 将有助于解决此问题。
  4. 我们的Webhook监听器不是很安全。任何人都可以伪造网络挂钩并免费获得高级会员角色。 Ko-fi似乎并没有签署Webhook, 因此你不必依靠任何人都不知道Webhook地址(错误)或IP白名单(更好)。
  5. 该机器人仅可在一个公会中使用。

当机器人变大时

有十二个网站列出Discord僵尸程序并向公众开放, 包括DiscordBots.org和Discord.Bots.gg。尽管Discord僵尸程序主要是业余爱好者的尝试, 但有些僵尸程序受到了极大的欢迎, 并且将其保持为一项复杂而艰巨的工作。

按行业协会统计, Rythm目前是Discord上使用最广泛的机器人。 Rythm是音乐机器人, 其专长是连接Discord中的语音通道并播放用户请求的音乐。目前, Rythm存在于超过285万个行会中, 总人口约为9000万, 在高峰时, 可以在20, 000个独立行会中同时播放约100, 000个用户的音频。 Rythm的创建者和主要开发人员ImBursting同意回答一些有关开发和维护Rythm之类的大型机器人的问题。

访者:你能告诉我们一些有关Rythm的高级架构及其托管方式的信息吗?

ImBursting:Rythm跨9台物理服务器进行扩展, 每台服务器具有32个核心, 96GB RAM和10gbps连接。这些服务器在小型托管公司GalaxyGate的帮助下并置在数据中心。

我想当你开始使用Rythm时, 并没有将其设计为可扩展到任何程度。你能告诉我们有关节奏的开始以及它随着时间的技术发展吗?

Rythm的第一个演变是用Python编写的, 这不是一种性能很好的语言, 所以大约在我们遇到10, 000台服务器(经过多次扩展尝试)之后, 我意识到这是最大的障碍, 所以我开始将bot编码为Java, 原因是Java的音频库已进行了很多优化, 并且通常是适用于如此庞大的应用程序的语言。重新编码后, 性能提高了十倍, 并使问题暂时搁浅了一段时间。然后, 当问题再次开始浮出水面时, 我们达到了30万台服务器的里程碑, 这时我意识到, 由于一个JVM无法处理所有这些问题, 因此需要更多的扩展。因此, 我们缓慢地开始实施改进和重大更改, 例如使用称为Lavalink的开源服务器调整垃圾收集器并将语音连接拆分到单独的微服务上。性能得到了很大的改善, 但是基础架构的最后一轮是当我们将其划分为9个独立的集群以在9台物理服务器上运行时, 并制作了自定义网关和统计微服务以确保一切都像在一台计算机上一样平稳运行。

我注意到Rythm有一个canary版本, 你会从其他开发人员和工作人员那里得到一些帮助。我想你和你的团队必须付出很多努力来确保事情做对了。你能告诉我们有关更新节奏的过程吗?

Rythm canary是Alpha机器人, 我们通常在将其部署到Rythm 2进行更广泛的规模测试后再测试Rythm, 然后使用它来测试新制作的功能和性能改进。由于Discord速率限制, 我们遇到的最大问题是重启时间确实很长, 这也是我在决定推送更新之前尽力确保更新准备就绪的原因。

我确实从志愿者开发人员和真正想为社区提供帮助的人那里得到了很多帮助, 我想确保一切都做得正确, 并且人们将始终回答他们的问题并获得可能的最佳支持, 这意味着我将不断寻找新的机会。

本文总结

Discord成为新手的日子已经过去, 现在它已成为世界上最大的实时通信平台之一。尽管Discord机器人在很大程度上是业余爱好者的尝试, 但随着服务数量的不断增加, 我们很可能会看到商业机会的增加。一些公司, 例如前面提到的Patreon, 已经涉足其中。

在本文中, 我们对Discord的用户界面进行了高级概述, 对其API进行了高级概述, 对Discord僵尸程序进行了完整的学习, 并且我们还了解了在企业范围内操作僵尸程序的感觉。我希望你对这项技术失去兴趣, 并希望你了解其工作原理。

聊天机器人通常很有趣, 但当聊天机器人对你的复杂查询的回答使你的知识渊博时, 就可以了。要确保为你的用户提供出色的UX, 请参阅srcmini Design Blog的”聊天崩溃-当聊天机器人失败时”, 以避免5个设计问题。

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

赞(3)
未经允许不得转载:srcmini » 如何制作Discord机器人:概述和教程

评论 3

评论前必须登录!