#discord-api #discord #api-client #applications #interface #response #accord

bin+lib passcod-accord

通过爱与友谊的力量,利用 HTTP 推动Discord API客户端

8个版本

0.0.10 2021年4月13日
0.0.9 2020年12月12日
0.0.7 2020年11月15日
0.0.3 2020年10月7日
0.0.2 2020年9月14日

#1865 in Web 编程

自定义许可证

110KB
1.5K SLoC

Crate release version Crate license: CC BY-NC-SA 4.0 MSRV: latest stable Uses Caretaker Maintainership

  • 状态
    • alpha
    • 仅用于我的生产使用
    • 仅覆盖 API 的一小部分
  • 版本
    • 请查看 版本标签 了解已标记版本
    • 尚未提供预构建的二进制文件,请从源代码构建
    • 或使用 cargo install passcod-accord --locked
  • 许可证: CC-BY-NC-SA 4.0
    • “嗯...这不是软件许可证?”
      • 的确。它仍然作为一个“作品”许可证。
    • 它不是开源的!
      • 是的,这是设计上的选择。
    • 如果我想在商业环境中使用它怎么办?
  • 贡献:你可以!
    • 本项目使用 Caretaker Maintainership
    • 需要关爱的领域:无处不在。
    • 更详细的错误和警告会很有帮助!
    • 基本响应时间统计可能会很有帮助!
    • 任何有 TODO 注释的地方...
    • 一些示例应用程序会很棒!
    • 当然,处理更多事件也非常欢迎。

文档

要开始,启动服务器(例如,一个路由所有内容到 index.php 的 PHP 独立服务器): php -S 127.0.0.1:8080 index.php) 并将其地址添加到 ACCORD_TARGET 环境变量中。

然后添加你的机器人 Discord 令牌到 DISCORD_TOKEN,并启动 Accord。

Accord 将会在 Discord 上发生事件时向你的服务器发送请求,这些事件是你的机器人可以看到的。

注意(待解决):你的机器人当前需要启用 Members 特权意图。这将在以后进行配置。

配置

通过环境变量进行。

名称 默认值 用途 示例
DISCORD_TOKEN 必需 Discord 应用令牌。
ACCORD_TARGET 必需 要发送 Accord 请求的服务器的 Base URL。 http://127.0.0.1:8080
ACCORD_BIND localhost:8181 绑定反向接口的地址。 0.0.0.0:1234
ACCORD_COMMAND_MATCH 运行在消息上以匹配(true/false)作为命令的正则表达式。 ^~\w+
ACCORD_COMMAND_PARSE 运行在命令上以解析它们的正则表达式(带有捕获)。 (?:^~|\s+)(\w+)
RUST_LOG info 设置日志级别。参见tracing info,accord=debug

事件到端点表

事件 端点 负载类型 允许的响应
MessageCreate(来自服务器) POST /服务器/{服务器-id}/频道/{频道-id}/消息 消息 text/plain 回复内容application/json 动作
MessageCreate(来自直接消息) POST /直接/{频道-id}/消息 消息 text/plain 回复内容application/json 动作
MessageCreate(匹配命令正则表达式) POST /命令/{命令...} 命令 text/plain 回复内容application/json 动作
MemberAdd POST /服务器/{服务器-id}/加入/{用户-id} 成员 application/json 动作
ShardConnected POST /discord/已连接 已连接 application/json 动作
在建立连接之前 GET /discord/连接中 application/json 存在

有效载荷

所有非GET端点请求都携带有效载荷,它是由事件生成的特定类型的JSON值(见表)。某些类型有子类型,等等。这里以TypeScript表示法给出类型

负载类型:Message

{
  id: number, // u64
  server_id?: number, // always present for guild messages, never for DMs
  channel_id: number,
  author: Member | User, // Member for guild messages, User for DMs

  timestamp_created: string, // as provided from discord
  timestamp_edited?: string, // as provided from discord

  kind?: "regular", // usually "regular" (default), see source for others
  content: string,

  attachments: Array<Attachment>, // from twilight, type not stable/documented
  embeds: Array<Embed>, // idem
  reactions: Array<MessageReaction>, // idem

  application?: MessageApplication, // idem
  flags: Array<"crossposted" | "is-crosspost" | "suppress-embeds" | "source-message-deleted" | "urgent">,
}

负载类型:Member

{
  user: User,
  server_id: number,
  roles?: Array<number>, // IDs of the roles
  pseudonym?: string, // Aka the "server nick"
}

负载类型:User

{
  id: number, // u64
  name: string,
  bot: boolean,
}

负载类型:Connected

{
  shard: number,
}

负载类型:Command

{
  command: Array<string>, // captures from the ACCORD_COMMAND_PARSE regex
  message: Message,
}

头部

