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
- 状态
- 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
— 在服务器中为text
或voice
,在DM中为direct
。accord-message-id
— 在消息上下文中;accord-author-type
或accord-user-type
—bot
或user
;accord-author-id
或accord-user-id
;accord-author-name
或accord-user-name
;accord-author-role-ids
或accord-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-create
act。
通过在每个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进行操作,目前有两种选择
- 直接向Discord发送自己的请求。
- 在反向接口上创建和响应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
。
致谢
- 该 标志 是从 hands-helping solid Font Awesome 图标 中混合而来,许可证为 CC-BY 4.0。
背景和愿景
Accord 是一个 Discord API 客户端,用于驱动 Discord API 客户端。就像机器人一样。它本身是在 Twilight Discord API 库的基础上构建的。因此,也许它应该被称为中间件。
Accord 的目的是将专用接口(Discord 的 API)转换为非常通用的接口(向服务器发送 HTTP 调用),然后再转换回来。
当我编写 Discord 机器人时,我发现很多逻辑已经由比 Discord 本身还要老的软件可靠地实现,但必须经常为机器人的特定用例重新实现,而我更愿意编写业务逻辑。
另一个原因是,每当我开始一个Discord机器人项目时,我总是想用不同的语言或不同的技术栈来编写它的一部分。如果我有Accord,我就能做到。
因此,在Accord中,与机器人交互通常是这样的
- 有人调用了机器人,例如通过说
!roll d20
- Discord通过WebSocket将消息发送给Accord
- Accord向
http://127.0.0.1:1234/server/123/channel/456/message
发送POST请求,将消息内容作为正文,同时在头部添加各种元数据 - 您的“机器人”,实际上是监听1234端口的服务器,接收这个请求,处理它(掷一个d20),并在响应正文中以200状态码返回答案
- Accord读取响应,看到需要回复,如果响应头中没有提供,则添加频道和服务器/公会信息
- Accord向Discord发送包含回复的消息。
您不需要让您的机器人自己监听1234端口。实际上,这是不建议的。您应该做的,是运行它位于nginx后面。为什么?让我们通过几个场景来回答
-
如果命令的答案是始终相同的怎么办?
与其保持一个活跃的服务器进程并每次都回答相同的内容,不如编写一个nginx规则来匹配该请求,并使用磁盘上的静态文件内容作为回复。
-
如果答案不经常变化怎么办?
添加缓存。这可以通过nginx的几种内置方式实现,或者使用Varnish等。
-
如果答案成本高昂,或者您不希望它被滥用怎么办?
添加速率限制。这是nginx内置的。
-
如果您想扩展后端数量怎么办?
水平扩展,使用nginx的轮询上游支持。
-
如果您想部分缩小,例如,因为您为许多公会提供服务,需要为昂贵的端点进行分片,但您的便宜端点完全能够处理负载怎么办?
将分片后的Accord指向它们自己的nginx,并将便宜请求转发到一个后端服务器。
有更多相对常见的情况,在通常的Discord机器人中,可能需要大量的工程,但使用Accord方法,这些情况已经得到解决。
好吧,但,这可能适用于查询-回复机器人,但如果您的机器人需要多次回复,或者需要自发地发布消息,例如对外部事件做出反应,那就另当别论了。
Accord有四种方法。
-
自己执行调用。在您的应用程序中有一个Discord客户端进行调用。Accord不会阻止您这样做。
-
使用反向接口。Accord公开了自己的服务器,您可以向该服务器发送请求,以使用Accord的Discord连接进行请求。Accord在Discord之上添加了认证,因此您不需要在两个地方处理凭证。
-
召唤一个幽灵。您可以通过上述反向接口进行特殊调用,这将导致Accord以通常的方式发送请求,就像它是在响应消息或其他Discord操作一样,但实际上该消息或操作并不存在。通过这种方式,您可以实现相同的代码,并利用现有的分层(缓存等)。
-
在需要多次对事件做出响应的特殊情况下,您可以使用分块输出进行响应,通过每30-60秒发送空字节来保持输出流的活动状态,并通过至少两个空字节分隔多个有效负载发送。有效负载将在接收到后立即发送。
如果您需要将一些音频流到一个语音频道怎么办?
-
您可以将音频以任何Accord支持的格式进行流式传输(如果不支持,它将实时转码),作为响应。
-
您可以用302重定向回复到一个静态音频文件,Accord也会这样做(如果检测到可以执行范围请求,它可能还会在缓冲方面更聪明一些)。您甚至可以将重定向到外部资源(出于安全和性能的考虑,不推荐这样做……但您确实可以这样做)。
超越Discord
这……只是开始。
因为Discord是一回事,但如果您有Twitter、Matrix、Zulip、IRC、Slack、电子邮件等等相同类型的网关呢?
所有这些在操作方式上都有所不同,从轻微到重大,但您仍然有发布消息并期望得到回答的核心机制。您可能会遇到一些微妙的变化和调整,但如果您能够通过重写一些路由来重用大量功能呢?
上面第一个例子,一个掷骰子的机器人,可以是用于Slack和Discord的后端程序的完全相同的程序。同时,您可以有一个Discord特定的语音端点和一个Slack特定的投票端点。
这难道不是很低效吗?
是的,有点。您不是直接与Discord交互的机器人,而是至少增加了两个额外的层。这增加的只是一些几十毫秒。您所获得的可能值得这么多。很多。
依赖关系
~31–45MB
~791K SLoC