有一组以accord-开头的头部,由事件设置。头部中的所有信息也都在有效载荷中(除了accord-version头部,它在所有请求中都存在,但在任何有效载荷中都不存在),这些信息更少用于应用程序(应用程序应解析有效载荷)而是更多用于请求路由器(可能没有检查体或解析JSON的能力)。例如,nginx可以将DM事件(accord-channel-type: direct)路由到不同的应用程序。

  • accord-version — 总是提供,Accord本身的版本;
  • accord-server-id — 仅在服务器上下文中;
  • accord-channel-id — 在频道上下文中;
  • accord-channel-type — 在服务器中为textvoice,在DM中为direct
  • accord-message-id — 在消息上下文中;
  • accord-author-typeaccord-user-typebotuser
  • accord-author-idaccord-user-id
  • accord-author-nameaccord-user-name
  • accord-author-role-idsaccord-user-role-ids
  • accord-content-length — 在消息上下文中,消息的长度。

状态

响应状态代码在处理时相同

  • 除非curl内部处理,否则不支持1xx;
  • 204和404将终止读取响应并返回,不采取任何进一步操作;
  • 多选(300)不受支持(但可能在未来支持);
  • 未修改(304)尚未支持;
  • 重定向由curl内部处理(限制8个);
  • 代理重定向(305和306)不受支持;
  • 错误状态(400及以上)记录错误,以后可能会做更多操作;
  • 所有其他成功状态都被解释为200,并且按照以下方式继续处理。

响应

任何端点预期的响应都可能不同。通常,正文需要是JSON格式,但有一些端点为了方便接受其他类型,如文本。

通用的JSON响应格式称为"act",代表Accord要执行的单个操作。一个act是一个对象,包含一个键描述其类型,以及该特定act的属性作为子对象。

响应的content-type头部必须为application/json以支持该格式,且JSON必须不包含字面换行符(即不能是“美观”的JSON)。

消息创建和命令端点接受content-type: text/plain响应,并将其解释为带有响应正文内容的message-createact。

通过在每个act之间使用换行符(这就是为什么单个act不能跨越多行)可以在JSON格式中执行多个操作。空行将被忽略而不会出错。每一行在收到后立即解析为act并执行,连接保持打开直到收到EOL,因此可以在之间有任意延迟的情况下流式传输多个act,并发送额外的换行符“keepalives”以确保连接保持打开。在解析为JSON之前,行首尾的空白将被删除,因此您可以将消息填充到约4096字节以达到缓冲阈值。

(因此,除了简单的性能问题之外,您的服务器必须支持多个并发连接。)

一些端点具有特殊的格式,不支持JSON act。

响应:JSON acts

Act:create-message

发布一条新消息。

{ "create-message": {
  content: string,
  channel_id?: number, // u64 channel id to post in
} }

content将内部转换为UTF-16代码点,且不能超过2000个(这是Discord的限制)。

频道ID是全球唯一的,因此不需要提供服务器ID。如果act中没有频道ID,Accord将尝试填写它。按照优先级顺序

  • act的channel_id
  • 如果存在,响应头accord-channel-id
  • 如果请求来自消息上下文,则该消息的频道
Act:assign-role

将角色分配给成员。

{ "assign-role": {
  role_id: number,
  user_id: number,
  server_id?: number,
  reason?: string,
} }

如果act中没有服务器ID,Accord将尝试填写它。按照优先级顺序

  • act的server_id
  • 如果存在,响应头accord-server-id
  • 如果请求来自公会上下文,则是该公会

当提供时,reason字符串将显示在公会的审计日志中。

Act:remove-role

从成员中删除角色。

{ "assign-role": {
  role_id: number,
  user_id: number,
  server_id?: number,
  reason?: string,
} }

如果act中没有服务器ID,Accord将尝试填写它。按照优先级顺序

  • act的server_id
  • 如果存在,响应头accord-server-id
  • 如果请求来自公会上下文,则是该公会

当提供时,reason字符串将显示在公会的审计日志中。

响应:文本回复

在消息创建上下文(包括命令)中,如果响应类型为text/plain,则将其完全读取为UTF-8字符串,然后将其作为包含该字符串内容的单个act处理,且没有提供的channel_id(回退到上下文或头部)。

响应:JSON存在状态

特殊的端点/discord/connecting在Accord连接到Discord之前被调用,并提供设置机器人存在的机会。也就是说,其“在线/离线/繁忙/等”状态,是否标记为AFK,以及它显示的活动,如果有的话(用户下的“正在玩某个游戏...”消息)。

目前尚无法在连接时更改存在状态。

{
  afk?: boolean,
  status?: "offline" | "online" | "dnd" | "idle" | "invisible",
  since?: number,
  activity?: Activity
}

Activity类型可以是以下之一

{ playing: { name: string } }   // displays as `Playing {name}`
{ streaming: { name: string } } // displays as `Streaming {name}`
{ listening: { name: string } } // displays as `Listening to {name}`
{ watching: { name: string } }  // displays as `Watching {name}`
{ custom: { name: string } }    // may not be supported for bots yet

命令

消息创建事件可以发送到通用的端点,或者如果它们匹配并解析为命令,将发送到/command/...端点。

有两个(可选)环境变量控制此操作

  • ACCORD_COMMAND_MATCH执行简单的正则表达式测试。如果匹配,则消息被视为命令,否则不是。

  • ACCORD_COMMAND_PARSE(如果存在)将在消息上运行,并收集所有非重叠捕获,并视为消息的“命令”部分。

然后构造端点为/command/,后面跟上述解析和收集的“命令”部分,由斜杠连接。

例如,!pick me可以解析为端点/command/pick/me,或者为/command/pick,或者仅为/command/,具体取决于解析器正则表达式是什么,或者它是否存在。

如果不存在ACCORD_COMMAND_MATCH,则不会发送到/command/...

正则表达式引擎是带有所有默认设置的regex包。您可以使用此在线工具来玩耍/测试正则表达式:https://rustexp.lpil.uk

要开始,请尝试这些

ACCORD_COMMAND_MATCH = ^!\w+
ACCORD_COMMAND_PARSE = (?:^!|\s+)(\w+)

反向接口

Accord还有自己的HTTP服务器在监听,由ACCORD_BIND变量配置。这允许客户端发起功能。

目前,只有Ghosts已实现。

Ghosts

要自发地对Discord进行操作,目前有两种选择

  1. 直接向Discord发送自己的请求。
  2. 在反向接口上创建和响应ghost事件。

Ghost事件是您的应用程序生成并发送到Accord的事件,Accord将其注入自身,就像它们来自Discord一样。然后一切照常进行。Ghost永远不会发送到Discord,并且仅存在于发送到的事件的Accord实例中。

Ghost的主要目的是在没有外部刺激的情况下启动操作。例如,“时钟”机器人每小时发布一条消息,可以每小时召唤一个发送消息!clock的ghost。然后您的服务器将在/command/clock上接收请求,并相应地回答,Accord然后将回复发布到Discord。

Ghost也可以用来从另一个命令调用命令。例如,调用!roll 1-9可以检测出参数更适合!random命令,并发送包含!random 1-9的ghost。这可能比其他方法简单(或者可能不简单,由您自己判断)。

要召唤幽灵,您需要向以下地址发送请求:{ACCORD_BIND}/ghost/{ENDPOINT} 其中 {ENDPOINT} 是与正向接口相同的端点 端点,其中包含您从该端点收到的有效载荷。主要区别是您不需要设置任何 accord- 标头(因为不需要它们进行路由)。您也不需要设置任何标记为可选的有效载荷字段。

例如,要发送与上面相同的 !clock 幽灵,您需要向 /ghost/server/123/channel/456/message 发送 POST 请求,并附带 JSON 主体

{
  "id": 0,
  "server_id": 123,
  "channel_id": 456,
  "author": {
    "server_id": 123,
    "user": {
      "id": 0,
      "name": "a ghost"
    },
  },
  "timestamp_created": "2020-01-02T03:04:05Z",
  "content": "!clock"
}

主体中的服务器和频道 ID 将优于 URL 中的 ID,但您仍然应该在 URL 中正确设置它们(以保持未来兼容性)。

目前仅在幽灵接口上实现了以下端点

  • 服务器消息:/ghosts/server/{guild-id}/channel/{channel-id}/message
  • 直接消息:/ghosts/direct/channel/{channel-id}/message

测试设施

要测试 Accord 服务器实现,您可以编写一个查询您服务器的 harness,但由于 Discord 中有多种方式可以响应以实现相同的功能,您可能需要开始复制一些 Accord 功能以合并这些形式,或者测试可能过于严格。

Accord 提供了一个 accord-tester 工具,它的工作方式与主程序完全相同,并接受相同的变量(除了 DISCORD_TOKEN),除了它不会连接到 Discord,而是仅在反向接口上监听,并将原本应在 Discord 上执行的操作以标准化的形式 POST 回您的服务器到 /test/act

处理异步性和请求与收到的回复之间缺乏关系可能很困难;当然,您可以选择不使用此工具,或者仅用于某些集成测试。

此工具默认为 Accord 设置 trace 级别日志记录,您可以通过将 pretty 添加到 RUST_LOG 列表来选择更漂亮的日志消息,例如 RUST_LOG=pretty,info,accord=trace

致谢

背景和愿景

Accord 是一个 Discord API 客户端,用于驱动 Discord API 客户端。就像机器人一样。它本身是在 Twilight Discord API 库的基础上构建的。因此,也许它应该被称为中间件。

Accord 的目的是将专用接口(Discord 的 API)转换为非常通用的接口(向服务器发送 HTTP 调用),然后再转换回来。

当我编写 Discord 机器人时,我发现很多逻辑已经由比 Discord 本身还要老的软件可靠地实现,但必须经常为机器人的特定用例重新实现,而我更愿意编写业务逻辑。

另一个原因是,每当我开始一个Discord机器人项目时,我总是想用不同的语言或不同的技术栈来编写它的一部分。如果我有Accord,我就能做到。

因此,在Accord中,与机器人交互通常是这样的

  1. 有人调用了机器人,例如通过说 !roll d20
  2. Discord通过WebSocket将消息发送给Accord
  3. Accord向 http://127.0.0.1:1234/server/123/channel/456/message 发送POST请求,将消息内容作为正文,同时在头部添加各种元数据
  4. 您的“机器人”,实际上是监听1234端口的服务器,接收这个请求,处理它(掷一个d20),并在响应正文中以200状态码返回答案
  5. Accord读取响应,看到需要回复,如果响应头中没有提供,则添加频道和服务器/公会信息
  6. Accord向Discord发送包含回复的消息。

您不需要让您的机器人自己监听1234端口。实际上,这是不建议的。您应该做的,是运行它位于nginx后面。为什么?让我们通过几个场景来回答

  • 如果命令的答案是始终相同的怎么办?

    与其保持一个活跃的服务器进程并每次都回答相同的内容,不如编写一个nginx规则来匹配该请求,并使用磁盘上的静态文件内容作为回复。

  • 如果答案不经常变化怎么办?

    添加缓存。这可以通过nginx的几种内置方式实现,或者使用Varnish等。

  • 如果答案成本高昂,或者您不希望它被滥用怎么办?

    添加速率限制。这是nginx内置的。

  • 如果您想扩展后端数量怎么办?

    水平扩展,使用nginx的轮询上游支持。

  • 如果您想部分缩小,例如,因为您为许多公会提供服务,需要为昂贵的端点进行分片,但您的便宜端点完全能够处理负载怎么办?

    将分片后的Accord指向它们自己的nginx,并将便宜请求转发到一个后端服务器。

有更多相对常见的情况,在通常的Discord机器人中,可能需要大量的工程,但使用Accord方法,这些情况已经得到解决。

好吧,但,这可能适用于查询-回复机器人,但如果您的机器人需要多次回复,或者需要自发地发布消息,例如对外部事件做出反应,那就另当别论了。

Accord有四种方法。

  1. 自己执行调用。在您的应用程序中有一个Discord客户端进行调用。Accord不会阻止您这样做。

  2. 使用反向接口。Accord公开了自己的服务器,您可以向该服务器发送请求,以使用Accord的Discord连接进行请求。Accord在Discord之上添加了认证,因此您不需要在两个地方处理凭证。

  3. 召唤一个幽灵。您可以通过上述反向接口进行特殊调用,这将导致Accord以通常的方式发送请求,就像它是在响应消息或其他Discord操作一样,但实际上该消息或操作并不存在。通过这种方式,您可以实现相同的代码,并利用现有的分层(缓存等)。

  4. 在需要多次对事件做出响应的特殊情况下,您可以使用分块输出进行响应,通过每30-60秒发送空字节来保持输出流的活动状态,并通过至少两个空字节分隔多个有效负载发送。有效负载将在接收到后立即发送。

如果您需要将一些音频流到一个语音频道怎么办?

  • 您可以将音频以任何Accord支持的格式进行流式传输(如果不支持,它将实时转码),作为响应。

  • 您可以用302重定向回复到一个静态音频文件,Accord也会这样做(如果检测到可以执行范围请求,它可能还会在缓冲方面更聪明一些)。您甚至可以将重定向到外部资源(出于安全和性能的考虑,不推荐这样做……但您确实可以这样做)。

超越Discord

这……只是开始。

因为Discord是一回事,但如果您有Twitter、Matrix、Zulip、IRC、Slack、电子邮件等等相同类型的网关呢?

所有这些在操作方式上都有所不同,从轻微到重大,但您仍然有发布消息并期望得到回答的核心机制。您可能会遇到一些微妙的变化和调整,但如果您能够通过重写一些路由来重用大量功能呢?

上面第一个例子,一个掷骰子的机器人,可以是用于Slack和Discord的后端程序的完全相同的程序。同时,您可以有一个Discord特定的语音端点和一个Slack特定的投票端点。

这难道不是很低效吗?

是的,有点。您不是直接与Discord交互的机器人,而是至少增加了两个额外的层。这增加的只是一些几十毫秒。您所获得的可能值得这么多。很多。

依赖关系

~31–45MB
~791K SLoC