From c78313f395d6b7e26364e52aae3ab76aa24db4a5 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 12:08:43 +0800 Subject: [PATCH 01/34] docs: remove common plugin examples --- docs/guide/command/execution.md | 2 +- docs/guide/command/help.md | 2 +- docs/guide/introduction/coding.md | 28 +++++++++++++--------------- docs/guide/message/middleware.md | 2 +- docs/guide/misc/docker.md | 6 +++--- docs/guide/plugin/plugin.md | 2 +- 6 files changed, 20 insertions(+), 22 deletions(-) diff --git a/docs/guide/command/execution.md b/docs/guide/command/execution.md index bbbd0af259..786e276749 100644 --- a/docs/guide/command/execution.md +++ b/docs/guide/command/execution.md @@ -49,7 +49,7 @@ app.command('echo').alias('say') ### 快捷方式 -Koishi 的指令机制虽然能够尽可能避免冲突和误触发,但是也带来了一些麻烦。一方面,一些常用指令的调用会受到指令前缀的限制;另一方面,一些指令可能有较长的选项和参数,但它们调用时却往往是相同的。面对这些情况,**快捷方式**能有效地解决你的问题。 +Koishi 的指令机制虽然能够尽可能避免冲突和误触发,但是也带来了一些麻烦。一方面,一些常用指令的调用会受到指令前缀的限制;另一方面,一些指令可能有较长的选项和参数,但它们调用时却往往是相同的。面对这些情况,**快捷方式 (Shortcut)** 能有效地解决你的问题。 假设你实现了一个货币系统和 rank 指令,调用 `rank wealth --global` 可以实现查看全服所有人财富排行,你可以这样做: diff --git a/docs/guide/command/help.md b/docs/guide/command/help.md index 935b486f43..bdeaaf3408 100644 --- a/docs/guide/command/help.md +++ b/docs/guide/command/help.md @@ -5,7 +5,7 @@ sidebarDepth: 2 # 查看和编写帮助 ::: tip -下面的 echo 指令是为了理解方便而举的例子,与 @koishijs/plugin-common 中实际的 echo 指令并不相同。 +下面的 echo 指令是为了理解方便而举的例子,与 @koishijs/plugin-echo 中实际的 echo 指令并不相同。 ::: ## 查看帮助 diff --git a/docs/guide/introduction/coding.md b/docs/guide/introduction/coding.md index 97f6eead4d..2e47ac55bc 100644 --- a/docs/guide/introduction/coding.md +++ b/docs/guide/introduction/coding.md @@ -39,14 +39,14 @@ Koishi 支持多个聊天平台,对于不同的平台,你也需要做好相 npm init # 安装 koishi 和相关库 -npm i koishi @koishijs/plugin-adapter-onebot @koishijs/plugin-common +npm i koishi @koishijs/plugin-adapter-onebot @koishijs/plugin-echo ``` ```yarn # 初始化项目 yarn init # 安装 koishi 和相关库 -yarn add koishi @koishijs/plugin-adapter-onebot @koishijs/plugin-common +yarn add koishi @koishijs/plugin-adapter-onebot @koishijs/plugin-echo ``` ::: @@ -66,9 +66,8 @@ app.plugin('adapter-onebot', { endpoint: 'ws://127.0.0.1:6700', }) -// 安装 common 插件,你可以不传任何配置项 -// 这个插件提供了下面要用到的 echo 指令 -app.plugin('common') +// 安装 echo 插件 +app.plugin('echo') // 启动应用 app.start() @@ -86,9 +85,8 @@ app.plugin('adapter-onebot', { endpoint: 'ws://127.0.0.1:6700', }) -// 安装 common 插件,你可以不传任何配置项 -// 这个插件提供了下面要用到的 echo 指令 -app.plugin('common') +// 安装 echo 插件 +app.plugin('echo') // 启动应用 app.start() @@ -124,7 +122,7 @@ Koishi 插件可以在 [npm](https://www.npmjs.com/) 上获取。要下载的包 ```ts import onebot from '@koishijs/plugin-adapter-onebot' -import * as common from '@koishijs/plugin-common' +import * as echo from '@koishijs/plugin-echo' app.plugin(onebot, { protocol: 'ws', @@ -132,15 +130,15 @@ app.plugin(onebot, { endpoint: 'ws://127.0.0.1:6700', }) -app.plugin(common) +app.plugin(echo) ``` -请注意到上面的两个插件的导入方式的微妙差异。onebot 插件使用了默认导出,而 common 插件使用了导出的命名空间。这两种写法存在本质的区别,不能混用。虽然这可能产生一些困扰,但对 TypeScript 用户来说,只需注意到写代码时的类型提示就足以确定自己应该采用的写法。 +请注意到上面的两个插件的导入方式的微妙差异。onebot 插件使用了默认导出,而 echo 插件使用了导出的命名空间。这两种写法存在本质的区别,不能混用。虽然这可能产生一些困扰,但对 TypeScript 用户来说,只需注意到写代码时的类型提示就足以确定自己应该采用的写法。 -同理,对于 cjs 的使用者,如果要使用 `require` 来获取插件对象,也应注意到这种区别: +同理,对于 commonjs 的使用者,如果要使用 `require` 来获取插件对象,也应注意到这种区别: ```js -// 注意这里的 .default 是不可省略的 +// 这里的 .default 是不可省略的 app.plugin(require('@koishijs/plugin-adapter-onebot').default, { protocol: 'ws', selfId: '123456789', @@ -148,10 +146,10 @@ app.plugin(require('@koishijs/plugin-adapter-onebot').default, { }) // 这里则不能写上 .default -app.plugin(require('@koishijs/plugin-common')) +app.plugin(require('@koishijs/plugin-echo')) ``` -为了避免混淆,我们建议 cjs 的使用者直接使用插件的短名安装插件。 +为了避免混淆,我们建议 commonjs 的使用者直接使用插件的短名安装插件。 ## 添加交互逻辑 diff --git a/docs/guide/message/middleware.md b/docs/guide/message/middleware.md index 07eb0a740c..f1f99c8e36 100644 --- a/docs/guide/message/middleware.md +++ b/docs/guide/message/middleware.md @@ -160,4 +160,4 @@ ctx.middleware((session, next) => { }, true) ``` -搭配使用上面几种中间件,你的机器人便拥有了无限可能。在 @koishijs/plugin-common 库中,就有着一个官方实现的复读功能,它远比上面的示例所显示的更加强大。如果想深入了解中间件机制,可以去研究一下这个功能的 [源代码](https://github.com/koishijs/koishi/blob/master/plugins/common/src/handler.ts)。 +搭配使用上面几种中间件,你的机器人便拥有了无限可能。在 @koishijs/plugin-repeater 库中,就有着一个官方实现的复读功能,它远比上面的示例所显示的更加强大。如果想深入了解中间件机制,可以去研究一下这个功能的 [源代码](https://github.com/koishijs/koishi/blob/master/plugins/common/repeater/src/index.ts)。 diff --git a/docs/guide/misc/docker.md b/docs/guide/misc/docker.md index 08744d78a7..b6b3d78f5c 100644 --- a/docs/guide/misc/docker.md +++ b/docs/guide/misc/docker.md @@ -44,11 +44,11 @@ docker run -d --name koishi \ docker exec -it koishi sh ``` -在容器内,你可以安装所需要的插件(这里以 @koishijs/plugin-onebot 和 @koishijs/plugin-common 为例): +在容器内,你可以安装所需要的插件(这里以 @koishijs/plugin-onebot 和 @koishijs/plugin-echo 为例): ```cli # 安装插件 -npm i @koishijs/plugin-onebot @koishijs/plugin-common +npm i @koishijs/plugin-onebot @koishijs/plugin-echo # 退出容器 exit @@ -64,7 +64,7 @@ module.exports = { selfId: 123456789, // 插件列表 plugins: { - common: {}, + echo: {}, }, } ``` diff --git a/docs/guide/plugin/plugin.md b/docs/guide/plugin/plugin.md index d454e47333..66a45491d4 100644 --- a/docs/guide/plugin/plugin.md +++ b/docs/guide/plugin/plugin.md @@ -143,7 +143,7 @@ export default function (ctx: Context) { 这样当你加载 nested-plugin 时,就相当于同时加载了 a 和 b 两个插件。 -Koishi 的许多官方插件都采用了这种写法,例如 [@koishijs/plugin-common](https://github.com/koishijs/koishi/blob/master/plugins/common/src/index.ts)。 +Koishi 的许多插件都采用了这种写法,例如 [koishi-plugin-tools](https://github.com/koishijs/koishi-plugin-tools)。 ## 卸载插件 From 90062b5a02613be277d5ee4927596942528b343f Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 14:15:53 +0800 Subject: [PATCH 02/34] docs: common plugin docs --- docs/.vuepress/config.js | 42 +++--- docs/api/tools/cli.md | 5 - docs/guide/service/cache.md | 5 - docs/plugins/accessibility/admin.md | 41 ++++++ docs/plugins/accessibility/bind.md | 22 ++++ docs/plugins/accessibility/callme.md | 15 +++ .../{other => accessibility}/schedule.md | 10 +- docs/plugins/accessibility/sudo.md | 36 ++++++ docs/plugins/accessibility/verifier.md | 39 ++++++ docs/plugins/common/admin.md | 79 ------------ docs/plugins/common/basic.md | 120 ------------------ docs/plugins/common/handler.md | 107 ---------------- docs/plugins/common/index.md | 55 -------- docs/plugins/database/level.md | 7 + docs/plugins/index.md | 3 + docs/plugins/message/broadcast.md | 25 ++++ docs/plugins/message/echo.md | 26 ++++ docs/plugins/message/feedback.md | 34 +++++ docs/plugins/message/forward.md | 27 ++++ docs/plugins/message/recall.md | 18 +++ docs/plugins/{common => message}/repeater.md | 2 +- docs/plugins/message/respondent.md | 39 ++++++ 22 files changed, 362 insertions(+), 395 deletions(-) delete mode 100644 docs/api/tools/cli.md delete mode 100644 docs/guide/service/cache.md create mode 100644 docs/plugins/accessibility/admin.md create mode 100644 docs/plugins/accessibility/bind.md create mode 100644 docs/plugins/accessibility/callme.md rename docs/plugins/{other => accessibility}/schedule.md (79%) create mode 100644 docs/plugins/accessibility/sudo.md create mode 100644 docs/plugins/accessibility/verifier.md delete mode 100644 docs/plugins/common/admin.md delete mode 100644 docs/plugins/common/basic.md delete mode 100644 docs/plugins/common/handler.md delete mode 100644 docs/plugins/common/index.md create mode 100644 docs/plugins/database/level.md create mode 100644 docs/plugins/message/broadcast.md create mode 100644 docs/plugins/message/echo.md create mode 100644 docs/plugins/message/feedback.md create mode 100644 docs/plugins/message/forward.md create mode 100644 docs/plugins/message/recall.md rename docs/plugins/{common => message}/repeater.md (99%) create mode 100644 docs/plugins/message/respondent.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 7565665484..8965a1e6c5 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -113,7 +113,6 @@ module.exports = { isGroup: true, children: [ '/guide/service/assets.md', - '/guide/service/cache.md', '/guide/service/http.md', '/guide/service/route.md', '/guide/service/logger.md', @@ -164,12 +163,6 @@ module.exports = { '/api/utils/logger.md', '/api/utils/misc.md', ], - }, { - text: '其他官方包', - isGroup: true, - children: [ - '/api/tools/cli.md', - ], }, { text: '更新与迁移', isGroup: true, @@ -210,6 +203,30 @@ module.exports = { '/plugins/assets/remote.md', '/plugins/assets/s3.md', ], + }, { + text: '交互功能', + isGroup: true, + children: [ + '/plugins/message/broadcast.md', + '/plugins/message/echo.md', + '/plugins/message/feedback.md', + '/plugins/message/forward.md', + '/plugins/message/recall.md', + '/plugins/message/repeater.md', + '/plugins/message/respondent.md', + ], + }, { + text: '辅助功能', + isGroup: true, + children: [ + '/plugins/accessibility/admin.md', + '/plugins/accessibility/bind.md', + '/plugins/accessibility/callme.md', + '/plugins/accessibility/rate-limit.md', + '/plugins/accessibility/schedule.md', + '/plugins/accessibility/sudo.md', + '/plugins/accessibility/verifier.md', + ], }, { text: '控制台开发', isGroup: true, @@ -221,16 +238,6 @@ module.exports = { '/plugins/console/server.md', '/plugins/console/client.md', ], - }, { - text: '交互功能', - isGroup: true, - children: [ - '/plugins/common/index.md', - '/plugins/common/basic.md', - '/plugins/common/handler.md', - '/plugins/common/repeater.md', - '/plugins/common/admin.md', - ], }, { text: '教学系统 (Teach)', isGroup: true, @@ -269,7 +276,6 @@ module.exports = { '/plugins/other/github.md', '/plugins/other/mock.md', '/plugins/other/puppeteer.md', - '/plugins/other/schedule.md', ], }], }, diff --git a/docs/api/tools/cli.md b/docs/api/tools/cli.md deleted file mode 100644 index 005f1f32cd..0000000000 --- a/docs/api/tools/cli.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebarDepth: 2 ---- - -# 命令行工具 (CLI) diff --git a/docs/guide/service/cache.md b/docs/guide/service/cache.md deleted file mode 100644 index 1a0c03e7b4..0000000000 --- a/docs/guide/service/cache.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebarDepth: 2 ---- - -# 使用缓存数据 diff --git a/docs/plugins/accessibility/admin.md b/docs/plugins/accessibility/admin.md new file mode 100644 index 0000000000..c7fde72568 --- /dev/null +++ b/docs/plugins/accessibility/admin.md @@ -0,0 +1,41 @@ +--- +sidebarDepth: 2 +--- + +# 数据管理 (Admin) + +::: tip +本章中介绍的内容需要你安装数据库支持,同时建议提前阅读 [指南 · 用户系统管理](../../guide/manage.md)。 +::: + +## 指令:authorize + +- 别名:auth +- 基本语法:`authorize -t ` +- 最低权限:4 + +authorize 指令用于设置用户的权限等级。该指令 4 级权限才能调用,且需要满足目标用户的权限和要设定的权限都严格小于自己的权限等级,否则无法设置。 + +## 指令:assign + +- 基本语法:`assign -t [channel] [assignee]` +- 最低权限:4 + +assign 指令可用于设置频道的 [代理者](../../guide/manage.md#平台相关字段)。该指令 4 级权限才能调用。 + +如果 `-t [channel]` 缺省,则表示目标频道为当前频道(因此私聊状态下不能缺省);如果 `assignee` 缺省,则表示当前接收消息的机器人账号。举个例子,如果要设定一个频道 A 的代理者为 B,下面的两种做法是等价的: + +1. 私聊机器人 B,发送 `assign -t #A` +2. 在频道 A 中发送 `@B assign`(假设 B 能收到此消息) + +## 指令:user.flag +## 指令:channel.flag + +- 基本语法:`xxx.flag [...names]` +- 选项: + - `-l, --list` 标记列表 + - `-s, --set` 添加标记(需要 4 级权限) + - `-S, --unset` 删除标记(需要 4 级权限) + - `-t, --target [@user | #channel]` 目标用户 / 频道(需要 3 级权限) + +这两个指令用于查看和修改用户或频道的状态标签。如果不提供选项,则会显示当前的状态标签。如果使用了 `-l`,就会列出所有可用的状态标签。如果使用了 `-s` 或 `-S`,则会添加 / 删除 `names` 中的每一个状态标签。 diff --git a/docs/plugins/accessibility/bind.md b/docs/plugins/accessibility/bind.md new file mode 100644 index 0000000000..3d2d72434a --- /dev/null +++ b/docs/plugins/accessibility/bind.md @@ -0,0 +1,22 @@ +--- +sidebarDepth: 2 +--- + +# 账号绑定 (Bind) + +::: tip +要使用本插件,你需要安装数据库支持。 +::: + +@koishijs/plugin-bind 提供了一个指令,允许用户进行跨平台的账号绑定。 + +## 指令:bind + +- 基本语法:`bind` +- 最低权限:0 + +bind 指令用于跨平台绑定账号。该指令 0 级权限即可调用。 + +如果此指令在私聊环境下被调用,则 Koishi 会生成一串随机码。你只需在 5 分钟内使用你的其他账号在要绑定的平台内向机器人发送这串随机码,即可完成绑定。 + +如果此指令在群聊环境下被调用,由于此时生成的随机码是公开的,你需要首先按照上述流程发送一次随机码。接着,收到并核验过随机码的机器人将再向你发送一串新的随机码。你仍需要在 5 分钟内使用你一开始的账号在之前的平台内向机器人发送这串随机码,即可完成绑定。 diff --git a/docs/plugins/accessibility/callme.md b/docs/plugins/accessibility/callme.md new file mode 100644 index 0000000000..6a0a2539d7 --- /dev/null +++ b/docs/plugins/accessibility/callme.md @@ -0,0 +1,15 @@ +--- +sidebarDepth: 2 +--- + +# 设置昵称 (Callme) + +::: tip +要使用本插件,你需要安装数据库支持。 +::: + +## 指令:callme + +- 基本语法:`callme [name]` + +callme 指令用于修改用户的昵称。如果不传入参数,则机器人会返回你当前的昵称。重复的昵称、空昵称和含有消息段的昵称是不被接受的。 diff --git a/docs/plugins/other/schedule.md b/docs/plugins/accessibility/schedule.md similarity index 79% rename from docs/plugins/other/schedule.md rename to docs/plugins/accessibility/schedule.md index 379a9930f0..aae2b07eb1 100644 --- a/docs/plugins/other/schedule.md +++ b/docs/plugins/accessibility/schedule.md @@ -4,16 +4,16 @@ sidebarDepth: 2 # 计划任务 (Schedule) -::: warning -要使用本插件,你需要安装 mysql 或 mongo 数据库支持。 +::: tip +要使用本插件,你需要安装数据库支持。 ::: -koishi-plugin-schedule 用于设置和触发计划任务。 +@koishijs/plugin-schedule 用于设置和触发计划任务。 -.schedule 1m -- echo 233 +schedule 1m -- echo 233 日程已创建,编号为 1。 -.schedule -l +schedule -l 1. 今天 10:01:echo 233

——— 1 分钟后 ———

233 diff --git a/docs/plugins/accessibility/sudo.md b/docs/plugins/accessibility/sudo.md new file mode 100644 index 0000000000..565e3aecc0 --- /dev/null +++ b/docs/plugins/accessibility/sudo.md @@ -0,0 +1,36 @@ +--- +sidebarDepth: 2 +--- + +# 模拟调用 (Sudo) + +## 指令:sudo + +- 基本语法:`sudo ` +- 最低权限:3 +- 选项: + - `-u, --user [@user]` 目标用户(私聊) + - `-m, --member [@user]` 目标用户(群聊) + - `-c, --channel [#channel]` 目标频道 + +sudo 指令允许你模拟其他用户调用指令。例如当你在私聊上下文时: + +```sh +teach foo bar # 无效,因为 teach 指令只对群上下文生效 +ctxf -g #456 teach foo bar # 有效,相当于在群 456 调用 teach foo bar +``` + +除此以外,你还可以模拟在其他频道中调用(假设你现在在频道 123 中调用指令): + +```sh +ctxf -g #456 command # 模拟你在群 456 的上下文 +ctxf -u @789 command # 模拟用户 789 的私聊上下文 +ctxf -m @789 command # 模拟用户 789 在当前频道的上下文 +ctxf -u @789 -g #456 command # 模拟用户 789 在频道 456 的上下文 +``` + +尽管切换了调用上下文,但 sudo 指令的输出仍然产生在原上下文中。这在你想调用群指令的时候是很有用的。 + +::: tip 提示 +为了安全性考虑,sudo 命令设计的最低使用权限为 3 级,同时切换的用户等级不能高于或等于调用者自身。 +::: diff --git a/docs/plugins/accessibility/verifier.md b/docs/plugins/accessibility/verifier.md new file mode 100644 index 0000000000..53fe2c1bd6 --- /dev/null +++ b/docs/plugins/accessibility/verifier.md @@ -0,0 +1,39 @@ +--- +sidebarDepth: 2 +--- + +# 处理申请 (Verifier) + +@koishijs/plugin-verifier 可用于配置机器人接收到各类申请时的行为。 + +```js koishi.config.js +module.exports = { + plugins: { + verifier: { + onFriendRequest: true, // 通过所有好友申请 + onGroupMemberRequest: undefined, // 忽略所有加群申请(当然这没必要写出来) + async onGroupRequest(session) { + // 拒绝所有来自 1 级以下,通过所有来自 3 级或以上权限用户的加群邀请,其他不处理 + const user = await session.observeUser(['authority']) + if (user.authority >= 3) { + return true + } else if (user.authority <= 1) { + return false + } + }, + }, + }, +} +``` + +在上面的例子中,`onFriendRequest`, `onGroupMemberRequest` 和 `onGroupRequest` 分别用于处理好友申请,加群申请和加群邀请。每个选项的值都可以是下面几种类型: + +- true: 表示通过申请 +- false: 表示拒绝申请 +- undefined: 表示不做处理 +- 字符串 + - 如果是好友申请,则表示通过,并使用该字符串作为该好友的备注名 + - 如果是加群申请或邀请,则表示拒绝,并使用该字符串作为拒绝的理由 +- 函数 + - 传入两个参数,第一个是请求对应的 Session 对象,第二个是所在的 App 实例 + - 返回值同样可以是 true, false, undefined, 字符串或对应的 Promise,将按照上面所说的方式来解读 diff --git a/docs/plugins/common/admin.md b/docs/plugins/common/admin.md deleted file mode 100644 index a91557f0b3..0000000000 --- a/docs/plugins/common/admin.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -sidebarDepth: 2 ---- - -# 数据管理 - -::: tip -本章中介绍的内容需要你安装数据库支持,同时建议提前阅读 [指南 · 用户系统管理](../../guide/manage.md)。 -::: - -## 基础功能 - -### 指令:callme - -- 基本语法:`callme [name]` - -callme 指令用于修改用户的昵称。如果不传入参数,则机器人会返回你当前的昵称。重复的昵称、空昵称和含有消息段的昵称是不被接受的。 - -### 指令:bind - -- 基本语法:`bind` -- 最低权限:0 - -bind 指令用于跨平台绑定账号。该指令 0 级权限即可调用。 - -如果此指令在私聊环境下被调用,则 Koishi 会生成一串随机码。你只需在 5 分钟内使用你的其他账号在要绑定的平台内向机器人发送这串随机码,即可完成绑定。 - -如果此指令在群聊环境下被调用,由于此时生成的随机码是公开的,你需要首先按照上述流程发送一次随机码。接着,收到并核验过随机码的机器人将再向你发送一串新的随机码。你仍需要在 5 分钟内使用你一开始的账号在之前的平台内向机器人发送这串随机码,即可完成绑定。 - -### 指令:authorize - -- 别名:auth -- 基本语法:`authorize -t ` -- 最低权限:4 - -authorize 指令用于设置用户的权限等级。该指令 4 级权限才能调用,且需要满足目标用户的权限和要设定的权限都严格小于自己的权限等级,否则无法设置。 - -### 指令:assign - -- 基本语法:`assign -t [channel] [assignee]` -- 最低权限:4 - -assign 指令可用于设置频道的 [代理者](../../guide/manage.md#平台相关字段)。该指令 4 级权限才能调用。 - -如果 `-t [channel]` 缺省,则表示目标频道为当前频道(因此私聊状态下不能缺省);如果 `assignee` 缺省,则表示当前接收消息的机器人账号。举个例子,如果要设定一个频道 A 的代理者为 B,下面的两种做法是等价的: - -1. 私聊机器人 B,发送 `assign -t #A` -2. 在频道 A 中发送 `@B assign`(假设 B 能收到此消息) - -## 高级用法 - -所有本节中介绍的指令都是指令 user 和 channel 的子指令,且它们都拥有下面的基本选项: - -- `-t, --target [@user|#channel]` 目标用户 / 频道(需要 3 级权限) - -与上一节介绍的两个指令类似,当这个选项缺省时,默认的目标都是当前用户或当前频道。 - -### 指令:user.usage -### 指令:user.timer - -- 基本语法:`user.xxx [key] [value]` -- 选项: - - `-s, --set` 设置访问记录(需要 4 级权限) - - `-c, --clear` 清除访问记录(需要 4 级权限) - -这两个指令用于查看和修改用户的访问记录,参见 [指令调用管理](../../guide/manage.md#指令调用管理)。 - -如果不提供 `-s` 和 `-c` 选项,则会显示当前的访问记录。如果使用了 `-s`,就会设置名为 `key` 的访问记录为 `value`。如果使用了 `-c` 且提供了 `key`,就会清除名为 `key` 的访问记录;否则会清除所有的访问记录。 - -### 指令:user.flag -### 指令:channel.flag - -- 基本语法:`xxx.flag [...names]` -- 选项: - - `-l, --list` 标记列表 - - `-s, --set` 添加标记(需要 4 级权限) - - `-S, --unset` 删除标记(需要 4 级权限) - -这两个指令用于查看和修改用户或频道的状态标签。如果不提供选项,则会显示当前的状态标签。如果使用了 `-l`,就会列出所有可用的状态标签。如果使用了 `-s` 或 `-S`,则会添加 / 删除 `names` 中的每一个状态标签。 diff --git a/docs/plugins/common/basic.md b/docs/plugins/common/basic.md deleted file mode 100644 index 72235a30a9..0000000000 --- a/docs/plugins/common/basic.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -sidebarDepth: 2 ---- - -# 基础指令 - -- 标有 的功能只能在群聊环境触发 -- 标有 的功能需要你安装数据库支持 - -## 指令:echo - -- 基本语法:`echo ` -- 最低权限:2 -- 选项: - - `-e, --escape` 发送转义消息(需要 3 级权限) - - `-u, --user [@user]` 目标用户(需要 3 级权限) - - `-c, --channel [#channel]` 目标频道(需要 3 级权限) - -你可以使用 echo 指令发送消息到特定的上下文: - -```sh -echo foo bar # 向当前上下文发送 foo bar -echo -u @foo foo bar # 向用户 foo 私聊发送 foo bar -echo -c #bar foo bar # 向频道 bar 发送 foo bar -``` - -::: tip 提示 -echo 指令的 message 参数是一个 [文本参数](../../guide/command.md#文本参数),因此你应该把所有的选项写到消息前面,否则会被认为是消息的一部分。下面的几个指令也是如此。 -::: - -## 指令:broadcast - -- 基本语法:`broadcast ` -- 最低权限:4 -- 选项: - - `-o, --only` 仅向当前账号负责的群进行广播 - - `-f, --forced` 无视 silent 标签进行广播 - -broadcast 指令用于按照 [代理者](../guide/manage.md#平台相关字段) 向所有机器人所负责的频道发送一段文本(默认情况下有 silent 标签的群不发送)。你可以这样调用它: - -```sh -broadcast foo bar baz # 向所有频道发送 foo bar baz -``` - -当一个机器人账号同时向多个频道发送广播消息时,为了避免风控,Koishi 会给每条消息发送后添加一段延迟,可以通过 [`delay.broadcast`](../../api/core/app.md#options-delay) 进行配置。 - -## 指令:contextify - -- 别名:ctxf -- 基本语法:`contextify ` -- 最低权限:3 -- 选项: - - `-u, --user [@user]` 目标用户(私聊) - - `-m, --member [@user]` 目标用户(群聊) - - `-c, --channel [#channel]` 目标频道 - -与上面的两个指令相反,contextify 指令可以让你临时切换上下文调用指令。例如当你在私聊上下文时: - -```sh -teach foo bar # 无效,因为 teach 指令只对群上下文生效 -ctxf -g #456 teach foo bar # 有效,相当于在群 456 调用 teach foo bar -``` - -除此以外,你还可以模拟其他上下文调用(假设你现在在群 123 中调用指令): - -```sh -ctxf -g #456 command # 模拟你在群 456 的上下文 -ctxf -u @789 command # 模拟用户 789 的私聊上下文 -ctxf -m @789 command # 模拟用户 789 在当前频道的上下文 -ctxf -u @789 -g #456 command # 模拟用户 789 在频道 456 的上下文 -``` - -尽管切换了调用上下文,但 contextify 指令的输出仍然产生在原上下文中。这在你想调用群指令的时候是很有用的。 - -::: tip 提示 -为了安全性考虑,contextify 命令设计的最低使用权限为 3 级,同时切换的用户等级不能高于或等于调用者自身。 -::: - -## 指令:feedback - -- 基本语法:`feedback ` - -feedback 指令用于向开发者反馈信息。你需要首先配置 `operator` 配置项: - -```js koishi.config.js -module.exports = { - plugins: { - common: { - // 填你自己的账号,格式为 {platform}:{userId} - // 也可以设置为一个数组,消息会被发送给每一个账户 - operator: 'onebot:123456789', - }, - }, -} -``` - -这样,当有人调用 feedback 指令时,传入的 message 就会自动被私聊发送给你。你也可以直接回复收到的反馈信息,机器人会把这些消息重新发回到调用 feedback 指令的上下文。这里的用法类似后面将介绍的 [跨频道消息转发](./handler.md#跨频道消息转发)。 - - - -

收到来自 Alice 的反馈信息:

-

我也不知道该写什么总之这是一句话

-
- -
-

收到来自 Alice 的反馈信息:

-

我也不知道该写什么总之这是一句话

-
-

那么这是一句回复

-
-
- -## 指令:recall - -- 基本语法:`recall [count]` -- 最低权限:2 - -recall 指令用于撤回机器人在当前频道发送的最后几条消息。count 是要撤回的消息的数量,缺省时为 1。 - -与 broadcast 类似,为了避免风控,每撤回一条消息后 Koishi 也会等待一段时间,同样可以通过 [`delay.broadcast`](../../api/core/app.md#options-delay) 进行配置。 diff --git a/docs/plugins/common/handler.md b/docs/plugins/common/handler.md deleted file mode 100644 index 217e8f0dbc..0000000000 --- a/docs/plugins/common/handler.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -sidebarDepth: 2 ---- - -# 处理事件 - -## 处理好友和群申请 - -当使用了 koishi-plugin-common 并配置了数据库时,默认情况下 Koishi 会通过所有 1 级以上用户的好友申请,忽略所有群申请。你可以手动设置忽略和通过的函数: - -```js koishi.config.js -module.exports = { - plugins: { - common: { - onFriendRequest: true, // 通过所有好友申请 - onGroupMemberRequest: undefined, // 忽略所有加群申请(当然这没必要写出来) - async onGroupRequest(session) { - // 拒绝所有来自 1 级以下,通过所有来自 3 级或以上权限用户的加群邀请,其他不处理 - const user = await session.observeUser(['authority']) - if (user.authority >= 3) { - return true - } else if (user.authority <= 1) { - return false - } - }, - }, - }, -} -``` - -在上面的例子中,`onFriendRequest`, `onGroupMemberRequest` 和 `onGroupRequest` 分别用于处理好友申请,加群申请和加群邀请。每个选项的值都可以是下面几种类型: - -- true: 表示通过申请 -- false: 表示拒绝申请 -- undefined: 表示不做处理 -- 字符串 - - 如果是好友申请,则表示通过,并使用该字符串作为该好友的备注名 - - 如果是加群申请或邀请,则表示拒绝,并使用该字符串作为拒绝的理由 -- 函数 - - 传入两个参数,第一个是请求对应的 Session 对象,第二个是所在的 App 实例 - - 返回值同样可以是 true, false, undefined, 字符串或对应的 Promise,将按照上面所说的方式来解读 - -## 配置内置问答 - -respondent 插件允许设置一套内置问答,就像这样: - -```js koishi.config.js -module.exports = { - plugins: { - common: { - respondent: [{ - match: 'awsl', - reply: '爱我苏联', - }, { - match: /^\s*(\S +){2,}\S\s*$/, - reply: '空格警察,出动!', - }, { - match: /^(.+)一时爽$/, - reply: (_, str) => `一直${str}一直爽`, - }], - }, - }, -} -``` - - - -其中 `match` 可以是一个字符串或正则表达式,用来表示要匹配的内容;`reply` 可以是一个字符串或传入字符串的函数,用来表示输出的结果。`respondent` 数组会按照从上到下的顺序进行匹配。 - -如果想要加入更高级和用户可定义的问答系统,可以参见 [koishi-plugin-teach](../teach.md)。 - -## 跨频道消息转发 - -koishi-plugin-common 也支持在不同的频道之间转发消息。 - -```js koishi.config.js -module.exports = { - plugins: { - common: { - relay: [{ - // 请使用 {platform}:{channelId} 的格式 - source: 'onebot:123456789', - destination: 'discord:987654321', - }], - }, - }, -} -``` - -当用户 Alice 在频道 `source` 中发送消息 foo 的时候,koishi 就会在频道 `destination` 中发送如下的内容。接着,频道 `destination` 中的用户 Bob 也可以通过引用回复这条消息的方式将自己想说的话发回到频道 `source` 中去。 - - - -

Alice: foo

-
- -

Alice: foo

-

bar

-
-
diff --git a/docs/plugins/common/index.md b/docs/plugins/common/index.md deleted file mode 100644 index 088aef6fe6..0000000000 --- a/docs/plugins/common/index.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: 总览 -sidebarDepth: 2 ---- - -# 常用功能 (common) - -::: tip 提示 -本章介绍的功能都由 koishi-plugin-common 插件提供。 -::: - -koishi-plugin-common 包含了一些基本插件,它们在你使用 `koishi` 的命令行工具时是默认安装的。 - -## 部分安装 - -如果你觉得某些功能不需要的话,你也可以选择在配置项中排除部分功能: - -```js koishi.config.js -module.exports = { - plugins: { - common: { - // 不安装 broadcast 指令 - broadcast: false, - }, - }, -} -``` - -或者通过使用导入子功能的方式只安装部分功能: - -```js index.js -import { broadcast } from 'koishi-plugin-common' - -// 只安装 broadcast 指令 -app.plugin(broadcast) -``` - -## 功能列表 - -以下列出了这个插件包含的功能列表: - -| 功能名称 | 需要数据库 | 支持部分排除 | -| :-: | :-: | :-: | -| [admin](./admin.md) | 是 | 是 | -| [bind](./admin.md#指令-bind) | 是 | 是 | -| [broadcast](./basic.md#指令-broadcast) | 是 | 是 | -| [callme](./admin.md#指令-callme) | 是 | 是 | -| [contextify](./basic.md#指令-contextify) | 是 | 是 | -| [echo](./basic.md#指令-echo) | 否 | 是 | -| [feedback](./basic.md#指令-feedback) | 否 | 通过 `operator` 配置 | -| [recall](./basic.md#指令-recall) | 否 | 是 | -| [relay](./handler.md#跨频道消息转发) | 否 | 是 | -| [repeater](./repeater.md) | 否 | 是 | -| [respondent](./handler.md#配置内置问答) | 否 | 是 | -| [verifier](./handler.md#处理好友和群申请) | 否 | 是 | diff --git a/docs/plugins/database/level.md b/docs/plugins/database/level.md new file mode 100644 index 0000000000..6504217e81 --- /dev/null +++ b/docs/plugins/database/level.md @@ -0,0 +1,7 @@ +--- +title: 数据库:Level +sidebarDepth: 2 +--- + +# @koishijs/plugin-database-level + diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 9f47cc9f3e..5f58ecc1e5 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -17,6 +17,7 @@ Koishi 官方提供了许多插件。为了更好地模块化开发,它们被 ## 数据库支持 +- [@koishijs/plugin-database-level](./database/level.md) - [@koishijs/plugin-database-memory](./database/memory.md) - [@koishijs/plugin-database-mongo](./database/mongo.md) - [@koishijs/plugin-database-mysql](./database/mysql.md) @@ -29,6 +30,8 @@ Koishi 官方提供了许多插件。为了更好地模块化开发,它们被 - [@koishijs/plugin-assets-remote](./assets/remote.md) - [@koishijs/plugin-assets-s3](./assets/s3.md) +## 常用功能 + ## 控制台开发 - [@koishijs/plugin-console](./console/console.md) diff --git a/docs/plugins/message/broadcast.md b/docs/plugins/message/broadcast.md new file mode 100644 index 0000000000..d3e58cfc79 --- /dev/null +++ b/docs/plugins/message/broadcast.md @@ -0,0 +1,25 @@ +--- +sidebarDepth: 2 +--- + +# 发送广播 (Broadcast) + +::: tip +要使用本插件,你需要安装数据库支持。 +::: + +## 指令:broadcast + +- 基本语法:`broadcast ` +- 最低权限:4 +- 选项: + - `-o, --only` 仅向当前账号负责的群进行广播 + - `-f, --forced` 无视 silent 标签进行广播 + +broadcast 指令用于按照 [代理者](../guide/manage.md#平台相关字段) 向所有机器人所负责的频道发送一段文本(默认情况下有 silent 标签的群不发送)。你可以这样调用它: + +```sh +broadcast foo bar baz # 向所有频道发送 foo bar baz +``` + +当一个机器人账号同时向多个频道发送广播消息时,为了避免风控,Koishi 会给每条消息发送后添加一段延迟,可以通过 [`delay.broadcast`](../../api/core/app.md#options-delay) 进行配置。 diff --git a/docs/plugins/message/echo.md b/docs/plugins/message/echo.md new file mode 100644 index 0000000000..16a7f3a337 --- /dev/null +++ b/docs/plugins/message/echo.md @@ -0,0 +1,26 @@ +--- +sidebarDepth: 2 +--- + +# 发送消息 (Echo) + +## 指令:echo + +- 基本语法:`echo ` +- 最低权限:2 +- 选项: + - `-e, --escape` 发送转义消息(需要 3 级权限) + - `-u, --user [@user]` 目标用户(需要 3 级权限) + - `-c, --channel [#channel]` 目标频道(需要 3 级权限) + +你可以使用 echo 指令发送消息到特定的上下文: + +```sh +echo foo bar # 向当前上下文发送 foo bar +echo -u @foo foo bar # 向用户 foo 私聊发送 foo bar +echo -c #bar foo bar # 向频道 bar 发送 foo bar +``` + +::: tip 提示 +echo 指令的 message 参数是一个 [文本参数](../../guide/command.md#文本参数),因此你应该把所有的选项写到消息前面,否则会被认为是消息的一部分。下面的几个指令也是如此。 +::: diff --git a/docs/plugins/message/feedback.md b/docs/plugins/message/feedback.md new file mode 100644 index 0000000000..06a731549d --- /dev/null +++ b/docs/plugins/message/feedback.md @@ -0,0 +1,34 @@ +--- +sidebarDepth: 2 +--- + +# 发送反馈 (Feedback) + +## 指令:feedback + +- 基本语法:`feedback ` + +feedback 指令用于向开发者反馈信息: + +```yaml koishi.config.yaml +plugins: + feedback: + # 填写接收者的账号,格式为 {platform}:{userId} + - onebot:123456789 +``` + +这样,当有人调用 feedback 指令时,传入的 message 就会自动被私聊发送给你。你也可以直接回复收到的反馈信息,机器人会把这些消息重新发回到调用 feedback 指令的上下文。这里的用法类似后面将介绍的 [消息转发](./forward.md)。 + + + +

收到来自 Alice 的反馈信息:

+

我也不知道该写什么总之这是一句话

+
+ +
+

收到来自 Alice 的反馈信息:

+

我也不知道该写什么总之这是一句话

+
+

那么这是一句回复

+
+
diff --git a/docs/plugins/message/forward.md b/docs/plugins/message/forward.md new file mode 100644 index 0000000000..4839d721c9 --- /dev/null +++ b/docs/plugins/message/forward.md @@ -0,0 +1,27 @@ +--- +sidebarDepth: 2 +--- + +# 转发消息 (Forward) + +@koishijs/plugin-forward 支持在不同的频道之间转发消息。 + +```yaml koishi.config.yaml +plugins: + forward: + # 请使用 {platform}:{channelId} 的格式 + - source: onebot:123456789 + destination: discord:987654321 +``` + +当用户 Alice 在频道 `source` 中发送消息 foo 的时候,koishi 就会在频道 `destination` 中发送如下的内容。接着,频道 `destination` 中的用户 Bob 也可以通过引用回复这条消息的方式将自己想说的话发回到频道 `source` 中去。 + + + +

Alice: foo

+
+ +

Alice: foo

+

bar

+
+
diff --git a/docs/plugins/message/recall.md b/docs/plugins/message/recall.md new file mode 100644 index 0000000000..41dbea325c --- /dev/null +++ b/docs/plugins/message/recall.md @@ -0,0 +1,18 @@ +--- +sidebarDepth: 2 +--- + +# 撤回消息 (Recall) + +::: tip +此插件仅限在群聊环境中使用。 +::: + +## 指令:recall + +- 基本语法:`recall [count]` +- 最低权限:2 + +recall 指令用于撤回机器人在当前频道发送的最后几条消息。count 是要撤回的消息的数量,缺省时为 1。 + +与 broadcast 类似,为了避免风控,每撤回一条消息后 Koishi 也会等待一段时间,同样可以通过 [`delay.broadcast`](../../api/core/app.md#options-delay) 进行配置。 diff --git a/docs/plugins/common/repeater.md b/docs/plugins/message/repeater.md similarity index 99% rename from docs/plugins/common/repeater.md rename to docs/plugins/message/repeater.md index a1a51c213e..b296d5b9ae 100644 --- a/docs/plugins/common/repeater.md +++ b/docs/plugins/message/repeater.md @@ -2,7 +2,7 @@ sidebarDepth: 2 --- -# 配置复读机 +# 复读机 (Repeater) 复读功能一直是很多机器人的传统艺能,但是 Koishi 敢说自己能做得更多。利用内置的复读插件,你的机器人不仅可以实现概率复读,还可以概率打断,甚至可以检测他人重复复读或打断复读的行为并做出回应。让我们开始吧! diff --git a/docs/plugins/message/respondent.md b/docs/plugins/message/respondent.md new file mode 100644 index 0000000000..063c4861e5 --- /dev/null +++ b/docs/plugins/message/respondent.md @@ -0,0 +1,39 @@ +--- +sidebarDepth: 2 +--- + +# 快捷回复 (Respondent) + +@koishijs/plugin-respondent 允许设置一套内置问答,就像这样: + +```js koishi.config.js +module.exports = { + plugins: { + common: { + respondent: [{ + match: 'awsl', + reply: '爱我苏联', + }, { + match: /^\s*(\S +){2,}\S\s*$/, + reply: '空格警察,出动!', + }, { + match: /^(.+)一时爽$/, + reply: (_, str) => `一直${str}一直爽`, + }], + }, + }, +} +``` + + + +其中 `match` 可以是一个字符串或正则表达式,用来表示要匹配的内容;`reply` 可以是一个字符串或传入字符串的函数,用来表示输出的结果。`respondent` 数组会按照从上到下的顺序进行匹配。 + +如果想要加入更高级和用户可定义的问答系统,可以参见 [@koishijs/plugin-teach](../teach.md)。 From 92e31fc3a0a87f1f3b89df7ae420fcfae0ab005a Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 14:28:52 +0800 Subject: [PATCH 03/34] docs: add plugin-rate-limit --- docs/.vuepress/config.js | 17 ++--- docs/api/core/context.md | 3 - docs/guide/database/builtin.md | 37 ----------- docs/plugins/accessibility/rate-limit.md | 65 +++++++++++++++++++ docs/plugins/accessibility/sudo.md | 10 +-- docs/plugins/{message => common}/broadcast.md | 0 docs/plugins/{message => common}/echo.md | 0 docs/plugins/{message => common}/feedback.md | 0 docs/plugins/{message => common}/forward.md | 0 docs/plugins/{message => common}/recall.md | 0 docs/plugins/{message => common}/repeater.md | 0 .../plugins/{message => common}/respondent.md | 0 docs/plugins/console/commands.md | 6 ++ docs/plugins/index.md | 29 +++++++-- 14 files changed, 108 insertions(+), 59 deletions(-) create mode 100644 docs/plugins/accessibility/rate-limit.md rename docs/plugins/{message => common}/broadcast.md (100%) rename docs/plugins/{message => common}/echo.md (100%) rename docs/plugins/{message => common}/feedback.md (100%) rename docs/plugins/{message => common}/forward.md (100%) rename docs/plugins/{message => common}/recall.md (100%) rename docs/plugins/{message => common}/repeater.md (100%) rename docs/plugins/{message => common}/respondent.md (100%) create mode 100644 docs/plugins/console/commands.md diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 8965a1e6c5..823be27f7b 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -204,16 +204,16 @@ module.exports = { '/plugins/assets/s3.md', ], }, { - text: '交互功能', + text: '常用功能', isGroup: true, children: [ - '/plugins/message/broadcast.md', - '/plugins/message/echo.md', - '/plugins/message/feedback.md', - '/plugins/message/forward.md', - '/plugins/message/recall.md', - '/plugins/message/repeater.md', - '/plugins/message/respondent.md', + '/plugins/common/broadcast.md', + '/plugins/common/echo.md', + '/plugins/common/feedback.md', + '/plugins/common/forward.md', + '/plugins/common/recall.md', + '/plugins/common/repeater.md', + '/plugins/common/respondent.md', ], }, { text: '辅助功能', @@ -234,6 +234,7 @@ module.exports = { '/plugins/console/index.md', '/plugins/console/chat.md', '/plugins/console/manager.md', + '/plugins/console/commands.md', '/plugins/console/status.md', '/plugins/console/server.md', '/plugins/console/client.md', diff --git a/docs/api/core/context.md b/docs/api/core/context.md index 483fe8eedf..100129dfcf 100644 --- a/docs/api/core/context.md +++ b/docs/api/core/context.md @@ -224,10 +224,7 @@ type Plugin = PluginFunction | PluginObject - **checkUnknown:** `boolean` 是否对未知选项进行检测,默认为 `false` - **checkArgCount:** `boolean` 是否对参数个数进行检测,默认为 `false` - **authority:** `number` 最低调用权限,默认为 `1` - - **maxUsage:** `number` 每天最多调用次数,默认为 `Infinity` - - **minInterval:** `number` 每次调用最短时间间隔,默认为 `0` - **showWarning:** `boolean` 当小于最短间隔时是否进行提醒,默认为 `false` - - **usageName:** `string` 调用标识符,默认为指令名,如果多个指令使用同一个标识符,则它们的调用次数将合并计算 - 返回值:[`Command`](./command.md) 注册或修改的指令 在当前上下文中注册或修改一个指令。 diff --git a/docs/guide/database/builtin.md b/docs/guide/database/builtin.md index d5aa2be991..0fff94e75d 100644 --- a/docs/guide/database/builtin.md +++ b/docs/guide/database/builtin.md @@ -136,40 +136,3 @@ ctx.command('echo 输出收到的信息', { authority: 2 }) ``` 这样一来,1 级或以下权限的用户就无法调用 echo 指令;2 级权限用户只能调用 echo 指令但不能使用 -t 参数;3 级或以上权限的用户不受限制。对于受限的用户,机器人将会回复“权限不足”。 - -### 设置访问次数上限 - -有些指令(例如签到抽卡点赞,高性能损耗的计算,限制次数的 API 调用等)我们并不希望被无限制调用,这时我们可以设置每天访问次数的上限: - -```js -// 设置 lottery 指令每人每天只能调用 10 次 -ctx.command('lottery 抽卡', { maxUsage: 10 }) - // 设置使用了 -s 的调用不计入总次数 - .option('--show', '-s 查看已经抽到的物品列表', { notUsage: true }) -``` - -这样一来,所有访问 lottery 指令且不含 -s 选项的调用次数上限便被设成了 10 次。当超出总次数后,机器人将回复“调用次数已达上限”。 - -### 设置最短触发间隔 - -有些指令(例如高强度刷屏)我们并不希望被短时间内重复调用,这时我们可以设置最短触发间隔: - -```js -const { Time } = require('koishi') - -// 设置 lottery 指令每 60 秒只能调用 1 次 -ctx.command('lottery', { minInterval: Time.minute }) -``` - -这样一来,lottery 指令被调用后 60 秒内,如果再次被调用,将会提示“调用过于频繁,请稍后再试”。当然,`notUsage` 对 `minInterval` 也同样生效。 - -### 多指令共享调用限制 - -如果我们希望让多个指令共同同一个调用限制,可以通过 `usageName` 来实现: - -```js -ctx.command('lottery 常驻抽卡', { maxUsage: 10 }) -ctx.command('accurate 精准抽卡', { maxUsage: 10, usageName: 'lottery' }) -``` - -这样一来,就能限制每天的 lottery 和 accurate 指令的调用次数之和不超过 10 了。 diff --git a/docs/plugins/accessibility/rate-limit.md b/docs/plugins/accessibility/rate-limit.md new file mode 100644 index 0000000000..a4d28e7ddf --- /dev/null +++ b/docs/plugins/accessibility/rate-limit.md @@ -0,0 +1,65 @@ +--- +sidebarDepth: 2 +--- + +# 速率控制 (Rate Limit) + +::: tip +要使用本插件,你需要安装数据库支持。 +::: + +## 指令配置项 + +@koishijs/plugin-rate-limit 会在当前应用激活以下的指令配置项: + +### maxUsage + +有些指令(例如签到抽卡点赞,高性能损耗的计算,限制次数的 API 调用等)我们并不希望被无限制调用,这时我们可以设置每天访问次数的上限: + +```js +// 设置 lottery 指令每人每天只能调用 10 次 +ctx.command('lottery 抽卡', { maxUsage: 10 }) + // 设置使用了 -s 的调用不计入总次数 + .option('--show', '-s 查看已经抽到的物品列表', { notUsage: true }) +``` + +这样一来,所有访问 lottery 指令且不含 -s 选项的调用次数上限便被设成了 10 次。当超出总次数后,机器人将回复“调用次数已达上限”。 + +### minInterval + +有些指令(例如高强度刷屏)我们并不希望被短时间内重复调用,这时我们可以设置最短触发间隔: + +```js +const { Time } = require('koishi') + +// 设置 lottery 指令每 60 秒只能调用 1 次 +ctx.command('lottery', { minInterval: Time.minute }) +``` + +这样一来,lottery 指令被调用后 60 秒内,如果再次被调用,将会提示“调用过于频繁,请稍后再试”。当然,`notUsage` 对 `minInterval` 也同样生效。 + +### usageName + +如果我们希望让多个指令共同同一个调用限制,可以通过 `usageName` 来实现: + +```js +ctx.command('lottery 常驻抽卡', { maxUsage: 10 }) +ctx.command('accurate 精准抽卡', { maxUsage: 10, usageName: 'lottery' }) +``` + +这样一来,就能限制每天的 lottery 和 accurate 指令的调用次数之和不超过 10 了。 + +## 扩展功能 + +### 指令:user.usage +### 指令:user.timer + +- 基本语法:`user.xxx [key] [value]` +- 选项: + - `-s, --set` 设置访问记录(需要 4 级权限) + - `-c, --clear` 清除访问记录(需要 4 级权限) + - `-t, --target [@user]` 目标用户(需要 3 级权限) + +这两个指令用于查看和修改用户的访问记录,参见 [指令调用管理](../../guide/manage.md#指令调用管理)。 + +如果不提供 `-s` 和 `-c` 选项,则会显示当前的访问记录。如果使用了 `-s`,就会设置名为 `key` 的访问记录为 `value`。如果使用了 `-c` 且提供了 `key`,就会清除名为 `key` 的访问记录;否则会清除所有的访问记录。 diff --git a/docs/plugins/accessibility/sudo.md b/docs/plugins/accessibility/sudo.md index 565e3aecc0..728c670819 100644 --- a/docs/plugins/accessibility/sudo.md +++ b/docs/plugins/accessibility/sudo.md @@ -17,16 +17,16 @@ sudo 指令允许你模拟其他用户调用指令。例如当你在私聊上下 ```sh teach foo bar # 无效,因为 teach 指令只对群上下文生效 -ctxf -g #456 teach foo bar # 有效,相当于在群 456 调用 teach foo bar +sudo -g #456 teach foo bar # 有效,相当于在群 456 调用 teach foo bar ``` 除此以外,你还可以模拟在其他频道中调用(假设你现在在频道 123 中调用指令): ```sh -ctxf -g #456 command # 模拟你在群 456 的上下文 -ctxf -u @789 command # 模拟用户 789 的私聊上下文 -ctxf -m @789 command # 模拟用户 789 在当前频道的上下文 -ctxf -u @789 -g #456 command # 模拟用户 789 在频道 456 的上下文 +sudo -g #456 command # 模拟你在群 456 的上下文 +sudo -u @789 command # 模拟用户 789 的私聊上下文 +sudo -m @789 command # 模拟用户 789 在当前频道的上下文 +sudo -u @789 -g #456 command # 模拟用户 789 在频道 456 的上下文 ``` 尽管切换了调用上下文,但 sudo 指令的输出仍然产生在原上下文中。这在你想调用群指令的时候是很有用的。 diff --git a/docs/plugins/message/broadcast.md b/docs/plugins/common/broadcast.md similarity index 100% rename from docs/plugins/message/broadcast.md rename to docs/plugins/common/broadcast.md diff --git a/docs/plugins/message/echo.md b/docs/plugins/common/echo.md similarity index 100% rename from docs/plugins/message/echo.md rename to docs/plugins/common/echo.md diff --git a/docs/plugins/message/feedback.md b/docs/plugins/common/feedback.md similarity index 100% rename from docs/plugins/message/feedback.md rename to docs/plugins/common/feedback.md diff --git a/docs/plugins/message/forward.md b/docs/plugins/common/forward.md similarity index 100% rename from docs/plugins/message/forward.md rename to docs/plugins/common/forward.md diff --git a/docs/plugins/message/recall.md b/docs/plugins/common/recall.md similarity index 100% rename from docs/plugins/message/recall.md rename to docs/plugins/common/recall.md diff --git a/docs/plugins/message/repeater.md b/docs/plugins/common/repeater.md similarity index 100% rename from docs/plugins/message/repeater.md rename to docs/plugins/common/repeater.md diff --git a/docs/plugins/message/respondent.md b/docs/plugins/common/respondent.md similarity index 100% rename from docs/plugins/message/respondent.md rename to docs/plugins/common/respondent.md diff --git a/docs/plugins/console/commands.md b/docs/plugins/console/commands.md new file mode 100644 index 0000000000..21d4f31a13 --- /dev/null +++ b/docs/plugins/console/commands.md @@ -0,0 +1,6 @@ +--- +sidebarDepth: 2 +--- + +# 指令管理 (Commands) + diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 5f58ecc1e5..35c553085e 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -32,12 +32,31 @@ Koishi 官方提供了许多插件。为了更好地模块化开发,它们被 ## 常用功能 +- [@koishijs/plugin-broadcast](./accessibility/broadcast.md):发送广播 +- [@koishijs/plugin-echo](./accessibility/echo.md):发送消息 +- [@koishijs/plugin-feedback](./accessibility/feedback.md):发送反馈 +- [@koishijs/plugin-forward](./accessibility/forward.md):转发消息 +- [@koishijs/plugin-recall](./accessibility/recall.md):撤回消息 +- [@koishijs/plugin-repeater](./accessibility/repeater.md):复读机 +- [@koishijs/plugin-respondent](./accessibility/respondent.md):快捷回复 + +## 辅助功能 + +- [@koishijs/plugin-admin](./accessibility/admin.md):数据管理 +- [@koishijs/plugin-bind](./accessibility/bind.md):账号绑定 +- [@koishijs/plugin-callme](./accessibility/callme.md):设置昵称 +- [@koishijs/plugin-rate-limit](./accessibility/rate-limit.md):速率控制 +- [@koishijs/plugin-schedule](./accessibility/schedule.md):计划任务 +- [@koishijs/plugin-sudo](./accessibility/sudo.md):模拟调用 +- [@koishijs/plugin-verifier](./accessibility/verifier.md):处理申请 + ## 控制台开发 -- [@koishijs/plugin-console](./console/console.md) -- [@koishijs/plugin-chat](./console/chat.md) -- [@koishijs/plugin-manager](./console/manager.md) -- [@koishijs/plugin-status](./console/status.md) +- [@koishijs/plugin-console](./console/console.md):控制台 +- [@koishijs/plugin-chat](./console/chat.md):聊天工具 +- [@koishijs/plugin-manager](./console/manager.md):插件管理 +- [@koishijs/plugin-commands](./console/commands.md):指令管理 +- [@koishijs/plugin-status](./console/status.md):运行状态 ## 大型插件 @@ -50,8 +69,6 @@ Koishi 官方提供了许多插件。为了更好地模块化开发,它们被 此外,官方还维护了其他大量功能插件,它们同样会在本栏有介绍: -- [@koishijs/plugin-common](./common/):常用指令 - [@koishijs/plugin-github](./other/github.md):接入 GitHub - [@koishijs/plugin-mock](./other/mock.md):测试工具 - [@koishijs/plugin-puppeteer](./other/puppeteer.md):网页截图 -- [@koishijs/plugin-schedule](./other/schedule.md):计划任务 From 504238a96adc25ce09715628175bbd1f93a94554 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 14:31:53 +0800 Subject: [PATCH 04/34] feat(feedback): enhance config schema --- plugins/common/feedback/package.json | 2 +- plugins/common/feedback/src/index.ts | 9 ++++++++- plugins/common/feedback/tests/index.spec.ts | 4 +--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/common/feedback/package.json b/plugins/common/feedback/package.json index a8fa6ef20f..c478cc4a9c 100644 --- a/plugins/common/feedback/package.json +++ b/plugins/common/feedback/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-feedback", "description": "Echo plugin for Koishi", - "version": "1.0.0", + "version": "1.0.1", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ diff --git a/plugins/common/feedback/src/index.ts b/plugins/common/feedback/src/index.ts index fa6810476c..6d9398b005 100644 --- a/plugins/common/feedback/src/index.ts +++ b/plugins/common/feedback/src/index.ts @@ -1,4 +1,4 @@ -import { Context, noop, sleep, template } from 'koishi' +import { Context, noop, Schema, sleep, template } from 'koishi' import { parsePlatform } from '@koishijs/command-utils' template.set('feedback', { @@ -11,6 +11,13 @@ export interface Config { operators?: string[] } +export const schema: Schema = Schema.union([ + Schema.object({ + operators: Schema.array(Schema.string()), + }), + Schema.transform(Schema.array(Schema.string()), (operators) => ({ operators })), +]) + export const name = 'feedback' export function apply(ctx: Context, { operators = [] }: Config) { diff --git a/plugins/common/feedback/tests/index.spec.ts b/plugins/common/feedback/tests/index.spec.ts index da8e08766c..97e5ae3145 100644 --- a/plugins/common/feedback/tests/index.spec.ts +++ b/plugins/common/feedback/tests/index.spec.ts @@ -8,9 +8,7 @@ import 'chai-shape' const app = new App() app.plugin(mock) -app.plugin(feedback, { - operators: ['mock:999'], -}) +app.plugin(feedback, ['mock:999']) const client = app.mock.client('123') From b150aa43822b03f56f917994a92cec31776fa16e Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 14:37:56 +0800 Subject: [PATCH 05/34] chore: add new category a11y --- build/bump.ts | 1 + build/utils.ts | 1 + docs/guide/introduction/cli.md | 4 ++-- docs/guide/introduction/coding.md | 2 +- docs/plugins/common/repeater.md | 8 +++---- docs/plugins/common/respondent.md | 22 +++++++++---------- package.json | 1 + plugins/{common => a11y}/admin/package.json | 0 plugins/{common => a11y}/admin/src/index.ts | 0 .../admin/tests/index.spec.ts | 0 plugins/{common => a11y}/admin/tsconfig.json | 0 plugins/{common => a11y}/bind/package.json | 0 plugins/{common => a11y}/bind/src/index.ts | 0 plugins/{common => a11y}/bind/tsconfig.json | 0 plugins/{common => a11y}/callme/package.json | 0 plugins/{common => a11y}/callme/src/index.ts | 0 plugins/{common => a11y}/callme/tsconfig.json | 0 .../command-utils/package.json | 0 .../command-utils/src/index.ts | 0 .../command-utils/tsconfig.json | 0 .../{common => a11y}/contextify/package.json | 0 .../{common => a11y}/contextify/src/index.ts | 0 .../contextify/tests/index.spec.ts | 0 .../{common => a11y}/contextify/tsconfig.json | 0 .../{common => a11y}/rate-limit/package.json | 0 .../{common => a11y}/rate-limit/src/index.ts | 0 .../rate-limit/tests/index.spec.ts | 0 .../{common => a11y}/rate-limit/tsconfig.json | 0 plugins/{ => a11y}/schedule/README.md | 0 plugins/{ => a11y}/schedule/package.json | 0 plugins/{ => a11y}/schedule/src/index.ts | 0 .../{ => a11y}/schedule/tests/index.spec.ts | 0 plugins/{ => a11y}/schedule/tsconfig.json | 2 +- plugins/{common => a11y}/switch/package.json | 0 plugins/{common => a11y}/switch/src/index.ts | 0 .../switch/tests/index.spec.ts | 0 plugins/{common => a11y}/switch/tsconfig.json | 0 .../{common => a11y}/verifier/package.json | 0 .../{common => a11y}/verifier/src/index.ts | 0 .../verifier/tests/index.spec.ts | 0 .../{common => a11y}/verifier/tsconfig.json | 0 plugins/common/feedback/src/index.ts | 2 +- 42 files changed, 22 insertions(+), 21 deletions(-) rename plugins/{common => a11y}/admin/package.json (100%) rename plugins/{common => a11y}/admin/src/index.ts (100%) rename plugins/{common => a11y}/admin/tests/index.spec.ts (100%) rename plugins/{common => a11y}/admin/tsconfig.json (100%) rename plugins/{common => a11y}/bind/package.json (100%) rename plugins/{common => a11y}/bind/src/index.ts (100%) rename plugins/{common => a11y}/bind/tsconfig.json (100%) rename plugins/{common => a11y}/callme/package.json (100%) rename plugins/{common => a11y}/callme/src/index.ts (100%) rename plugins/{common => a11y}/callme/tsconfig.json (100%) rename plugins/{common => a11y}/command-utils/package.json (100%) rename plugins/{common => a11y}/command-utils/src/index.ts (100%) rename plugins/{common => a11y}/command-utils/tsconfig.json (100%) rename plugins/{common => a11y}/contextify/package.json (100%) rename plugins/{common => a11y}/contextify/src/index.ts (100%) rename plugins/{common => a11y}/contextify/tests/index.spec.ts (100%) rename plugins/{common => a11y}/contextify/tsconfig.json (100%) rename plugins/{common => a11y}/rate-limit/package.json (100%) rename plugins/{common => a11y}/rate-limit/src/index.ts (100%) rename plugins/{common => a11y}/rate-limit/tests/index.spec.ts (100%) rename plugins/{common => a11y}/rate-limit/tsconfig.json (100%) rename plugins/{ => a11y}/schedule/README.md (100%) rename plugins/{ => a11y}/schedule/package.json (100%) rename plugins/{ => a11y}/schedule/src/index.ts (100%) rename plugins/{ => a11y}/schedule/tests/index.spec.ts (100%) rename plugins/{ => a11y}/schedule/tsconfig.json (72%) rename plugins/{common => a11y}/switch/package.json (100%) rename plugins/{common => a11y}/switch/src/index.ts (100%) rename plugins/{common => a11y}/switch/tests/index.spec.ts (100%) rename plugins/{common => a11y}/switch/tsconfig.json (100%) rename plugins/{common => a11y}/verifier/package.json (100%) rename plugins/{common => a11y}/verifier/src/index.ts (100%) rename plugins/{common => a11y}/verifier/tests/index.spec.ts (100%) rename plugins/{common => a11y}/verifier/tsconfig.json (100%) diff --git a/build/bump.ts b/build/bump.ts index 60059d0f9f..353cbb9b03 100644 --- a/build/bump.ts +++ b/build/bump.ts @@ -99,6 +99,7 @@ const packages: Record = {} function getPackage(name: string) { return packages[`packages/${name}`] || packages[`plugins/${name}`] + || packages[`plugins/a11y/${name}`] || packages[`plugins/adapter/${name}`] || packages[`plugins/assets/${name}`] || packages[`plugins/cache/${name}`] diff --git a/build/utils.ts b/build/utils.ts index e8f9884a5c..45ac26a19f 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -17,6 +17,7 @@ export function getWorkspaces() { const categories = [ 'packages', 'plugins', + 'plugins/a11y', 'plugins/adapter', 'plugins/assets', 'plugins/cache', diff --git a/docs/guide/introduction/cli.md b/docs/guide/introduction/cli.md index e6c28b1374..014cc1ef54 100644 --- a/docs/guide/introduction/cli.md +++ b/docs/guide/introduction/cli.md @@ -44,7 +44,7 @@ plugins: protocol: 'ws' selfId: '123456789' endpoint: 'ws://127.0.0.1:6700' - common: + echo: ``` 让我们对比一下代码示例中的 `index.js` 文件,不难发现它们之间的相似: @@ -56,7 +56,7 @@ app.plugin('adapter-onebot', { endpoint: 'ws://127.0.0.1:6700', }) -app.plugin('common') +app.plugin('echo') ``` 没错,配置文件中的 `plugins` 是一个对象,其中的每一个键表示一个插件的名称,而值则表示该插件的配置。而代码示例中的 `app.plugin()` 则接受最多两个参数,分别也是插件的短名和配置。 diff --git a/docs/guide/introduction/coding.md b/docs/guide/introduction/coding.md index 2e47ac55bc..cd354e2786 100644 --- a/docs/guide/introduction/coding.md +++ b/docs/guide/introduction/coding.md @@ -31,7 +31,7 @@ Koishi 支持多个聊天平台,对于不同的平台,你也需要做好相 ## 初始化项目 -首先初始化你的机器人目录并安装 Koishi 和所需的插件 (这里以官方插件 onebot 和 common 为例): +首先初始化你的机器人目录并安装 Koishi 和所需的插件 (这里以官方插件 onebot 和 echo 为例): ::: code-group manager ```npm diff --git a/docs/plugins/common/repeater.md b/docs/plugins/common/repeater.md index b296d5b9ae..0e8f047b9a 100644 --- a/docs/plugins/common/repeater.md +++ b/docs/plugins/common/repeater.md @@ -13,7 +13,7 @@ sidebarDepth: 2 ```js koishi.config.js module.exports = { plugins: { - common: { + repeater: { onRepeat: { minTimes: 3, probability: 0.5, @@ -44,7 +44,7 @@ module.exports = { ```js koishi.config.js module.exports = { plugins: { - common: { + repeater: { onRepeat: (state) => state.times >= 2 && state.content === "这机器人又开始复读了" && @@ -67,7 +67,7 @@ module.exports = { ```js koishi.config.js module.exports = { plugins: { - common: { + repeater: { onRepeat: (state) => state.users[session.userId] > 1 && segment.at(session.userId) + "不许重复复读!" @@ -92,7 +92,7 @@ module.exports = { ```js koishi.config.js module.exports = { plugins: { - common: { + repeater: { onRepeat:{ minTimes: 2 }, diff --git a/docs/plugins/common/respondent.md b/docs/plugins/common/respondent.md index 063c4861e5..ce9050d314 100644 --- a/docs/plugins/common/respondent.md +++ b/docs/plugins/common/respondent.md @@ -9,18 +9,16 @@ sidebarDepth: 2 ```js koishi.config.js module.exports = { plugins: { - common: { - respondent: [{ - match: 'awsl', - reply: '爱我苏联', - }, { - match: /^\s*(\S +){2,}\S\s*$/, - reply: '空格警察,出动!', - }, { - match: /^(.+)一时爽$/, - reply: (_, str) => `一直${str}一直爽`, - }], - }, + respondent: [{ + match: 'awsl', + reply: '爱我苏联', + }, { + match: /^\s*(\S +){2,}\S\s*$/, + reply: '空格警察,出动!', + }, { + match: /^(.+)一时爽$/, + reply: (_, str) => `一直${str}一直爽`, + }], }, } ``` diff --git a/package.json b/package.json index b864ac3275..5ead9b8d6b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test", "community/*", "packages/*", + "plugins/a11y/*", "plugins/adapter/*", "plugins/assets/*", "plugins/cache/*", diff --git a/plugins/common/admin/package.json b/plugins/a11y/admin/package.json similarity index 100% rename from plugins/common/admin/package.json rename to plugins/a11y/admin/package.json diff --git a/plugins/common/admin/src/index.ts b/plugins/a11y/admin/src/index.ts similarity index 100% rename from plugins/common/admin/src/index.ts rename to plugins/a11y/admin/src/index.ts diff --git a/plugins/common/admin/tests/index.spec.ts b/plugins/a11y/admin/tests/index.spec.ts similarity index 100% rename from plugins/common/admin/tests/index.spec.ts rename to plugins/a11y/admin/tests/index.spec.ts diff --git a/plugins/common/admin/tsconfig.json b/plugins/a11y/admin/tsconfig.json similarity index 100% rename from plugins/common/admin/tsconfig.json rename to plugins/a11y/admin/tsconfig.json diff --git a/plugins/common/bind/package.json b/plugins/a11y/bind/package.json similarity index 100% rename from plugins/common/bind/package.json rename to plugins/a11y/bind/package.json diff --git a/plugins/common/bind/src/index.ts b/plugins/a11y/bind/src/index.ts similarity index 100% rename from plugins/common/bind/src/index.ts rename to plugins/a11y/bind/src/index.ts diff --git a/plugins/common/bind/tsconfig.json b/plugins/a11y/bind/tsconfig.json similarity index 100% rename from plugins/common/bind/tsconfig.json rename to plugins/a11y/bind/tsconfig.json diff --git a/plugins/common/callme/package.json b/plugins/a11y/callme/package.json similarity index 100% rename from plugins/common/callme/package.json rename to plugins/a11y/callme/package.json diff --git a/plugins/common/callme/src/index.ts b/plugins/a11y/callme/src/index.ts similarity index 100% rename from plugins/common/callme/src/index.ts rename to plugins/a11y/callme/src/index.ts diff --git a/plugins/common/callme/tsconfig.json b/plugins/a11y/callme/tsconfig.json similarity index 100% rename from plugins/common/callme/tsconfig.json rename to plugins/a11y/callme/tsconfig.json diff --git a/plugins/common/command-utils/package.json b/plugins/a11y/command-utils/package.json similarity index 100% rename from plugins/common/command-utils/package.json rename to plugins/a11y/command-utils/package.json diff --git a/plugins/common/command-utils/src/index.ts b/plugins/a11y/command-utils/src/index.ts similarity index 100% rename from plugins/common/command-utils/src/index.ts rename to plugins/a11y/command-utils/src/index.ts diff --git a/plugins/common/command-utils/tsconfig.json b/plugins/a11y/command-utils/tsconfig.json similarity index 100% rename from plugins/common/command-utils/tsconfig.json rename to plugins/a11y/command-utils/tsconfig.json diff --git a/plugins/common/contextify/package.json b/plugins/a11y/contextify/package.json similarity index 100% rename from plugins/common/contextify/package.json rename to plugins/a11y/contextify/package.json diff --git a/plugins/common/contextify/src/index.ts b/plugins/a11y/contextify/src/index.ts similarity index 100% rename from plugins/common/contextify/src/index.ts rename to plugins/a11y/contextify/src/index.ts diff --git a/plugins/common/contextify/tests/index.spec.ts b/plugins/a11y/contextify/tests/index.spec.ts similarity index 100% rename from plugins/common/contextify/tests/index.spec.ts rename to plugins/a11y/contextify/tests/index.spec.ts diff --git a/plugins/common/contextify/tsconfig.json b/plugins/a11y/contextify/tsconfig.json similarity index 100% rename from plugins/common/contextify/tsconfig.json rename to plugins/a11y/contextify/tsconfig.json diff --git a/plugins/common/rate-limit/package.json b/plugins/a11y/rate-limit/package.json similarity index 100% rename from plugins/common/rate-limit/package.json rename to plugins/a11y/rate-limit/package.json diff --git a/plugins/common/rate-limit/src/index.ts b/plugins/a11y/rate-limit/src/index.ts similarity index 100% rename from plugins/common/rate-limit/src/index.ts rename to plugins/a11y/rate-limit/src/index.ts diff --git a/plugins/common/rate-limit/tests/index.spec.ts b/plugins/a11y/rate-limit/tests/index.spec.ts similarity index 100% rename from plugins/common/rate-limit/tests/index.spec.ts rename to plugins/a11y/rate-limit/tests/index.spec.ts diff --git a/plugins/common/rate-limit/tsconfig.json b/plugins/a11y/rate-limit/tsconfig.json similarity index 100% rename from plugins/common/rate-limit/tsconfig.json rename to plugins/a11y/rate-limit/tsconfig.json diff --git a/plugins/schedule/README.md b/plugins/a11y/schedule/README.md similarity index 100% rename from plugins/schedule/README.md rename to plugins/a11y/schedule/README.md diff --git a/plugins/schedule/package.json b/plugins/a11y/schedule/package.json similarity index 100% rename from plugins/schedule/package.json rename to plugins/a11y/schedule/package.json diff --git a/plugins/schedule/src/index.ts b/plugins/a11y/schedule/src/index.ts similarity index 100% rename from plugins/schedule/src/index.ts rename to plugins/a11y/schedule/src/index.ts diff --git a/plugins/schedule/tests/index.spec.ts b/plugins/a11y/schedule/tests/index.spec.ts similarity index 100% rename from plugins/schedule/tests/index.spec.ts rename to plugins/a11y/schedule/tests/index.spec.ts diff --git a/plugins/schedule/tsconfig.json b/plugins/a11y/schedule/tsconfig.json similarity index 72% rename from plugins/schedule/tsconfig.json rename to plugins/a11y/schedule/tsconfig.json index 74ac2c8ddb..52cb76c703 100644 --- a/plugins/schedule/tsconfig.json +++ b/plugins/a11y/schedule/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base", + "extends": "../../../tsconfig.base", "compilerOptions": { "outDir": "lib", "rootDir": "src", diff --git a/plugins/common/switch/package.json b/plugins/a11y/switch/package.json similarity index 100% rename from plugins/common/switch/package.json rename to plugins/a11y/switch/package.json diff --git a/plugins/common/switch/src/index.ts b/plugins/a11y/switch/src/index.ts similarity index 100% rename from plugins/common/switch/src/index.ts rename to plugins/a11y/switch/src/index.ts diff --git a/plugins/common/switch/tests/index.spec.ts b/plugins/a11y/switch/tests/index.spec.ts similarity index 100% rename from plugins/common/switch/tests/index.spec.ts rename to plugins/a11y/switch/tests/index.spec.ts diff --git a/plugins/common/switch/tsconfig.json b/plugins/a11y/switch/tsconfig.json similarity index 100% rename from plugins/common/switch/tsconfig.json rename to plugins/a11y/switch/tsconfig.json diff --git a/plugins/common/verifier/package.json b/plugins/a11y/verifier/package.json similarity index 100% rename from plugins/common/verifier/package.json rename to plugins/a11y/verifier/package.json diff --git a/plugins/common/verifier/src/index.ts b/plugins/a11y/verifier/src/index.ts similarity index 100% rename from plugins/common/verifier/src/index.ts rename to plugins/a11y/verifier/src/index.ts diff --git a/plugins/common/verifier/tests/index.spec.ts b/plugins/a11y/verifier/tests/index.spec.ts similarity index 100% rename from plugins/common/verifier/tests/index.spec.ts rename to plugins/a11y/verifier/tests/index.spec.ts diff --git a/plugins/common/verifier/tsconfig.json b/plugins/a11y/verifier/tsconfig.json similarity index 100% rename from plugins/common/verifier/tsconfig.json rename to plugins/a11y/verifier/tsconfig.json diff --git a/plugins/common/feedback/src/index.ts b/plugins/common/feedback/src/index.ts index 6d9398b005..68d98cd776 100644 --- a/plugins/common/feedback/src/index.ts +++ b/plugins/common/feedback/src/index.ts @@ -24,7 +24,7 @@ export function apply(ctx: Context, { operators = [] }: Config) { type FeedbackData = [sid: string, channelId: string, guildId: string] const feedbacks: Record = {} - ctx.command('common/feedback ', '发送反馈信息给作者') + ctx.command('feedback ', '发送反馈信息给作者') .userFields(['name', 'id']) .action(async ({ session }, text) => { if (!text) return template('feedback.expect-text') From a05c2b6f27dd421c93706e471bbc037c9c1670f1 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 14:43:10 +0800 Subject: [PATCH 06/34] chore: contextify -> sudo --- .mocharc.js | 10 ++++---- .../a11y/{contextify => sudo}/package.json | 6 ++--- .../a11y/{contextify => sudo}/src/index.ts | 13 +++++----- .../{contextify => sudo}/tests/index.spec.ts | 24 +++++++++---------- .../a11y/{contextify => sudo}/tsconfig.json | 0 5 files changed, 26 insertions(+), 27 deletions(-) rename plugins/a11y/{contextify => sudo}/package.json (87%) rename plugins/a11y/{contextify => sudo}/src/index.ts (85%) rename plugins/a11y/{contextify => sudo}/tests/index.spec.ts (57%) rename plugins/a11y/{contextify => sudo}/tsconfig.json (100%) diff --git a/.mocharc.js b/.mocharc.js index 3d7a579ba8..34df968a70 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -5,18 +5,18 @@ const specs = [ 'community/schemastery/tests/*.spec.ts', 'packages/core/tests/*.spec.ts', 'packages/utils/tests/*.spec.ts', - 'plugins/common/admin/tests/*.spec.ts', + 'plugins/a11y/admin/tests/*.spec.ts', + 'plugins/a11y/rate-limit/tests/*.spec.ts', + 'plugins/a11y/switch/tests/*.spec.ts', + 'plugins/a11y/sudo/tests/*.spec.ts', + // 'plugins/a11y/verifier/tests/*.spec.ts', 'plugins/common/broadcast/tests/*.spec.ts', - 'plugins/common/contextify/tests/*.spec.ts', 'plugins/common/echo/tests/*.spec.ts', 'plugins/common/feedback/tests/*.spec.ts', 'plugins/common/forward/tests/*.spec.ts', - 'plugins/common/rate-limit/tests/*.spec.ts', 'plugins/common/recall/tests/*.spec.ts', 'plugins/common/repeater/tests/*.spec.ts', 'plugins/common/respondent/tests/*.spec.ts', - 'plugins/common/switch/tests/*.spec.ts', - // 'plugins/common/verifier/tests/*.spec.ts', 'plugins/database/level/tests/*.spec.ts', 'plugins/database/memory/tests/*.spec.ts', 'plugins/database/mongo/tests/*.spec.ts', diff --git a/plugins/a11y/contextify/package.json b/plugins/a11y/sudo/package.json similarity index 87% rename from plugins/a11y/contextify/package.json rename to plugins/a11y/sudo/package.json index d22d114317..206d1dfd06 100644 --- a/plugins/a11y/contextify/package.json +++ b/plugins/a11y/sudo/package.json @@ -1,5 +1,5 @@ { - "name": "@koishijs/plugin-contextify", + "name": "@koishijs/plugin-sudo", "description": "Simulate other contexts in Koishi", "version": "1.0.0", "main": "lib/index.js", @@ -19,7 +19,7 @@ "bugs": { "url": "https://github.com/koishijs/koishi/issues" }, - "homepage": "https://koishi.js.org/plugins/contextify/", + "homepage": "https://koishi.js.org/plugins/accessibility/sudo/", "keywords": [ "bot", "qqbot", @@ -28,7 +28,7 @@ "chatbot", "koishi", "plugin", - "contextify" + "sudo" ], "peerDependencies": { "koishi": "^4.0.0-rc.3" diff --git a/plugins/a11y/contextify/src/index.ts b/plugins/a11y/sudo/src/index.ts similarity index 85% rename from plugins/a11y/contextify/src/index.ts rename to plugins/a11y/sudo/src/index.ts index 14e9c23919..19af487a5f 100644 --- a/plugins/a11y/contextify/src/index.ts +++ b/plugins/a11y/sudo/src/index.ts @@ -1,35 +1,34 @@ import { Context, Session, template } from 'koishi' import { parsePlatform } from '@koishijs/command-utils' -template.set('contextify', { +template.set('sudo', { 'expect-command': '请输入要触发的指令。', 'expect-context': '请提供新的上下文。', 'invalid-private-member': '无法在私聊上下文使用 --member 选项。', }) -export const name = 'contextify' +export const name = 'sudo' export const using = ['database'] as const export function apply(ctx: Context) { - ctx.command('contextify ', '在特定上下文中触发指令', { authority: 3 }) - .alias('ctxf') + ctx.command('sudo ', '在特定上下文中触发指令', { authority: 3 }) .userFields(['authority']) .option('user', '-u [id:user] 使用用户私聊上下文') .option('member', '-m [id:user] 使用当前频道成员上下文') .option('channel', '-c [id:channel] 使用群聊上下文') .action(async ({ session, options }, message) => { - if (!message) return template('contextify.expect-command') + if (!message) return template('sudo.expect-command') if (options.member) { if (session.subtype === 'private') { - return template('contextify.invalid-private-member') + return template('sudo.invalid-private-member') } options.channel = session.cid options.user = options.member } if (!options.user && !options.channel) { - return template('contextify.expect-context') + return template('sudo.expect-context') } const sess = new Session(session.bot, session) diff --git a/plugins/a11y/contextify/tests/index.spec.ts b/plugins/a11y/sudo/tests/index.spec.ts similarity index 57% rename from plugins/a11y/contextify/tests/index.spec.ts rename to plugins/a11y/sudo/tests/index.spec.ts index 541196148a..b7cbd96da9 100644 --- a/plugins/a11y/contextify/tests/index.spec.ts +++ b/plugins/a11y/sudo/tests/index.spec.ts @@ -1,13 +1,13 @@ import { App } from 'koishi' import memory from '@koishijs/plugin-database-memory' import mock from '@koishijs/plugin-mock' -import * as contextify from '@koishijs/plugin-contextify' +import * as sudo from '@koishijs/plugin-sudo' const app = new App() app.plugin(memory) app.plugin(mock) -app.plugin(contextify) +app.plugin(sudo) const client1 = app.mock.client('123') const client2 = app.mock.client('123', '456') @@ -27,22 +27,22 @@ before(async () => { await app.mock.initChannel('456') }) -describe('@koishijs/plugin-contextify', () => { +describe('@koishijs/plugin-sudo', () => { it('check input', async () => { - await client1.shouldReply('ctxf -u @456', '请输入要触发的指令。') - await client1.shouldReply('ctxf -m @456 show-context', '无法在私聊上下文使用 --member 选项。') - await client2.shouldReply('ctxf show-context', '请提供新的上下文。') - await client2.shouldReply('ctxf -u @789 show-context', '权限不足。') + await client1.shouldReply('sudo -u @456', '请输入要触发的指令。') + await client1.shouldReply('sudo -m @456 show-context', '无法在私聊上下文使用 --member 选项。') + await client2.shouldReply('sudo show-context', '请提供新的上下文。') + await client2.shouldReply('sudo -u @789 show-context', '权限不足。') }) it('private context', async () => { - await client1.shouldReply('ctxf -u @456 show-context', '456,456,undefined') - await client1.shouldReply('ctxf -c #456 show-context', '123,123,456') - await client1.shouldReply('ctxf -u @456 -c #456 show-context', '456,456,456') + await client1.shouldReply('sudo -u @456 show-context', '456,456,undefined') + await client1.shouldReply('sudo -c #456 show-context', '123,123,456') + await client1.shouldReply('sudo -u @456 -c #456 show-context', '456,456,456') }) it('guild context', async () => { - await client2.shouldReply('ctxf -u @456 show-context', '456,456,undefined') - await client2.shouldReply('ctxf -m @456 show-context', '456,456,456') + await client2.shouldReply('sudo -u @456 show-context', '456,456,undefined') + await client2.shouldReply('sudo -m @456 show-context', '456,456,456') }) }) diff --git a/plugins/a11y/contextify/tsconfig.json b/plugins/a11y/sudo/tsconfig.json similarity index 100% rename from plugins/a11y/contextify/tsconfig.json rename to plugins/a11y/sudo/tsconfig.json From 8d74e56c1e198d9aacf948bd5ad8ee00b3db9c6f Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 15:54:10 +0800 Subject: [PATCH 07/34] feat(commands): support create new commands --- build/publish.ts | 2 +- plugins/frontend/commands/src/index.ts | 20 ++++++++++++---- plugins/frontend/commands/tests/index.spec.ts | 23 +++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/build/publish.ts b/build/publish.ts index 638be1bbcf..6c23b044fe 100644 --- a/build/publish.ts +++ b/build/publish.ts @@ -73,7 +73,7 @@ function getVersion(name: string, isNext = false) { } const { version } = require('../packages/koishi/package') as PackageJson - if (isNext(version)) return + if (!CI || isNext(version)) return const tags = spawnSync(['git', 'tag', '-l']).split(/\r?\n/) if (tags.includes(version)) { diff --git a/plugins/frontend/commands/src/index.ts b/plugins/frontend/commands/src/index.ts index b978498877..f4b0ea0719 100644 --- a/plugins/frontend/commands/src/index.ts +++ b/plugins/frontend/commands/src/index.ts @@ -56,10 +56,15 @@ export function apply(ctx: Context, config: Dict) { return rest[0] === '.' ? name : rest.slice(1) } - function accept(target: Command, config: Config) { - const { name, alias, create, ...options } = config + function patch(target: Command) { const command: Command = Object.create(target) command._disposables = ctx.state.disposables + return command + } + + function accept(target: Command, config: Config) { + const { name, alias, create, ...options } = config + const command = create ? target : patch(target) const snapshot: Snapshot = pick(target, ['name', 'parent']) for (const key in options) { @@ -84,8 +89,13 @@ export function apply(ctx: Context, config: Dict) { } for (const key in config) { - const cmd = ctx.app._commands.resolve(key) - if (cmd) accept(cmd, config[key]) + const command = ctx.app._commands.resolve(key) + if (command) { + accept(command, config[key]) + } else if (config[key].create) { + const command = ctx.command(key) + accept(command, config[key]) + } } ctx.on('command-added', (cmd) => { @@ -108,7 +118,7 @@ export function apply(ctx: Context, config: Dict) { teleport(cmd, parent) cmd.name = name } - }) + }, true) ctx.using(['console'], (ctx) => { ctx.plugin(CommandProvider) diff --git a/plugins/frontend/commands/tests/index.spec.ts b/plugins/frontend/commands/tests/index.spec.ts index b95dd986cc..8ed367bafe 100644 --- a/plugins/frontend/commands/tests/index.spec.ts +++ b/plugins/frontend/commands/tests/index.spec.ts @@ -128,4 +128,27 @@ describe('@koishijs/plugin-override', () => { baz.dispose() }) }) + + describe('create', () => { + it('basic usage', async () => { + const bar = app.command('bar').action(() => 'test') + + app.plugin(commands, { + foo: { create: true }, + bar: 'foo/baz', + }) + + const foo = app.command('foo') + expect(foo.children).to.have.length(1) + await client.shouldReply('foo', /baz/) + await client.shouldReply('baz', 'test') + + await app.dispose(commands) + await client.shouldNotReply('foo') + await client.shouldNotReply('baz') + await client.shouldReply('bar', 'test') + + bar.dispose() + }) + }) }) From 43fafa90fa639d959e043295db4a7b3567bd6ae4 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 16:53:13 +0800 Subject: [PATCH 08/34] docs: update readme --- README.md | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6578853399..d089b22690 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Koishi 完全基于 TypeScript 开发,拥有顶级的类型支持,丰富的 ## 官方插件 -### 平台支持 +### 适配器支持 - [adapter-discord](https://koishi.js.org/plugins/adapter/discord.html): [Discord](https://discord.com/) 平台支持 - [adapter-kaiheila](https://koishi.js.org/plugins/adapter/kaiheila.html): [开黑啦](https://kaiheila.cn/) 平台支持 @@ -95,38 +95,49 @@ Koishi 完全基于 TypeScript 开发,拥有顶级的类型支持,丰富的 - [assets-remote](https://koishi.js.org/plugins/assets/remote.html): 使用远程 Koishi 服务器存储静态资源 - [assets-s3](https://koishi.js.org/plugins/assets/s3.html): 使用 S3 存储静态资源 -### 缓存支持 - -- [cache-lru](https://koishi.js.org/plugins/cache/lru.html): LRU 缓存支持 -- [cache-redis](https://koishi.js.org/plugins/cache/redis.html): Redis 缓存支持 - ### 数据库支持 +- [database-level](https://koishi.js.org/plugins/database/level.html): LevelDB 数据库支持 - [database-memory](https://koishi.js.org/plugins/database/memory.html): 测试用的内存数据库支持 - [database-mongo](https://koishi.js.org/plugins/database/mongo.html): MongoDB 数据库支持 - [database-mysql](https://koishi.js.org/plugins/database/mysql.html): MySQL 数据库支持 - [database-sqlite](https://koishi.js.org/plugins/database/sqlite.html): SQLite 数据库支持 +## 常用功能 + +- [broadcast](https://koishi.js.org/plugins/common/broadcast.html):发送广播 +- [echo](https://koishi.js.org/plugins/common/echo.html):发送消息 +- [feedback](https://koishi.js.org/plugins/common/feedback.html):发送反馈 +- [forward](https://koishi.js.org/plugins/common/forward.html):转发消息 +- [recall](https://koishi.js.org/plugins/common/recall.html):撤回消息 +- [repeater](https://koishi.js.org/plugins/common/repeater.html):复读机 +- [respondent](https://koishi.js.org/plugins/common/respondent.html):快捷回复 + +## 辅助功能 + +- [admin](https://koishi.js.org/plugins/accessibility/admin.html):数据管理 +- [bind](https://koishi.js.org/plugins/accessibility/bind.html):账号绑定 +- [callme](https://koishi.js.org/plugins/accessibility/callme.html):设置昵称 +- [rate-limit](https://koishi.js.org/plugins/accessibility/rate-limit.html):速率控制 +- [schedule](https://koishi.js.org/plugins/accessibility//schedule.html):计划任务 +- [sudo](https://koishi.js.org/plugins/accessibility/sudo.html):模拟调用 +- [verifier](https://koishi.js.org/plugins/accessibility/verifier.html):处理申请 + ### 网页控制台 -- [chat](https://koishi.js.org/plugins/console/chat.html): 使用机器人账号聊天 +- [chat](https://koishi.js.org/plugins/console/chat.html): 聊天工具 - [console](https://koishi.js.org/plugins/console/): 网页控制台 -- [manager](https://koishi.js.org/plugins/console/manager.html): 管理插件和机器人 -- [status](https://koishi.js.org/plugins/console/status.html): 查看运行状态和统计数据 +- [commands](https://koishi.js.org/plugins/console/commands.html): 指令管理 +- [manager](https://koishi.js.org/plugins/console/manager.html): 插件管理 +- [status](https://koishi.js.org/plugins/console/status.html): 运行状态 ### 其他官方插件 -- [admin](https://koishi.js.org/plugins/admin.html): 操作用户数据和频道数据 -- [common](https://koishi.js.org/plugins/common.html): 常用指令合集 -- [eval](https://koishi.js.org/plugins/eval.html): 对话机器人执行脚本 -- [forward](https://koishi.js.org/plugins/forward.html): 转发消息到其他频道 +- [eval](https://koishi.js.org/plugins/eval/): 对话机器人执行脚本 - [github](https://koishi.js.org/plugins/github.html): GitHub 相关功能 - [mock](https://koishi.js.org/plugins/mock.html): 模拟消息、会话、网络请求 - [puppeteer](https://koishi.js.org/plugins/puppeteer.html): 网页截图和图片渲染 -- [repeater](https://koishi.js.org/plugins/repeater.html): 复读机相关功能 -- [schedule](https://koishi.js.org/plugins/schedule.html): 设置和执行计划任务 -- [teach](https://koishi.js.org/plugins/teach.html): 教学问答系统 -- [verifier](https://koishi.js.org/plugins/verifier.html): 处理好友和群组请求 +- [teach](https://koishi.js.org/plugins/teach/): 教学问答系统 ## 应用案例 @@ -142,6 +153,7 @@ Koishi 完全基于 TypeScript 开发,拥有顶级的类型支持,丰富的 | [assets-smms](https://github.com/koishijs/koishi-plugin-assets-smms) | 使用 sm.ms 存储静态资源文件 | | [chess](https://github.com/koishijs/koishi-plugin-chess) | 棋类游戏 | | [dice](https://github.com/koishijs/koishi-plugin-dice) | 掷骰 | +| [gocqhttp](https://github.com/koishijs/koishi-plugin-gocqhttp) | [gocqhttp](https://github.com/Mrs4s/go-cqhttp) 启动器 | | [image-search](https://github.com/koishijs/koishi-plugin-image-search) | 图源搜索 | | [pics](https://github.com/koishijs/koishi-plugin-pics) | 随机图片 | | [rss](https://github.com/koishijs/koishi-plugin-rss) | RSS 订阅 | From 03085f7e9e5276c450f6cdc5a522b8b2f35f61e3 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 23:21:22 +0800 Subject: [PATCH 09/34] feat(discord): add guild scheduled event support --- plugins/adapter/discord/build/route.js | 18 +- plugins/adapter/discord/build/types.js | 3 +- plugins/adapter/discord/package.json | 3 +- .../src/types/guild-scheduled-event.ts | 194 ++++++++++++++++++ plugins/adapter/discord/src/types/internal.ts | 27 ++- 5 files changed, 236 insertions(+), 9 deletions(-) create mode 100644 plugins/adapter/discord/src/types/guild-scheduled-event.ts diff --git a/plugins/adapter/discord/build/route.js b/plugins/adapter/discord/build/route.js index a94762b027..8f72177b23 100644 --- a/plugins/adapter/discord/build/route.js +++ b/plugins/adapter/discord/build/route.js @@ -3,10 +3,26 @@ function getRoutes() { Array.from(document.querySelectorAll('.http-req')).map(el => { const [title, verb, url] = el.children - ;(routes[url.innerText] ||= []).push([verb.innerText, title.innerText[0].toLowerCase() + title.innerText.slice(1).replace(/[ -]/g, '')]) + ;(routes[url.innerText] ||= []).push([verb.innerText, camelize(title.innerText)]) }) return 'Internal.define({' + Object.entries(routes).map(([url, methods]) => { return `\n '${url}': {${methods.map(([verb, name]) => `\n ${verb}: '${name}',`).join('')}\n },` }).join('') + '\n})' } + +function camelize(text) { + return text[0].toLowerCase() + text.slice(1).replace(/[ -]/g, '') +} + +function getRouteDecls() { + return Array.from(document.querySelectorAll('.http-req')).map(el => { + return [ + ' /**', + ' * ' + el.nextElementSibling.innerText, + ' * @see ' + el.querySelector('a').href, + ' */', + ' ' + camelize(el.children[0].innerText) + '(): Promise', + ].join('\n') + }).join('\n') +} diff --git a/plugins/adapter/discord/build/types.js b/plugins/adapter/discord/build/types.js index b1593da9d7..e143ae2d28 100644 --- a/plugins/adapter/discord/build/types.js +++ b/plugins/adapter/discord/build/types.js @@ -105,7 +105,6 @@ function generateDecls() { const type = segments.pop() if (type === 'Structure') { - if (header.textContent === 'Response Structure') continue addDecl(`interface ${segments.join('')}`, toFieldDecl) } else if (type === 'Object' || type === 'Fields') { addDecl(`interface ${segments.join('')}`, toFieldDecl) @@ -119,6 +118,8 @@ function generateDecls() { } else if (type === 'Enum') { const callback = table.querySelector('th').textContent === 'Value' ? toValueDecl1 : toValueDecl2 addDecl(`enum ${header.textContent.replace(/ /g, '')}`, callback) + } else { + addDecl(`interface ${segments.join('') + type}`, toFieldDecl) } function addDecl(fullname, callback) { diff --git a/plugins/adapter/discord/package.json b/plugins/adapter/discord/package.json index 5d307ecb50..5a99fcea40 100644 --- a/plugins/adapter/discord/package.json +++ b/plugins/adapter/discord/package.json @@ -33,7 +33,8 @@ "devDependencies": { "@koishijs/plugin-mock": "^1.0.0", "@types/es-aggregate-error": "^1.0.2", - "@types/ws": "^7.4.7" + "@types/ws": "^7.4.7", + "axios": "^0.21.4" }, "dependencies": { "es-aggregate-error": "^1.0.5", diff --git a/plugins/adapter/discord/src/types/guild-scheduled-event.ts b/plugins/adapter/discord/src/types/guild-scheduled-event.ts new file mode 100644 index 0000000000..c59114ae77 --- /dev/null +++ b/plugins/adapter/discord/src/types/guild-scheduled-event.ts @@ -0,0 +1,194 @@ +import { GuildMember, integer, Internal, snowflake, timestamp, User } from '.' + +/** https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-structure */ +export interface GuildScheduledEvent { + /** the id of the scheduled event */ + id: snowflake + /** the guild id which the scheduled event belongs to */ + guild_id: snowflake + /** the channel id in which the scheduled event will be hosted, or null if scheduled entity type is EXTERNAL */ + channel_id?: snowflake + /** the id of the user that created the scheduled event * */ + creator_id?: snowflake + /** the name of the scheduled event (1-100 characters) */ + name: string + /** the description of the scheduled event (1-1000 characters) */ + description?: string + /** the time the scheduled event will start */ + scheduled_start_time: timestamp + /** the time the scheduled event will end, required if entity_type is EXTERNAL */ + scheduled_end_time?: timestamp + /** the privacy level of the scheduled event */ + privacy_level: GuildScheduledEvent.PrivacyLevel + /** the status of the scheduled event */ + status: GuildScheduledEvent.Status + /** the type of the scheduled event */ + entity_type: GuildScheduledEvent.EntityType + /** the id of an entity associated with a guild scheduled event */ + entity_id?: snowflake + /** additional metadata for the guild scheduled event */ + entity_metadata?: GuildScheduledEvent.EntityMetadata + /** the user that created the scheduled event */ + creator?: User + /** the number of users subscribed to the scheduled event */ + user_count?: integer +} + +export namespace GuildScheduledEvent { + /** https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-privacy-level */ + export enum PrivacyLevel { + /** the scheduled event is only accessible to guild members */ + GUILD_ONLY = 2, + } + + /** https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types */ + export enum EntityType { + STAGE_INSTANCE = 1, + VOICE = 2, + EXTERNAL = 3, + } + + /** https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-status */ + export enum Status { + SCHEDULED = 1, + ACTIVE = 2, + COMPLETED = 3, + CANCELLED = 4, + } + + /** https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-metadata */ + export interface EntityMetadata { + /** location of the event (1-100 characters) */ + location?: string + } + + /** https://discord.com/developers/docs/resources/guild-scheduled-event#list-scheduled-events-for-guild-query-string-params */ + export interface ListParams { + /** include number of users subscribed to each event */ + with_user_count?: boolean + } + + /** https://discord.com/developers/docs/resources/guild-scheduled-event#create-guild-scheduled-event-json-params */ + export interface CreateParams { + /** the channel id of the scheduled event. */ + channel_id?: snowflake + /** the entity metadata of the scheduled event */ + entity_metadata?: EntityMetadata + /** the name of the scheduled event */ + name: string + /** the privacy level of the scheduled event */ + privacy_level: PrivacyLevel + /** the time to schedule the scheduled event */ + scheduled_start_time: timestamp + /** the time when the scheduled event is scheduled to end */ + scheduled_end_time?: timestamp + /** the description of the scheduled event */ + description?: string + /** the entity type of the scheduled event */ + entity_type: EntityType + } + + /** https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event-query-string-params */ + export interface GetParams { + /** include number of users subscribed to this event */ + with_user_count?: boolean + } + + /** https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event-json-params */ + export interface ModifyParams { + /** the channel id of the scheduled event, set to null if changing entity type to EXTERNAL */ + channel_id?: snowflake + /** the entity metadata of the scheduled event */ + entity_metadata?: EntityMetadata + /** the name of the scheduled event */ + name?: string + /** the privacy level of the scheduled event */ + privacy_level?: PrivacyLevel + /** the time to schedule the scheduled event */ + scheduled_start_time?: timestamp + /** the time when the scheduled event is scheduled to end */ + scheduled_end_time?: timestamp + /** the description of the scheduled event */ + description?: string + /** the entity type of the scheduled event */ + entity_type?: EntityType + /** the status of the scheduled event */ + status?: Status + } +} + +/** https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-user-object-guild-scheduled-event-user-structure */ +export interface GuildScheduledEventUser { + /** the scheduled event id which the user subscribed to */ + guild_scheduled_event_id: snowflake + /** user which subscribed to an event */ + user: User + /** guild member data for this user for the guild which this event belongs to, if any */ + member?: GuildMember +} + +export namespace GuildScheduledEventUser { + /** https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event-users-query-string-params */ + export interface GetParams { + /** number of users to return (up to maximum 100) */ + limit?: number + /** include guild member data if it exists */ + with_member?: boolean + /** consider only users before given user id */ + before?: snowflake + /** consider only users after given user id */ + after?: snowflake + } +} + +declare module './internal' { + interface Internal { + /** + * Returns a list of guild scheduled event objects for the given guild. + * @see https://discord.com/developers/docs/resources/guild-scheduled-event#list-scheduled-events-for-guild + */ + listScheduledEventsforGuild(guildId: snowflake, params?: GuildScheduledEvent.ListParams): Promise + /** + * Create a guild scheduled event in the guild. Returns a guild scheduled event object on success. + * @see https://discord.com/developers/docs/resources/guild-scheduled-event#create-guild-scheduled-event + */ + createGuildScheduledEvent(guildId: snowflake, params: GuildScheduledEvent.CreateParams): Promise + /** + * Get a guild scheduled event. Returns a guild scheduled event object on success. + * @see https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event + */ + getGuildScheduledEvent(guildId: snowflake, eventId: snowflake, params?: GuildScheduledEvent.GetParams): Promise + /** + * Modify a guild scheduled event. Returns the modified guild scheduled event object on success. + * @see https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event + */ + modifyGuildScheduledEvent(guildId: snowflake, eventId: snowflake, params: GuildScheduledEvent.ModifyParams): Promise + /** + * Delete a guild scheduled event. Returns a 204 on success. + * @see https://discord.com/developers/docs/resources/guild-scheduled-event#delete-guild-scheduled-event + */ + deleteGuildScheduledEvent(guildId: snowflake, eventId: snowflake): Promise + /** + * Get a list of guild scheduled event users subscribed to a guild scheduled event. + * Returns a list of guild scheduled event user objects on success. + * Guild member data, if it exists, is included if the with_member query parameter is set. + * @see https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event-users + */ + getGuildScheduledEventUsers(guildId: snowflake, eventId: snowflake, params?: GuildScheduledEventUser.GetParams): Promise + } +} + +Internal.define({ + '/guilds/{guild.id}/scheduled-events': { + GET: 'listScheduledEventsforGuild', + POST: 'createGuildScheduledEvent', + }, + '/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}': { + GET: 'getGuildScheduledEvent', + PATCH: 'modifyGuildScheduledEvent', + DELETE: 'deleteGuildScheduledEvent', + }, + '/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}/users': { + GET: 'getGuildScheduledEventUsers', + }, +}) diff --git a/plugins/adapter/discord/src/types/internal.ts b/plugins/adapter/discord/src/types/internal.ts index b955c7df81..003cdf0b1c 100644 --- a/plugins/adapter/discord/src/types/internal.ts +++ b/plugins/adapter/discord/src/types/internal.ts @@ -1,15 +1,30 @@ -import { Quester } from 'koishi' +import { Dict, Quester } from 'koishi' +import { AxiosRequestConfig } from 'axios' -type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' +type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' export class Internal { - static define(routes: Record>>) { + static define(routes: Dict>>) { for (const path in routes) { - for (const method in routes[path]) { + for (const key in routes[path]) { + const method = key as Method const name = routes[path][method] Internal.prototype[name] = function (this: Internal, ...args: any[]) { - const url = path.replace(/\{([^}]+)\}/g, () => args.shift()) - return this.http(method as any, url) + const url = path.replace(/\{([^}]+)\}/g, () => { + if (!args.length) throw new Error('too few arguments') + return args.shift() + }) + const config: AxiosRequestConfig = {} + if (args.length === 1) { + if (method === 'GET' || method === 'DELETE') { + config.params = args[0] + } else { + config.data = args[0] + } + } else if (args.length > 1) { + throw new Error('too many arguments') + } + return this.http(method, url, config) } } } From 2be2d7f2bce675656d1b305bee8946dc103ff3ce Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 23:30:32 +0800 Subject: [PATCH 10/34] feat(discord): enhance typings for audit log --- .../adapter/discord/src/types/audit-log.ts | 207 ++++++++++-------- 1 file changed, 112 insertions(+), 95 deletions(-) diff --git a/plugins/adapter/discord/src/types/audit-log.ts b/plugins/adapter/discord/src/types/audit-log.ts index eff4f6f996..5b1c84525b 100644 --- a/plugins/adapter/discord/src/types/audit-log.ts +++ b/plugins/adapter/discord/src/types/audit-log.ts @@ -1,9 +1,9 @@ -import { Channel, Integration, Internal, snowflake, User, Webhook } from '.' +import { Channel, integer, Integration, Internal, snowflake, User, Webhook } from '.' /** https://discord.com/developers/docs/resources/audit-log#audit-log-object-audit-log-structure */ export interface AuditLog { /** list of audit log entries */ - audit_log_entries: AuditLogEntry[] + audit_log_entries: AuditLog.Entry[] /** list of partial integration objects */ integrations: Partial[] /** list of threads found in the audit log* */ @@ -14,106 +14,123 @@ export interface AuditLog { webhooks: Webhook[] } -/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-entry-structure */ -export interface AuditLogEntry { - /** id of the affected entity (webhook, user, role, etc.) */ - target_id?: string - /** changes made to the target_id */ - changes?: AuditLogChange[] - /** the user who made the changes */ - user_id?: snowflake - /** id of the entry */ - id: snowflake - /** type of action that occurred */ - action_type: AuditLogEvent - /** additional info for certain action types */ - options?: OptionalAuditEntryInfo - /** the reason for the change (0-512 characters) */ - reason?: string -} +export namespace AuditLog { + /** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-entry-structure */ + export interface Entry { + /** id of the affected entity (webhook, user, role, etc.) */ + target_id?: string + /** changes made to the target_id */ + changes?: Change[] + /** the user who made the changes */ + user_id?: snowflake + /** id of the entry */ + id: snowflake + /** type of action that occurred */ + action_type: Type + /** additional info for certain action types */ + options?: OptionalInfo + /** the reason for the change (0-512 characters) */ + reason?: string + } -/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events */ -export enum AuditLogEvent { - GUILD_UPDATE = 1, - CHANNEL_CREATE = 10, - CHANNEL_UPDATE = 11, - CHANNEL_DELETE = 12, - CHANNEL_OVERWRITE_CREATE = 13, - CHANNEL_OVERWRITE_UPDATE = 14, - CHANNEL_OVERWRITE_DELETE = 15, - MEMBER_KICK = 20, - MEMBER_PRUNE = 21, - MEMBER_BAN_ADD = 22, - MEMBER_BAN_REMOVE = 23, - MEMBER_UPDATE = 24, - MEMBER_ROLE_UPDATE = 25, - MEMBER_MOVE = 26, - MEMBER_DISCONNECT = 27, - BOT_ADD = 28, - ROLE_CREATE = 30, - ROLE_UPDATE = 31, - ROLE_DELETE = 32, - INVITE_CREATE = 40, - INVITE_UPDATE = 41, - INVITE_DELETE = 42, - WEBHOOK_CREATE = 50, - WEBHOOK_UPDATE = 51, - WEBHOOK_DELETE = 52, - EMOJI_CREATE = 60, - EMOJI_UPDATE = 61, - EMOJI_DELETE = 62, - MESSAGE_DELETE = 72, - MESSAGE_BULK_DELETE = 73, - MESSAGE_PIN = 74, - MESSAGE_UNPIN = 75, - INTEGRATION_CREATE = 80, - INTEGRATION_UPDATE = 81, - INTEGRATION_DELETE = 82, - STAGE_INSTANCE_CREATE = 83, - STAGE_INSTANCE_UPDATE = 84, - STAGE_INSTANCE_DELETE = 85, - STICKER_CREATE = 90, - STICKER_UPDATE = 91, - STICKER_DELETE = 92, - THREAD_CREATE = 110, - THREAD_UPDATE = 111, - THREAD_DELETE = 112, -} + /** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events */ + export enum Type { + GUILD_UPDATE = 1, + CHANNEL_CREATE = 10, + CHANNEL_UPDATE = 11, + CHANNEL_DELETE = 12, + CHANNEL_OVERWRITE_CREATE = 13, + CHANNEL_OVERWRITE_UPDATE = 14, + CHANNEL_OVERWRITE_DELETE = 15, + MEMBER_KICK = 20, + MEMBER_PRUNE = 21, + MEMBER_BAN_ADD = 22, + MEMBER_BAN_REMOVE = 23, + MEMBER_UPDATE = 24, + MEMBER_ROLE_UPDATE = 25, + MEMBER_MOVE = 26, + MEMBER_DISCONNECT = 27, + BOT_ADD = 28, + ROLE_CREATE = 30, + ROLE_UPDATE = 31, + ROLE_DELETE = 32, + INVITE_CREATE = 40, + INVITE_UPDATE = 41, + INVITE_DELETE = 42, + WEBHOOK_CREATE = 50, + WEBHOOK_UPDATE = 51, + WEBHOOK_DELETE = 52, + EMOJI_CREATE = 60, + EMOJI_UPDATE = 61, + EMOJI_DELETE = 62, + MESSAGE_DELETE = 72, + MESSAGE_BULK_DELETE = 73, + MESSAGE_PIN = 74, + MESSAGE_UNPIN = 75, + INTEGRATION_CREATE = 80, + INTEGRATION_UPDATE = 81, + INTEGRATION_DELETE = 82, + STAGE_INSTANCE_CREATE = 83, + STAGE_INSTANCE_UPDATE = 84, + STAGE_INSTANCE_DELETE = 85, + STICKER_CREATE = 90, + STICKER_UPDATE = 91, + STICKER_DELETE = 92, + THREAD_CREATE = 110, + THREAD_UPDATE = 111, + THREAD_DELETE = 112, + } -/** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info */ -export interface OptionalAuditEntryInfo { - /** channel in which the entities were targeted */ - channel_id: snowflake - /** number of entities that were targeted */ - count: string - /** number of days after which inactive members were kicked */ - delete_member_days: string - /** id of the overwritten entity */ - id: snowflake - /** number of members removed by the prune */ - members_removed: string - /** id of the message that was targeted */ - message_id: snowflake - /** name of the role if type is "0" (not present if type is "1") */ - role_name: string - /** type of overwritten entity - "0" for "role" or "1" for "member" */ - type: string -} + /** https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info */ + export interface OptionalInfo { + /** channel in which the entities were targeted */ + channel_id: snowflake + /** number of entities that were targeted */ + count: string + /** number of days after which inactive members were kicked */ + delete_member_days: string + /** id of the overwritten entity */ + id: snowflake + /** number of members removed by the prune */ + members_removed: string + /** id of the message that was targeted */ + message_id: snowflake + /** name of the role if type is "0" (not present if type is "1") */ + role_name: string + /** type of overwritten entity - "0" for "role" or "1" for "member" */ + type: string + } + + /** https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-structure */ + export interface Change { + /** new value of the key */ + new_value?: any + /** old value of the key */ + old_value?: any + /** name of audit log change key */ + key: string + } -/** https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-structure */ -export interface AuditLogChange { - /** new value of the key */ - new_value?: any - /** old value of the key */ - old_value?: any - /** name of audit log change key */ - key: string + /** https://discord.com/developers/docs/resources/audit-log#get-guild-audit-log-query-string-params */ + export interface GetParams { + /** filter the log for actions made by a user */ + user_id?: snowflake + /** the type of audit log event */ + action_type?: Type + /** filter the log before a certain entry id */ + before?: snowflake + /** how many entries are returned (default 50, minimum 1, maximum 100) */ + limit?: integer + } } declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/audit-log#get-guild-audit-log */ - getGuildAuditLog(guildId: snowflake): Promise + /** + * Returns an audit log object for the guild. Requires the 'VIEW_AUDIT_LOG' permission. + * @see https://discord.com/developers/docs/resources/audit-log#get-guild-audit-log + */ + getGuildAuditLog(guildId: snowflake, params?: AuditLog.GetParams): Promise } } From 8cfbc1eac8aceb498ca41198355fb03663df4b26 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 11 Jan 2022 23:38:51 +0800 Subject: [PATCH 11/34] feat(discord): enhance typings for emoji & reaction --- plugins/adapter/discord/src/types/emoji.ts | 155 +++--------------- plugins/adapter/discord/src/types/index.ts | 1 + plugins/adapter/discord/src/types/reaction.ts | 139 ++++++++++++++++ 3 files changed, 165 insertions(+), 130 deletions(-) create mode 100644 plugins/adapter/discord/src/types/reaction.ts diff --git a/plugins/adapter/discord/src/types/emoji.ts b/plugins/adapter/discord/src/types/emoji.ts index d722fa75f2..5f5a911aee 100644 --- a/plugins/adapter/discord/src/types/emoji.ts +++ b/plugins/adapter/discord/src/types/emoji.ts @@ -20,103 +20,39 @@ export interface Emoji { available?: boolean } -/** https://discord.com/developers/docs/resources/channel#reaction-object-reaction-structure */ -export interface Reaction { - /** times this emoji has been used to react */ - count: integer - /** whether the current user reacted using this emoji */ - me: boolean - /** emoji information */ - emoji: Partial -} - -/** https://discord.com/developers/docs/topics/gateway#guild-emojis-update-guild-emojis-update-event-fields */ -export interface GuildEmojisUpdateEvent { - /** id of the guild */ - guild_id: snowflake - /** array of emojis */ - emojis: Emoji[] -} - -/** https://discord.com/developers/docs/topics/gateway#message-reaction-add-message-reaction-add-event-fields */ -export interface MessageReactionAddEvent { - /** the id of the user */ - user_id: snowflake - /** the id of the channel */ - channel_id: snowflake - /** the id of the message */ - message_id: snowflake - /** the id of the guild */ - guild_id?: snowflake - /** the member who reacted if this happened in a guild */ - member?: GuildMember - /** the emoji used to react - example */ - emoji: Partial -} - -/** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-message-reaction-remove-event-fields */ -export interface MessageReactionRemoveEvent { - /** the id of the user */ - user_id: snowflake - /** the id of the channel */ - channel_id: snowflake - /** the id of the message */ - message_id: snowflake - /** the id of the guild */ - guild_id?: snowflake - /** the emoji used to react - example */ - emoji: Partial -} +export namespace Emoji { + export namespace Event { + /** https://discord.com/developers/docs/topics/gateway#guild-emojis-update-guild-emojis-update-event-fields */ + export interface Update { + /** id of the guild */ + guild_id: snowflake + /** array of emojis */ + emojis: Emoji[] + } + } -/** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all-message-reaction-remove-all-event-fields */ -export interface MessageReactionRemoveAllEvent { - /** the id of the channel */ - channel_id: snowflake - /** the id of the message */ - message_id: snowflake - /** the id of the guild */ - guild_id?: snowflake -} + /** https://discord.com/developers/docs/resources/emoji#create-guild-emoji-json-params */ + export interface CreateParams extends ModifyParams { + /** the 128x128 emoji image */ + image: string + } -/** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji-message-reaction-remove-emoji */ -export interface MessageReactionRemoveEmojiEvent { - /** the id of the channel */ - channel_id: snowflake - /** the id of the guild */ - guild_id?: snowflake - /** the id of the message */ - message_id: snowflake - /** the emoji that was removed */ - emoji: Partial + /** https://discord.com/developers/docs/resources/emoji#modify-guild-emoji-json-params */ + export interface ModifyParams { + /** name of the emoji */ + name?: string + /** array of snowflakes roles allowed to use this emoji */ + roles?: snowflake[] + } } declare module './gateway' { interface GatewayEvents { /** guild emojis were updated */ - GUILD_EMOJIS_UPDATE: GuildEmojisUpdateEvent - /** user reacted to a message */ - MESSAGE_REACTION_ADD: MessageReactionAddEvent - /** user removed a reaction from a message */ - MESSAGE_REACTION_REMOVE: MessageReactionRemoveEvent - /** all reactions were explicitly removed from a message */ - MESSAGE_REACTION_REMOVE_ALL: MessageReactionRemoveAllEvent - /** all reactions for a given emoji were explicitly removed from a message */ - MESSAGE_REACTION_REMOVE_EMOJI: MessageReactionRemoveEmojiEvent + GUILD_EMOJIS_UPDATE: Emoji.Event.Update } } -export interface ModifyGuildEmojiOptions { - /** name of the emoji */ - name?: string - /** array of snowflakes roles allowed to use this emoji */ - roles?: snowflake[] -} - -export interface CreateGuildEmojiOptions extends ModifyGuildEmojiOptions { - /** the 128x128 emoji image */ - image: string -} - declare module './internal' { interface Internal { /** https://discord.com/developers/docs/resources/emoji#list-guild-emojis */ @@ -124,9 +60,9 @@ declare module './internal' { /** https://discord.com/developers/docs/resources/emoji#get-guild-emoji */ getGuildEmoji(guild_id: snowflake, emoji_id: snowflake): Promise /** https://discord.com/developers/docs/resources/emoji#create-guild-emoji */ - createGuildEmoji(guild_id: snowflake, options: CreateGuildEmojiOptions): Promise + createGuildEmoji(guild_id: snowflake, options: Emoji.CreateParams): Promise /** https://discord.com/developers/docs/resources/emoji#modify-guild-emoji */ - modifyGuildEmoji(guild_id: snowflake, emoji_id: snowflake, options: ModifyGuildEmojiOptions): Promise + modifyGuildEmoji(guild_id: snowflake, emoji_id: snowflake, options: Emoji.ModifyParams): Promise /** https://discord.com/developers/docs/resources/emoji#delete-guild-emoji */ deleteGuildEmoji(guild_id: snowflake, emoji_id: snowflake): Promise } @@ -143,44 +79,3 @@ Internal.define({ DELETE: 'deleteGuildEmoji', }, }) - -export interface GetReactionsOptions { - /** get users after this user ID */ - after?: snowflake - /** max number of users to return (1-100) */ - limit?: integer -} - -declare module './internal' { - interface Internal { - /** https://discord.com/developers/docs/resources/channel#create-reaction */ - createReaction(channel_id: snowflake, message_id: snowflake, emoji: string): Promise - /** https://discord.com/developers/docs/resources/channel#delete-own-reaction */ - deleteOwnReaction(channel_id: snowflake, message_id: snowflake, emoji: string): Promise - /** https://discord.com/developers/docs/resources/channel#delete-user-reaction */ - deleteUserReaction(channel_id: snowflake, message_id: snowflake, emoji: string, user_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/channel#get-reactions */ - getReactions(channel_id: snowflake, message_id: snowflake, emoji: string, options?: GetReactionsOptions): Promise - /** https://discord.com/developers/docs/resources/channel#delete-all-reactions */ - deleteAllReactions(channel_id: snowflake, message_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji */ - deleteAllReactionsForEmoji(channel_id: snowflake, message_id: snowflake, emoji: string): Promise - } -} - -Internal.define({ - '/channels/{channel.id}/messages/{message.id}/reactions/{emoji}/@me': { - PUT: 'createReaction', - DELETE: 'deleteOwnReaction', - }, - '/channels/{channel.id}/messages/{message.id}/reactions/{emoji}/{user.id}': { - DELETE: 'deleteUserReaction', - }, - '/channels/{channel.id}/messages/{message.id}/reactions/{emoji}': { - GET: 'getReactions', - DELETE: 'deleteAllReactionsforEmoji', - }, - '/channels/{channel.id}/messages/{message.id}/reactions': { - DELETE: 'deleteAllReactions', - }, -}) diff --git a/plugins/adapter/discord/src/types/index.ts b/plugins/adapter/discord/src/types/index.ts index 0fe3f8f013..059dcb7929 100644 --- a/plugins/adapter/discord/src/types/index.ts +++ b/plugins/adapter/discord/src/types/index.ts @@ -16,6 +16,7 @@ export * from './interaction' export * from './invite' export * from './message' export * from './presence' +export * from './reaction' export * from './role' export * from './stage-instance' export * from './sticker' diff --git a/plugins/adapter/discord/src/types/reaction.ts b/plugins/adapter/discord/src/types/reaction.ts new file mode 100644 index 0000000000..57eac7dcb7 --- /dev/null +++ b/plugins/adapter/discord/src/types/reaction.ts @@ -0,0 +1,139 @@ +import { Emoji, GuildMember, integer, Internal, snowflake } from '.' + +/** https://discord.com/developers/docs/resources/channel#reaction-object-reaction-structure */ +export interface Reaction { + /** times this emoji has been used to react */ + count: integer + /** whether the current user reacted using this emoji */ + me: boolean + /** emoji information */ + emoji: Partial +} + +export namespace Reaction { + export namespace Event { + /** https://discord.com/developers/docs/topics/gateway#message-reaction-add-message-reaction-add-event-fields */ + export interface Add { + /** the id of the user */ + user_id: snowflake + /** the id of the channel */ + channel_id: snowflake + /** the id of the message */ + message_id: snowflake + /** the id of the guild */ + guild_id?: snowflake + /** the member who reacted if this happened in a guild */ + member?: GuildMember + /** the emoji used to react - example */ + emoji: Partial + } + + /** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-message-reaction-remove-event-fields */ + export interface Remove { + /** the id of the user */ + user_id: snowflake + /** the id of the channel */ + channel_id: snowflake + /** the id of the message */ + message_id: snowflake + /** the id of the guild */ + guild_id?: snowflake + /** the emoji used to react - example */ + emoji: Partial + } + + /** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-all-message-reaction-remove-all-event-fields */ + export interface RemoveAll { + /** the id of the channel */ + channel_id: snowflake + /** the id of the message */ + message_id: snowflake + /** the id of the guild */ + guild_id?: snowflake + } + + /** https://discord.com/developers/docs/topics/gateway#message-reaction-remove-emoji-message-reaction-remove-emoji */ + export interface RemoveEmoji { + /** the id of the channel */ + channel_id: snowflake + /** the id of the guild */ + guild_id?: snowflake + /** the id of the message */ + message_id: snowflake + /** the emoji that was removed */ + emoji: Partial + } + } + + export interface GetParams { + /** get users after this user ID */ + after?: snowflake + /** max number of users to return (1-100) */ + limit?: integer + } +} + +declare module './gateway' { + interface GatewayEvents { + /** user reacted to a message */ + MESSAGE_REACTION_ADD: Reaction.Event.Add + /** user removed a reaction from a message */ + MESSAGE_REACTION_REMOVE: Reaction.Event.Remove + /** all reactions were explicitly removed from a message */ + MESSAGE_REACTION_REMOVE_ALL: Reaction.Event.RemoveAll + /** all reactions for a given emoji were explicitly removed from a message */ + MESSAGE_REACTION_REMOVE_EMOJI: Reaction.Event.RemoveEmoji + } +} + +declare module './internal' { + interface Internal { + /** + * Create a reaction for the message. This endpoint requires the 'READ_MESSAGE_HISTORY' permission to be present on the current user. Additionally, if nobody else has reacted to the message using this emoji, this endpoint requires the 'ADD_REACTIONS' permission to be present on the current user. Returns a 204 empty response on success. The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the format name:id with the emoji name and emoji id. + * @see https://discord.com/developers/docs/resources/channel#create-reaction + */ + createReaction(channel_id: snowflake, message_id: snowflake, emoji: string): Promise + /** + * Delete a reaction the current user has made for the message. Returns a 204 empty response on success. The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the format name:id with the emoji name and emoji id. + * @see https://discord.com/developers/docs/resources/channel#delete-own-reaction + */ + deleteOwnReaction(channel_id: snowflake, message_id: snowflake, emoji: string): Promise + /** + * Deletes another user's reaction. This endpoint requires the 'MANAGE_MESSAGES' permission to be present on the current user. Returns a 204 empty response on success. The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the format name:id with the emoji name and emoji id. + * @see https://discord.com/developers/docs/resources/channel#delete-user-reaction + */ + deleteUserReaction(channel_id: snowflake, message_id: snowflake, emoji: string, user_id: snowflake): Promise + /** + * Get a list of users that reacted with this emoji. Returns an array of user objects on success. The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the format name:id with the emoji name and emoji id. + * @see https://discord.com/developers/docs/resources/channel#get-reactions + */ + getReactions(channel_id: snowflake, message_id: snowflake, emoji: string, params?: Reaction.GetParams): Promise + /** + * Deletes all reactions on a message. This endpoint requires the 'MANAGE_MESSAGES' permission to be present on the current user. Fires a Message Reaction Remove All Gateway event. + * @see https://discord.com/developers/docs/resources/channel#delete-all-reactions + */ + deleteAllReactions(channel_id: snowflake, message_id: snowflake): Promise + /** + * Deletes all the reactions for a given emoji on a message. This endpoint requires the MANAGE_MESSAGES permission to be present on the current user. Fires a Message Reaction Remove Emoji Gateway event. The emoji must be URL Encoded or the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the format name:id with the emoji name and emoji id. + * @see https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji + */ + deleteAllReactionsForEmoji(channel_id: snowflake, message_id: snowflake, emoji: string): Promise + } +} + +Internal.define({ + '/channels/{channel.id}/messages/{message.id}/reactions/{emoji}/@me': { + PUT: 'createReaction', + DELETE: 'deleteOwnReaction', + }, + '/channels/{channel.id}/messages/{message.id}/reactions/{emoji}/{user.id}': { + DELETE: 'deleteUserReaction', + }, + '/channels/{channel.id}/messages/{message.id}/reactions/{emoji}': { + GET: 'getReactions', + DELETE: 'deleteAllReactionsforEmoji', + }, + '/channels/{channel.id}/messages/{message.id}/reactions': { + DELETE: 'deleteAllReactions', + }, +}) From a3c4a4fe72985edf33fd5c12a9a0c17879fc7227 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 00:22:05 +0800 Subject: [PATCH 12/34] feat(discord): enhance typings for message & thread --- plugins/adapter/discord/src/types/channel.ts | 295 ++++++------ plugins/adapter/discord/src/types/command.ts | 4 +- plugins/adapter/discord/src/types/index.ts | 1 + plugins/adapter/discord/src/types/invite.ts | 201 ++++---- plugins/adapter/discord/src/types/message.ts | 465 +++++++++++-------- plugins/adapter/discord/src/types/thread.ts | 243 ++++++++++ 6 files changed, 806 insertions(+), 403 deletions(-) create mode 100644 plugins/adapter/discord/src/types/thread.ts diff --git a/plugins/adapter/discord/src/types/channel.ts b/plugins/adapter/discord/src/types/channel.ts index f0c1fcd255..be00ee5288 100644 --- a/plugins/adapter/discord/src/types/channel.ts +++ b/plugins/adapter/discord/src/types/channel.ts @@ -5,7 +5,7 @@ export interface Channel { /** the id of this channel */ id: snowflake /** the type of channel */ - type: integer + type: Channel.Type /** the id of the guild (may be missing for some channel objects received over gateway guild dispatches) */ guild_id?: snowflake /** sorting position of the channel */ @@ -42,44 +42,121 @@ export interface Channel { rtc_region?: string /** the camera video quality mode of the voice channel, 1 when not present */ video_quality_mode?: integer - /** an approximate count of messages in a thread, stops counting at 50 */ - message_count?: integer - /** an approximate count of users in a thread, stops counting at 50 */ - member_count?: integer - /** thread-specific fields not needed by other channels */ - thread_metadata?: ThreadMetadata - /** thread member object for the current user, if they have joined the thread, only included on certain API endpoints */ - member?: ThreadMember - /** default duration for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ - default_auto_archive_duration?: integer /** computed permissions for the invoking user in the channel, including overwrites, only included when part of the resolved data received on a slash command interaction */ permissions?: string } -/** https://discord.com/developers/docs/resources/channel#channel-object-channel-types */ -export enum ChannelType { - /** a text channel within a server */ - GUILD_TEXT = 0, - /** a direct message between users */ - DM = 1, - /** a voice channel within a server */ - GUILD_VOICE = 2, - /** a direct message between multiple users */ - GROUP_DM = 3, - /** an organizational category that contains up to 50 channels */ - GUILD_CATEGORY = 4, - /** a channel that users can follow and crosspost into their own server */ - GUILD_NEWS = 5, - /** a channel in which game developers can sell their game on Discord */ - GUILD_STORE = 6, - /** a temporary sub-channel within a GUILD_NEWS channel */ - GUILD_NEWS_THREAD = 10, - /** a temporary sub-channel within a GUILD_TEXT channel */ - GUILD_PUBLIC_THREAD = 11, - /** a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission */ - GUILD_PRIVATE_THREAD = 12, - /** a voice channel for hosting events with an audience */ - GUILD_STAGE_VOICE = 13, +export namespace Channel { + /** https://discord.com/developers/docs/resources/channel#channel-object-channel-types */ + export enum Type { + /** a text channel within a server */ + GUILD_TEXT = 0, + /** a direct message between users */ + DM = 1, + /** a voice channel within a server */ + GUILD_VOICE = 2, + /** a direct message between multiple users */ + GROUP_DM = 3, + /** an organizational category that contains up to 50 channels */ + GUILD_CATEGORY = 4, + /** a channel that users can follow and crosspost into their own server */ + GUILD_NEWS = 5, + /** a channel in which game developers can sell their game on Discord */ + GUILD_STORE = 6, + /** a temporary sub-channel within a GUILD_NEWS channel */ + GUILD_NEWS_THREAD = 10, + /** a temporary sub-channel within a GUILD_TEXT channel */ + GUILD_PUBLIC_THREAD = 11, + /** a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission */ + GUILD_PRIVATE_THREAD = 12, + /** a voice channel for hosting events with an audience */ + GUILD_STAGE_VOICE = 13, + } + + export type ModifyParams = + | ModifyParams.GroupDM + | ModifyParams.GuildChannel + | ModifyParams.Thread + + export namespace ModifyParams { + /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-group-dm */ + export interface GroupDM { + /** 1-100 character channel name */ + name: string + /** base64 encoded icon */ + icon: string + } + + /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel */ + export interface GuildChannel { + /** 1-100 character channel name */ + name: string + /** the type of channel; only conversion between text and news is supported and only in guilds with the "NEWS" feature */ + type: integer + /** the position of the channel in the left-hand listing */ + position?: integer + /** 0-1024 character channel topic */ + topic?: string + /** whether the channel is nsfw */ + nsfw?: boolean + /** amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */ + rate_limit_per_user?: integer + /** the bitrate (in bits) of the voice channel; 8000 to 96000 (128000 for VIP servers) */ + bitrate?: integer + /** the user limit of the voice channel; 0 refers to no limit, 1 to 99 refers to a user limit */ + user_limit?: integer + /** channel or category-specific permissions */ + permission_overwrites?: Overwrite[] + /** id of the new parent category for a channel */ + parent_id?: snowflake + /** channel voice region id, automatic when set to null */ + rtc_region?: string + /** the camera video quality mode of the voice channel */ + video_quality_mode?: integer + /** the default duration that the clients use (not the API) for newly created threads in the channel, in minutes, to automatically archive the thread after recent activity */ + default_auto_archive_duration?: integer + } + + /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-thread */ + export interface Thread { + /** 1-100 character channel name */ + name: string + /** whether the thread is archived */ + archived: boolean + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + auto_archive_duration: integer + /** whether the thread is locked; when a thread is locked, only users with MANAGE_THREADS can unarchive it */ + locked: boolean + /** whether non-moderators can add other non-moderators to a thread; only available on private threads */ + invitable: boolean + /** amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages, manage_thread, or manage_channel, are unaffected */ + rate_limit_per_user?: integer + } + } + + /** https://discord.com/developers/docs/resources/channel#edit-channel-permissions-json-params */ + export interface EditPermissionsParams { + /** the bitwise value of all allowed permissions */ + allow: string + /** the bitwise value of all disallowed permissions */ + deny: string + /** 0 for a role or 1 for a member */ + type: integer + } + + /** https://discord.com/developers/docs/resources/channel#follow-news-channel-json-params */ + export interface FollowParams { + /** id of target channel */ + webhook_channel_id: snowflake + } + + /** https://discord.com/developers/docs/resources/channel#group-dm-add-recipient-json-params */ + export interface AddRecipientParams { + /** access token of a user that has granted your app the gdm.join scope */ + access_token: string + /** nickname of the user being added */ + nick: string + } } /** https://discord.com/developers/docs/resources/channel#followed-channel-object-followed-channel-structure */ @@ -102,32 +179,6 @@ export interface Overwrite { deny: string } -/** https://discord.com/developers/docs/resources/channel#thread-metadata-object-thread-metadata-structure */ -export interface ThreadMetadata { - /** whether the thread is archived */ - archived: boolean - /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ - auto_archive_duration: integer - /** timestamp when the thread's archive status was last changed, used for calculating recent activity */ - archive_timestamp: timestamp - /** whether the thread is locked; when a thread is locked, only users with MANAGE_THREADS can unarchive it */ - locked: boolean - /** whether non-moderators can add other non-moderators to a thread; only available on private threads */ - invitable?: boolean -} - -/** https://discord.com/developers/docs/resources/channel#thread-member-object-thread-member-structure */ -export interface ThreadMember { - /** the id of the thread */ - id?: snowflake - /** the id of the user */ - user_id?: snowflake - /** the time the current user last joined the thread */ - join_timestamp: timestamp - /** any user-thread settings, currently only used for notifications */ - flags: integer -} - /** https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types */ export enum AllowedMentionType { /** Controls role mentions */ @@ -166,34 +217,6 @@ export interface ChannelPinsUpdateEvent { last_pin_timestamp?: timestamp } -/** https://discord.com/developers/docs/topics/gateway#thread-list-sync-thread-list-sync-event-fields */ -export interface ThreadListSyncEvent { - /** the id of the guild */ - guild_id: snowflake - /** the parent channel ids whose threads are being synced. If omitted, then threads were synced for the entire guild. This array may contain channel_ids that have no active threads as well, so you know to clear that data. */ - channel_ids?: snowflake[] - /** all active threads in the given channels that the current user can access */ - threads: Channel[] - /** all thread member objects from the synced threads for the current user, indicating which threads the current user has been added to */ - members: ThreadMember[] -} - -export interface ThreadMemberUpdateEvent extends ThreadMember {} - -/** https://discord.com/developers/docs/topics/gateway#thread-members-update-thread-members-update-event-fields */ -export interface ThreadMembersUpdateEvent { - /** the id of the thread */ - id: snowflake - /** the id of the guild */ - guild_id: snowflake - /** the approximate number of members in the thread, capped at 50 */ - member_count: integer - /** the users who were added to the thread */ - added_members?: ThreadMember[] - /** the id of the users who were removed from the thread */ - removed_member_ids?: snowflake[] -} - /** https://discord.com/developers/docs/topics/gateway#typing-start-typing-start-event-fields */ export interface TypingStartEvent { /** id of the channel */ @@ -218,12 +241,6 @@ declare module './gateway' { CHANNEL_DELETE: ChannelDeleteEvent /** message was pinned or unpinned */ CHANNEL_PINS_UPDATE: ChannelPinsUpdateEvent - /** sent when gaining access to a channel, contains all active threads in that channel */ - THREAD_LIST_SYNC: ThreadListSyncEvent - /** thread member for the current user was updated */ - THREAD_MEMBER_UPDATE: ThreadMemberUpdateEvent - /** some user(s) were added to or removed from a thread */ - THREAD_MEMBERS_UPDATE: ThreadMembersUpdateEvent /** user started typing in a channel */ TYPING_START: TypingStartEvent } @@ -261,12 +278,51 @@ Internal.define({ declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/channel#get-channel */ + /** + * Get a channel by ID. Returns a channel object. If the channel is a thread, a thread member object is included in the returned result. + * @see https://discord.com/developers/docs/resources/channel#get-channel + */ getChannel(channel_id: string): Promise - /** https://discord.com/developers/docs/resources/channel#modify-channel */ - modifyChannel(channel_id: string, data: Partial): Promise - /** https://discord.com/developers/docs/resources/channel#deleteclose-channel */ + /** + * Update a channel's settings. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. All JSON parameters are optional. + * @see https://discord.com/developers/docs/resources/channel#modify-channel + */ + modifyChannel(channel_id: string, params: Channel.ModifyParams): Promise + /** + * Delete a channel, or close a private message. Requires the MANAGE_CHANNELS permission for the guild, or MANAGE_THREADS if the channel is a thread. Deleting a category does not delete its child channels; they will have their parent_id removed and a Channel Update Gateway event will fire for each of them. Returns a channel object on success. Fires a Channel Delete Gateway event (or Thread Delete if the channel was a thread). + * @see https://discord.com/developers/docs/resources/channel#deleteclose-channel + */ deleteChannel(channel_id: string): Promise + /** + * Edit the channel permission overwrites for a user or role in a channel. Only usable for guild channels. Requires the MANAGE_ROLES permission. Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel). Returns a 204 empty response on success. For more information about permissions, see permissions. + * @see https://discord.com/developers/docs/resources/channel#edit-channel-permissions + */ + editChannelPermissions(channel_id: string, overwrite_id: string, params: Channel.EditPermissionsParams): Promise + /** + * Delete a channel permission overwrite for a user or role in a channel. Only usable for guild channels. Requires the MANAGE_ROLES permission. Returns a 204 empty response on success. For more information about permissions, see permissions + * @see https://discord.com/developers/docs/resources/channel#delete-channel-permission + */ + deleteChannelPermission(channel_id: string, overwrite_id: string): Promise + /** + * Follow a News Channel to send messages to a target channel. Requires the MANAGE_WEBHOOKS permission in the target channel. Returns a followed channel object. + * @see https://discord.com/developers/docs/resources/channel#follow-news-channel + */ + followNewsChannel(channel_id: string, params: Channel.FollowParams): Promise + /** + * Post a typing indicator for the specified channel. Generally bots should not implement this route. However, if a bot is responding to a command and expects the computation to take a few seconds, this endpoint may be called to let the user know that the bot is processing their message. Returns a 204 empty response on success. Fires a Typing Start Gateway event. + * @see https://discord.com/developers/docs/resources/channel#trigger-typing-indicator + */ + triggerTypingIndicator(channel_id: string): Promise + /** + * Adds a recipient to a Group DM using their access token. + * @see https://discord.com/developers/docs/resources/channel#group-dm-add-recipient + */ + groupDMAddRecipient(channel_id: snowflake, user_id: snowflake, params: Channel.AddRecipientParams): Promise + /** + * Removes a recipient from a Group DM. + * @see https://discord.com/developers/docs/resources/channel#group-dm-remove-recipient + */ + groupDMRemoveRecipient(channel_id: snowflake, user_id: snowflake): Promise } } @@ -280,55 +336,14 @@ Internal.define({ PUT: 'editChannelPermissions', DELETE: 'deleteChannelPermission', }, - '/channels/{channel.id}/invites': { - GET: 'getChannelInvites', - POST: 'createChannelInvite', - }, '/channels/{channel.id}/followers': { POST: 'followNewsChannel', }, '/channels/{channel.id}/typing': { POST: 'triggerTypingIndicator', }, - '/channels/{channel.id}/pins': { - GET: 'getPinnedMessages', - }, - '/channels/{channel.id}/pins/{message.id}': { - PUT: 'pinMessage', - DELETE: 'unpinMessage', - }, '/channels/{channel.id}/recipients/{user.id}': { PUT: 'groupDMAddRecipient', DELETE: 'groupDMRemoveRecipient', }, - '/channels/{channel.id}/messages/{message.id}/threads': { - POST: 'startThreadwithMessage', - }, - '/channels/{channel.id}/threads': { - POST: 'startThreadwithoutMessage', - }, - '/channels/{channel.id}/thread-members/@me': { - PUT: 'joinThread', - DELETE: 'leaveThread', - }, - '/channels/{channel.id}/thread-members/{user.id}': { - PUT: 'addThreadMember', - DELETE: 'removeThreadMember', - GET: 'getThreadMember', - }, - '/channels/{channel.id}/thread-members': { - GET: 'listThreadMembers', - }, - '/channels/{channel.id}/threads/active': { - GET: 'listActiveThreads', - }, - '/channels/{channel.id}/threads/archived/public': { - GET: 'listPublicArchivedThreads', - }, - '/channels/{channel.id}/threads/archived/private': { - GET: 'listPrivateArchivedThreads', - }, - '/channels/{channel.id}/users/@me/threads/archived/private': { - GET: 'listJoinedPrivateArchivedThreads', - }, }) diff --git a/plugins/adapter/discord/src/types/command.ts b/plugins/adapter/discord/src/types/command.ts index 8f6cc35473..f0ee638920 100644 --- a/plugins/adapter/discord/src/types/command.ts +++ b/plugins/adapter/discord/src/types/command.ts @@ -1,4 +1,4 @@ -import { ChannelType, Internal, snowflake } from '.' +import { Channel, Internal, snowflake } from '.' /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure */ export interface ApplicationCommand { @@ -47,7 +47,7 @@ export interface ApplicationCommandOption { /** if the option is a subcommand or subcommand group type, this nested options will be the parameters */ options?: ApplicationCommandOption[] /** if the option is a channel type, the channels shown will be restricted to these types */ - channel_types?: ChannelType[] + channel_types?: Channel.Type[] } /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type */ diff --git a/plugins/adapter/discord/src/types/index.ts b/plugins/adapter/discord/src/types/index.ts index 059dcb7929..1983b41914 100644 --- a/plugins/adapter/discord/src/types/index.ts +++ b/plugins/adapter/discord/src/types/index.ts @@ -21,6 +21,7 @@ export * from './role' export * from './stage-instance' export * from './sticker' export * from './team' +export * from './thread' export * from './user' export * from './voice' export * from './webhook' diff --git a/plugins/adapter/discord/src/types/invite.ts b/plugins/adapter/discord/src/types/invite.ts index cd46589f3e..db90ca1673 100644 --- a/plugins/adapter/discord/src/types/invite.ts +++ b/plugins/adapter/discord/src/types/invite.ts @@ -11,7 +11,7 @@ export interface Invite { /** the user who created the invite */ inviter?: User /** the type of target for this voice channel invite */ - target_type?: integer + target_type?: Invite.TargetType /** the user whose stream to display for this voice channel stream invite */ target_user?: User /** the embedded application to open for this voice channel embedded application invite */ @@ -23,101 +23,142 @@ export interface Invite { /** the expiration date of this invite, returned from the GET /invites/ endpoint when with_expiration is true */ expires_at?: timestamp /** stage instance data if there is a public Stage instance in the Stage channel this invite is for */ - stage_instance?: InviteStageInstance + stage_instance?: Invite.StageInstance } -/** https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types */ -export enum InviteTargetType { - STREAM = 1, - EMBEDDED_APPLICATION = 2, -} +export namespace Invite { + /** https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types */ + export enum TargetType { + STREAM = 1, + EMBEDDED_APPLICATION = 2, + } -/** https://discord.com/developers/docs/resources/invite#invite-metadata-object-invite-metadata-structure */ -export interface InviteMetadata { - /** number of times this invite has been used */ - uses: integer - /** max number of times this invite can be used */ - max_uses: integer - /** duration (in seconds) after which the invite expires */ - max_age: integer - /** whether this invite only grants temporary membership */ - temporary: boolean - /** when this invite was created */ - created_at: timestamp -} + /** https://discord.com/developers/docs/resources/invite#invite-metadata-object-invite-metadata-structure */ + export interface Metadata extends Invite { + /** number of times this invite has been used */ + uses: integer + /** max number of times this invite can be used */ + max_uses: integer + /** duration (in seconds) after which the invite expires */ + max_age: integer + /** whether this invite only grants temporary membership */ + temporary: boolean + /** when this invite was created */ + created_at: timestamp + } -/** https://discord.com/developers/docs/resources/invite#invite-stage-instance-object-invite-stage-instance-structure */ -export interface InviteStageInstance { - /** the members speaking in the Stage */ - members: Partial[] - /** the number of users in the Stage */ - participant_count: integer - /** the number of users speaking in the Stage */ - speaker_count: integer - /** the topic of the Stage instance (1-120 characters) */ - topic: string -} + /** https://discord.com/developers/docs/resources/invite#invite-stage-instance-object-invite-stage-instance-structure */ + export interface StageInstance { + /** the members speaking in the Stage */ + members: Partial[] + /** the number of users in the Stage */ + participant_count: integer + /** the number of users speaking in the Stage */ + speaker_count: integer + /** the topic of the Stage instance (1-120 characters) */ + topic: string + } -/** https://discord.com/developers/docs/topics/gateway#invite-create-invite-create-event-fields */ -export interface InviteCreateEvent { - /** the channel the invite is for */ - channel_id: snowflake - /** the unique invite code */ - code: string - /** the time at which the invite was created */ - created_at: timestamp - /** the guild of the invite */ - guild_id?: snowflake - /** the user that created the invite */ - inviter?: User - /** how long the invite is valid for (in seconds) */ - max_age: integer - /** the maximum number of times the invite can be used */ - max_uses: integer - /** the type of target for this voice channel invite */ - target_type?: integer - /** the user whose stream to display for this voice channel stream invite */ - target_user?: User - /** the embedded application to open for this voice channel embedded application invite */ - target_application?: Partial - /** whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) */ - temporary: boolean - /** how many times the invite has been used (always will be 0) */ - uses: integer -} + export namespace Event { + /** https://discord.com/developers/docs/topics/gateway#invite-create-invite-create-event-fields */ + export interface Create { + /** the channel the invite is for */ + channel_id: snowflake + /** the unique invite code */ + code: string + /** the time at which the invite was created */ + created_at: timestamp + /** the guild of the invite */ + guild_id?: snowflake + /** the user that created the invite */ + inviter?: User + /** how long the invite is valid for (in seconds) */ + max_age: integer + /** the maximum number of times the invite can be used */ + max_uses: integer + /** the type of target for this voice channel invite */ + target_type?: integer + /** the user whose stream to display for this voice channel stream invite */ + target_user?: User + /** the embedded application to open for this voice channel embedded application invite */ + target_application?: Partial + /** whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) */ + temporary: boolean + /** how many times the invite has been used (always will be 0) */ + uses: integer + } -/** https://discord.com/developers/docs/topics/gateway#invite-delete-invite-delete-event-fields */ -export interface InviteDeleteEvent { - /** the channel of the invite */ - channel_id: snowflake - /** the guild of the invite */ - guild_id?: snowflake - /** the unique invite code */ - code: string + /** https://discord.com/developers/docs/topics/gateway#invite-delete-invite-delete-event-fields */ + export interface Delete { + /** the channel of the invite */ + channel_id: snowflake + /** the guild of the invite */ + guild_id?: snowflake + /** the unique invite code */ + code: string + } + } + + /** https://discord.com/developers/docs/resources/invite#get-invite-query-string-params */ + export interface GetOptions { + /** whether to include invite metadata */ + with_counts?: boolean + /** whether to include invite expiration date */ + with_expiration?: boolean + /** the guild scheduled event to include with the invite */ + guild_scheduled_event_id?: snowflake + } + + /** https://discord.com/developers/docs/resources/channel#create-channel-invite-json-params */ + export interface CreateParams { + /** duration of invite in seconds before expiry, or 0 for never. between 0 and 604800 (7 days) */ + max_age: integer + /** max number of uses or 0 for unlimited. between 0 and 100 */ + max_uses: integer + /** whether this invite only grants temporary membership */ + temporary: boolean + /** if true, don't try to reuse a similar invite (useful for creating many unique one time use invites) */ + unique: boolean + /** the type of target for this voice channel invite */ + target_type: integer + /** the id of the user whose stream to display for this invite, required if target_type is 1, the user must be streaming in the channel */ + target_user_id: snowflake + /** the id of the embedded application to open for this invite, required if target_type is 2, the application must have the EMBEDDED flag */ + target_application_id: snowflake + } } declare module './gateway' { interface GatewayEvents { /** invite to a channel was created */ - INVITE_CREATE: InviteCreateEvent + INVITE_CREATE: Invite.Event.Create /** invite to a channel was deleted */ - INVITE_DELETE: InviteDeleteEvent + INVITE_DELETE: Invite.Event.Delete } } -export interface GetInviteOptions { - /** whether to include invite metadata */ - with_counts?: boolean - /** whether to include invite expiration date */ - with_expiration?: boolean -} - declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/invite#get-invite */ - getInvite(code: string, options?: GetInviteOptions): Promise - /** https://discord.com/developers/docs/resources/invite#delete-invite */ + /** + * Returns an invite object for the given code. + * @see https://discord.com/developers/docs/resources/invite#get-invite + */ + getInvite(code: string, params?: Invite.GetOptions): Promise + /** + * Delete an invite. Requires the MANAGE_CHANNELS permission on the channel this invite belongs to, or MANAGE_GUILD to remove any invite across the guild. Returns an invite object on success. Fires a Invite Delete Gateway event. + * @see https://discord.com/developers/docs/resources/invite#delete-invite + */ deleteInvite(code: string): Promise + /** + * Returns a list of invite objects (with invite metadata) for the channel. Only usable for guild channels. Requires the MANAGE_CHANNELS permission. + * @see https://discord.com/developers/docs/resources/channel#get-channel-invites + */ + getChannelInvites(channel_id: string): Promise + /** + * Create a new invite object for the channel. Only usable for guild channels. Requires the CREATE_INSTANT_INVITE permission. All JSON parameters for this route are optional, however the request body is not. If you are not sending any fields, you still have to send an empty JSON object ({}). Returns an invite object. Fires an Invite Create Gateway event. + * @see https://discord.com/developers/docs/resources/channel#create-channel-invite + */ + createChannelInvite(channel_id: string, params: Invite.CreateParams): Promise } } @@ -126,4 +167,8 @@ Internal.define({ GET: 'getInvite', DELETE: 'deleteInvite', }, + '/channels/{channel.id}/invites': { + GET: 'getChannelInvites', + POST: 'createChannelInvite', + }, }) diff --git a/plugins/adapter/discord/src/types/message.ts b/plugins/adapter/discord/src/types/message.ts index aa976ae00c..08ea0885e6 100644 --- a/plugins/adapter/discord/src/types/message.ts +++ b/plugins/adapter/discord/src/types/message.ts @@ -1,4 +1,4 @@ -import { Application, Channel, ChannelType, Component, GuildMember, integer, Internal, MessageInteraction, Reaction, snowflake, Sticker, StickerItem, timestamp, User } from '.' +import { AllowedMentions, Application, Channel, Component, GuildMember, integer, Internal, MessageInteraction, Reaction, snowflake, Sticker, StickerItem, timestamp, User } from '.' /** https://discord.com/developers/docs/resources/channel#message-object-message-structure */ export interface Message { @@ -41,15 +41,15 @@ export interface Message { /** if the message is generated by a webhook, this is the webhook's id */ webhook_id?: snowflake /** type of message */ - type: integer + type: Message.Type /** sent with Rich Presence-related chat embeds */ - activity?: MessageActivity + activity?: Message.Activity /** sent with Rich Presence-related chat embeds */ application?: Partial /** if the message is a response to an Interaction, this is the id of the interaction's application */ application_id?: snowflake /** data showing the source of a crosspost, channel follow add, pin, or reply message */ - message_reference?: MessageReference + message_reference?: Message.Reference /** message flags combined as a bitfield */ flags?: integer /** the message associated with the message_reference */ @@ -66,79 +66,129 @@ export interface Message { stickers?: Sticker[] } -/** https://discord.com/developers/docs/resources/channel#message-object-message-types */ -export enum MessageType { - DEFAULT = 0, - RECIPIENT_ADD = 1, - RECIPIENT_REMOVE = 2, - CALL = 3, - CHANNEL_NAME_CHANGE = 4, - CHANNEL_ICON_CHANGE = 5, - CHANNEL_PINNED_MESSAGE = 6, - GUILD_MEMBER_JOIN = 7, - USER_PREMIUM_GUILD_SUBSCRIPTION = 8, - USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9, - USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10, - USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11, - CHANNEL_FOLLOW_ADD = 12, - GUILD_DISCOVERY_DISQUALIFIED = 14, - GUILD_DISCOVERY_REQUALIFIED = 15, - GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16, - GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17, - THREAD_CREATED = 18, - REPLY = 19, - CHAT_INPUT_COMMAND = 20, - THREAD_STARTER_MESSAGE = 21, - GUILD_INVITE_REMINDER = 22, - CONTEXT_MENU_COMMAND = 23, -} +export namespace Message { + /** https://discord.com/developers/docs/resources/channel#message-object-message-types */ + export enum Type { + DEFAULT = 0, + RECIPIENT_ADD = 1, + RECIPIENT_REMOVE = 2, + CALL = 3, + CHANNEL_NAME_CHANGE = 4, + CHANNEL_ICON_CHANGE = 5, + CHANNEL_PINNED_MESSAGE = 6, + GUILD_MEMBER_JOIN = 7, + USER_PREMIUM_GUILD_SUBSCRIPTION = 8, + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9, + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10, + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11, + CHANNEL_FOLLOW_ADD = 12, + GUILD_DISCOVERY_DISQUALIFIED = 14, + GUILD_DISCOVERY_REQUALIFIED = 15, + GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16, + GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17, + THREAD_CREATED = 18, + REPLY = 19, + CHAT_INPUT_COMMAND = 20, + THREAD_STARTER_MESSAGE = 21, + GUILD_INVITE_REMINDER = 22, + CONTEXT_MENU_COMMAND = 23, + } -/** https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure */ -export interface MessageActivity { - /** type of message activity */ - type: integer - /** party_id from a Rich Presence event */ - party_id?: string -} + /** https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure */ + export interface Activity { + /** type of message activity */ + type: ActivityType + /** party_id from a Rich Presence event */ + party_id?: string + } -/** https://discord.com/developers/docs/resources/channel#message-object-message-activity-types */ -export enum MessageActivityType { - JOIN = 1, - SPECTATE = 2, - LISTEN = 3, - JOIN_REQUEST = 5, -} + /** https://discord.com/developers/docs/resources/channel#message-object-message-activity-types */ + export enum ActivityType { + JOIN = 1, + SPECTATE = 2, + LISTEN = 3, + JOIN_REQUEST = 5, + } + + /** https://discord.com/developers/docs/resources/channel#message-object-message-flags */ + export enum Flag { + /** this message has been published to subscribed channels (via Channel Following) */ + CROSSPOSTED = 1 << 0, + /** this message originated from a message in another channel (via Channel Following) */ + IS_CROSSPOST = 1 << 1, + /** do not include any embeds when serializing this message */ + SUPPRESS_EMBEDS = 1 << 2, + /** the source message for this crosspost has been deleted (via Channel Following) */ + SOURCE_MESSAGE_DELETED = 1 << 3, + /** this message came from the urgent message system */ + URGENT = 1 << 4, + /** this message has an associated thread, with the same id as the message */ + HAS_THREAD = 1 << 5, + /** this message is only visible to the user who invoked the Interaction */ + EPHEMERAL = 1 << 6, + /** this message is an Interaction Response and the bot is "thinking" */ + LOADING = 1 << 7, + } + + /** https://discord.com/developers/docs/resources/channel#message-reference-object-message-reference-structure */ + export interface Reference { + /** id of the originating message */ + message_id?: snowflake + /** id of the originating message's channel */ + channel_id?: snowflake + /** id of the originating message's guild */ + guild_id?: snowflake + /** when sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */ + fail_if_not_exists?: boolean + } -/** https://discord.com/developers/docs/resources/channel#message-object-message-flags */ -export enum MessageFlag { - /** this message has been published to subscribed channels (via Channel Following) */ - CROSSPOSTED = 1 << 0, - /** this message originated from a message in another channel (via Channel Following) */ - IS_CROSSPOST = 1 << 1, - /** do not include any embeds when serializing this message */ - SUPPRESS_EMBEDS = 1 << 2, - /** the source message for this crosspost has been deleted (via Channel Following) */ - SOURCE_MESSAGE_DELETED = 1 << 3, - /** this message came from the urgent message system */ - URGENT = 1 << 4, - /** this message has an associated thread, with the same id as the message */ - HAS_THREAD = 1 << 5, - /** this message is only visible to the user who invoked the Interaction */ - EPHEMERAL = 1 << 6, - /** this message is an Interaction Response and the bot is "thinking" */ - LOADING = 1 << 7, -} + /** https://discord.com/developers/docs/resources/channel#get-channel-messages-query-string-params */ + export interface GetParams { + /** get messages around this message ID */ + around: snowflake + /** get messages before this message ID */ + before: snowflake + /** get messages after this message ID */ + after: snowflake + /** max number of messages to return (1-100) */ + limit: integer + } -/** https://discord.com/developers/docs/resources/channel#message-reference-object-message-reference-structure */ -export interface MessageReference { - /** id of the originating message */ - message_id?: snowflake - /** id of the originating message's channel */ - channel_id?: snowflake - /** id of the originating message's guild */ - guild_id?: snowflake - /** when sending, whether to error if the referenced message doesn't exist instead of sending as a normal (non-reply) message, default true */ - fail_if_not_exists?: boolean + /** https://discord.com/developers/docs/resources/channel#create-message-jsonform-params */ + export interface CreateParams extends EditParams { + /** true if this is a TTS message */ + tts: boolean + /** include to make your message a reply */ + message_reference: Reference + /** IDs of up to 3 stickers in the server to send in the message */ + sticker_ids: snowflake[] + } + + /** https://discord.com/developers/docs/resources/channel#edit-message-jsonform-params */ + export interface EditParams { + /** the message contents (up to 2000 characters) */ + content?: string + /** embedded rich content (up to 6000 characters) */ + embeds?: Embed[] + /** edit the flags of a message (only SUPPRESS_EMBEDS can currently be set/unset) */ + flags?: integer + /** allowed mentions for the message */ + allowed_mentions?: AllowedMentions + /** the components to include with the message */ + components?: Component[] + /** the contents of the file being sent/edited */ + files?: any + /** JSON encoded body of non-file params (multipart/form-data only) */ + payload_json?: string + /** attached files to keep and possible descriptions for new files */ + attachments?: Attachment[] + } + + /** https://discord.com/developers/docs/resources/channel#bulk-delete-messages-json-params */ + export interface BulkDeleteParams { + /** an array of message ids to delete (2-100) */ + messages: snowflake[] + } } /** https://discord.com/developers/docs/resources/channel#embed-object-embed-structure */ @@ -156,95 +206,97 @@ export interface Embed { /** color code of the embed */ color?: integer /** footer information */ - footer?: EmbedFooter + footer?: Embed.Footer /** image information */ - image?: EmbedImage + image?: Embed.Image /** thumbnail information */ - thumbnail?: EmbedThumbnail + thumbnail?: Embed.Thumbnail /** video information */ - video?: EmbedVideo + video?: Embed.Video /** provider information */ - provider?: EmbedProvider + provider?: Embed.Provider /** author information */ - author?: EmbedAuthor + author?: Embed.Author /** fields information */ - fields?: EmbedField[] + fields?: Embed.Field[] } -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure */ -export interface EmbedThumbnail { - /** source url of thumbnail (only supports http(s) and attachments) */ - url: string - /** a proxied url of the thumbnail */ - proxy_url?: string - /** height of thumbnail */ - height?: integer - /** width of thumbnail */ - width?: integer -} +export namespace Embed { + /** https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure */ + export interface Thumbnail { + /** source url of thumbnail (only supports http(s) and attachments) */ + url: string + /** a proxied url of the thumbnail */ + proxy_url?: string + /** height of thumbnail */ + height?: integer + /** width of thumbnail */ + width?: integer + } -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure */ -export interface EmbedVideo { - /** source url of video */ - url?: string - /** a proxied url of the video */ - proxy_url?: string - /** height of video */ - height?: integer - /** width of video */ - width?: integer -} + /** https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure */ + export interface Video { + /** source url of video */ + url?: string + /** a proxied url of the video */ + proxy_url?: string + /** height of video */ + height?: integer + /** width of video */ + width?: integer + } -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure */ -export interface EmbedImage { - /** source url of image (only supports http(s) and attachments) */ - url: string - /** a proxied url of the image */ - proxy_url?: string - /** height of image */ - height?: integer - /** width of image */ - width?: integer -} + /** https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure */ + export interface Image { + /** source url of image (only supports http(s) and attachments) */ + url: string + /** a proxied url of the image */ + proxy_url?: string + /** height of image */ + height?: integer + /** width of image */ + width?: integer + } -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure */ -export interface EmbedProvider { - /** name of provider */ - name?: string - /** url of provider */ - url?: string -} + /** https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure */ + export interface Provider { + /** name of provider */ + name?: string + /** url of provider */ + url?: string + } -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure */ -export interface EmbedAuthor { - /** name of author */ - name: string - /** url of author */ - url?: string - /** url of author icon (only supports http(s) and attachments) */ - icon_url?: string - /** a proxied url of author icon */ - proxy_icon_url?: string -} + /** https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure */ + export interface Author { + /** name of author */ + name: string + /** url of author */ + url?: string + /** url of author icon (only supports http(s) and attachments) */ + icon_url?: string + /** a proxied url of author icon */ + proxy_icon_url?: string + } -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure */ -export interface EmbedFooter { - /** footer text */ - text: string - /** url of footer icon (only supports http(s) and attachments) */ - icon_url?: string - /** a proxied url of footer icon */ - proxy_icon_url?: string -} + /** https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure */ + export interface Footer { + /** footer text */ + text: string + /** url of footer icon (only supports http(s) and attachments) */ + icon_url?: string + /** a proxied url of footer icon */ + proxy_icon_url?: string + } -/** https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure */ -export interface EmbedField { - /** name of the field */ - name: string - /** value of the field */ - value: string - /** whether or not this field should display inline */ - inline?: boolean + /** https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure */ + export interface Field { + /** name of the field */ + name: string + /** value of the field */ + value: string + /** whether or not this field should display inline */ + inline?: boolean + } } /** https://discord.com/developers/docs/resources/channel#attachment-object-attachment-structure */ @@ -276,64 +328,104 @@ export interface ChannelMention { /** id of the guild containing the channel */ guild_id: snowflake /** the type of channel */ - type: ChannelType + type: Channel.Type /** the name of the channel */ name: string } -export interface MessageCreateEvent extends Message {} +export namespace Message { + export namespace Event { + export interface Create extends Message {} -export interface MessageUpdateEvent extends Message {} + export interface Update extends Message {} -/** https://discord.com/developers/docs/topics/gateway#message-delete-message-delete-event-fields */ -export interface MessageDeleteEvent { - /** the id of the message */ - id: snowflake - /** the id of the channel */ - channel_id: snowflake - /** the id of the guild */ - guild_id?: snowflake -} + /** https://discord.com/developers/docs/topics/gateway#message-delete-message-delete-event-fields */ + export interface Delete { + /** the id of the message */ + id: snowflake + /** the id of the channel */ + channel_id: snowflake + /** the id of the guild */ + guild_id?: snowflake + } -/** https://discord.com/developers/docs/topics/gateway#message-delete-bulk-message-delete-bulk-event-fields */ -export interface MessageDeleteBulkEvent { - /** the ids of the messages */ - ids: snowflake[] - /** the id of the channel */ - channel_id: snowflake - /** the id of the guild */ - guild_id?: snowflake + /** https://discord.com/developers/docs/topics/gateway#message-delete-bulk-message-delete-bulk-event-fields */ + export interface DeleteBulk { + /** the ids of the messages */ + ids: snowflake[] + /** the id of the channel */ + channel_id: snowflake + /** the id of the guild */ + guild_id?: snowflake + } + } } declare module './gateway' { interface GatewayEvents { /** message was created */ - MESSAGE_CREATE: MessageCreateEvent + MESSAGE_CREATE: Message.Event.Create /** message was edited */ - MESSAGE_UPDATE: MessageUpdateEvent + MESSAGE_UPDATE: Message.Event.Update /** message was deleted */ - MESSAGE_DELETE: MessageDeleteEvent + MESSAGE_DELETE: Message.Event.Delete /** multiple messages were deleted at once */ - MESSAGE_DELETE_BULK: MessageDeleteBulkEvent + MESSAGE_DELETE_BULK: Message.Event.DeleteBulk } } declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/channel#get-channel-messages */ - getChannelMessages(channel_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/channel#get-channel-message */ + /** + * Returns the messages for a channel. If operating on a guild channel, this endpoint requires the VIEW_CHANNEL permission to be present on the current user. If the current user is missing the 'READ_MESSAGE_HISTORY' permission in the channel then this will return no messages (since they cannot read the message history). Returns an array of message objects on success. + * @see https://discord.com/developers/docs/resources/channel#get-channel-messages + */ + getChannelMessages(channel_id: snowflake, params?: Message.GetParams): Promise + /** + * Returns a specific message in the channel. If operating on a guild channel, this endpoint requires the 'READ_MESSAGE_HISTORY' permission to be present on the current user. Returns a message object on success. + * @see https://discord.com/developers/docs/resources/channel#get-channel-message + */ getChannelMessage(channel_id: snowflake, message_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/channel#create-message */ - createMessage(channel_id: snowflake, data: Partial): Promise - /** https://discord.com/developers/docs/resources/channel#crosspost-message */ + /** + * Post a message to a guild text or DM channel. Returns a message object. Fires a Message Create Gateway event. See message formatting for more information on how to properly format messages. + * @see https://discord.com/developers/docs/resources/channel#create-message + */ + createMessage(channel_id: snowflake, params: Message.CreateParams): Promise + /** + * Crosspost a message in a News Channel to following channels. This endpoint requires the 'SEND_MESSAGES' permission, if the current user sent the message, or additionally the 'MANAGE_MESSAGES' permission, for all other messages, to be present for the current user. + * @see https://discord.com/developers/docs/resources/channel#crosspost-message + */ crosspostMessage(channel_id: snowflake, message_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/channel#edit-message */ - editMessage(channel_id: snowflake, message_id: snowflake, data: Partial): Promise - /** https://discord.com/developers/docs/resources/channel#delete-message */ + /** + * Edit a previously sent message. The fields content, embeds, and flags can be edited by the original message author. Other users can only edit flags and only if they have the MANAGE_MESSAGES permission in the corresponding channel. When specifying flags, ensure to include all previously set flags/bits in addition to ones that you are modifying. Only flags documented in the table below may be modified by users (unsupported flag changes are currently ignored without error). + * @see https://discord.com/developers/docs/resources/channel#edit-message + */ + editMessage(channel_id: snowflake, message_id: snowflake, params: Message.EditParams): Promise + /** + * Delete a message. If operating on a guild channel and trying to delete a message that was not sent by the current user, this endpoint requires the MANAGE_MESSAGES permission. Returns a 204 empty response on success. Fires a Message Delete Gateway event. + * @see https://discord.com/developers/docs/resources/channel#delete-message + */ deleteMessage(channel_id: snowflake, message_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/channel#bulk-delete-messages */ - bulkDeleteMessages(channel_id: snowflake, message_ids: snowflake[]): Promise + /** + * Delete multiple messages in a single request. This endpoint can only be used on guild channels and requires the MANAGE_MESSAGES permission. Returns a 204 empty response on success. Fires a Message Delete Bulk Gateway event. + * @see https://discord.com/developers/docs/resources/channel#bulk-delete-messages + */ + bulkDeleteMessages(channel_id: snowflake, params: Message.BulkDeleteParams): Promise + /** + * Returns all pinned messages in the channel as an array of message objects. + * @see https://discord.com/developers/docs/resources/channel#get-pinned-messages + */ + getPinnedMessages(channel_id: snowflake): Promise + /** + * Pin a message in a channel. Requires the MANAGE_MESSAGES permission. Returns a 204 empty response on success. + * @see https://discord.com/developers/docs/resources/channel#pin-message + */ + pinMessage(channel_id: snowflake, message_id: snowflake): Promise + /** + * Unpin a message in a channel. Requires the MANAGE_MESSAGES permission. Returns a 204 empty response on success. + * @see https://discord.com/developers/docs/resources/channel#unpin-message + */ + unpinMessage(channel_id: snowflake, message_id: snowflake): Promise } } @@ -353,4 +445,11 @@ Internal.define({ '/channels/{channel.id}/messages/bulk-delete': { POST: 'bulkDeleteMessages', }, + '/channels/{channel.id}/pins': { + GET: 'getPinnedMessages', + }, + '/channels/{channel.id}/pins/{message.id}': { + PUT: 'pinMessage', + DELETE: 'unpinMessage', + }, }) diff --git a/plugins/adapter/discord/src/types/thread.ts b/plugins/adapter/discord/src/types/thread.ts new file mode 100644 index 0000000000..490763ab91 --- /dev/null +++ b/plugins/adapter/discord/src/types/thread.ts @@ -0,0 +1,243 @@ +import { Channel, integer, Internal, snowflake, timestamp } from '.' + +declare module './channel' { + interface Channel { + /** an approximate count of messages in a thread, stops counting at 50 */ + message_count?: integer + /** an approximate count of users in a thread, stops counting at 50 */ + member_count?: integer + /** thread-specific fields not needed by other channels */ + thread_metadata?: ThreadMetadata + /** thread member object for the current user, if they have joined the thread, only included on certain API endpoints */ + member?: ThreadMember + /** default duration for newly created threads, in minutes, to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + default_auto_archive_duration?: integer + } +} + +/** https://discord.com/developers/docs/resources/channel#thread-member-object-thread-member-structure */ +export interface ThreadMember { + /** the id of the thread */ + id?: snowflake + /** the id of the user */ + user_id?: snowflake + /** the time the current user last joined the thread */ + join_timestamp: timestamp + /** any user-thread settings, currently only used for notifications */ + flags: integer +} + +/** https://discord.com/developers/docs/resources/channel#thread-metadata-object-thread-metadata-structure */ +export interface ThreadMetadata { + /** whether the thread is archived */ + archived: boolean + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + auto_archive_duration: integer + /** timestamp when the thread's archive status was last changed, used for calculating recent activity */ + archive_timestamp: timestamp + /** whether the thread is locked; when a thread is locked, only users with MANAGE_THREADS can unarchive it */ + locked: boolean + /** whether non-moderators can add other non-moderators to a thread; only available on private threads */ + invitable?: boolean +} + +export interface Thread extends Channel {} + +export namespace Thread { + /** https://discord.com/developers/docs/resources/channel#start-thread-with-message-json-params */ + export interface StartWithMessageParams { + /** 1-100 character channel name */ + name: string + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + auto_archive_duration?: integer + /** amount of seconds a user has to wait before sending another message (0-21600) */ + rate_limit_per_user?: integer + } + + /** https://discord.com/developers/docs/resources/channel#start-thread-without-message-json-params */ + export interface StartWithoutMessageParams { + /** 1-100 character channel name */ + name: string + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + auto_archive_duration?: integer + /** the type of thread to create */ + type?: integer + /** whether non-moderators can add other non-moderators to a thread; only available when creating a private thread */ + invitable?: boolean + /** amount of seconds a user has to wait before sending another message (0-21600) */ + rate_limit_per_user?: integer + } + + /** https://discord.com/developers/docs/resources/channel#list-active-threads-response-body */ + export interface List { + /** the active threads */ + threads: Channel[] + /** a thread member object for each returned thread the current user has joined */ + members: ThreadMember[] + /** whether there are potentially additional threads that could be returned on a subsequent call */ + has_more: boolean + } + + /** https://discord.com/developers/docs/resources/channel#list-public-archived-threads-query-string-params */ + export interface ListPublicArchivedParams { + /** returns threads before this timestamp */ + before?: timestamp + /** optional maximum number of threads to return */ + limit?: integer + } + + /** https://discord.com/developers/docs/resources/channel#list-private-archived-threads-query-string-params */ + export interface ListPrivateArchivedParams { + /** returns threads before this timestamp */ + before?: timestamp + /** optional maximum number of threads to return */ + limit?: integer + } + + /** https://discord.com/developers/docs/resources/channel#list-joined-private-archived-threads-query-string-params */ + export interface ListJoinedPrivateArchivedParams { + /** returns threads before this id */ + before?: snowflake + /** optional maximum number of threads to return */ + limit?: integer + } + + export namespace Event { + /** https://discord.com/developers/docs/topics/gateway#thread-list-sync-thread-list-sync-event-fields */ + export interface ListSync { + /** the id of the guild */ + guild_id: snowflake + /** the parent channel ids whose threads are being synced. If omitted, then threads were synced for the entire guild. This array may contain channel_ids that have no active threads as well, so you know to clear that data. */ + channel_ids?: snowflake[] + /** all active threads in the given channels that the current user can access */ + threads: Channel[] + /** all thread member objects from the synced threads for the current user, indicating which threads the current user has been added to */ + members: ThreadMember[] + } + + export interface MemberUpdate extends ThreadMember {} + + /** https://discord.com/developers/docs/topics/gateway#thread-members-update-thread-members-update-event-fields */ + export interface MembersUpdate { + /** the id of the thread */ + id: snowflake + /** the id of the guild */ + guild_id: snowflake + /** the approximate number of members in the thread, capped at 50 */ + member_count: integer + /** the users who were added to the thread */ + added_members?: ThreadMember[] + /** the id of the users who were removed from the thread */ + removed_member_ids?: snowflake[] + } + } +} + +declare module './gateway' { + interface GatewayEvents { + /** sent when gaining access to a channel, contains all active threads in that channel */ + THREAD_LIST_SYNC: Thread.Event.ListSync + /** thread member for the current user was updated */ + THREAD_MEMBER_UPDATE: Thread.Event.MemberUpdate + /** some user(s) were added to or removed from a thread */ + THREAD_MEMBERS_UPDATE: Thread.Event.MembersUpdate + } +} + +declare module './internal' { + interface Internal { + /** + * Creates a new thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. + * @see https://discord.com/developers/docs/resources/channel#start-thread-with-message + */ + startThreadWithMessage(channel_id: snowflake, message_id: snowflake, params: Thread.StartWithMessageParams): Promise + /** + * Creates a new thread that is not connected to an existing message. The created thread defaults to a GUILD_PRIVATE_THREAD*. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. + * @see https://discord.com/developers/docs/resources/channel#start-thread-without-message + */ + startThreadWithoutMessage(channel_id: snowflake, params: Thread.StartWithoutMessageParams): Promise + /** + * Adds the current user to a thread. Also requires the thread is not archived. Returns a 204 empty response on success. Fires a Thread Members Update Gateway event. + * @see https://discord.com/developers/docs/resources/channel#join-thread + */ + joinThread(channel_id: snowflake): Promise + /** + * Adds another member to a thread. Requires the ability to send messages in the thread. Also requires the thread is not archived. Returns a 204 empty response if the member is successfully added or was already a member of the thread. Fires a Thread Members Update Gateway event. + * @see https://discord.com/developers/docs/resources/channel#add-thread-member + */ + addThreadMember(channel_id: snowflake, user_id: snowflake): Promise + /** + * Removes the current user from a thread. Also requires the thread is not archived. Returns a 204 empty response on success. Fires a Thread Members Update Gateway event. + * @see https://discord.com/developers/docs/resources/channel#leave-thread + */ + leaveThread(channel_id: snowflake): Promise + /** + * Removes another member from a thread. Requires the MANAGE_THREADS permission, or the creator of the thread if it is a GUILD_PRIVATE_THREAD. Also requires the thread is not archived. Returns a 204 empty response on success. Fires a Thread Members Update Gateway event. + * @see https://discord.com/developers/docs/resources/channel#remove-thread-member + */ + removeThreadMember(channel_id: snowflake, user_id: snowflake): Promise + /** + * Returns a thread member object for the specified user if they are a member of the thread, returns a 404 response otherwise. + * @see https://discord.com/developers/docs/resources/channel#get-thread-member + */ + getThreadMember(channel_id: snowflake, user_id: snowflake): Promise + /** + * Returns array of thread members objects that are members of the thread. + * @see https://discord.com/developers/docs/resources/channel#list-thread-members + */ + listThreadMembers(channel_id: snowflake): Promise + /** + * Returns all active threads in the channel, including public and private threads. Threads are ordered by their id, in descending order. + * @see https://discord.com/developers/docs/resources/channel#list-active-threads + */ + listActiveThreads(channel_id: snowflake): Promise + /** + * Returns archived threads in the channel that are public. When called on a GUILD_TEXT channel, returns threads of type GUILD_PUBLIC_THREAD. When called on a GUILD_NEWS channel returns threads of type GUILD_NEWS_THREAD. Threads are ordered by archive_timestamp, in descending order. Requires the READ_MESSAGE_HISTORY permission. + * @see https://discord.com/developers/docs/resources/channel#list-public-archived-threads + */ + listPublicArchivedThreads(channel_id: snowflake, params?: Thread.ListPublicArchivedParams): Promise + /** + * Returns archived threads in the channel that are of type GUILD_PRIVATE_THREAD. Threads are ordered by archive_timestamp, in descending order. Requires both the READ_MESSAGE_HISTORY and MANAGE_THREADS permissions. + * @see https://discord.com/developers/docs/resources/channel#list-private-archived-threads + */ + listPrivateArchivedThreads(channel_id: snowflake, params?: Thread.ListPrivateArchivedParams): Promise + /** + * Returns archived threads in the channel that are of type GUILD_PRIVATE_THREAD, and the user has joined. Threads are ordered by their id, in descending order. Requires the READ_MESSAGE_HISTORY permission. + * @see https://discord.com/developers/docs/resources/channel#list-joined-private-archived-threads + */ + listJoinedPrivateArchivedThreads(channel_id: snowflake, params?: Thread.ListJoinedPrivateArchivedParams): Promise + } +} + +Internal.define({ + '/channels/{channel.id}/messages/{message.id}/threads': { + POST: 'startThreadwithMessage', + }, + '/channels/{channel.id}/threads': { + POST: 'startThreadwithoutMessage', + }, + '/channels/{channel.id}/thread-members/@me': { + PUT: 'joinThread', + DELETE: 'leaveThread', + }, + '/channels/{channel.id}/thread-members/{user.id}': { + PUT: 'addThreadMember', + DELETE: 'removeThreadMember', + GET: 'getThreadMember', + }, + '/channels/{channel.id}/thread-members': { + GET: 'listThreadMembers', + }, + '/channels/{channel.id}/threads/active': { + GET: 'listActiveThreads', + }, + '/channels/{channel.id}/threads/archived/public': { + GET: 'listPublicArchivedThreads', + }, + '/channels/{channel.id}/threads/archived/private': { + GET: 'listPrivateArchivedThreads', + }, + '/channels/{channel.id}/users/@me/threads/archived/private': { + GET: 'listJoinedPrivateArchivedThreads', + }, +}) From 3141c5ff7e368a63a27568a4a4ad76b4299ce652 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 00:48:30 +0800 Subject: [PATCH 13/34] feat(discord): enhance more typings --- plugins/adapter/discord/src/types/channel.ts | 14 +- .../discord/src/types/guild-template.ts | 66 +++++++ plugins/adapter/discord/src/types/message.ts | 6 +- .../discord/src/types/stage-instance.ts | 61 ++++++- plugins/adapter/discord/src/types/sticker.ts | 169 +++++++++++++----- plugins/adapter/discord/src/types/voice.ts | 5 +- 6 files changed, 255 insertions(+), 66 deletions(-) diff --git a/plugins/adapter/discord/src/types/channel.ts b/plugins/adapter/discord/src/types/channel.ts index be00ee5288..14e457018a 100644 --- a/plugins/adapter/discord/src/types/channel.ts +++ b/plugins/adapter/discord/src/types/channel.ts @@ -282,37 +282,37 @@ declare module './internal' { * Get a channel by ID. Returns a channel object. If the channel is a thread, a thread member object is included in the returned result. * @see https://discord.com/developers/docs/resources/channel#get-channel */ - getChannel(channel_id: string): Promise + getChannel(channel_id: snowflake): Promise /** * Update a channel's settings. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. All JSON parameters are optional. * @see https://discord.com/developers/docs/resources/channel#modify-channel */ - modifyChannel(channel_id: string, params: Channel.ModifyParams): Promise + modifyChannel(channel_id: snowflake, params: Channel.ModifyParams): Promise /** * Delete a channel, or close a private message. Requires the MANAGE_CHANNELS permission for the guild, or MANAGE_THREADS if the channel is a thread. Deleting a category does not delete its child channels; they will have their parent_id removed and a Channel Update Gateway event will fire for each of them. Returns a channel object on success. Fires a Channel Delete Gateway event (or Thread Delete if the channel was a thread). * @see https://discord.com/developers/docs/resources/channel#deleteclose-channel */ - deleteChannel(channel_id: string): Promise + deleteChannel(channel_id: snowflake): Promise /** * Edit the channel permission overwrites for a user or role in a channel. Only usable for guild channels. Requires the MANAGE_ROLES permission. Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel). Returns a 204 empty response on success. For more information about permissions, see permissions. * @see https://discord.com/developers/docs/resources/channel#edit-channel-permissions */ - editChannelPermissions(channel_id: string, overwrite_id: string, params: Channel.EditPermissionsParams): Promise + editChannelPermissions(channel_id: snowflake, overwrite_id: string, params: Channel.EditPermissionsParams): Promise /** * Delete a channel permission overwrite for a user or role in a channel. Only usable for guild channels. Requires the MANAGE_ROLES permission. Returns a 204 empty response on success. For more information about permissions, see permissions * @see https://discord.com/developers/docs/resources/channel#delete-channel-permission */ - deleteChannelPermission(channel_id: string, overwrite_id: string): Promise + deleteChannelPermission(channel_id: snowflake, overwrite_id: string): Promise /** * Follow a News Channel to send messages to a target channel. Requires the MANAGE_WEBHOOKS permission in the target channel. Returns a followed channel object. * @see https://discord.com/developers/docs/resources/channel#follow-news-channel */ - followNewsChannel(channel_id: string, params: Channel.FollowParams): Promise + followNewsChannel(channel_id: snowflake, params: Channel.FollowParams): Promise /** * Post a typing indicator for the specified channel. Generally bots should not implement this route. However, if a bot is responding to a command and expects the computation to take a few seconds, this endpoint may be called to let the user know that the bot is processing their message. Returns a 204 empty response on success. Fires a Typing Start Gateway event. * @see https://discord.com/developers/docs/resources/channel#trigger-typing-indicator */ - triggerTypingIndicator(channel_id: string): Promise + triggerTypingIndicator(channel_id: snowflake): Promise /** * Adds a recipient to a Group DM using their access token. * @see https://discord.com/developers/docs/resources/channel#group-dm-add-recipient diff --git a/plugins/adapter/discord/src/types/guild-template.ts b/plugins/adapter/discord/src/types/guild-template.ts index 00bb9f678f..06b80159d7 100644 --- a/plugins/adapter/discord/src/types/guild-template.ts +++ b/plugins/adapter/discord/src/types/guild-template.ts @@ -26,6 +26,72 @@ export interface GuildTemplate { is_dirty?: boolean } +export namespace GuildTemplate { + /** https://discord.com/developers/docs/resources/guild-template#create-guild-from-guild-template-json-params */ + export interface CreateGuildParams { + /** name of the guild (2-100 characters) */ + name: string + /** base64 128x128 image for the guild icon */ + icon?: string + } + + /** https://discord.com/developers/docs/resources/guild-template#create-guild-template-json-params */ + export interface CreateParams { + /** name of the template (1-100 characters) */ + name: string + /** description for the template (0-120 characters) */ + description?: string + } + + /** https://discord.com/developers/docs/resources/guild-template#modify-guild-template-json-params */ + export interface ModifyParams { + /** name of the template (1-100 characters) */ + name?: string + /** description for the template (0-120 characters) */ + description?: string + } +} + +declare module './internal' { + interface Internal { + /** + * Returns a guild template object for the given code. + * @see https://discord.com/developers/docs/resources/guild-template#get-guild-template + */ + getGuildTemplate(code: string): Promise + /** + * Create a new guild based on a template. Returns a guild object on success. Fires a Guild Create Gateway event. + * @see https://discord.com/developers/docs/resources/guild-template#create-guild-from-guild-template + */ + createGuildfromGuildTemplate(code: string, params: GuildTemplate.CreateGuildParams): Promise + /** + * Returns an array of guild template objects. Requires the MANAGE_GUILD permission. + * @see https://discord.com/developers/docs/resources/guild-template#get-guild-templates + */ + getGuildTemplates(guild_id: snowflake): Promise + /** + * Creates a template for the guild. Requires the MANAGE_GUILD permission. Returns the created guild template object on success. + * @see https://discord.com/developers/docs/resources/guild-template#create-guild-template + */ + createGuildTemplate(guild_id: snowflake, params: GuildTemplate.CreateParams): Promise + /** + * Syncs the template to the guild's current state. Requires the MANAGE_GUILD permission. Returns the guild template object on success. + * @see https://discord.com/developers/docs/resources/guild-template#sync-guild-template + */ + syncGuildTemplate(guild_id: snowflake, code: string): Promise + /** + * Modifies the template's metadata. Requires the MANAGE_GUILD permission. Returns the guild template object on success. + * @see https://discord.com/developers/docs/resources/guild-template#modify-guild-template + */ + modifyGuildTemplate(guild_id: snowflake, code: string, params: GuildTemplate.ModifyParams): Promise + /** + * Deletes the template. Requires the MANAGE_GUILD permission. Returns the deleted guild template object on success. + * @see https://discord.com/developers/docs/resources/guild-template#delete-guild-template + */ + deleteGuildTemplate(guild_id: snowflake, code: string): Promise + } +} + Internal.define({ '/guilds/templates/{template.code}': { GET: 'getGuildTemplate', diff --git a/plugins/adapter/discord/src/types/message.ts b/plugins/adapter/discord/src/types/message.ts index 08ea0885e6..4df5435423 100644 --- a/plugins/adapter/discord/src/types/message.ts +++ b/plugins/adapter/discord/src/types/message.ts @@ -1,4 +1,4 @@ -import { AllowedMentions, Application, Channel, Component, GuildMember, integer, Internal, MessageInteraction, Reaction, snowflake, Sticker, StickerItem, timestamp, User } from '.' +import { AllowedMentions, Application, Channel, Component, GuildMember, integer, Internal, MessageInteraction, Reaction, snowflake, Sticker, timestamp, User } from '.' /** https://discord.com/developers/docs/resources/channel#message-object-message-structure */ export interface Message { @@ -61,9 +61,7 @@ export interface Message { /** sent if the message contains components like buttons, action rows, or other interactive components */ components?: Component[] /** sent if the message contains stickers */ - sticker_items?: StickerItem[] - /** Deprecated the stickers sent with the message */ - stickers?: Sticker[] + sticker_items?: Sticker.Item[] } export namespace Message { diff --git a/plugins/adapter/discord/src/types/stage-instance.ts b/plugins/adapter/discord/src/types/stage-instance.ts index 4d103af4f1..aa25f5a19e 100644 --- a/plugins/adapter/discord/src/types/stage-instance.ts +++ b/plugins/adapter/discord/src/types/stage-instance.ts @@ -16,20 +16,69 @@ export interface StageInstance { discoverable_disabled: boolean } -export interface StageInstanceCreateEvent extends StageInstance {} +export namespace StageInstance { + export namespace Event { + export interface Create extends StageInstance {} -export interface StageInstanceDeleteEvent extends StageInstance {} + export interface Delete extends StageInstance {} -export interface StageInstanceUpdateEvent extends StageInstance {} + export interface Update extends StageInstance {} + } + + export namespace Params { + /** https://discord.com/developers/docs/resources/stage-instance#create-stage-instance-json-params */ + export interface Create { + /** The id of the Stage channel */ + channel_id: snowflake + /** The topic of the Stage instance (1-120 characters) */ + topic: string + /** The privacy level of the Stage instance (default GUILD_ONLY) */ + privacy_level?: integer + } + + /** https://discord.com/developers/docs/resources/stage-instance#modify-stage-instance-json-params */ + export interface Modify { + /** The topic of the Stage instance (1-120 characters) */ + topic?: string + /** The privacy level of the Stage instance */ + privacy_level?: integer + } + } +} declare module './gateway' { interface GatewayEvents { /** stage instance was created */ - STAGE_INSTANCE_CREATE: StageInstanceCreateEvent + STAGE_INSTANCE_CREATE: StageInstance.Event.Create /** stage instance was deleted or closed */ - STAGE_INSTANCE_DELETE: StageInstanceDeleteEvent + STAGE_INSTANCE_DELETE: StageInstance.Event.Delete /** stage instance was updated */ - STAGE_INSTANCE_UPDATE: StageInstanceUpdateEvent + STAGE_INSTANCE_UPDATE: StageInstance.Event.Update + } +} + +declare module './internal' { + interface Internal { + /** + * Creates a new Stage instance associated to a Stage channel. + * @see https://discord.com/developers/docs/resources/stage-instance#create-stage-instance + */ + createStageInstance(params: StageInstance.Params.Create): Promise + /** + * Gets the stage instance associated with the Stage channel, if it exists. + * @see https://discord.com/developers/docs/resources/stage-instance#get-stage-instance + */ + getStageInstance(channel_id: snowflake): Promise + /** + * Updates fields of an existing Stage instance. + * @see https://discord.com/developers/docs/resources/stage-instance#modify-stage-instance + */ + modifyStageInstance(channel_id: snowflake, params: StageInstance.Params.Modify): Promise + /** + * Deletes the Stage instance. + * @see https://discord.com/developers/docs/resources/stage-instance#delete-stage-instance + */ + deleteStageInstance(channel_id: snowflake): Promise } } diff --git a/plugins/adapter/discord/src/types/sticker.ts b/plugins/adapter/discord/src/types/sticker.ts index 3439d3923f..acc3d12d14 100644 --- a/plugins/adapter/discord/src/types/sticker.ts +++ b/plugins/adapter/discord/src/types/sticker.ts @@ -15,9 +15,9 @@ export interface Sticker { /** Deprecated previously the sticker asset hash, now an empty string */ asset: string /** type of sticker */ - type: integer + type: Sticker.Type /** type of sticker format */ - format_type: integer + format_type: Sticker.FormatType /** whether this guild sticker can be used, may be false due to loss of Server Boosts */ available?: boolean /** id of the guild that owns this sticker */ @@ -28,61 +28,134 @@ export interface Sticker { sort_value?: integer } -/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types */ -export enum StickerType { - /** an official sticker in a pack, part of Nitro or in a removed purchasable pack */ - STANDARD = 1, - /** a sticker uploaded to a Boosted guild for the guild's members */ - GUILD = 2, -} +export namespace Sticker { + /** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types */ + export enum Type { + /** an official sticker in a pack, part of Nitro or in a removed purchasable pack */ + STANDARD = 1, + /** a sticker uploaded to a Boosted guild for the guild's members */ + GUILD = 2, + } -/** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types */ -export enum StickerFormatType { - PNG = 1, - APNG = 2, - LOTTIE = 3, -} + /** https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types */ + export enum FormatType { + PNG = 1, + APNG = 2, + LOTTIE = 3, + } -/** https://discord.com/developers/docs/resources/sticker#sticker-item-object-sticker-item-structure */ -export interface StickerItem { - /** id of the sticker */ - id: snowflake - /** name of the sticker */ - name: string - /** type of sticker format */ - format_type: integer -} + /** https://discord.com/developers/docs/resources/sticker#sticker-item-object-sticker-item-structure */ + export interface Item { + /** id of the sticker */ + id: snowflake + /** name of the sticker */ + name: string + /** type of sticker format */ + format_type: FormatType + } -/** https://discord.com/developers/docs/resources/sticker#sticker-pack-object-sticker-pack-structure */ -export interface StickerPack { - /** id of the sticker pack */ - id: snowflake - /** the stickers in the pack */ - stickers: Sticker[] - /** name of the sticker pack */ - name: string - /** id of the pack's SKU */ - sku_id: snowflake - /** id of a sticker in the pack which is shown as the pack's icon */ - cover_sticker_id?: snowflake - /** description of the sticker pack */ - description: string - /** id of the sticker pack's banner image */ - banner_asset_id: snowflake -} + /** https://discord.com/developers/docs/resources/sticker#sticker-pack-object-sticker-pack-structure */ + export interface Pack { + /** id of the sticker pack */ + id: snowflake + /** the stickers in the pack */ + stickers: Sticker[] + /** name of the sticker pack */ + name: string + /** id of the pack's SKU */ + sku_id: snowflake + /** id of a sticker in the pack which is shown as the pack's icon */ + cover_sticker_id?: snowflake + /** description of the sticker pack */ + description: string + /** id of the sticker pack's banner image */ + banner_asset_id: snowflake + } + + export namespace Event { + /** https://discord.com/developers/docs/topics/gateway#guild-stickers-update-guild-stickers-update-event-fields */ + export interface Update { + /** id of the guild */ + guild_id: snowflake + /** array of stickers */ + stickers: Sticker[] + } + } + + /** https://discord.com/developers/docs/resources/sticker#list-nitro-sticker-packs-response-structure */ + export interface PackResult { + sticker_packs: Pack[] + } + + export namespace Params { + /** https://discord.com/developers/docs/resources/sticker#create-guild-sticker-form-params */ + export interface Create { + /** name of the sticker (2-30 characters) */ + name: string + /** description of the sticker (empty or 2-100 characters) */ + description: string + /** autocomplete/suggestion tags for the sticker (max 200 characters) */ + tags: string + /** the sticker file to upload, must be a PNG, APNG, or Lottie JSON file, max 500 KB */ + file: any + } -/** https://discord.com/developers/docs/topics/gateway#guild-stickers-update-guild-stickers-update-event-fields */ -export interface GuildStickersUpdateEvent { - /** id of the guild */ - guild_id: snowflake - /** array of stickers */ - stickers: Sticker[] + /** https://discord.com/developers/docs/resources/sticker#modify-guild-sticker-json-params */ + export interface Modify { + /** name of the sticker (2-30 characters) */ + name: string + /** description of the sticker (2-100 characters) */ + description?: string + /** autocomplete/suggestion tags for the sticker (max 200 characters) */ + tags: string + } + } } declare module './gateway' { interface GatewayEvents { /** guild stickers were updated */ - GUILD_STICKERS_UPDATE: GuildStickersUpdateEvent + GUILD_STICKERS_UPDATE: Sticker.Event.Update + } +} + +declare module './internal' { + interface Internal { + /** + * Returns a sticker object for the given sticker ID. + * @see https://discord.com/developers/docs/resources/sticker#get-sticker + */ + getSticker(sticker_id: snowflake): Promise + /** + * Returns the list of sticker packs available to Nitro subscribers. + * @see https://discord.com/developers/docs/resources/sticker#list-nitro-sticker-packs + */ + listNitroStickerPacks(): Promise + /** + * Returns an array of sticker objects for the given guild. Includes user fields if the bot has the MANAGE_EMOJIS_AND_STICKERS permission. + * @see https://discord.com/developers/docs/resources/sticker#list-guild-stickers + */ + listGuildStickers(guild_id: snowflake): Promise + /** + * Returns a sticker object for the given guild and sticker IDs. Includes the user field if the bot has the MANAGE_EMOJIS_AND_STICKERS permission. + * @see https://discord.com/developers/docs/resources/sticker#get-guild-sticker + */ + getGuildSticker(guild_id: snowflake, sticker_id: snowflake): Promise + /** + * Create a new sticker for the guild. Send a multipart/form-data body. Requires the MANAGE_EMOJIS_AND_STICKERS permission. Returns the new sticker object on success. + * @see https://discord.com/developers/docs/resources/sticker#create-guild-sticker + */ + createGuildSticker(guild_id: snowflake, params: Sticker.Params.Create): Promise + /** + * Modify the given sticker. Requires the MANAGE_EMOJIS_AND_STICKERS permission. Returns the updated sticker object on success. + * @see https://discord.com/developers/docs/resources/sticker#modify-guild-sticker + */ + modifyGuildSticker(guild_id: snowflake, sticker_id: snowflake, params: Sticker.Params.Modify): Promise + /** + * Delete the given sticker. Requires the MANAGE_EMOJIS_AND_STICKERS permission. Returns 204 No Content on success. + * @see https://discord.com/developers/docs/resources/sticker#delete-guild-sticker + */ + deleteGuildSticker(guild_id: snowflake, sticker_id: snowflake): Promise } } diff --git a/plugins/adapter/discord/src/types/voice.ts b/plugins/adapter/discord/src/types/voice.ts index c79e186cff..5a3c83fe3e 100644 --- a/plugins/adapter/discord/src/types/voice.ts +++ b/plugins/adapter/discord/src/types/voice.ts @@ -67,7 +67,10 @@ declare module './gateway' { declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/voice#list-voice-regions */ + /** + * Returns an array of voice region objects that can be used when setting a voice or stage channel's rtc_region. + * @see https://discord.com/developers/docs/resources/voice#list-voice-regions + */ listVoiceRegions(): Promise } } From d945769bde4f2b8978689f097decf0000fb70992 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 01:51:56 +0800 Subject: [PATCH 14/34] feat(discord): enhance typings for guild & user --- plugins/adapter/discord/src/types/ban.ts | 84 +++++ plugins/adapter/discord/src/types/channel.ts | 240 +++++++++----- plugins/adapter/discord/src/types/emoji.ts | 2 +- .../adapter/discord/src/types/guild-member.ts | 266 +++++++++++----- plugins/adapter/discord/src/types/guild.ts | 294 ++++++++++++------ plugins/adapter/discord/src/types/index.ts | 1 + .../adapter/discord/src/types/integration.ts | 26 +- plugins/adapter/discord/src/types/internal.ts | 35 ++- plugins/adapter/discord/src/types/invite.ts | 16 + plugins/adapter/discord/src/types/role.ts | 92 +++++- plugins/adapter/discord/src/types/thread.ts | 8 + plugins/adapter/discord/src/types/user.ts | 45 ++- plugins/adapter/discord/src/types/voice.ts | 42 +++ 13 files changed, 869 insertions(+), 282 deletions(-) create mode 100644 plugins/adapter/discord/src/types/ban.ts diff --git a/plugins/adapter/discord/src/types/ban.ts b/plugins/adapter/discord/src/types/ban.ts new file mode 100644 index 0000000000..5f9bed6961 --- /dev/null +++ b/plugins/adapter/discord/src/types/ban.ts @@ -0,0 +1,84 @@ +import { integer, Internal, snowflake, User } from '.' + +/** https://discord.com/developers/docs/resources/guild#ban-object-ban-structure */ +export interface Ban { + /** the reason for the ban */ + reason?: string + /** the banned user */ + user: User +} + +export namespace Ban { + export namespace Event { + /** https://discord.com/developers/docs/topics/gateway#guild-ban-add-guild-ban-add-event-fields */ + export interface Add { + /** id of the guild */ + guild_id: snowflake + /** the banned user */ + user: User + } + + /** https://discord.com/developers/docs/topics/gateway#guild-ban-remove-guild-ban-remove-event-fields */ + export interface Remove { + /** id of the guild */ + guild_id: snowflake + /** the unbanned user */ + user: User + } + } + + export namespace Params { + /** https://discord.com/developers/docs/resources/guild#create-guild-ban-json-params */ + export interface Create { + /** number of days to delete messages for (0-7) */ + delete_message_days?: integer + /** reason for the ban (deprecated) */ + reason?: string + } + } +} + +declare module './gateway' { + interface GatewayEvents { + /** user was banned from a guild */ + GUILD_BAN_ADD: Ban.Event.Add + /** user was unbanned from a guild */ + GUILD_BAN_REMOVE: Ban.Event.Remove + } +} + +declare module './internal' { + interface Internal { + /** + * Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. + * @see https://discord.com/developers/docs/resources/guild#get-guild-bans + */ + getGuildBans(guild_id: snowflake): Promise + /** + * Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. + * @see https://discord.com/developers/docs/resources/guild#get-guild-ban + */ + getGuildBan(guild_id: snowflake, user_id: snowflake): Promise + /** + * Create a guild ban, and optionally delete previous messages sent by the banned user. Requires the BAN_MEMBERS permission. Returns a 204 empty response on success. Fires a Guild Ban Add Gateway event. + * @see https://discord.com/developers/docs/resources/guild#create-guild-ban + */ + createGuildBan(guild_id: snowflake, user_id: snowflake, params: Ban.Params.Create): Promise + /** + * Remove the ban for a user. Requires the BAN_MEMBERS permissions. Returns a 204 empty response on success. Fires a Guild Ban Remove Gateway event. + * @see https://discord.com/developers/docs/resources/guild#remove-guild-ban + */ + removeGuildBan(guild_id: snowflake, user_id: snowflake): Promise + } +} + +Internal.define({ + '/guilds/{guild.id}/bans': { + GET: 'getGuildBans', + }, + '/guilds/{guild.id}/bans/{user.id}': { + GET: 'getGuildBan', + PUT: 'createGuildBan', + DELETE: 'removeGuildBan', + }, +}) diff --git a/plugins/adapter/discord/src/types/channel.ts b/plugins/adapter/discord/src/types/channel.ts index 14e457018a..69600275ab 100644 --- a/plugins/adapter/discord/src/types/channel.ts +++ b/plugins/adapter/discord/src/types/channel.ts @@ -73,89 +73,141 @@ export namespace Channel { GUILD_STAGE_VOICE = 13, } - export type ModifyParams = - | ModifyParams.GroupDM - | ModifyParams.GuildChannel - | ModifyParams.Thread + export namespace Params { + /** https://discord.com/developers/docs/resources/user#create-dm-json-params */ + export interface CreateDM { + /** the recipient to open a DM channel with */ + recipient_id: snowflake + } - export namespace ModifyParams { - /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-group-dm */ - export interface GroupDM { - /** 1-100 character channel name */ - name: string - /** base64 encoded icon */ - icon: string + /** https://discord.com/developers/docs/resources/user#create-group-dm-json-params */ + export interface CreateGroupDM { + /** access tokens of users that have granted your app the gdm.join scope */ + access_tokens: string[] + /** a dictionary of user ids to their respective nicknames */ + nicks: Record } - /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel */ - export interface GuildChannel { - /** 1-100 character channel name */ + /** https://discord.com/developers/docs/resources/guild#create-guild-channel-json-params */ + export interface Create { + /** channel name (1-100 characters) */ name: string - /** the type of channel; only conversion between text and news is supported and only in guilds with the "NEWS" feature */ + /** the type of channel */ type: integer - /** the position of the channel in the left-hand listing */ - position?: integer - /** 0-1024 character channel topic */ - topic?: string - /** whether the channel is nsfw */ - nsfw?: boolean + /** channel topic (0-1024 characters) */ + topic: string + /** the bitrate (in bits) of the voice channel (voice only) */ + bitrate: integer + /** the user limit of the voice channel (voice only) */ + user_limit: integer /** amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */ - rate_limit_per_user?: integer - /** the bitrate (in bits) of the voice channel; 8000 to 96000 (128000 for VIP servers) */ - bitrate?: integer - /** the user limit of the voice channel; 0 refers to no limit, 1 to 99 refers to a user limit */ - user_limit?: integer - /** channel or category-specific permissions */ - permission_overwrites?: Overwrite[] - /** id of the new parent category for a channel */ + rate_limit_per_user: integer + /** sorting position of the channel */ + position: integer + /** the channel's permission overwrites */ + permission_overwrites: Overwrite[] + /** id of the parent category for a channel */ + parent_id: snowflake + /** whether the channel is nsfw */ + nsfw: boolean + } + + /** https://discord.com/developers/docs/resources/guild#modify-guild-channel-positions-json-params */ + export interface ModifyPositions { + /** channel id */ + id: snowflake + /** sorting position of the channel */ + position?: integer + /** syncs the permission overwrites with the new parent, if moving to a new category */ + lock_permissions?: boolean + /** the new parent ID for the channel that is moved */ parent_id?: snowflake - /** channel voice region id, automatic when set to null */ - rtc_region?: string - /** the camera video quality mode of the voice channel */ - video_quality_mode?: integer - /** the default duration that the clients use (not the API) for newly created threads in the channel, in minutes, to automatically archive the thread after recent activity */ - default_auto_archive_duration?: integer } - /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-thread */ - export interface Thread { - /** 1-100 character channel name */ - name: string - /** whether the thread is archived */ - archived: boolean - /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ - auto_archive_duration: integer - /** whether the thread is locked; when a thread is locked, only users with MANAGE_THREADS can unarchive it */ - locked: boolean - /** whether non-moderators can add other non-moderators to a thread; only available on private threads */ - invitable: boolean - /** amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages, manage_thread, or manage_channel, are unaffected */ - rate_limit_per_user?: integer + export type Modify = + | Modify.GroupDM + | Modify.GuildChannel + | Modify.Thread + + export namespace Modify { + /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-group-dm */ + export interface GroupDM { + /** 1-100 character channel name */ + name: string + /** base64 encoded icon */ + icon: string + } + + /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel */ + export interface GuildChannel { + /** 1-100 character channel name */ + name: string + /** the type of channel; only conversion between text and news is supported and only in guilds with the "NEWS" feature */ + type: integer + /** the position of the channel in the left-hand listing */ + position?: integer + /** 0-1024 character channel topic */ + topic?: string + /** whether the channel is nsfw */ + nsfw?: boolean + /** amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages or manage_channel, are unaffected */ + rate_limit_per_user?: integer + /** the bitrate (in bits) of the voice channel; 8000 to 96000 (128000 for VIP servers) */ + bitrate?: integer + /** the user limit of the voice channel; 0 refers to no limit, 1 to 99 refers to a user limit */ + user_limit?: integer + /** channel or category-specific permissions */ + permission_overwrites?: Overwrite[] + /** id of the new parent category for a channel */ + parent_id?: snowflake + /** channel voice region id, automatic when set to null */ + rtc_region?: string + /** the camera video quality mode of the voice channel */ + video_quality_mode?: integer + /** the default duration that the clients use (not the API) for newly created threads in the channel, in minutes, to automatically archive the thread after recent activity */ + default_auto_archive_duration?: integer + } + + /** https://discord.com/developers/docs/resources/channel#modify-channel-json-params-thread */ + export interface Thread { + /** 1-100 character channel name */ + name: string + /** whether the thread is archived */ + archived: boolean + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + auto_archive_duration: integer + /** whether the thread is locked; when a thread is locked, only users with MANAGE_THREADS can unarchive it */ + locked: boolean + /** whether non-moderators can add other non-moderators to a thread; only available on private threads */ + invitable: boolean + /** amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission manage_messages, manage_thread, or manage_channel, are unaffected */ + rate_limit_per_user?: integer + } } - } - /** https://discord.com/developers/docs/resources/channel#edit-channel-permissions-json-params */ - export interface EditPermissionsParams { - /** the bitwise value of all allowed permissions */ - allow: string - /** the bitwise value of all disallowed permissions */ - deny: string - /** 0 for a role or 1 for a member */ - type: integer - } + /** https://discord.com/developers/docs/resources/channel#edit-channel-permissions-json-params */ + export interface EditPermissions { + /** the bitwise value of all allowed permissions */ + allow: string + /** the bitwise value of all disallowed permissions */ + deny: string + /** 0 for a role or 1 for a member */ + type: integer + } - /** https://discord.com/developers/docs/resources/channel#follow-news-channel-json-params */ - export interface FollowParams { - /** id of target channel */ - webhook_channel_id: snowflake - } + /** https://discord.com/developers/docs/resources/channel#follow-news-channel-json-params */ + export interface Follow { + /** id of target channel */ + webhook_channel_id: snowflake + } - /** https://discord.com/developers/docs/resources/channel#group-dm-add-recipient-json-params */ - export interface AddRecipientParams { - /** access token of a user that has granted your app the gdm.join scope */ - access_token: string - /** nickname of the user being added */ - nick: string + /** https://discord.com/developers/docs/resources/channel#group-dm-add-recipient-json-params */ + export interface AddRecipient { + /** access token of a user that has granted your app the gdm.join scope */ + access_token: string + /** nickname of the user being added */ + nick: string + } } } @@ -259,12 +311,42 @@ export interface ChannelPosition { declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/guild#get-guild-channels */ + /** + * Create a new DM channel with a user. Returns a DM channel object. + * @see https://discord.com/developers/docs/resources/user#create-dm + */ + createDM(params: Channel.Params.CreateDM): Promise + /** + * Create a new group DM channel with multiple users. Returns a DM channel object. This endpoint was intended to be used with the now-deprecated GameBridge SDK. DMs created with this endpoint will not be shown in the Discord client + * @see https://discord.com/developers/docs/resources/user#create-group-dm + */ + createGroupDM(params: Channel.Params.CreateGroupDM): Promise + } +} + +Internal.define({ + '/users/@me/channels': { + POST: ['createDM', 'createGroupDM'], + }, +}) + +declare module './internal' { + interface Internal { + /** + * Returns a list of guild channel objects. Does not include threads. + * @see https://discord.com/developers/docs/resources/guild#get-guild-channels + */ getGuildChannels(guild_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/guild#create-guild-channel */ - createGuildChannel(guild_id: snowflake, options: Partial): Promise - /** https://discord.com/developers/docs/resources/guild#modify-guild-channel-positions */ - modifyGuildChannelPositions(guild_id: snowflake, positions: ChannelPosition[]): Promise + /** + * Create a new channel object for the guild. Requires the MANAGE_CHANNELS permission. If setting permission overwrites, only permissions your bot has in the guild can be allowed/denied. Setting MANAGE_ROLES permission in channels is only possible for guild administrators. Returns the new channel object on success. Fires a Channel Create Gateway event. + * @see https://discord.com/developers/docs/resources/guild#create-guild-channel + */ + createGuildChannel(guild_id: snowflake, params: Channel.Params.Create): Promise + /** + * Modify the positions of a set of channel objects for the guild. Requires MANAGE_CHANNELS permission. Returns a 204 empty response on success. Fires multiple Channel Update Gateway events. + * @see https://discord.com/developers/docs/resources/guild#modify-guild-channel-positions + */ + modifyGuildChannelPositions(guild_id: snowflake, params: Channel.Params.ModifyPositions): Promise } } @@ -287,7 +369,7 @@ declare module './internal' { * Update a channel's settings. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. All JSON parameters are optional. * @see https://discord.com/developers/docs/resources/channel#modify-channel */ - modifyChannel(channel_id: snowflake, params: Channel.ModifyParams): Promise + modifyChannel(channel_id: snowflake, params: Channel.Params.Modify): Promise /** * Delete a channel, or close a private message. Requires the MANAGE_CHANNELS permission for the guild, or MANAGE_THREADS if the channel is a thread. Deleting a category does not delete its child channels; they will have their parent_id removed and a Channel Update Gateway event will fire for each of them. Returns a channel object on success. Fires a Channel Delete Gateway event (or Thread Delete if the channel was a thread). * @see https://discord.com/developers/docs/resources/channel#deleteclose-channel @@ -297,7 +379,7 @@ declare module './internal' { * Edit the channel permission overwrites for a user or role in a channel. Only usable for guild channels. Requires the MANAGE_ROLES permission. Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel). Returns a 204 empty response on success. For more information about permissions, see permissions. * @see https://discord.com/developers/docs/resources/channel#edit-channel-permissions */ - editChannelPermissions(channel_id: snowflake, overwrite_id: string, params: Channel.EditPermissionsParams): Promise + editChannelPermissions(channel_id: snowflake, overwrite_id: string, params: Channel.Params.EditPermissions): Promise /** * Delete a channel permission overwrite for a user or role in a channel. Only usable for guild channels. Requires the MANAGE_ROLES permission. Returns a 204 empty response on success. For more information about permissions, see permissions * @see https://discord.com/developers/docs/resources/channel#delete-channel-permission @@ -307,7 +389,7 @@ declare module './internal' { * Follow a News Channel to send messages to a target channel. Requires the MANAGE_WEBHOOKS permission in the target channel. Returns a followed channel object. * @see https://discord.com/developers/docs/resources/channel#follow-news-channel */ - followNewsChannel(channel_id: snowflake, params: Channel.FollowParams): Promise + followNewsChannel(channel_id: snowflake, params: Channel.Params.Follow): Promise /** * Post a typing indicator for the specified channel. Generally bots should not implement this route. However, if a bot is responding to a command and expects the computation to take a few seconds, this endpoint may be called to let the user know that the bot is processing their message. Returns a 204 empty response on success. Fires a Typing Start Gateway event. * @see https://discord.com/developers/docs/resources/channel#trigger-typing-indicator @@ -317,7 +399,7 @@ declare module './internal' { * Adds a recipient to a Group DM using their access token. * @see https://discord.com/developers/docs/resources/channel#group-dm-add-recipient */ - groupDMAddRecipient(channel_id: snowflake, user_id: snowflake, params: Channel.AddRecipientParams): Promise + groupDMAddRecipient(channel_id: snowflake, user_id: snowflake, params: Channel.Params.AddRecipient): Promise /** * Removes a recipient from a Group DM. * @see https://discord.com/developers/docs/resources/channel#group-dm-remove-recipient diff --git a/plugins/adapter/discord/src/types/emoji.ts b/plugins/adapter/discord/src/types/emoji.ts index 5f5a911aee..557cea68f3 100644 --- a/plugins/adapter/discord/src/types/emoji.ts +++ b/plugins/adapter/discord/src/types/emoji.ts @@ -1,4 +1,4 @@ -import { GuildMember, integer, Internal, snowflake, User } from '.' +import { Internal, snowflake, User } from '.' /** https://discord.com/developers/docs/resources/emoji#emoji-object-emoji-structure */ export interface Emoji { diff --git a/plugins/adapter/discord/src/types/guild-member.ts b/plugins/adapter/discord/src/types/guild-member.ts index 71d8e9bbee..eb4bae7968 100644 --- a/plugins/adapter/discord/src/types/guild-member.ts +++ b/plugins/adapter/discord/src/types/guild-member.ts @@ -24,97 +24,208 @@ export interface GuildMember { permissions?: string } -/** https://discord.com/developers/docs/topics/gateway#guild-member-add-guild-member-add-extra-fields */ -export interface GuildMemberAddEvent extends GuildMember { - /** id of the guild */ - guild_id: snowflake -} +export namespace GuildMember { + export namespace Params { + /** https://discord.com/developers/docs/resources/guild#list-guild-members-query-string-params */ + export interface List { + /** max number of members to return (1-1000) */ + limit: integer + /** the highest user id in the previous page */ + after: snowflake + } -/** https://discord.com/developers/docs/topics/gateway#guild-member-remove-guild-member-remove-event-fields */ -export interface GuildMemberRemoveEvent { - /** the id of the guild */ - guild_id: snowflake - /** the user who was removed */ - user: User -} + /** https://discord.com/developers/docs/resources/guild#search-guild-members-query-string-params */ + export interface Search { + /** Query string to match username(s) and nickname(s) against. */ + query: string + /** max number of members to return (1-1000) */ + limit: integer + } -/** https://discord.com/developers/docs/topics/gateway#guild-member-update-guild-member-update-event-fields */ -export interface GuildMemberUpdateEvent { - /** the id of the guild */ - guild_id: snowflake - /** user role ids */ - roles: snowflake[] - /** the user */ - user: User - /** nickname of the user in the guild */ - nick?: string - /** the member's guild avatar hash */ - avatar?: string - /** when the user joined the guild */ - joined_at?: timestamp - /** when the user starting boosting the guild */ - premium_since?: timestamp - /** whether the user is deafened in voice channels */ - deaf?: boolean - /** whether the user is muted in voice channels */ - mute?: boolean - /** whether the user has not yet passed the guild's Membership Screening requirements */ - pending?: boolean -} + /** https://discord.com/developers/docs/resources/guild#add-guild-member-json-params */ + export interface Add { + /** an oauth2 access token granted with the guilds.join to the bot's application for the user you want to add to the guild */ + access_token: string + /** value to set user's nickname to */ + nick: string + /** array of role ids the member is assigned */ + roles: snowflake[] + /** whether the user is muted in voice channels */ + mute: boolean + /** whether the user is deafened in voice channels */ + deaf: boolean + } + + /** https://discord.com/developers/docs/resources/guild#modify-guild-member-json-params */ + export interface Modify { + /** value to set user's nickname to */ + nick: string + /** array of role ids the member is assigned */ + roles: snowflake[] + /** whether the user is muted in voice channels. Will throw a 400 if the user is not in a voice channel */ + mute: boolean + /** whether the user is deafened in voice channels. Will throw a 400 if the user is not in a voice channel */ + deaf: boolean + /** id of channel to move user to (if they are connected to voice) */ + channel_id: snowflake + /** when the user's timeout will expire and the user will be able to communicate in the guild again (up to 28 days in the future), set to null to remove timeout */ + communication_disabled_until?: timestamp + } -/** https://discord.com/developers/docs/topics/gateway#guild-members-chunk-guild-members-chunk-event-fields */ -export interface GuildMembersChunkEvent { - /** the id of the guild */ - guild_id: snowflake - /** set of guild members */ - members: GuildMember[] - /** the chunk index in the expected chunks for this response (0 <= chunk_index < chunk_count) */ - chunk_index: integer - /** the total number of expected chunks for this response */ - chunk_count: integer - /** if passing an invalid id to REQUEST_GUILD_MEMBERS, it will be returned here */ - not_found?: snowflake[] - /** if passing true to REQUEST_GUILD_MEMBERS, presences of the returned members will be here */ - presences?: PresenceUpdateParams[] - /** the nonce used in the Guild Members Request */ - nonce?: string + /** https://discord.com/developers/docs/resources/guild#modify-current-member-json-params */ + export interface ModifyCurrent { + /** value to set user's nickname to */ + nick?: string + } + + /** https://discord.com/developers/docs/resources/guild#get-guild-prune-count-query-string-params */ + export interface GetPruneCount { + /** number of days to count prune for (1-30) */ + days: integer + /** role(s) to include */ + include_roles: string + } + + /** https://discord.com/developers/docs/resources/guild#begin-guild-prune-json-params */ + export interface BeginPrune { + /** number of days to prune (1-30) */ + days: integer + /** whether 'pruned' is returned, discouraged for large guilds */ + compute_prune_count: boolean + /** role(s) to include */ + include_roles: snowflake[] + } + } + + export namespace Event { + /** https://discord.com/developers/docs/topics/gateway#guild-member-add-guild-member-add-extra-fields */ + export interface Add extends GuildMember { + /** id of the guild */ + guild_id: snowflake + } + + /** https://discord.com/developers/docs/topics/gateway#guild-member-remove-guild-member-remove-event-fields */ + export interface Remove { + /** the id of the guild */ + guild_id: snowflake + /** the user who was removed */ + user: User + } + + /** https://discord.com/developers/docs/topics/gateway#guild-member-update-guild-member-update-event-fields */ + export interface Update { + /** the id of the guild */ + guild_id: snowflake + /** user role ids */ + roles: snowflake[] + /** the user */ + user: User + /** nickname of the user in the guild */ + nick?: string + /** the member's guild avatar hash */ + avatar?: string + /** when the user joined the guild */ + joined_at?: timestamp + /** when the user starting boosting the guild */ + premium_since?: timestamp + /** whether the user is deafened in voice channels */ + deaf?: boolean + /** whether the user is muted in voice channels */ + mute?: boolean + /** whether the user has not yet passed the guild's Membership Screening requirements */ + pending?: boolean + } + + /** https://discord.com/developers/docs/topics/gateway#guild-members-chunk-guild-members-chunk-event-fields */ + export interface Chunk { + /** the id of the guild */ + guild_id: snowflake + /** set of guild members */ + members: GuildMember[] + /** the chunk index in the expected chunks for this response (0 <= chunk_index < chunk_count) */ + chunk_index: integer + /** the total number of expected chunks for this response */ + chunk_count: integer + /** if passing an invalid id to REQUEST_GUILD_MEMBERS, it will be returned here */ + not_found?: snowflake[] + /** if passing true to REQUEST_GUILD_MEMBERS, presences of the returned members will be here */ + presences?: PresenceUpdateParams[] + /** the nonce used in the Guild Members Request */ + nonce?: string + } + } } declare module './gateway' { interface GatewayEvents { /** new user joined a guild */ - GUILD_MEMBER_ADD: GuildMemberAddEvent + GUILD_MEMBER_ADD: GuildMember.Event.Add /** user was removed from a guild */ - GUILD_MEMBER_REMOVE: GuildMemberRemoveEvent + GUILD_MEMBER_REMOVE: GuildMember.Event.Remove /** guild member was updated */ - GUILD_MEMBER_UPDATE: GuildMemberUpdateEvent + GUILD_MEMBER_UPDATE: GuildMember.Event.Update /** response to Request Guild Members */ - GUILD_MEMBERS_CHUNK: GuildMembersChunkEvent + GUILD_MEMBERS_CHUNK: GuildMember.Event.Chunk } } -export interface ListGuildMembersOptions { - /** max number of members to return (1-1000) */ - limit?: integer - /** the highest user id in the previous page */ - after?: snowflake -} - -export interface SearchGuildMembersOptions { - /** query string to match username(s) and nickname(s) against */ - query: string - /** max number of members to return (1-1000) */ - limit?: integer -} - declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/guild#get-guild-member */ + /** + * Returns a guild member object for the specified user. + * @see https://discord.com/developers/docs/resources/guild#get-guild-member + */ getGuildMember(guild_id: snowflake, user_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/guild#list-guild-members */ - listGuildMembers(guild_id: snowflake, options?: ListGuildMembersOptions): Promise - /** https://discord.com/developers/docs/resources/guild#search-guild-members */ - searchGuildMembers(guild_id: snowflake, options: SearchGuildMembersOptions): Promise + /** + * Returns a list of guild member objects that are members of the guild. + * @see https://discord.com/developers/docs/resources/guild#list-guild-members + */ + listGuildMembers(guild_id: snowflake, params?: GuildMember.Params.List): Promise + /** + * Returns a list of guild member objects whose username or nickname starts with a provided string. + * @see https://discord.com/developers/docs/resources/guild#search-guild-members + */ + searchGuildMembers(guild_id: snowflake, params?: GuildMember.Params.Search): Promise + /** + * Adds a user to the guild, provided you have a valid oauth2 access token for the user with the guilds.join scope. Returns a 201 Created with the guild member as the body, or 204 No Content if the user is already a member of the guild. Fires a Guild Member Add Gateway event. + * @see https://discord.com/developers/docs/resources/guild#add-guild-member + */ + addGuildMember(guild_id: snowflake, user_id: snowflake, params: GuildMember.Params.Add): Promise + /** + * Modify attributes of a guild member. Returns a 200 OK with the guild member as the body. Fires a Guild Member Update Gateway event. If the channel_id is set to null, this will force the target user to be disconnected from voice. + * @see https://discord.com/developers/docs/resources/guild#modify-guild-member + */ + modifyGuildMember(guild_id: snowflake, user_id: snowflake, params: GuildMember.Params.Modify): Promise + /** + * Modifies the current member in a guild. Returns a 200 with the updated member object on success. Fires a Guild Member Update Gateway event. + * @see https://discord.com/developers/docs/resources/guild#modify-current-member + */ + modifyCurrentMember(guild_id: snowflake, params: GuildMember.Params.ModifyCurrent): Promise + /** + * Adds a role to a guild member. Requires the MANAGE_ROLES permission. Returns a 204 empty response on success. Fires a Guild Member Update Gateway event. + * @see https://discord.com/developers/docs/resources/guild#add-guild-member-role + */ + addGuildMemberRole(guild_id: snowflake, user_id: snowflake, role_id: snowflake): Promise + /** + * Removes a role from a guild member. Requires the MANAGE_ROLES permission. Returns a 204 empty response on success. Fires a Guild Member Update Gateway event. + * @see https://discord.com/developers/docs/resources/guild#remove-guild-member-role + */ + removeGuildMemberRole(guild_id: snowflake, user_id: snowflake, role_id: snowflake): Promise + /** + * Remove a member from a guild. Requires KICK_MEMBERS permission. Returns a 204 empty response on success. Fires a Guild Member Remove Gateway event. + * @see https://discord.com/developers/docs/resources/guild#remove-guild-member + */ + removeGuildMember(guild_id: snowflake, user_id: snowflake): Promise + /** + * Returns an object with one 'pruned' key indicating the number of members that would be removed in a prune operation. Requires the KICK_MEMBERS permission. + * @see https://discord.com/developers/docs/resources/guild#get-guild-prune-count + */ + getGuildPruneCount(guild_id: snowflake, params?: GuildMember.Params.GetPruneCount): Promise + /** + * Begin a prune operation. Requires the KICK_MEMBERS permission. Returns an object with one 'pruned' key indicating the number of members that were removed in the prune operation. For large guilds it's recommended to set the compute_prune_count option to false, forcing 'pruned' to null. Fires multiple Guild Member Remove Gateway events. + * @see https://discord.com/developers/docs/resources/guild#begin-guild-prune + */ + beginGuildPrune(guild_id: snowflake, params: GuildMember.Params.BeginPrune): Promise } } @@ -134,11 +245,12 @@ Internal.define({ '/guilds/{guild.id}/members/@me': { PATCH: 'modifyCurrentMember', }, - '/guilds/{guild.id}/members/@me/nick': { - PATCH: 'modifyCurrentUserNick', - }, '/guilds/{guild.id}/members/{user.id}/roles/{role.id}': { PUT: 'addGuildMemberRole', DELETE: 'removeGuildMemberRole', }, + '/guilds/{guild.id}/prune': { + GET: 'getGuildPruneCount', + POST: 'beginGuildPrune', + }, }) diff --git a/plugins/adapter/discord/src/types/guild.ts b/plugins/adapter/discord/src/types/guild.ts index 13559fa645..6d3c8efef2 100644 --- a/plugins/adapter/discord/src/types/guild.ts +++ b/plugins/adapter/discord/src/types/guild.ts @@ -104,6 +104,136 @@ export interface Guild { stickers?: Sticker[] } +export namespace Guild { + export namespace Event { + export interface Create extends Guild {} + + export interface Update extends Guild {} + + export interface Delete extends Guild {} + } + + export namespace Params { + /** https://discord.com/developers/docs/resources/user#get-current-user-guilds-query-string-params */ + export interface List { + /** get guilds before this guild ID */ + before: snowflake + /** get guilds after this guild ID */ + after: snowflake + /** max number of guilds to return (1-200) */ + limit: integer + } + + /** https://discord.com/developers/docs/resources/guild#create-guild-json-params */ + export interface Create { + /** name of the guild (2-100 characters) */ + name: string + /** voice region id (deprecated) */ + region?: string + /** base64 128x128 image for the guild icon */ + icon?: string + /** verification level */ + verification_level?: integer + /** default message notification level */ + default_message_notifications?: integer + /** explicit content filter level */ + explicit_content_filter?: integer + /** new guild roles */ + roles?: Role[] + /** new guild's channels */ + channels?: Partial[] + /** id for afk channel */ + afk_channel_id?: snowflake + /** afk timeout in seconds */ + afk_timeout?: integer + /** the id of the channel where guild notices such as welcome messages and boost events are posted */ + system_channel_id?: snowflake + /** system channel flags */ + system_channel_flags?: integer + } + + /** https://discord.com/developers/docs/resources/guild#get-guild-query-string-params */ + export interface Get { + /** when true, will return approximate member and presence counts for the guild */ + with_counts?: boolean + } + + /** https://discord.com/developers/docs/resources/guild#modify-guild-json-params */ + export interface Modify { + /** guild name */ + name: string + /** guild voice region id (deprecated) */ + region?: string + /** verification level */ + verification_level?: integer + /** default message notification level */ + default_message_notifications?: integer + /** explicit content filter level */ + explicit_content_filter?: integer + /** id for afk channel */ + afk_channel_id?: snowflake + /** afk timeout in seconds */ + afk_timeout: integer + /** base64 1024x1024 png/jpeg/gif image for the guild icon (can be animated gif when the server has the ANIMATED_ICON feature) */ + icon?: string + /** user id to transfer guild ownership to (must be owner) */ + owner_id: snowflake + /** base64 16:9 png/jpeg image for the guild splash (when the server has the INVITE_SPLASH feature) */ + splash?: string + /** base64 16:9 png/jpeg image for the guild discovery splash (when the server has the DISCOVERABLE feature) */ + discovery_splash?: string + /** base64 16:9 png/jpeg image for the guild banner (when the server has the BANNER feature) */ + banner?: string + /** the id of the channel where guild notices such as welcome messages and boost events are posted */ + system_channel_id?: snowflake + /** system channel flags */ + system_channel_flags: integer + /** the id of the channel where Community guilds display rules and/or guidelines */ + rules_channel_id?: snowflake + /** the id of the channel where admins and moderators of Community guilds receive notices from Discord */ + public_updates_channel_id?: snowflake + /** the preferred locale of a Community guild used in server discovery and notices from Discord; defaults to "en-US" */ + preferred_locale?: string + /** enabled guild features */ + features: GuildFeature[] + /** the description for the guild, if the guild is discoverable */ + description?: string + /** whether the guild's boost progress bar should be enabled. */ + premium_progress_bar_enabled: boolean + } + + /** https://discord.com/developers/docs/resources/guild#get-guild-widget-image-query-string-params */ + export interface GetWidgetImage { + /** style of the widget image returned (see below) */ + style: WidgetStyleOptions + } + + /** https://discord.com/developers/docs/resources/guild#get-guild-widget-image-widget-style-options */ + export enum WidgetStyleOptions { + /** shield style widget with Discord icon and guild members online count */ + shield = 'shield', + /** large image with guild icon, name and online count. "POWERED BY DISCORD" as the footer of the widget */ + banner1 = 'banner1', + /** smaller widget style with guild icon, name and online count. Split on the right with Discord logo */ + banner2 = 'banner2', + /** large image with guild icon, name and online count. In the footer, Discord logo on the left and "Chat Now" on the right */ + banner3 = 'banner3', + /** large Discord logo at the top of the widget. Guild icon, name and online count in the middle portion of the widget and a "JOIN MY SERVER" button at the bottom */ + banner4 = 'banner4', + } + + /** https://discord.com/developers/docs/resources/guild#modify-guild-welcome-screen-json-params */ + export interface ModifyWelcomeScreen { + /** whether the welcome screen is enabled */ + enabled: boolean + /** channels linked in the welcome screen and their display options */ + welcome_channels: WelcomeScreenChannel[] + /** the server description to show in the welcome screen */ + description: string + } + } +} + /** https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags */ export enum SystemChannelFlag { /** Suppress member join notifications */ @@ -194,14 +324,6 @@ export interface GuildWidget { channel_id?: snowflake } -/** https://discord.com/developers/docs/resources/guild#ban-object-ban-structure */ -export interface Ban { - /** the reason for the ban */ - reason?: string - /** the banned user */ - user: User -} - /** https://discord.com/developers/docs/resources/guild#welcome-screen-object-welcome-screen-structure */ export interface WelcomeScreen { /** the server description shown in the welcome screen */ @@ -222,48 +344,33 @@ export interface WelcomeScreenChannel { emoji_name?: string } -export interface GuildCreateEvent extends Guild {} - -export interface GuildUpdateEvent extends Guild {} - -export interface GuildDeleteEvent extends Guild {} - -/** https://discord.com/developers/docs/topics/gateway#guild-ban-add-guild-ban-add-event-fields */ -export interface GuildBanAddEvent { - /** id of the guild */ - guild_id: snowflake - /** the banned user */ - user: User -} - -/** https://discord.com/developers/docs/topics/gateway#guild-ban-remove-guild-ban-remove-event-fields */ -export interface GuildBanRemoveEvent { - /** id of the guild */ - guild_id: snowflake - /** the unbanned user */ - user: User -} - declare module './gateway' { interface GatewayEvents { /** lazy-load for unavailable guild, guild became available, or user joined a new guild */ - GUILD_CREATE: GuildCreateEvent + GUILD_CREATE: Guild.Event.Create /** guild was updated */ - GUILD_UPDATE: GuildUpdateEvent + GUILD_UPDATE: Guild.Event.Update /** guild became unavailable, or user left/was removed from a guild */ - GUILD_DELETE: GuildDeleteEvent - /** user was banned from a guild */ - GUILD_BAN_ADD: GuildBanAddEvent - /** user was unbanned from a guild */ - GUILD_BAN_REMOVE: GuildBanRemoveEvent + GUILD_DELETE: Guild.Event.Delete } } declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/user#get-current-user-guilds */ - getCurrentUserGuilds(): Promise - /** https://discord.com/developers/docs/resources/user#leave-guild */ + /** + * Returns a list of partial guild objects the current user is a member of. Requires the guilds OAuth2 scope. + * @see https://discord.com/developers/docs/resources/user#get-current-user-guilds + */ + getCurrentUserGuilds(params?: Guild.Params.List): Promise + /** + * Returns a guild member object for the current user. Requires the guilds.members.read OAuth2 scope. + * @see https://discord.com/developers/docs/resources/user#get-current-user-guild-member + */ + getCurrentUserGuildMember(guild_id: snowflake): Promise + /** + * Leave a guild. Returns a 204 empty response on success. + * @see https://discord.com/developers/docs/resources/user#leave-guild + */ leaveGuild(guild_id: snowflake): Promise } } @@ -272,6 +379,9 @@ Internal.define({ '/users/@me/guilds': { GET: 'getCurrentUserGuilds', }, + '/users/@me/guilds/{guild.id}/member': { + GET: 'getCurrentUserGuildMember', + }, '/users/@me/guilds/{guild.id}': { DELETE: 'leaveGuild', }, @@ -279,12 +389,61 @@ Internal.define({ declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/guild#get-guild */ - getGuild(guild_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/guild#get-guild-preview */ + /** + * Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. + * @see https://discord.com/developers/docs/resources/guild#create-guild + */ + createGuild(params: Guild.Params.Create): Promise + /** + * Returns the guild object for the given id. If with_counts is set to true, this endpoint will also return approximate_member_count and approximate_presence_count for the guild. + * @see https://discord.com/developers/docs/resources/guild#get-guild + */ + getGuild(guild_id: snowflake, params?: Guild.Params.Get): Promise + /** + * Returns the guild preview object for the given id. If the user is not in the guild, then the guild must be lurkable. + * @see https://discord.com/developers/docs/resources/guild#get-guild-preview + */ getGuildPreview(guild_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/guild#modify-guild */ - modifyGuild(guild_id: snowflake, options: Partial): Promise + /** + * Modify a guild's settings. Requires the MANAGE_GUILD permission. Returns the updated guild object on success. Fires a Guild Update Gateway event. + * @see https://discord.com/developers/docs/resources/guild#modify-guild + */ + modifyGuild(guild_id: snowflake, params: Guild.Params.Modify): Promise + /** + * Delete a guild permanently. User must be owner. Returns 204 No Content on success. Fires a Guild Delete Gateway event. + * @see https://discord.com/developers/docs/resources/guild#delete-guild + */ + deleteGuild(guild_id: snowflake): Promise + /** + * Returns a guild widget object. Requires the MANAGE_GUILD permission. + * @see https://discord.com/developers/docs/resources/guild#get-guild-widget-settings + */ + getGuildWidgetSettings(guild_id: snowflake): Promise + /** + * Modify a guild widget object for the guild. All attributes may be passed in with JSON and modified. Requires the MANAGE_GUILD permission. Returns the updated guild widget object. + * @see https://discord.com/developers/docs/resources/guild#modify-guild-widget + */ + modifyGuildWidget(guild_id: snowflake, params: Partial): Promise + /** + * Returns the widget for the guild. + * @see https://discord.com/developers/docs/resources/guild#get-guild-widget + */ + getGuildWidget(guild_id: snowflake): Promise + /** + * Returns a PNG image widget for the guild. Requires no permissions or authentication. + * @see https://discord.com/developers/docs/resources/guild#get-guild-widget-image + */ + getGuildWidgetImage(guild_id: snowflake, params?: Guild.Params.GetWidgetImage): Promise + /** + * Returns the Welcome Screen object for the guild. + * @see https://discord.com/developers/docs/resources/guild#get-guild-welcome-screen + */ + getGuildWelcomeScreen(guild_id: snowflake): Promise + /** + * Modify the guild's Welcome Screen. Requires the MANAGE_GUILD permission. Returns the updated Welcome Screen object. + * @see https://discord.com/developers/docs/resources/guild#modify-guild-welcome-screen + */ + modifyGuildWelcomeScreen(guild_id: snowflake, params: Guild.Params.ModifyWelcomeScreen): Promise } } @@ -300,42 +459,6 @@ Internal.define({ '/guilds/{guild.id}/preview': { GET: 'getGuildPreview', }, - '/guilds/{guild.id}/threads/active': { - GET: 'listActiveThreads', - }, - '/guilds/{guild.id}/bans': { - GET: 'getGuildBans', - }, - '/guilds/{guild.id}/bans/{user.id}': { - GET: 'getGuildBan', - PUT: 'createGuildBan', - DELETE: 'removeGuildBan', - }, - '/guilds/{guild.id}/roles': { - GET: 'getGuildRoles', - POST: 'createGuildRole', - PATCH: 'modifyGuildRolePositions', - }, - '/guilds/{guild.id}/roles/{role.id}': { - PATCH: 'modifyGuildRole', - DELETE: 'deleteGuildRole', - }, - '/guilds/{guild.id}/prune': { - GET: 'getGuildPruneCount', - POST: 'beginGuildPrune', - }, - '/guilds/{guild.id}/regions': { - GET: 'getGuildVoiceRegions', - }, - '/guilds/{guild.id}/invites': { - GET: 'getGuildInvites', - }, - '/guilds/{guild.id}/integrations': { - GET: 'getGuildIntegrations', - }, - '/guilds/{guild.id}/integrations/{integration.id}': { - DELETE: 'deleteGuildIntegration', - }, '/guilds/{guild.id}/widget': { GET: 'getGuildWidgetSettings', PATCH: 'modifyGuildWidget', @@ -343,9 +466,6 @@ Internal.define({ '/guilds/{guild.id}/widget.json': { GET: 'getGuildWidget', }, - '/guilds/{guild.id}/vanity-url': { - GET: 'getGuildVanityURL', - }, '/guilds/{guild.id}/widget.png': { GET: 'getGuildWidgetImage', }, @@ -353,10 +473,4 @@ Internal.define({ GET: 'getGuildWelcomeScreen', PATCH: 'modifyGuildWelcomeScreen', }, - '/guilds/{guild.id}/voice-states/@me': { - PATCH: 'modifyCurrentUserVoiceState', - }, - '/guilds/{guild.id}/voice-states/{user.id}': { - PATCH: 'modifyUserVoiceState', - }, }) diff --git a/plugins/adapter/discord/src/types/index.ts b/plugins/adapter/discord/src/types/index.ts index 1983b41914..6b3674869a 100644 --- a/plugins/adapter/discord/src/types/index.ts +++ b/plugins/adapter/discord/src/types/index.ts @@ -2,6 +2,7 @@ export * from './internal' export * from './application' export * from './audit-log' +export * from './ban' export * from './channel' export * from './command' export * from './component' diff --git a/plugins/adapter/discord/src/types/integration.ts b/plugins/adapter/discord/src/types/integration.ts index 36b0a8fb37..1f6dd7a81b 100644 --- a/plugins/adapter/discord/src/types/integration.ts +++ b/plugins/adapter/discord/src/types/integration.ts @@ -1,4 +1,4 @@ -import { integer, snowflake, timestamp, User } from '.' +import { integer, Internal, snowflake, timestamp, User } from '.' /** https://discord.com/developers/docs/resources/guild#integration-object-integration-structure */ export interface Integration { @@ -104,3 +104,27 @@ declare module './gateway' { INTEGRATION_DELETE: IntegrationDeleteEvent } } + +declare module './internal' { + interface Internal { + /** + * Returns a list of integration objects for the guild. Requires the MANAGE_GUILD permission. + * @see https://discord.com/developers/docs/resources/guild#get-guild-integrations + */ + getGuildIntegrations(guild_id: snowflake): Promise + /** + * Delete the attached integration object for the guild. Deletes any associated webhooks and kicks the associated bot if there is one. Requires the MANAGE_GUILD permission. Returns a 204 empty response on success. Fires a Guild Integrations Update Gateway event. + * @see https://discord.com/developers/docs/resources/guild#delete-guild-integration + */ + deleteGuildIntegration(guild_id: snowflake, integration_id: snowflake): Promise + } +} + +Internal.define({ + '/guilds/{guild.id}/integrations': { + GET: 'getGuildIntegrations', + }, + '/guilds/{guild.id}/integrations/{integration.id}': { + DELETE: 'deleteGuildIntegration', + }, +}) diff --git a/plugins/adapter/discord/src/types/internal.ts b/plugins/adapter/discord/src/types/internal.ts index 003cdf0b1c..bdd40973a1 100644 --- a/plugins/adapter/discord/src/types/internal.ts +++ b/plugins/adapter/discord/src/types/internal.ts @@ -1,30 +1,31 @@ -import { Dict, Quester } from 'koishi' +import { Dict, makeArray, Quester } from 'koishi' import { AxiosRequestConfig } from 'axios' type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' export class Internal { - static define(routes: Dict>>) { + static define(routes: Dict>>) { for (const path in routes) { for (const key in routes[path]) { const method = key as Method - const name = routes[path][method] - Internal.prototype[name] = function (this: Internal, ...args: any[]) { - const url = path.replace(/\{([^}]+)\}/g, () => { - if (!args.length) throw new Error('too few arguments') - return args.shift() - }) - const config: AxiosRequestConfig = {} - if (args.length === 1) { - if (method === 'GET' || method === 'DELETE') { - config.params = args[0] - } else { - config.data = args[0] + for (const name of makeArray(routes[path][method])) { + Internal.prototype[name] = function (this: Internal, ...args: any[]) { + const url = path.replace(/\{([^}]+)\}/g, () => { + if (!args.length) throw new Error('too few arguments') + return args.shift() + }) + const config: AxiosRequestConfig = {} + if (args.length === 1) { + if (method === 'GET' || method === 'DELETE') { + config.params = args[0] + } else { + config.data = args[0] + } + } else if (args.length > 1) { + throw new Error('too many arguments') } - } else if (args.length > 1) { - throw new Error('too many arguments') + return this.http(method, url, config) } - return this.http(method, url, config) } } } diff --git a/plugins/adapter/discord/src/types/invite.ts b/plugins/adapter/discord/src/types/invite.ts index db90ca1673..9c55d43bb2 100644 --- a/plugins/adapter/discord/src/types/invite.ts +++ b/plugins/adapter/discord/src/types/invite.ts @@ -149,6 +149,16 @@ declare module './internal' { * @see https://discord.com/developers/docs/resources/invite#delete-invite */ deleteInvite(code: string): Promise + /** + * Returns a list of invite objects (with invite metadata) for the guild. Requires the MANAGE_GUILD permission. + * @see https://discord.com/developers/docs/resources/guild#get-guild-invites + */ + getGuildInvites(guild_id: snowflake): Promise + /** + * Returns a partial invite object for guilds with that feature enabled. Requires the MANAGE_GUILD permission. code will be null if a vanity url for the guild is not set. + * @see https://discord.com/developers/docs/resources/guild#get-guild-vanity-url + */ + getGuildVanityURL(guild_id: snowflake): Promise> /** * Returns a list of invite objects (with invite metadata) for the channel. Only usable for guild channels. Requires the MANAGE_CHANNELS permission. * @see https://discord.com/developers/docs/resources/channel#get-channel-invites @@ -167,6 +177,12 @@ Internal.define({ GET: 'getInvite', DELETE: 'deleteInvite', }, + '/guilds/{guild.id}/invites': { + GET: 'getGuildInvites', + }, + '/guilds/{guild.id}/vanity-url': { + GET: 'getGuildVanityURL', + }, '/channels/{channel.id}/invites': { GET: 'getChannelInvites', POST: 'createChannelInvite', diff --git a/plugins/adapter/discord/src/types/role.ts b/plugins/adapter/discord/src/types/role.ts index 3dc364b3f6..853d745d8a 100644 --- a/plugins/adapter/discord/src/types/role.ts +++ b/plugins/adapter/discord/src/types/role.ts @@ -1,4 +1,4 @@ -import { integer, snowflake } from '.' +import { integer, Internal, snowflake } from '.' /** https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags */ export enum Permission { @@ -108,6 +108,54 @@ export interface Role { tags?: RoleTags } +export namespace Role { + export namespace Params { + /** https://discord.com/developers/docs/resources/guild#create-guild-role-json-params */ + export interface Create { + /** name of the role */ + name: string + /** bitwise value of the enabled/disabled permissions */ + permissions: string + /** RGB color value */ + color: integer + /** whether the role should be displayed separately in the sidebar */ + hoist: boolean + /** the role's icon image (if the guild has the ROLE_ICONS feature) */ + icon: string + /** the role's unicode emoji as a standard emoji (if the guild has the ROLE_ICONS feature) */ + unicode_emoji: string + /** whether the role should be mentionable */ + mentionable: boolean + } + + /** https://discord.com/developers/docs/resources/guild#modify-guild-role-positions-json-params */ + export interface ModifyPositions { + /** role */ + id: snowflake + /** sorting position of the role */ + position?: integer + } + + /** https://discord.com/developers/docs/resources/guild#modify-guild-role-json-params */ + export interface Modify { + /** name of the role */ + name: string + /** bitwise value of the enabled/disabled permissions */ + permissions: string + /** RGB color value */ + color: integer + /** whether the role should be displayed separately in the sidebar */ + hoist: boolean + /** the role's icon image (if the guild has the ROLE_ICONS feature) */ + icon: string + /** the role's unicode emoji as a standard emoji (if the guild has the ROLE_ICONS feature) */ + unicode_emoji: string + /** whether the role should be mentionable */ + mentionable: boolean + } + } +} + /** https://discord.com/developers/docs/topics/permissions#role-object-role-tags-structure */ export interface RoleTags { /** the id of the bot this role belongs to */ @@ -152,3 +200,45 @@ declare module './gateway' { GUILD_ROLE_DELETE: GuildRoleDeleteEvent } } + +declare module './internal' { + interface Internal { + /** + * Returns a list of role objects for the guild. + * @see https://discord.com/developers/docs/resources/guild#get-guild-roles + */ + getGuildRoles(guild_id: snowflake): Promise + /** + * Create a new role for the guild. Requires the MANAGE_ROLES permission. Returns the new role object on success. Fires a Guild Role Create Gateway event. All JSON params are optional. + * @see https://discord.com/developers/docs/resources/guild#create-guild-role + */ + createGuildRole(guild_id: snowflake, param: Role.Params.Create): Promise + /** + * Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. Returns a list of all of the guild's role objects on success. Fires multiple Guild Role Update Gateway events. + * @see https://discord.com/developers/docs/resources/guild#modify-guild-role-positions + */ + modifyGuildRolePositions(guild_id: snowflake, param: Role.Params.ModifyPositions): Promise + /** + * Modify a guild role. Requires the MANAGE_ROLES permission. Returns the updated role on success. Fires a Guild Role Update Gateway event. + * @see https://discord.com/developers/docs/resources/guild#modify-guild-role + */ + modifyGuildRole(guild_id: snowflake, role_id: snowflake, param: Role.Params.Modify): Promise + /** + * Delete a guild role. Requires the MANAGE_ROLES permission. Returns a 204 empty response on success. Fires a Guild Role Delete Gateway event. + * @see https://discord.com/developers/docs/resources/guild#delete-guild-role + */ + deleteGuildRole(guild_id: snowflake, role_id: snowflake): Promise + } +} + +Internal.define({ + '/guilds/{guild.id}/roles': { + GET: 'getGuildRoles', + POST: 'createGuildRole', + PATCH: 'modifyGuildRolePositions', + }, + '/guilds/{guild.id}/roles/{role.id}': { + PATCH: 'modifyGuildRole', + DELETE: 'deleteGuildRole', + }, +}) diff --git a/plugins/adapter/discord/src/types/thread.ts b/plugins/adapter/discord/src/types/thread.ts index 490763ab91..e2525b66bf 100644 --- a/plugins/adapter/discord/src/types/thread.ts +++ b/plugins/adapter/discord/src/types/thread.ts @@ -146,6 +146,11 @@ declare module './gateway' { declare module './internal' { interface Internal { + /** + * Returns all active threads in the guild, including public and private threads. Threads are ordered by their id, in descending order. + * @see https://discord.com/developers/docs/resources/guild#list-active-threads + */ + // listActiveThreads(guild_id: snowflake): Promise /** * Creates a new thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. * @see https://discord.com/developers/docs/resources/channel#start-thread-with-message @@ -210,6 +215,9 @@ declare module './internal' { } Internal.define({ + // '/guilds/{guild.id}/threads/active': { + // GET: 'listActiveThreads', + // }, '/channels/{channel.id}/messages/{message.id}/threads': { POST: 'startThreadwithMessage', }, diff --git a/plugins/adapter/discord/src/types/user.ts b/plugins/adapter/discord/src/types/user.ts index 833b107973..2ffaf7f01a 100644 --- a/plugins/adapter/discord/src/types/user.ts +++ b/plugins/adapter/discord/src/types/user.ts @@ -34,6 +34,18 @@ export interface User { public_flags?: integer } +export namespace User { + export namespace Params { + /** https://discord.com/developers/docs/resources/user#modify-current-user-json-params */ + export interface Modify { + /** user's username, if changed may cause the user's discriminator to be randomized. */ + username: string + /** if passed, modifies the user's avatar */ + avatar?: string + } + } +} + /** https://discord.com/developers/docs/resources/user#user-object-user-flags */ export enum UserFlag { NONE = 0, @@ -91,22 +103,27 @@ declare module './gateway' { } } -export interface ModifyUserOptions { - /** user's username, if changed may cause the user's discriminator to be randomized. */ - username?: string - /** if passed, modifies the user's avatar */ - avatar?: string -} - declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/user#get-current-user */ + /** + * Returns the user object of the requester's account. For OAuth2, this requires the identify scope, which will return the object without an email, and optionally the email scope, which returns the object with an email. + * @see https://discord.com/developers/docs/resources/user#get-current-user + */ getCurrentUser(): Promise - /** https://discord.com/developers/docs/resources/user#get-user */ + /** + * Returns a user object for a given user ID. + * @see https://discord.com/developers/docs/resources/user#get-user + */ getUser(id: snowflake): Promise - /** https://discord.com/developers/docs/resources/user#modify-current-user */ - modifyCurrentUser(options: ModifyUserOptions): Promise - /** https://discord.com/developers/docs/resources/user#get-user-connections */ + /** + * Modify the requester's user account settings. Returns a user object on success. + * @see https://discord.com/developers/docs/resources/user#modify-current-user + */ + modifyCurrentUser(params: User.Params.Modify): Promise + /** + * Returns a list of connection objects. Requires the connections OAuth2 scope. + * @see https://discord.com/developers/docs/resources/user#get-user-connections + */ getUserConnections(): Promise } } @@ -119,10 +136,6 @@ Internal.define({ '/users/{user.id}': { GET: 'getUser', }, - '/users/@me/channels': { - POST: 'createDM', - // POST: 'createGroupDM', - }, '/users/@me/connections': { GET: 'getUserConnections', }, diff --git a/plugins/adapter/discord/src/types/voice.ts b/plugins/adapter/discord/src/types/voice.ts index 5a3c83fe3e..49cf20d45c 100644 --- a/plugins/adapter/discord/src/types/voice.ts +++ b/plugins/adapter/discord/src/types/voice.ts @@ -30,6 +30,24 @@ export interface VoiceState { request_to_speak_timestamp?: timestamp } +export namespace VoiceState { + export namespace Params { + /** https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state-json-params */ + export interface ModifyCurrent extends Modify { + /** sets the user's request to speak */ + request_to_speak_timestamp?: timestamp + } + + /** https://discord.com/developers/docs/resources/guild#modify-user-voice-state-json-params */ + export interface Modify { + /** the id of the channel the user is currently in */ + channel_id: snowflake + /** toggles the user's suppress state */ + suppress?: boolean + } + } +} + /** https://discord.com/developers/docs/resources/voice#voice-region-object-voice-region-structure */ export interface VoiceRegion { /** unique ID for the region */ @@ -72,6 +90,21 @@ declare module './internal' { * @see https://discord.com/developers/docs/resources/voice#list-voice-regions */ listVoiceRegions(): Promise + /** + * Returns a list of voice region objects for the guild. Unlike the similar /voice route, this returns VIP servers when the guild is VIP-enabled. + * @see https://discord.com/developers/docs/resources/guild#get-guild-voice-regions + */ + getGuildVoiceRegions(guild_id: snowflake): Promise + /** + * Updates the current user's voice state. + * @see https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state + */ + modifyCurrentUserVoiceState(guild_id: snowflake, params: VoiceState.Params.ModifyCurrent): Promise + /** + * Updates another user's voice state. + * @see https://discord.com/developers/docs/resources/guild#modify-user-voice-state + */ + modifyUserVoiceState(guild_id: snowflake, user_id: snowflake, params: VoiceState.Params.Modify): Promise } } @@ -79,4 +112,13 @@ Internal.define({ '/voice/regions': { GET: 'listVoiceRegions', }, + '/guilds/{guild.id}/regions': { + GET: 'getGuildVoiceRegions', + }, + '/guilds/{guild.id}/voice-states/@me': { + PATCH: 'modifyCurrentUserVoiceState', + }, + '/guilds/{guild.id}/voice-states/{user.id}': { + PATCH: 'modifyUserVoiceState', + }, }) From 55b6600fe35f9b33a05632e09fb7c6be9e3dfcd9 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 11:20:54 +0800 Subject: [PATCH 15/34] feat(discord): enhance typings for application --- .../adapter/discord/src/types/application.ts | 10 +- plugins/adapter/discord/src/types/command.ts | 306 +++++++++++------- plugins/adapter/discord/src/types/gateway.ts | 10 +- plugins/adapter/discord/src/types/index.ts | 1 + ...-scheduled-event.ts => scheduled-event.ts} | 0 plugins/database/mysql/src/index.ts | 11 +- 6 files changed, 217 insertions(+), 121 deletions(-) rename plugins/adapter/discord/src/types/{guild-scheduled-event.ts => scheduled-event.ts} (100%) diff --git a/plugins/adapter/discord/src/types/application.ts b/plugins/adapter/discord/src/types/application.ts index 7ccccd0cc1..f459423f59 100644 --- a/plugins/adapter/discord/src/types/application.ts +++ b/plugins/adapter/discord/src/types/application.ts @@ -75,9 +75,15 @@ declare module './gateway' { declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/topics/oauth2#get-current-bot-application-information */ + /** + * Returns the bot's application object. + * @see https://discord.com/developers/docs/topics/oauth2#get-current-bot-application-information + */ getCurrentBotApplicationInformation(): Promise - /** https://discord.com/developers/docs/topics/oauth2#get-current-authorization-information */ + /** + * Returns info about the current authorization. Requires authentication with a bearer token. + * @see https://discord.com/developers/docs/topics/oauth2#get-current-authorization-information + */ getCurrentAuthorizationInformation(): Promise } } diff --git a/plugins/adapter/discord/src/types/command.ts b/plugins/adapter/discord/src/types/command.ts index f0ee638920..440a8ef185 100644 --- a/plugins/adapter/discord/src/types/command.ts +++ b/plugins/adapter/discord/src/types/command.ts @@ -5,7 +5,7 @@ export interface ApplicationCommand { /** unique id of the command */ id: snowflake /** the type of command, defaults 1 if not set */ - type?: ApplicationCommandType + type?: ApplicationCommand.Type /** unique id of the parent application */ application_id: snowflake /** guild id of the command, if not global */ @@ -15,141 +15,225 @@ export interface ApplicationCommand { /** 1-100 character description for CHAT_INPUT commands, empty string for USER and MESSAGE commands */ description: string /** the parameters for the command, max 25 */ - options?: ApplicationCommandOption[] + options?: ApplicationCommand.Option[] /** whether the command is enabled by default when the app is added to a guild */ default_permission?: boolean /** autoincrementing version identifier updated during substantial record changes */ version: snowflake } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types */ -export enum ApplicationCommandType { - /** Slash commands; a text-based command that shows up when a user types / */ - CHAT_INPUT = 1, - /** A UI-based command that shows up when you right click or tap on a user */ - USER = 2, - /** A UI-based command that shows up when you right click or tap on a message */ - MESSAGE = 3, -} +export namespace ApplicationCommand { + /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types */ + export enum Type { + /** Slash commands; a text-based command that shows up when a user types / */ + CHAT_INPUT = 1, + /** A UI-based command that shows up when you right click or tap on a user */ + USER = 2, + /** A UI-based command that shows up when you right click or tap on a message */ + MESSAGE = 3, + } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure */ -export interface ApplicationCommandOption { - /** the type of option */ - type: ApplicationCommandOptionType - /** 1-32 character name */ - name: string - /** 1-100 character description */ - description: string - /** if the parameter is required or optional--default false */ - required?: boolean - /** choices for STRING, INTEGER, and NUMBER types for the user to pick from, max 25 */ - choices?: ApplicationCommandOptionChoice[] - /** if the option is a subcommand or subcommand group type, this nested options will be the parameters */ - options?: ApplicationCommandOption[] - /** if the option is a channel type, the channels shown will be restricted to these types */ - channel_types?: Channel.Type[] -} + /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure */ + export interface Option { + /** the type of option */ + type: OptionType + /** 1-32 character name */ + name: string + /** 1-100 character description */ + description: string + /** if the parameter is required or optional--default false */ + required?: boolean + /** choices for STRING, INTEGER, and NUMBER types for the user to pick from, max 25 */ + choices?: OptionChoice[] + /** if the option is a subcommand or subcommand group type, this nested options will be the parameters */ + options?: Option[] + /** if the option is a channel type, the channels shown will be restricted to these types */ + channel_types?: Channel.Type[] + } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type */ -export enum ApplicationCommandOptionType { - SUB_COMMAND = 1, - SUB_COMMAND_GROUP = 2, - STRING = 3, - /** Any integer between -2^53 and 2^53 */ - INTEGER = 4, - BOOLEAN = 5, - USER = 6, - /** Includes all channel types + categories */ - CHANNEL = 7, - ROLE = 8, - /** Includes users and roles */ - MENTIONABLE = 9, - /** Any double between -2^53 and 2^53 */ - NUMBER = 10, -} + /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type */ + export enum OptionType { + SUB_COMMAND = 1, + SUB_COMMAND_GROUP = 2, + STRING = 3, + /** Any integer between -2^53 and 2^53 */ + INTEGER = 4, + BOOLEAN = 5, + USER = 6, + /** Includes all channel types + categories */ + CHANNEL = 7, + ROLE = 8, + /** Includes users and roles */ + MENTIONABLE = 9, + /** Any double between -2^53 and 2^53 */ + NUMBER = 10, + } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure */ -export interface ApplicationCommandOptionChoice { - /** 1-100 character choice name */ - name: string - /** value of the choice, up to 100 characters if string */ - value: string | number -} + /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure */ + export interface OptionChoice { + /** 1-100 character choice name */ + name: string + /** value of the choice, up to 100 characters if string */ + value: string | number + } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-interaction-data-option-structure */ -export interface ApplicationCommandInteractionDataOption { - /** the name of the parameter */ - name: string - /** value of application command option type */ - type: ApplicationCommandOptionType - /** the value of the pair */ - value?: ApplicationCommandOptionType - /** present if this option is a group or subcommand */ - options?: ApplicationCommandInteractionDataOption[] -} + /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-interaction-data-option-structure */ + export interface InteractionDataOption { + /** the name of the parameter */ + name: string + /** value of application command option type */ + type: OptionType + /** the value of the pair */ + value?: OptionType + /** present if this option is a group or subcommand */ + options?: InteractionDataOption[] + } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-guild-application-command-permissions-structure */ -export interface GuildApplicationCommandPermissions { - /** the id of the command */ - id: snowflake - /** the id of the application the command belongs to */ - application_id: snowflake - /** the id of the guild */ - guild_id: snowflake - /** the permissions for the command in the guild */ - permissions: ApplicationCommandPermissions[] -} + /** https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-guild-application-command-permissions-structure */ + export interface GuildPermissions { + /** the id of the command */ + id: snowflake + /** the id of the application the command belongs to */ + application_id: snowflake + /** the id of the guild */ + guild_id: snowflake + /** the permissions for the command in the guild */ + permissions: Permissions[] + } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure */ -export interface ApplicationCommandPermissions { - /** the id of the role or user */ - id: snowflake - /** role or user */ - type: ApplicationCommandPermissionType - /** true to allow, false, to disallow */ - permission: boolean -} + /** https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure */ + export interface Permissions { + /** the id of the role or user */ + id: snowflake + /** role or user */ + type: PermissionType + /** true to allow, false, to disallow */ + permission: boolean + } + + /** https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permission-type */ + export enum PermissionType { + ROLE = 1, + USER = 2, + } -/** https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permission-type */ -export enum ApplicationCommandPermissionType { - ROLE = 1, - USER = 2, + export namespace Params { + /** https://discord.com/developers/docs/interactions/application-commands#create-global-application-command-json-params */ + export interface Create { + /** 1-32 character name */ + name: string + /** 1-100 character description */ + description: string + /** the parameters for the command */ + options?: Option[] + /** whether the command is enabled by default when the app is added to a guild */ + default_permission?: boolean + /** the type of command, defaults 1 if not set */ + type?: Type + } + + /** https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command-json-params */ + export interface Edit { + /** 1-32 character name */ + name?: string + /** 1-100 character description */ + description?: string + /** the parameters for the command */ + options?: Option[] + /** whether the command is enabled by default when the app is added to a guild */ + default_permission?: boolean + } + + /** https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions-json-params */ + export interface EditPermissions { + /** the permissions for the command in the guild */ + permissions: Permissions[] + } + } } declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/interactions/application-commands#get-global-application-commands */ + /** + * Fetch all of the global commands for your application. Returns an array of application command objects. + * @see https://discord.com/developers/docs/interactions/application-commands#get-global-application-commands + */ getGlobalApplicationCommands(application_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#create-global-application-command */ - createGlobalApplicationCommand(application_id: snowflake, command: Partial): Promise - /** https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands */ + /** + * Creating a command with the same name as an existing command for your application will overwrite the old command. + * @see https://discord.com/developers/docs/interactions/application-commands#create-global-application-command + */ + createGlobalApplicationCommand(application_id: snowflake, param: ApplicationCommand.Params.Create): Promise + /** + * Takes a list of application commands, overwriting the existing global command list for this application. Updates will be available in all guilds after 1 hour. Returns 200 and a list of application command objects. Commands that do not already exist will count toward daily application command create limits. + * @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands + */ bulkOverwriteGlobalApplicationCommands(application_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#get-global-application-command */ + /** + * Fetch a global command for your application. Returns an application command object. + * @see https://discord.com/developers/docs/interactions/application-commands#get-global-application-command + */ getGlobalApplicationCommand(application_id: snowflake, command_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command */ - editGlobalApplicationCommand(application_id: snowflake, command_id: snowflake, command: Partial): Promise - /** https://discord.com/developers/docs/interactions/application-commands#delete-global-application-command */ + /** + * All parameters for this endpoint are optional. + * @see https://discord.com/developers/docs/interactions/application-commands#edit-global-application-command + */ + editGlobalApplicationCommand(application_id: snowflake, command_id: snowflake, param: ApplicationCommand.Params.Edit): Promise + /** + * Deletes a global command. Returns 204 No Content on success. + * @see https://discord.com/developers/docs/interactions/application-commands#delete-global-application-command + */ deleteGlobalApplicationCommand(application_id: snowflake, command_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#get-guild-application-commands */ + /** + * Fetch all of the guild commands for your application for a specific guild. Returns an array of application command objects. + * @see https://discord.com/developers/docs/interactions/application-commands#get-guild-application-commands + */ getGuildApplicationCommands(application_id: snowflake, guild_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command */ - createGuildApplicationCommand(application_id: snowflake, guild_id: snowflake, command: Partial): Promise - /** https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands */ + /** + * Creating a command with the same name as an existing command for your application will overwrite the old command. + * @see https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command + */ + createGuildApplicationCommand(application_id: snowflake, guild_id: snowflake, param: ApplicationCommand.Params.Create): Promise + /** + * Takes a list of application commands, overwriting the existing command list for this application for the targeted guild. Returns 200 and a list of application command objects. + * @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands + */ bulkOverwriteGuildApplicationCommands(application_id: snowflake, guild_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions */ - getGuildApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#batch-edit-application-command-permissions */ - batchEditApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake, permissions: Partial[]): Promise - /** https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command */ + /** + * Fetches command permissions for all commands for your application in a guild. Returns an array of guild application command permissions objects. + * @see https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions + */ + getGuildApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake): Promise + /** + * This endpoint will overwrite all existing permissions for all commands in a guild, including slash commands, user commands, and message commands. + * @see https://discord.com/developers/docs/interactions/application-commands#batch-edit-application-command-permissions + */ + batchEditApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake, permissions: Partial[]): Promise + /** + * Fetch a guild command for your application. Returns an application command object. + * @see https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command + */ getGuildApplicationCommand(application_id: snowflake, guild_id: snowflake, command_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command */ - editGuildApplicationCommand(application_id: snowflake, guild_id: snowflake, command_id: snowflake, command: Partial): Promise - /** https://discord.com/developers/docs/interactions/application-commands#delete-guild-application-command */ + /** + * All parameters for this endpoint are optional. + * @see https://discord.com/developers/docs/interactions/application-commands#edit-guild-application-command + */ + editGuildApplicationCommand(application_id: snowflake, guild_id: snowflake, command_id: snowflake, param: ApplicationCommand.Params.Edit): Promise + /** + * Delete a guild command. Returns 204 No Content on success. + * @see https://discord.com/developers/docs/interactions/application-commands#delete-guild-application-command + */ deleteGuildApplicationCommand(application_id: snowflake, guild_id: snowflake, command_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#get-application-command-permissions */ - getApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake, command_id: snowflake): Promise - /** https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions */ - editApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake, command_id: snowflake, permissions: ApplicationCommandPermissions[]): Promise + /** + * Fetches command permissions for a specific command for your application in a guild. Returns a guild application command permissions object. + * @see https://discord.com/developers/docs/interactions/application-commands#get-application-command-permissions + */ + getApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake, command_id: snowflake): Promise + /** + * This endpoint will overwrite existing permissions for the command in that guild + * @see https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions + */ + editApplicationCommandPermissions(application_id: snowflake, guild_id: snowflake, command_id: snowflake, param: ApplicationCommand.Params.EditPermissions): Promise } } diff --git a/plugins/adapter/discord/src/types/gateway.ts b/plugins/adapter/discord/src/types/gateway.ts index 82544f434d..b018d158b7 100644 --- a/plugins/adapter/discord/src/types/gateway.ts +++ b/plugins/adapter/discord/src/types/gateway.ts @@ -249,9 +249,15 @@ export interface SessionStartLimit { declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/topics/gateway#get-gateway */ + /** + * Returns an object with a single valid WSS URL, which the client can use for Connecting. Clients should cache this value and only call this endpoint to retrieve a new URL if they are unable to properly establish a connection using the cached version of the URL. + * @see https://discord.com/developers/docs/topics/gateway#get-gateway + */ getGateway(): Promise - /** https://discord.com/developers/docs/topics/gateway#get-gateway-bot */ + /** + * Returns an object based on the information in Get Gateway, plus additional metadata that can help during the operation of large or sharded bots. Unlike the Get Gateway, this route should not be cached for extended periods of time as the value is not guaranteed to be the same per-call, and changes as the bot joins/leaves guilds. + * @see https://discord.com/developers/docs/topics/gateway#get-gateway-bot + */ getGatewayBot(): Promise } } diff --git a/plugins/adapter/discord/src/types/index.ts b/plugins/adapter/discord/src/types/index.ts index 6b3674869a..30f9aabf60 100644 --- a/plugins/adapter/discord/src/types/index.ts +++ b/plugins/adapter/discord/src/types/index.ts @@ -19,6 +19,7 @@ export * from './message' export * from './presence' export * from './reaction' export * from './role' +export * from './scheduled-event' export * from './stage-instance' export * from './sticker' export * from './team' diff --git a/plugins/adapter/discord/src/types/guild-scheduled-event.ts b/plugins/adapter/discord/src/types/scheduled-event.ts similarity index 100% rename from plugins/adapter/discord/src/types/guild-scheduled-event.ts rename to plugins/adapter/discord/src/types/scheduled-event.ts diff --git a/plugins/database/mysql/src/index.ts b/plugins/database/mysql/src/index.ts index 402afa3ef0..eefd700773 100644 --- a/plugins/database/mysql/src/index.ts +++ b/plugins/database/mysql/src/index.ts @@ -247,15 +247,14 @@ class MysqlDatabase extends Database { return new Promise((resolve, reject) => { sql = format(sql, values) logger.debug('[sql]', sql) - this.pool.query(sql, (err, results) => { + this.pool.query(sql, (err: Error, results) => { if (!err) return resolve(results) logger.warn(sql) - err.stack = err.message + error.stack.slice(5) - if (err.code === 'ER_DUP_ENTRY') { - reject(new KoishiError(err.message, 'database.duplicate-entry')) - } else { - reject(err) + if (err['code'] === 'ER_DUP_ENTRY') { + err = new KoishiError(err.message, 'database.duplicate-entry') } + err.stack = err.message + error.stack.slice(5) + reject(err) }) }) } From 56d51435030483fc45e2048a0e6f58d459eaba81 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 12:28:45 +0800 Subject: [PATCH 16/34] feat(discord): enhance typings for webhooks --- plugins/adapter/discord/src/types/command.ts | 12 -- .../adapter/discord/src/types/interaction.ts | 63 +++++- plugins/adapter/discord/src/types/internal.ts | 5 +- plugins/adapter/discord/src/types/webhook.ts | 194 ++++++++++++++---- 4 files changed, 221 insertions(+), 53 deletions(-) diff --git a/plugins/adapter/discord/src/types/command.ts b/plugins/adapter/discord/src/types/command.ts index 440a8ef185..50e886e9ef 100644 --- a/plugins/adapter/discord/src/types/command.ts +++ b/plugins/adapter/discord/src/types/command.ts @@ -77,18 +77,6 @@ export namespace ApplicationCommand { value: string | number } - /** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-interaction-data-option-structure */ - export interface InteractionDataOption { - /** the name of the parameter */ - name: string - /** value of application command option type */ - type: OptionType - /** the value of the pair */ - value?: OptionType - /** present if this option is a group or subcommand */ - options?: InteractionDataOption[] - } - /** https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-guild-application-command-permissions-structure */ export interface GuildPermissions { /** the id of the command */ diff --git a/plugins/adapter/discord/src/types/interaction.ts b/plugins/adapter/discord/src/types/interaction.ts index ccfd4c655b..5ed4201f6a 100644 --- a/plugins/adapter/discord/src/types/interaction.ts +++ b/plugins/adapter/discord/src/types/interaction.ts @@ -1,4 +1,4 @@ -import { AllowedMentions, ApplicationCommandInteractionDataOption, Channel, Component, Embed, GuildMember, integer, Internal, Message, Role, SelectOption, snowflake, User } from '.' +import { AllowedMentions, ApplicationCommand, Channel, Component, Embed, GuildMember, integer, Internal, Message, Role, SelectOption, snowflake, User } from '.' /** https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-structure */ export interface Interaction { @@ -33,6 +33,20 @@ export enum InteractionType { MESSAGE_COMPONENT = 3, } +/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-interaction-data-option-structure */ +export interface InteractionDataOption { + /** the name of the parameter */ + name: string + /** value of application command option type */ + type: ApplicationCommand.OptionType + /** the value of the pair */ + value?: any + /** present if this option is a group or subcommand */ + options?: InteractionDataOption[] + /** true if this option is the currently focused option for autocomplete */ + focused?: boolean +} + /** https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-data-structure */ export interface InteractionData { /** the ID of the invoked command */ @@ -44,7 +58,7 @@ export interface InteractionData { /** converted users + roles + channels */ resolved?: ResolvedData /** the params + values from the user */ - options?: ApplicationCommandInteractionDataOption[] + options?: InteractionDataOption[] /** the custom_id of the component */ custom_id?: string /** the type of the component */ @@ -134,6 +148,51 @@ declare module './gateway' { } } +declare module './internal' { + interface Internal { + /** + * Create a response to an Interaction from the gateway. Takes an interaction response. This endpoint also supports file attachments similar to the webhook endpoints. Refer to Uploading Files for details on uploading files and multipart/form-data requests. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response + */ + createInteractionResponse(interaction_id: snowflake, token: string, params: InteractionResponse): Promise + /** + * Returns the initial Interaction response. Functions the same as Get Webhook Message. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#get-original-interaction-response + */ + getOriginalInteractionResponse(application_id: snowflake, token: string): Promise + /** + * Edits the initial Interaction response. Functions the same as Edit Webhook Message. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#edit-original-interaction-response + */ + editOriginalInteractionResponse(application_id: snowflake, token: string): Promise + /** + * Deletes the initial Interaction response. Returns 204 No Content on success. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#delete-original-interaction-response + */ + deleteOriginalInteractionResponse(application_id: snowflake, token: string): Promise + /** + * Create a followup message for an Interaction. Functions the same as Execute Webhook, but wait is always true, and flags can be set to 64 in the body to send an ephemeral message. The thread_id, avatar_url, and username parameters are not supported when using this endpoint for interaction followups. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#create-followup-message + */ + createFollowupMessage(application_id: snowflake, token: string): Promise + /** + * Returns a followup message for an Interaction. Functions the same as Get Webhook Message. Does not support ephemeral followups. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#get-followup-message + */ + getFollowupMessage(application_id: snowflake, token: string, message_id: snowflake): Promise + /** + * Edits a followup message for an Interaction. Functions the same as Edit Webhook Message. Does not support ephemeral followups. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#edit-followup-message + */ + editFollowupMessage(application_id: snowflake, token: string, message_id: snowflake): Promise + /** + * Deletes a followup message for an Interaction. Returns 204 No Content on success. Does not support ephemeral followups. + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#delete-followup-message + */ + deleteFollowupMessage(application_id: snowflake, token: string, message_id: snowflake): Promise + } +} + Internal.define({ '/interactions/{interaction.id}/{interaction.token}/callback': { POST: 'createInteractionResponse', diff --git a/plugins/adapter/discord/src/types/internal.ts b/plugins/adapter/discord/src/types/internal.ts index bdd40973a1..7bec198116 100644 --- a/plugins/adapter/discord/src/types/internal.ts +++ b/plugins/adapter/discord/src/types/internal.ts @@ -21,7 +21,10 @@ export class Internal { } else { config.data = args[0] } - } else if (args.length > 1) { + } else if (args.length === 2 && method !== 'GET' && method !== 'DELETE') { + config.data = args[0] + config.params = args[1] + } else { throw new Error('too many arguments') } return this.http(method, url, config) diff --git a/plugins/adapter/discord/src/types/webhook.ts b/plugins/adapter/discord/src/types/webhook.ts index dce6649910..7bc3c9d71e 100644 --- a/plugins/adapter/discord/src/types/webhook.ts +++ b/plugins/adapter/discord/src/types/webhook.ts @@ -1,11 +1,11 @@ -import { Channel, Guild, integer, Internal, Message, snowflake, User } from '.' +import { AllowedMentions, Attachment, Channel, Component, Embed, Guild, Internal, Message, snowflake, User } from '.' /** https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-structure */ export interface Webhook { /** the id of the webhook */ id: snowflake /** the type of the webhook */ - type: integer + type: Webhook.Type /** the guild id this webhook is for, if any */ guild_id?: snowflake /** the channel id this webhook is for, if any */ @@ -28,14 +28,90 @@ export interface Webhook { url?: string } -/** https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types */ -export enum WebhookType { - /** Incoming Webhooks can post messages to channels with a generated token */ - INCOMING = 1, - /** Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels */ - CHANNEL_FOLLOWER = 2, - /** Application webhooks are webhooks used with Interactions */ - APPLICATION = 3, +export namespace Webhook { + /** https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types */ + export enum Type { + /** Incoming Webhooks can post messages to channels with a generated token */ + INCOMING = 1, + /** Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels */ + CHANNEL_FOLLOWER = 2, + /** Application webhooks are webhooks used with Interactions */ + APPLICATION = 3, + } + + /** https://discord.com/developers/docs/resources/webhook#create-webhook-json-params */ + export interface CreateParams { + /** name of the webhook (1-80 characters) */ + name: string + /** image for the default webhook avatar */ + avatar?: string + } + + /** https://discord.com/developers/docs/resources/webhook#modify-webhook-json-params */ + export interface ModifyParams { + /** the default name of the webhook */ + name: string + /** image for the default webhook avatar */ + avatar?: string + /** the new channel id this webhook should be moved to */ + channel_id: snowflake + } + + /** https://discord.com/developers/docs/resources/webhook#execute-webhook-query-string-params */ + export interface ExecuteParams { + /** waits for server confirmation of message send before response, and returns the created message body (defaults to false; when false a message that is not saved does not return an error) */ + wait: boolean + /** Send a message to the specified thread within a webhook's channel. The thread will automatically be unarchived. */ + thread_id: snowflake + } + + /** https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params */ + export interface ExecuteBody { + /** the message contents (up to 2000 characters) */ + content: string + /** override the default username of the webhook */ + username: string + /** override the default avatar of the webhook */ + avatar_url: string + /** true if this is a TTS message */ + tts: boolean + /** embedded rich content */ + embeds: Embed[] + /** allowed mentions for the message */ + allowed_mentions: AllowedMentions + /** the components to include with the message */ + components: Component[] + /** the contents of the file being sent */ + files: any + /** JSON encoded body of non-file params */ + payload_json: string + /** attachment objects with filename and description */ + attachments: Partial[] + } + + /** https://discord.com/developers/docs/resources/webhook#get-webhook-message-query-string-params */ + export interface MessageParams { + /** id of the thread the message is in */ + thread_id: snowflake + } + + /** https://discord.com/developers/docs/resources/webhook#edit-webhook-message-jsonform-params */ + export interface MessageBody { + /** the message contents (up to 2000 characters) */ + content: string + /** embedded rich content */ + embeds: Embed[] + /** allowed mentions for the message */ + allowed_mentions: AllowedMentions + /** the components to include with the message */ + components: Component[] + /** the contents of the file being sent/edited */ + files: any + /** JSON encoded body of non-file params (multipart/form-data only) */ + payload_json: string + /** attached files to keep and possible descriptions for new files */ + attachments: Partial[] + } } /** https://discord.com/developers/docs/topics/gateway#webhooks-update-webhook-update-event-fields */ @@ -53,41 +129,83 @@ declare module './gateway' { } } -export interface ModifyWebhookParams { - /** the default name of the webhook */ - name?: string - /** data image for the default webhook avatar */ - avatar?: string - /** the new channel id this webhook should be moved to */ - channel_id?: snowflake -} - declare module './internal' { interface Internal { - /** https://discord.com/developers/docs/resources/webhook#get-channel-webhooks */ + /** + * Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations: + * @see https://discord.com/developers/docs/resources/webhook#create-webhook + */ + createWebhook(channel_id: snowflake, params: Webhook.CreateParams): Promise + /** + * Returns a list of channel webhook objects. Requires the MANAGE_WEBHOOKS permission. + * @see https://discord.com/developers/docs/resources/webhook#get-channel-webhooks + */ getChannelWebhooks(channel_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/webhook#get-guild-webhooks */ + /** + * Returns a list of guild webhook objects. Requires the MANAGE_WEBHOOKS permission. + * @see https://discord.com/developers/docs/resources/webhook#get-guild-webhooks + */ getGuildWebhooks(guild_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/webhook#get-webhook */ + /** + * Returns the new webhook object for the given id. + * @see https://discord.com/developers/docs/resources/webhook#get-webhook + */ getWebhook(webhook_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/webhook#get-webhook-with-token */ + /** + * Same as above, except this call does not require authentication and returns no user in the webhook object. + * @see https://discord.com/developers/docs/resources/webhook#get-webhook-with-token + */ getWebhookWithToken(webhook_id: snowflake, token: string): Promise - /** https://discord.com/developers/docs/resources/webhook#modify-webhook */ - modifyWebhook(webhook_id: snowflake, options: ModifyWebhookParams): Promise - /** https://discord.com/developers/docs/resources/webhook#modify-webhook-with-token */ - modifyWebhookWithToken(webhook_id: snowflake, token: string, options: ModifyWebhookParams): Promise - /** https://discord.com/developers/docs/resources/webhook#delete-webhook */ + /** + * Modify a webhook. Requires the MANAGE_WEBHOOKS permission. Returns the updated webhook object on success. + * @see https://discord.com/developers/docs/resources/webhook#modify-webhook + */ + modifyWebhook(webhook_id: snowflake, params: Webhook.ModifyParams): Promise + /** + * Same as above, except this call does not require authentication, does not accept a channel_id parameter in the body, and does not return a user in the webhook object. + * @see https://discord.com/developers/docs/resources/webhook#modify-webhook-with-token + */ + modifyWebhookWithToken(webhook_id: snowflake, token: string, params: Webhook.ModifyParams): Promise + /** + * Delete a webhook permanently. Requires the MANAGE_WEBHOOKS permission. Returns a 204 No Content response on success. + * @see https://discord.com/developers/docs/resources/webhook#delete-webhook + */ deleteWebhook(webhook_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/webhook#delete-webhook-with-token */ - deleteWebhookWithToken(webhook_id: snowflake, token: string): Promise - /** https://discord.com/developers/docs/resources/webhook#execute-webhook */ - /** https://discord.com/developers/docs/resources/webhook#execute-slackcompatible-webhook */ - /** https://discord.com/developers/docs/resources/webhook#execute-githubcompatible-webhook */ - /** https://discord.com/developers/docs/resources/webhook#get-webhook-message */ - getWebhookMessage(webhook_id: snowflake, token: string, message_id: snowflake): Promise - /** https://discord.com/developers/docs/resources/webhook#edit-webhook-message */ - /** https://discord.com/developers/docs/resources/webhook#delete-webhook-message */ - deleteWebhookMessage(webhook_id: snowflake, token: string, message_id: snowflake): Promise + /** + * Same as above, except this call does not require authentication. + * @see https://discord.com/developers/docs/resources/webhook#delete-webhook-with-token + */ + deleteWebhookwithToken(webhook_id: snowflake, token: string): Promise + /** + * Refer to Uploading Files for details on attachments and multipart/form-data requests. + * @see https://discord.com/developers/docs/resources/webhook#execute-webhook + */ + executeWebhook(webhook_id: snowflake, token: string, body: Webhook.ExecuteBody, query: Webhook.ExecuteParams): Promise + /** + * Refer to Slack's documentation for more information. We do not support Slack's channel, icon_emoji, mrkdwn, or mrkdwn_in properties. + * @see https://discord.com/developers/docs/resources/webhook#execute-slackcompatible-webhook + */ + executeSlackCompatibleWebhook(webhook_id: snowflake, token: string, body: null, query: Webhook.ExecuteParams): Promise + /** + * Add a new webhook to your GitHub repo (in the repo's settings), and use this endpoint as the "Payload URL." You can choose what events your Discord channel receives by choosing the "Let me select individual events" option and selecting individual events for the new webhook you're configuring. + * @see https://discord.com/developers/docs/resources/webhook#execute-githubcompatible-webhook + */ + executeGitHubCompatibleWebhook(webhook_id: snowflake, token: string, body: null, query: Webhook.ExecuteParams): Promise + /** + * Returns a previously-sent webhook message from the same token. Returns a message object on success. + * @see https://discord.com/developers/docs/resources/webhook#get-webhook-message + */ + getWebhookMessage(webhook_id: snowflake, token: string, message_id: snowflake, params: Webhook.MessageParams): Promise + /** + * Edits a previously-sent webhook message from the same token. Returns a message object on success. + * @see https://discord.com/developers/docs/resources/webhook#edit-webhook-message + */ + editWebhookMessage(webhook_id: snowflake, token: string, message_id: snowflake, body: Webhook.MessageBody, query: Webhook.MessageParams): Promise + /** + * Deletes a message that was created by the webhook. Returns a 204 No Content response on success. + * @see https://discord.com/developers/docs/resources/webhook#delete-webhook-message + */ + deleteWebhookMessage(webhook_id: snowflake, token: string, message_id: snowflake, params: Webhook.MessageParams): Promise } } From d481bfe401292a2eca0034558ccec8dfe31381d3 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 15:12:31 +0800 Subject: [PATCH 17/34] feat(cli): inherit unknown options --- package.json | 2 +- packages/cli/src/start.ts | 28 ++++++++++++++++------------ packages/core/package.json | 2 +- packages/core/src/session.ts | 5 ++++- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 5ead9b8d6b..857b3a882b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "lint": "eslint packages/**/src/**/*.ts --fix --cache", "pub": "node -r ./build/register build/publish", "scaffold": "yarn compile koishi && yarn create-koishi test", - "start": "yarn compile && yarn workspace test koishi start", + "start": "yarn compile && cross-env TS_NODE_PROJECT=../tsconfig.test.json yarn workspace test koishi start --watch .. -r ../build/register", "shiki": "yarn workspace bot-shiki" }, "version": "1.0.0", diff --git a/packages/cli/src/start.ts b/packages/cli/src/start.ts index 69f888d5eb..08d22cffe8 100644 --- a/packages/cli/src/start.ts +++ b/packages/cli/src/start.ts @@ -1,13 +1,9 @@ -import { isInteger } from '@koishijs/utils' +import { Dict, hyphenate, isInteger } from '@koishijs/utils' import { fork, ChildProcess } from 'child_process' import { resolve } from 'path' import { CAC } from 'cac' import kleur from 'kleur' -interface WorkerOptions { - '--'?: string[] -} - let child: ChildProcess process.on('SIGINT', () => { @@ -25,9 +21,16 @@ interface Message { let buffer = null -function createWorker(options: WorkerOptions) { +function toArg(key: string) { + return key.length === 1 ? `-${key}` : `--${hyphenate(key)}` +} + +function createWorker(options: Dict) { + const execArgv = Object.entries(options).flatMap(([key, value]) => key === '--' ? [] : [toArg(key), value]) + execArgv.push(...options['--']) + child = fork(resolve(__dirname, 'worker'), [], { - execArgv: ['--enable-source-maps', ...options['--']], + execArgv: Object.entries(options).flatMap(([key, value]) => [toArg(key), value]), }) let config: { autoRestart: boolean } @@ -72,21 +75,22 @@ function setEnvArg(name: string, value: string | boolean) { export default function (cli: CAC) { cli.command('start [file]', 'start a koishi bot') .alias('run') + .allowUnknownOptions() .option('--debug [namespace]', 'specify debug namespace') .option('--log-level [level]', 'specify log level (default: 2)') .option('--log-time [format]', 'show timestamp in logs') .option('--watch [path]', 'watch and reload at change') .action((file, options) => { - const { logLevel } = options + const { logLevel, debug, logTime, watch, ...rest } = options if (logLevel !== undefined && (!isInteger(logLevel) || logLevel < 0)) { console.warn(`${kleur.red('error')} log level should be a positive integer.`) process.exit(1) } - setEnvArg('KOISHI_WATCH_ROOT', options.watch) - setEnvArg('KOISHI_LOG_TIME', options.logTime) + setEnvArg('KOISHI_WATCH_ROOT', watch) + setEnvArg('KOISHI_LOG_TIME', logTime) process.env.KOISHI_LOG_LEVEL = logLevel || '' - process.env.KOISHI_DEBUG = options.debug || '' + process.env.KOISHI_DEBUG = debug || '' process.env.KOISHI_CONFIG_FILE = file || '' - createWorker(options) + createWorker(rest) }) } diff --git a/packages/core/package.json b/packages/core/package.json index 6762fae607..3bd6278495 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,6 +39,6 @@ "dependencies": { "@koishijs/utils": "^5.0.0-rc.0", "fastest-levenshtein": "^1.0.12", - "schemastery": "^2.1.2" + "schemastery": "^2.1.3" } } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index d2d03c804c..655d822b87 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -140,7 +140,10 @@ export class Session { + logger.warn(error) + return null + }) } return content } From e159d0d4fb25451eb694c2cad08476be5b2461e5 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 16:55:16 +0800 Subject: [PATCH 18/34] cli+console+manager: fix resolve behavior --- packages/cli/src/addons/watcher.ts | 2 +- packages/cli/src/addons/writer.ts | 4 +-- packages/cli/src/loader.ts | 3 +- packages/cli/src/start.ts | 2 +- plugins/frontend/console/src/index.ts | 8 +++-- plugins/frontend/manager/src/packages.ts | 46 ++++++++++++++---------- tsconfig.test.json | 41 +++++++++++++++++---- 7 files changed, 72 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/addons/watcher.ts b/packages/cli/src/addons/watcher.ts index c2f8e1149f..d62dfc40f2 100644 --- a/packages/cli/src/addons/watcher.ts +++ b/packages/cli/src/addons/watcher.ts @@ -101,7 +101,7 @@ export default class FileWatcher extends Service { const declined = new Set(this.externals) const visited = new Set() - function traverse(filename: string) { + const traverse = (filename: string) => { if (declined.has(filename) || filename.includes('/node_modules/')) return visited.add(filename) const { children } = require.cache[filename] diff --git a/packages/cli/src/addons/writer.ts b/packages/cli/src/addons/writer.ts index 09f1a0880d..49c9e6f2cc 100644 --- a/packages/cli/src/addons/writer.ts +++ b/packages/cli/src/addons/writer.ts @@ -30,7 +30,7 @@ export default class ConfigWriter extends Service { } async unloadPlugin(name: string) { - const plugin = this.loader.cache[name] + const plugin = this.loader.resolvePlugin(name) await this.ctx.dispose(plugin) this.config.plugins['~' + name] = this.config.plugins[name] delete this.config.plugins[name] @@ -38,7 +38,7 @@ export default class ConfigWriter extends Service { } async reloadPlugin(name: string, config: any) { - const plugin = this.loader.cache[name] + const plugin = this.loader.resolvePlugin(name) const state = this.ctx.app.registry.get(plugin) await this.ctx.dispose(plugin) state.context.plugin(plugin, config) diff --git a/packages/cli/src/loader.ts b/packages/cli/src/loader.ts index 3a38d9e189..8178c5548c 100644 --- a/packages/cli/src/loader.ts +++ b/packages/cli/src/loader.ts @@ -62,7 +62,8 @@ export class Loader { } resolvePlugin(name: string) { - return this.cache[name] = Modules.require(name, true) + const path = Modules.resolve(name) + return this.cache[path] = Modules.require(name, true) } loadPlugin(name: string, options?: any) { diff --git a/packages/cli/src/start.ts b/packages/cli/src/start.ts index 08d22cffe8..4fc7dc33c5 100644 --- a/packages/cli/src/start.ts +++ b/packages/cli/src/start.ts @@ -30,7 +30,7 @@ function createWorker(options: Dict) { execArgv.push(...options['--']) child = fork(resolve(__dirname, 'worker'), [], { - execArgv: Object.entries(options).flatMap(([key, value]) => [toArg(key), value]), + execArgv, }) let config: { autoRestart: boolean } diff --git a/plugins/frontend/console/src/index.ts b/plugins/frontend/console/src/index.ts index 16e8b0c2c2..55e029c88d 100644 --- a/plugins/frontend/console/src/index.ts +++ b/plugins/frontend/console/src/index.ts @@ -1,4 +1,4 @@ -import { App, Context, Logger, noop, version, Dict, WebSocketLayer, Schema, Awaitable, Service } from 'koishi' +import { App, Context, Logger, noop, version, Dict, WebSocketLayer, Schema, Awaitable, Service, sleep } from 'koishi' import { resolve, extname } from 'path' import { promises as fs, Stats, createReadStream } from 'fs' import WebSocket from 'ws' @@ -34,8 +34,10 @@ export abstract class DataSource { constructor(protected ctx: Context, protected name: keyof Sources) { ctx.console.services[name] = this as never - ctx.on('ready', () => this.start()) - ctx.on('dispose', () => this.stop()) + sleep(0).then(() => { + ctx.on('ready', () => this.start()) + ctx.on('dispose', () => this.stop()) + }) } async broadcast(value?: T) { diff --git a/plugins/frontend/manager/src/packages.ts b/plugins/frontend/manager/src/packages.ts index cb00304c2b..dcc0a0f928 100644 --- a/plugins/frontend/manager/src/packages.ts +++ b/plugins/frontend/manager/src/packages.ts @@ -40,31 +40,37 @@ export class PackageProvider extends DataSource> { this.broadcast() } + private async loadDirectory(baseDir: string) { + const base = baseDir + '/node_modules' + const files = await readdir(base).catch(() => []) + for (const name of files) { + const base2 = base + '/' + name + if (name.startsWith('@')) { + const files = await readdir(base2).catch(() => []) + for (const name2 of files) { + if (name === '@koishijs' && name2.startsWith('plugin-') || name2.startsWith('koishi-plugin-')) { + this.loadPackage(name + '/' + name2, base2 + '/' + name2) + } + } + } else { + if (name.startsWith('koishi-plugin-')) { + this.loadPackage(name, base2) + } + } + } + } + async prepare() { // load local packages let { baseDir } = this.ctx.app + const tasks: Promise[] = [] while (1) { - const base = baseDir + '/node_modules' - const files = await readdir(base).catch(() => []) - for (const name of files) { - const base2 = base + '/' + name - if (name.startsWith('@')) { - const files = await readdir(base2).catch(() => []) - for (const name2 of files) { - if (name === '@koishijs' && name2.startsWith('plugin-') || name2.startsWith('koishi-plugin-')) { - this.loadPackage(base2 + '/' + name2) - } - } - } else { - if (name.startsWith('koishi-plugin-')) { - this.loadPackage(base2) - } - } - } + tasks.push(this.loadDirectory(baseDir)) const parent = dirname(baseDir) if (baseDir === parent) break baseDir = parent } + await Promise.all(tasks) } async get(forced = false) { @@ -85,8 +91,10 @@ export class PackageProvider extends DataSource> { return Object.fromEntries(packages.filter(x => x).map(data => [data.name, data])) } - private loadPackage(path: string) { - this.cache[require.resolve(path)] = this.parsePackage(path) + private loadPackage(name: string, path: string) { + // require.resolve(name) may be different from require.resolve(path) + // because tsconfig-paths may resolve the path differently + this.cache[require.resolve(name)] = this.parsePackage(path) } private async parsePackage(path: string) { diff --git a/tsconfig.test.json b/tsconfig.test.json index 05e0498f01..9439d2f932 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -3,23 +3,50 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@koishijs/plugin-eval/lib/worker": ["plugins/eval/src/worker"], - "@koishijs/plugin-adapter-*": ["plugins/adapter/*/src"], - "@koishijs/plugin-assets-*": ["plugins/assets/*/src"], - "@koishijs/plugin-cache-*": ["plugins/cache/*/src"], - "@koishijs/plugin-database-*": ["plugins/database/*/src"], + "@koishijs/plugin-eval/lib/worker": [ + "plugins/eval/src/worker", + ], + "@koishijs/plugin-adapter-*": [ + "plugins/adapter/*/src", + "community/adapter-*/src", + ], + "@koishijs/plugin-assets-*": [ + "plugins/assets/*/src", + "community/assets-*/src", + ], + "@koishijs/plugin-cache-*": [ + "plugins/cache/*/src", + "community/cache-*/src", + ], + "@koishijs/plugin-database-*": [ + "plugins/database/*/src", + "community/database-*/src", + ], "@koishijs/plugin-*": [ + "plugins/a11y/*/src", "plugins/common/*/src", "plugins/frontend/*/src", "plugins/*/src", ], "@koishijs/*": [ "packages/*/src", + "plugins/a11y/*/src", + "plugins/adapter/*/src", + "plugins/assets/*/src", + "plugins/cache/*/src", "plugins/common/*/src", "plugins/database/*/src", + "plugins/frontend/*/src", + ], + "koishi-plugin-*": [ + "community/*/src", + ], + "koishi": [ + "packages/koishi/src", + ], + "*": [ + "community/*/src", ], - "koishi": ["packages/koishi/src"], - "*": ["community/*/src"], }, }, } \ No newline at end of file From 363daa23da5cf5c402df7900a97fa1587f06da91 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Wed, 12 Jan 2022 17:04:28 +0800 Subject: [PATCH 19/34] fix(forward): get assignee if selfId is not present --- docs/api/migration.md | 1 - plugins/adapter/discord/src/types/internal.ts | 7 +++--- plugins/common/forward/src/index.ts | 25 +++++++++++++------ plugins/common/forward/tests/index.spec.ts | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/api/migration.md b/docs/api/migration.md index 260345f1d7..b763aa3ebc 100644 --- a/docs/api/migration.md +++ b/docs/api/migration.md @@ -125,7 +125,6 @@ export default { - 接口变更 - 新增了方法 `db.set(table, query, updates)` - - 废弃了方法 `db.getAssignedChannels()`(目前暂无替代品,原接口仍然可用) - `db.update()` 更名为 `db.upsert()`,语法不变 - 数据结构变更 - channel 表使用 `platform`+`id` 复合主键进行索引,这意味着 `channel.id` 语义将发生变化,同时新增了 `channel.platform` diff --git a/plugins/adapter/discord/src/types/internal.ts b/plugins/adapter/discord/src/types/internal.ts index 7bec198116..ec96073b9a 100644 --- a/plugins/adapter/discord/src/types/internal.ts +++ b/plugins/adapter/discord/src/types/internal.ts @@ -10,8 +10,9 @@ export class Internal { const method = key as Method for (const name of makeArray(routes[path][method])) { Internal.prototype[name] = function (this: Internal, ...args: any[]) { + const raw = args.join(', ') const url = path.replace(/\{([^}]+)\}/g, () => { - if (!args.length) throw new Error('too few arguments') + if (!args.length) throw new Error(`too few arguments for ${path}, received ${raw}`) return args.shift() }) const config: AxiosRequestConfig = {} @@ -24,8 +25,8 @@ export class Internal { } else if (args.length === 2 && method !== 'GET' && method !== 'DELETE') { config.data = args[0] config.params = args[1] - } else { - throw new Error('too many arguments') + } else if (args.length > 1) { + throw new Error(`too many arguments for ${path}, received ${raw}`) } return this.http(method, url, config) } diff --git a/plugins/common/forward/src/index.ts b/plugins/common/forward/src/index.ts index 738bdbe1a9..4d94ffe560 100644 --- a/plugins/common/forward/src/index.ts +++ b/plugins/common/forward/src/index.ts @@ -5,14 +5,14 @@ template.set('forward', '{0}: {1}') export interface Rule { source: string - destination: string + target: string selfId?: string lifespan?: number } export const Rule = Schema.object({ source: Schema.string().required(), - destination: Schema.string().required(), + target: Schema.string().required(), selfId: Schema.string(), lifespan: Schema.number(), }) @@ -33,14 +33,23 @@ export const schema = Schema.union([ export function apply(ctx: Context, { rules }: Config) { const relayMap: Dict = {} - async function sendRelay(session: Session, { destination, selfId, lifespan = Time.hour }: Rule) { - const [platform, channelId] = parsePlatform(destination) - const bot = ctx.bots.get(`${platform}:${selfId}`) + async function sendRelay(session: Session, { target, selfId, lifespan = Time.hour }: Rule) { const { author, parsed } = session if (!parsed.content) return + + // get selfId + const [platform, channelId] = parsePlatform(target) + if (!selfId) { + if (!ctx.database) throw new Error('database service is required when selfId is not specified') + const channel = await ctx.database.getChannel(platform, channelId, ['assignee']) + if (!channel || !channel.assignee) return + selfId = channel.assignee + } + + const bot = ctx.bots.get(`${platform}:${selfId}`) const content = template('forward', author.nickname || author.username, parsed.content) const id = await bot.sendMessage(channelId, content, 'unknown') - relayMap[id] = { source: destination, destination: session.cid, selfId: session.selfId, lifespan } + relayMap[id] = { source: target, target: session.cid, selfId: session.selfId, lifespan } setTimeout(() => delete relayMap[id], lifespan) } @@ -51,9 +60,9 @@ export function apply(ctx: Context, { rules }: Config) { const tasks: Promise[] = [] for (const options of rules) { if (session.cid !== options.source) continue - tasks.push(sendRelay(session, options).catch()) + tasks.push(sendRelay(session, options)) } - const [result] = await Promise.all([next(), ...tasks]) + const [result] = await Promise.all([next(), Promise.allSettled(tasks)]) return result }) } diff --git a/plugins/common/forward/tests/index.spec.ts b/plugins/common/forward/tests/index.spec.ts index c5f09f9d7c..dacd64f812 100644 --- a/plugins/common/forward/tests/index.spec.ts +++ b/plugins/common/forward/tests/index.spec.ts @@ -12,7 +12,7 @@ const session3 = app.mock.client('789', '654') app.plugin(forward, [{ source: 'mock:456', - destination: 'mock:654', + target: 'mock:654', selfId: DEFAULT_SELF_ID, }]) From 52274ee7da42174eec39833cc77e51b878b0586a Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Thu, 13 Jan 2022 22:08:48 +0800 Subject: [PATCH 20/34] feat(core): bot.sendMessage() return string[] --- docs/api/core/bot.md | 4 ++-- docs/guide/adapter/writing.md | 4 ++-- packages/core/src/bot.ts | 6 +++--- plugins/a11y/schedule/tests/index.spec.ts | 1 + plugins/common/feedback/src/index.ts | 18 ++++++++++++------ plugins/common/feedback/tests/index.spec.ts | 2 +- plugins/common/forward/src/index.ts | 19 ++++++++++++------- plugins/common/forward/tests/index.spec.ts | 6 +++--- plugins/github/tests/index.spec.ts | 5 +++-- 9 files changed, 39 insertions(+), 26 deletions(-) diff --git a/docs/api/core/bot.md b/docs/api/core/bot.md index 7a05c2941c..aa09751004 100644 --- a/docs/api/core/bot.md +++ b/docs/api/core/bot.md @@ -90,7 +90,7 @@ sidebarDepth: 2 - **channelId:** `string` 频道 ID - **content:** `string` 要发送的内容 -- 返回值: `Promise` 发送的消息 ID +- 返回值: `Promise` 发送的消息 ID 向特定频道发送消息。 @@ -98,7 +98,7 @@ sidebarDepth: 2 - **userId:** `string` 对方 ID - **content:** `string` 要发送的内容 -- 返回值: `Promise` 发送的消息 ID +- 返回值: `Promise` 发送的消息 ID 向特定用户发送私聊消息。 diff --git a/docs/guide/adapter/writing.md b/docs/guide/adapter/writing.md index 33bdcfceeb..b111d00698 100644 --- a/docs/guide/adapter/writing.md +++ b/docs/guide/adapter/writing.md @@ -55,7 +55,7 @@ class MyBot extends Bot { async sendMessage(channelId, content) { // 这里应该执行发送操作 this.logger.debug('send:', content) - return Random.uuid() + return [] } } @@ -84,7 +84,7 @@ class MyBot extends Bot { async sendMessage(channelId: string, content: string) { // 这里应该执行发送操作 this.logger.debug('send:', content) - return Random.uuid() + return [] } } diff --git a/packages/core/src/bot.ts b/packages/core/src/bot.ts index 24badb6187..7f79a5a970 100644 --- a/packages/core/src/bot.ts +++ b/packages/core/src/bot.ts @@ -103,7 +103,7 @@ export abstract class Bot { for (let index = 0; index < channels.length; index++) { if (index && delay) await sleep(delay) try { - messageIds.push(await this.sendMessage(channels[index], content, 'unknown')) + messageIds.push(...await this.sendMessage(channels[index], content, 'unknown')) } catch (error) { this.app.logger('bot').warn(error) } @@ -130,8 +130,8 @@ export namespace Bot { export interface Methods { // message - sendMessage(channelId: string, content: string, guildId?: string): Promise - sendPrivateMessage(userId: string, content: string): Promise + sendMessage(channelId: string, content: string, guildId?: string): Promise + sendPrivateMessage(userId: string, content: string): Promise getMessage(channelId: string, messageId: string): Promise editMessage(channelId: string, messageId: string, content: string): Promise deleteMessage(channelId: string, messageId: string): Promise diff --git a/plugins/a11y/schedule/tests/index.spec.ts b/plugins/a11y/schedule/tests/index.spec.ts index e5c0507dad..ae95c25e78 100644 --- a/plugins/a11y/schedule/tests/index.spec.ts +++ b/plugins/a11y/schedule/tests/index.spec.ts @@ -5,6 +5,7 @@ import memory from '@koishijs/plugin-database-memory' import mock from '@koishijs/plugin-mock' import jest from 'jest-mock' import { expect } from 'chai' +import 'chai-shape' const app = new App().plugin(mock) const client1 = app.mock.client('123', '456') diff --git a/plugins/common/feedback/src/index.ts b/plugins/common/feedback/src/index.ts index 68d98cd776..36b0f64666 100644 --- a/plugins/common/feedback/src/index.ts +++ b/plugins/common/feedback/src/index.ts @@ -1,4 +1,4 @@ -import { Context, noop, Schema, sleep, template } from 'koishi' +import { Context, Dict, Schema, sleep, template, Time } from 'koishi' import { parsePlatform } from '@koishijs/command-utils' template.set('feedback', { @@ -9,6 +9,7 @@ template.set('feedback', { export interface Config { operators?: string[] + replyTimeout?: number } export const schema: Schema = Schema.union([ @@ -20,9 +21,9 @@ export const schema: Schema = Schema.union([ export const name = 'feedback' -export function apply(ctx: Context, { operators = [] }: Config) { +export function apply(ctx: Context, { operators = [], replyTimeout = Time.day }: Config) { type FeedbackData = [sid: string, channelId: string, guildId: string] - const feedbacks: Record = {} + const feedbacks: Dict = {} ctx.command('feedback ', '发送反馈信息给作者') .userFields(['name', 'id']) @@ -37,9 +38,14 @@ export function apply(ctx: Context, { operators = [] }: Config) { if (index && delay) await sleep(delay) const [platform, userId] = parsePlatform(operators[index]) const bot = ctx.bots.find(bot => bot.platform === platform) - await bot - .sendPrivateMessage(userId, message) - .then(id => feedbacks[id] = data, noop) + await bot.sendPrivateMessage(userId, message).then((ids) => { + for (const id of ids) { + feedbacks[id] = data + ctx.setTimeout(() => delete feedbacks[id], replyTimeout) + } + }, (error) => { + ctx.logger('bot').warn(error) + }) } return template('feedback.success') }) diff --git a/plugins/common/feedback/tests/index.spec.ts b/plugins/common/feedback/tests/index.spec.ts index 97e5ae3145..4896603be4 100644 --- a/plugins/common/feedback/tests/index.spec.ts +++ b/plugins/common/feedback/tests/index.spec.ts @@ -16,7 +16,7 @@ before(() => app.start()) describe('@koishijs/plugin-feedback', () => { it('basic support', async () => { - const send1 = app.bots[0].sendPrivateMessage = jest.fn(async () => '1000') + const send1 = app.bots[0].sendPrivateMessage = jest.fn(async () => ['1000']) await client.shouldReply('feedback', '请输入要发送的文本。') expect(send1.mock.calls).to.have.length(0) await client.shouldReply('feedback foo', '反馈信息发送成功!') diff --git a/plugins/common/forward/src/index.ts b/plugins/common/forward/src/index.ts index 4d94ffe560..1962b09319 100644 --- a/plugins/common/forward/src/index.ts +++ b/plugins/common/forward/src/index.ts @@ -7,33 +7,33 @@ export interface Rule { source: string target: string selfId?: string - lifespan?: number } export const Rule = Schema.object({ source: Schema.string().required(), target: Schema.string().required(), selfId: Schema.string(), - lifespan: Schema.number(), }) export const name = 'forward' export interface Config { rules: Rule[] + lifespan?: number } export const schema = Schema.union([ Schema.object({ rules: Schema.array(Rule), + lifespan: Schema.number(), }), Schema.transform(Schema.array(Rule), (rules) => ({ rules })), ]) -export function apply(ctx: Context, { rules }: Config) { +export function apply(ctx: Context, { rules, lifespan = Time.hour }: Config) { const relayMap: Dict = {} - async function sendRelay(session: Session, { target, selfId, lifespan = Time.hour }: Rule) { + async function sendRelay(session: Session, { target, selfId }: Rule) { const { author, parsed } = session if (!parsed.content) return @@ -48,9 +48,14 @@ export function apply(ctx: Context, { rules }: Config) { const bot = ctx.bots.get(`${platform}:${selfId}`) const content = template('forward', author.nickname || author.username, parsed.content) - const id = await bot.sendMessage(channelId, content, 'unknown') - relayMap[id] = { source: target, target: session.cid, selfId: session.selfId, lifespan } - setTimeout(() => delete relayMap[id], lifespan) + await bot.sendMessage(channelId, content).then((ids) => { + for (const id of ids) { + relayMap[id] = { source: target, target: session.cid, selfId: session.selfId } + ctx.setTimeout(() => delete relayMap[id], lifespan) + } + }, (error) => { + ctx.logger('bot').warn(error) + }) } ctx.middleware(async (session, next) => { diff --git a/plugins/common/forward/tests/index.spec.ts b/plugins/common/forward/tests/index.spec.ts index dacd64f812..3ae1e75acf 100644 --- a/plugins/common/forward/tests/index.spec.ts +++ b/plugins/common/forward/tests/index.spec.ts @@ -18,9 +18,9 @@ app.plugin(forward, [{ before(() => app.start()) -describe('Relay Plugin', () => { +describe('@koishijs/plugin-forward', () => { it('basic support', async () => { - const send = app.bots[0].sendMessage = jest.fn(async () => '2000') + const send = app.bots[0].sendMessage = jest.fn(async () => ['2000']) await session2.shouldNotReply('hello') expect(send.mock.calls).to.have.length(1) expect(send.mock.calls).to.have.shape([['654', '123: hello']]) @@ -33,7 +33,7 @@ describe('Relay Plugin', () => { expect(send.mock.calls).to.have.shape([['456', '789: hello']]) send.mockClear() - send.mockImplementation(async () => '3000') + send.mockImplementation(async () => ['3000']) await session2.shouldNotReply('[CQ:quote,id=3000] hello') expect(send.mock.calls).to.have.length(1) expect(send.mock.calls).to.have.shape([['654', '123: hello']]) diff --git a/plugins/github/tests/index.spec.ts b/plugins/github/tests/index.spec.ts index e25958ceca..8147f59755 100644 --- a/plugins/github/tests/index.spec.ts +++ b/plugins/github/tests/index.spec.ts @@ -8,6 +8,7 @@ import * as github from '@koishijs/plugin-github' import memory from '@koishijs/plugin-database-memory' import mock from '@koishijs/plugin-mock' import { Method } from 'axios' +import 'chai-shape' const app = new App({ port: 10000, @@ -62,7 +63,7 @@ describe('GitHub Plugin', () => { }) it('github.authorize', async () => { - const uuid = jest.spyOn(Random, 'uuid') + const uuid = jest.spyOn(Random, 'id') uuid.mockReturnValue('foo-bar-baz') await ses.shouldReply('.github.authorize', '请输入用户名。') await ses.shouldReply('.github.authorize satori', /^请点击下面的链接继续操作:/) @@ -151,7 +152,7 @@ describe('GitHub Plugin', () => { it(title, async () => { sendMessage.mockClear() sendMessage.mockImplementation(() => { - return Promise.resolve(idMap[title] = Random.id()) + return Promise.resolve([idMap[title] = Random.id()]) }) const payload = require(`./fixtures/${title}`) From a254316809a4527d902bff7caaa7af03a21bffd7 Mon Sep 17 00:00:00 2001 From: Anillc Date: Thu, 13 Jan 2022 22:14:26 +0800 Subject: [PATCH 21/34] fix(adapter-telegram): enhance telegram adapter (#445) --- plugins/adapter/telegram/src/bot.ts | 60 ++++++++++++++++++++++------ plugins/adapter/telegram/src/http.ts | 36 ++++++++--------- 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/plugins/adapter/telegram/src/bot.ts b/plugins/adapter/telegram/src/bot.ts index f74b26ab85..85775cf6cb 100644 --- a/plugins/adapter/telegram/src/bot.ts +++ b/plugins/adapter/telegram/src/bot.ts @@ -1,8 +1,10 @@ import fileType from 'file-type' import { createReadStream } from 'fs' +import FormData from 'form-data' import { Adapter, assertProperty, Bot, camelCase, Dict, Logger, Quester, renameProperty, Schema, segment, snakeCase } from 'koishi' import * as Telegram from './types' import { AdapterConfig } from './utils' +import { AxiosError } from 'axios' const logger = new Logger('telegram') @@ -47,10 +49,6 @@ export const BotConfig: Schema = Schema.object({ ]).description('通过长轮询获取更新时请求的超时。单位为秒;true 为使用默认值 1 分钟。详情请见 Telegram API 中 getUpdate 的参数 timeout。不设置即使用 webhook 获取更新。'), }) -export interface TelegramBot { - _request?(action: `/${string}`, params: Dict, field?: string, content?: Buffer, filename?: string): Promise -} - async function maybeFile(payload: Dict, field: TLAssetType): Promise<[any, string?, Buffer?, string?]> { if (!payload[field]) return [payload] let content @@ -112,16 +110,45 @@ export class TelegramBot extends Bot { } /** - * Request telegram API (using post method actually) + * Request telegram API + * @param action method of telegram API. Starts with a '/' + * @param params params in camelCase + * @returns Respond form telegram + */ + async get(action: `/${string}`, params: P = undefined): Promise { + this.logger.debug('[request] %s %o', action, params) + const response = await this.http.get(action, { + params: snakeCase(params || {}) + }) + this.logger.debug('[response] %o', response) + const { ok, result } = response + if (ok) return camelCase(result) + throw new SenderError(params, action, -1, this.selfId) + } + + /** + * Request telegram API * @param action method of telegram API. Starts with a '/' * @param params params in camelCase * @param field file field key in fromData * @param content file stream * @returns Respond form telegram */ - async get(action: `/${string}`, params: P = undefined, field = '', content: Buffer = null, filename = 'file'): Promise { + async post(action: `/${string}`, params: P = undefined, field = '', content: Buffer = null, filename = 'file'): Promise { this.logger.debug('[request] %s %o', action, params) - const response = await this._request(action, snakeCase(params || {}), field, content, filename) + const payload = new FormData() + for (const key in params) { + payload.append(snakeCase(key), params[key].toString()) + } + if (field && content) payload.append(field, content, filename) + let response: any + try { + response = await this.http.post(action, payload, { + headers: payload.getHeaders() + }) + } catch (e) { + response = (e as AxiosError).response.data + } this.logger.debug('[response] %o', response) const { ok, result } = response if (ok) return camelCase(result) @@ -155,7 +182,7 @@ export class TelegramBot extends Bot { video: '/sendVideo', animation: '/sendAnimation', } - lastMsg = await this.get(assetApi[currAssetType], ...await maybeFile(payload, currAssetType)) + lastMsg = await this.post(assetApi[currAssetType], ...await maybeFile(payload, currAssetType)) currAssetType = null delete payload[currAssetType] delete payload.replyToMessage @@ -215,17 +242,26 @@ export class TelegramBot extends Bot { return lastMsg ? lastMsg.messageId.toString() : null } - async sendMessage(channelId: string, content: string, subtype: 'group' | 'private' = 'group') { + async sendMessage(channelId: string, content: string) { if (!content) return - const session = this.createSession({ content, channelId, subtype, guildId: channelId }) + let subtype: string + let chatId: string + if (channelId.startsWith('private:')) { + subtype = 'private' + chatId = channelId.slice(8) + } else { + subtype = 'group' + chatId = channelId + } + const session = this.createSession({ content, channelId, guildId: channelId }) if (await this.app.serial(session, 'before-send', session)) return - session.messageId = await this._sendMessage(channelId, session.content) + session.messageId = await this._sendMessage(chatId, session.content) this.app.emit(session, 'send', session) return session.messageId } async sendPrivateMessage(userId: string, content: string) { - return this.sendMessage(userId, content, 'private') + return this.sendMessage(userId, content) } async getMessage() { diff --git a/plugins/adapter/telegram/src/http.ts b/plugins/adapter/telegram/src/http.ts index 528b56c12a..2ea47780a2 100644 --- a/plugins/adapter/telegram/src/http.ts +++ b/plugins/adapter/telegram/src/http.ts @@ -1,4 +1,3 @@ -import axios, { AxiosError } from 'axios' import FormData from 'form-data' import { Adapter, assertProperty, camelCase, Context, Logger, sanitize, segment, Session, trimSlash } from 'koishi' import { BotConfig, TelegramBot } from './bot' @@ -31,18 +30,6 @@ abstract class TelegramAdapter extends Adapter { abstract listenUpdates(bot: TelegramBot): Promise async connect(bot: TelegramBot): Promise { - bot._request = async (action, params, field, content, filename) => { - const payload = new FormData() - for (const key in params) { - payload.append(key, params[key].toString()) - } - if (field && content) payload.append(field, content, filename) - try { - return await bot.http.post(action, payload, payload.getHeaders()) - } catch (e) { - return (e as AxiosError).response.data - } - } const { username, userId, avatar, nickname } = await bot.getLoginInfo() bot.username = username bot.avatar = avatar @@ -110,14 +97,17 @@ abstract class TelegramAdapter extends Adapter { const getFileData = async (fileId) => { try { const file = await bot.get('/getFile', { fileId }) - const downloadUrl = `${this.config.request.endpoint}/file/bot${token}/${file.filePath}` - const res = await axios.get(downloadUrl, { responseType: 'arraybuffer' }) - const base64 = `base64://` + Buffer.from(res.data, 'binary').toString('base64') - return { url: base64 } + return await getFileContent(file.filePath) } catch (e) { logger.warn('get file error', e) } } + const getFileContent = async (filePath) => { + const downloadUrl = `${this.config.request.endpoint}/file/bot${token}/${filePath}` + const res = await this.ctx.http.get(downloadUrl, { responseType: 'arraybuffer' }) + const base64 = `base64://` + res.toString('base64') + return { url: base64 } + } if (message.photo) { const photo = message.photo.sort((s1, s2) => s2.fileSize - s1.fileSize)[0] segments.push({ type: 'image', data: await getFileData(photo.fileId) }) @@ -126,7 +116,16 @@ abstract class TelegramAdapter extends Adapter { // TODO: Convert tgs to gif // https://github.com/ed-asriyan/tgs-to-gif // Currently use thumb only - segments.push({ type: 'text', data: { content: `[${message.sticker.setName || 'sticker'} ${message.sticker.emoji || ''}]` } }) + try { + const file = await bot.get('/getFile', { fileId: message.sticker.fileId }) + if (file.filePath.endsWith('.tgs')) { + throw 'tgs is not supported now' + } + segments.push({ type: 'image', data: await getFileContent(file.filePath) }) + } catch (e) { + logger.warn('get file error', e) + segments.push({ type: 'text', data: { content: `[${message.sticker.setName || 'sticker'} ${message.sticker.emoji || ''}]` } }) + } } else if (message.animation) segments.push({ type: 'image', data: await getFileData(message.animation.fileId) }) else if (message.voice) segments.push({ type: 'audio', data: await getFileData(message.voice.fileId) }) else if (message.video) segments.push({ type: 'video', data: await getFileData(message.video.fileId) }) @@ -140,6 +139,7 @@ abstract class TelegramAdapter extends Adapter { session.author = TelegramBot.adaptUser(message.from) if (message.chat.type === 'private') { session.subtype = 'private' + session.channelId = session.guildId = `private:${session.userId}` } else { session.subtype = 'group' session.channelId = session.guildId = message.chat.id.toString() From 18238e61de84d56cebc41f8e2361e59e73fa22ff Mon Sep 17 00:00:00 2001 From: Nanahira <78877@qq.com> Date: Thu, 13 Jan 2022 22:15:58 +0800 Subject: [PATCH 22/34] fix(onebot): don't dispatch any guild message without guild initialization (#454) --- plugins/adapter/onebot/src/utils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/adapter/onebot/src/utils.ts b/plugins/adapter/onebot/src/utils.ts index 8af47b7d1b..95dd94cbbe 100644 --- a/plugins/adapter/onebot/src/utils.ts +++ b/plugins/adapter/onebot/src/utils.ts @@ -109,7 +109,13 @@ export const adaptChannel = (info: OneBot.GroupInfo | OneBot.ChannelInfo): Bot.C export function dispatchSession(bot: OneBotBot, data: OneBot.Payload) { const payload = adaptSession(data) if (!payload) return - if (data.self_tiny_id) bot = bot.guildBot + if (data.self_tiny_id) { + if (!bot.guildBot) { + // don't dispatch any guild message without guild initialization + return + } + bot = bot.guildBot + } const session = new Session(bot, payload) defineProperty(session, 'onebot', Object.create(bot.internal)) Object.assign(session.onebot, data) From 1518bee9c9281398ea0aabca9b8b6513f250a1b0 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Thu, 13 Jan 2022 22:39:46 +0800 Subject: [PATCH 23/34] feat(onebot): sender returns string[] --- plugins/adapter/onebot/src/bot.ts | 6 +++--- plugins/adapter/onebot/src/utils.ts | 17 +++++++---------- plugins/adapter/qqguild/src/bot.ts | 10 +++++----- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/plugins/adapter/onebot/src/bot.ts b/plugins/adapter/onebot/src/bot.ts index 328eae2fc0..ec8cfe952d 100644 --- a/plugins/adapter/onebot/src/bot.ts +++ b/plugins/adapter/onebot/src/bot.ts @@ -142,7 +142,7 @@ export class OneBotBot extends Bot { if (this.app.bail(session, 'before-send', session)) return session.messageId = '' + await this.internal.sendGroupMsg(channelId, content) this.app.emit(session, 'send', session) - return session.messageId + return [session.messageId] } async sendPrivateMessage(userId: string, content: string) { @@ -151,7 +151,7 @@ export class OneBotBot extends Bot { if (this.app.bail(session, 'before-send', session)) return session.messageId = '' + await this.internal.sendPrivateMsg(userId, content) this.app.emit(session, 'send', session) - return session.messageId + return [session.messageId] } async handleFriendRequest(messageId: string, approve: boolean, comment?: string) { @@ -203,7 +203,7 @@ export class QQGuildBot extends OneBotBot { if (this.app.bail(session, 'before-send', session)) return session.messageId = '' + await this.internal.sendGuildChannelMsg(guildId, channelId, content) this.app.emit(session, 'send', session) - return session.messageId + return [session.messageId] } async getChannel(channelId: string, guildId?: string) { diff --git a/plugins/adapter/onebot/src/utils.ts b/plugins/adapter/onebot/src/utils.ts index 95dd94cbbe..22faa7863f 100644 --- a/plugins/adapter/onebot/src/utils.ts +++ b/plugins/adapter/onebot/src/utils.ts @@ -107,15 +107,15 @@ export const adaptChannel = (info: OneBot.GroupInfo | OneBot.ChannelInfo): Bot.C } export function dispatchSession(bot: OneBotBot, data: OneBot.Payload) { - const payload = adaptSession(data) - if (!payload) return if (data.self_tiny_id) { - if (!bot.guildBot) { - // don't dispatch any guild message without guild initialization - return - } + // don't dispatch any guild message without guild initialization + if (!bot.guildBot) return bot = bot.guildBot } + + const payload = adaptSession(data) + if (!payload) return + const session = new Session(bot, payload) defineProperty(session, 'onebot', Object.create(bot.internal)) Object.assign(session.onebot, data) @@ -124,10 +124,7 @@ export function dispatchSession(bot: OneBotBot, data: OneBot.Payload) { export function adaptSession(data: OneBot.Payload) { const session: Partial = {} - session.selfId = '' + data.self_id - if (data.self_tiny_id) { - session.selfId = data.self_tiny_id - } + session.selfId = data.self_tiny_id ? data.self_tiny_id : '' + data.self_id session.type = data.post_type if (data.post_type === 'message') { diff --git a/plugins/adapter/qqguild/src/bot.ts b/plugins/adapter/qqguild/src/bot.ts index 9c04e4391d..65e1ba9393 100644 --- a/plugins/adapter/qqguild/src/bot.ts +++ b/plugins/adapter/qqguild/src/bot.ts @@ -17,14 +17,14 @@ export class QQGuildBot extends Bot { } async getSelf() { - const u = adaptUser(await this.$innerBot.me) - renameProperty(u, 'selfId' as never, 'userId') - return u + const user = adaptUser(await this.$innerBot.me) + renameProperty(user, 'selfId' as never, 'userId') + return user } - async sendMessage(channelId: string, content: string, guildId?: string): Promise { + async sendMessage(channelId: string, content: string, guildId?: string) { const resp = await this.$innerBot.send.channel(channelId, content) - return resp.id + return [resp.id] } async getGuildList() { From a41446f80e4376b5d97b7e58507a390fdaacf6b2 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 01:16:25 +0800 Subject: [PATCH 24/34] feat(core): support async callback for autoAssign & autoAuthorize --- packages/core/src/app.ts | 11 +++++------ packages/core/src/session.ts | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index d7e24a31d6..d1dcd0d0c3 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -1,4 +1,4 @@ -import { defineProperty, Time, coerce, escapeRegExp, makeArray, trimSlash, merge, Dict } from '@koishijs/utils' +import { Awaitable, defineProperty, Time, coerce, escapeRegExp, makeArray, trimSlash, merge, Dict } from '@koishijs/utils' import { Context, Next, Plugin } from './context' import { Adapter } from './adapter' import { Channel, User } from './database' @@ -115,8 +115,7 @@ export class App extends Context { } private _resolvePrefixes(session: Session) { - const { prefix } = this.options - const temp = typeof prefix === 'function' ? prefix(session) : prefix + const temp = session.resolveValue(this.options.prefix) return Array.isArray(temp) ? temp : [temp || ''] } @@ -250,14 +249,14 @@ export namespace App { } export interface Config extends Config.Network { - prefix?: string | string[] | ((session: Session) => void | string | string[]) + prefix?: Computed nickname?: string | string[] maxListeners?: number prettyErrors?: boolean delay?: DelayConfig help?: boolean | HelpConfig - autoAssign?: Computed - autoAuthorize?: Computed + autoAssign?: Computed> + autoAuthorize?: Computed> minSimilarity?: number } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 655d822b87..be9c013745 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -261,7 +261,7 @@ export class Session Promise.resolve()) return this.user = user } @@ -273,7 +273,7 @@ export class Session this.app.database.setUser(this.platform, userId, diff), `user ${this.uid}`) this.app._userCache.set(this.id, this.uid, newUser) return this.user = newUser From 3a38976b8212ce33cb79d40aaac885f111533ebf Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 01:29:42 +0800 Subject: [PATCH 25/34] feat(discord): sender returning string[] --- plugins/adapter/discord/src/bot.ts | 10 +++++++--- plugins/adapter/discord/src/sender.ts | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/plugins/adapter/discord/src/bot.ts b/plugins/adapter/discord/src/bot.ts index d610dd6e7e..70aa9c11af 100644 --- a/plugins/adapter/discord/src/bot.ts +++ b/plugins/adapter/discord/src/bot.ts @@ -67,10 +67,14 @@ export class DiscordBot extends Bot { } : undefined const send = Sender.from(this, `/channels/${channelId}/messages`) - session.messageId = await send(session.content, { message_reference }) + const results = await send(session.content, { message_reference }) - this.app.emit(session, 'send', session) - return session.messageId + for (const id of results) { + session.messageId = id + this.app.emit(session, 'send', session) + } + + return results } async sendPrivateMessage(channelId: string, content: string) { diff --git a/plugins/adapter/discord/src/sender.ts b/plugins/adapter/discord/src/sender.ts index f6056105dd..b2f000fd05 100644 --- a/plugins/adapter/discord/src/sender.ts +++ b/plugins/adapter/discord/src/sender.ts @@ -29,18 +29,20 @@ export namespace Sender { } export class Sender { + private results: string[] = [] private errors: Error[] = [] private constructor(private bot: DiscordBot, private url: string) {} static from(bot: DiscordBot, url: string) { - return new Sender(bot, url).sendMessage + const sender = new Sender(bot, url) + return sender.sendMessage.bind(sender) } async post(data?: any, headers?: any) { try { const result = await this.bot.http.post(this.url, data, { headers }) - return result.id as string + this.results.push(result.id) } catch (e) { this.errors.push(e) } @@ -69,7 +71,7 @@ export class Sender { if (data.url.startsWith('file://')) { const filename = basename(data.url.slice(7)) - return this.sendEmbed(readFileSync(data.url.slice(7)), addition, data.file || filename) + return await this.sendEmbed(readFileSync(data.url.slice(7)), addition, data.file || filename) } else if (data.url.startsWith('base64://')) { const a = Buffer.from(data.url.slice(9), 'base64') return await this.sendEmbed(a, addition, data.file) @@ -110,16 +112,15 @@ export class Sender { }, sendDownload) } - sendMessage = async (content: string, addition: Dict = {}) => { + async sendMessage(content: string, addition: Dict = {}) { const chain = segment.parse(content) - let messageId = '0' let textBuffer = '' delete addition.content const sendBuffer = async () => { const content = textBuffer.trim() if (!content) return - messageId = await this.post({ ...addition, content }) + await this.post({ ...addition, content }) textBuffer = '' } @@ -138,14 +139,14 @@ export class Sender { } else if (type === 'face' && data.name && data.id) { textBuffer += `<:${data.name}:${data.id}>` } else if ((type === 'image' || type === 'video') && data.url) { - messageId = await this.sendAsset(type, data, { + await this.sendAsset(type, data, { ...addition, content: textBuffer.trim(), }) textBuffer = '' } else if (type === 'share') { await sendBuffer() - messageId = await this.post({ + await this.post({ ...addition, embeds: [{ ...data }], }) @@ -159,7 +160,7 @@ export class Sender { } await sendBuffer() - if (!this.errors.length) return messageId + if (!this.errors.length) return this.results throw new AggregateError(this.errors) } From 2cf17df68c843e03656cb6cefa46012131bb2d10 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 14:22:23 +0800 Subject: [PATCH 26/34] feat(telegram): support sender api --- plugins/adapter/telegram/package.json | 2 + plugins/adapter/telegram/src/bot.ts | 152 +++--------------------- plugins/adapter/telegram/src/sender.ts | 155 +++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 137 deletions(-) create mode 100644 plugins/adapter/telegram/src/sender.ts diff --git a/plugins/adapter/telegram/package.json b/plugins/adapter/telegram/package.json index 18fe45820d..de7f62b169 100644 --- a/plugins/adapter/telegram/package.json +++ b/plugins/adapter/telegram/package.json @@ -31,10 +31,12 @@ "koishi": "^4.0.0-rc.3" }, "devDependencies": { + "@types/es-aggregate-error": "^1.0.2", "@koishijs/plugin-mock": "^1.0.0" }, "dependencies": { "axios": "^0.21.4", + "es-aggregate-error": "^1.0.7", "file-type": "^16.5.3", "form-data": "^4.0.0" } diff --git a/plugins/adapter/telegram/src/bot.ts b/plugins/adapter/telegram/src/bot.ts index 85775cf6cb..10f66f5a0c 100644 --- a/plugins/adapter/telegram/src/bot.ts +++ b/plugins/adapter/telegram/src/bot.ts @@ -1,21 +1,9 @@ -import fileType from 'file-type' -import { createReadStream } from 'fs' import FormData from 'form-data' -import { Adapter, assertProperty, Bot, camelCase, Dict, Logger, Quester, renameProperty, Schema, segment, snakeCase } from 'koishi' +import { Adapter, assertProperty, Bot, camelCase, Dict, Quester, renameProperty, Schema, snakeCase } from 'koishi' import * as Telegram from './types' import { AdapterConfig } from './utils' import { AxiosError } from 'axios' - -const logger = new Logger('telegram') - -const prefixTypes = ['quote', 'card', 'anonymous', 'markdown'] - -type TLAssetType = - | 'photo' - | 'audio' - | 'document' - | 'video' - | 'animation' +import { Sender } from './sender' export class SenderError extends Error { constructor(args: Dict, url: string, retcode: number, selfId: string) { @@ -49,37 +37,6 @@ export const BotConfig: Schema = Schema.object({ ]).description('通过长轮询获取更新时请求的超时。单位为秒;true 为使用默认值 1 分钟。详情请见 Telegram API 中 getUpdate 的参数 timeout。不设置即使用 webhook 获取更新。'), }) -async function maybeFile(payload: Dict, field: TLAssetType): Promise<[any, string?, Buffer?, string?]> { - if (!payload[field]) return [payload] - let content - let filename = 'file' - const [schema, data] = payload[field].split('://') - if (['base64', 'file'].includes(schema)) { - content = (schema === 'base64' ? Buffer.from(data, 'base64') : createReadStream(data)) - delete payload[field] - } - // add file extension for base64 document (general file) - if (field === 'document' && schema === 'base64') { - const type = await fileType.fromBuffer(Buffer.from(data, 'base64')) - if (!type) { - logger.warn('Can not infer file mime') - } else filename = `file.${type.ext}` - } - return [payload, field, content, filename] -} - -async function isGif(url: string) { - if (url.toLowerCase().endsWith('.gif')) return true - const [schema, data] = url.split('://') - if (schema === 'base64') { - const type = await fileType.fromBuffer(Buffer.from(data, 'base64')) - if (!type) { - logger.warn('Can not infer file mime') - } else if (type.ext === 'gif') return true - } - return false -} - export class TelegramBot extends Bot { static adaptUser(data: Partial) { data.userId = data.id.toString() @@ -115,7 +72,7 @@ export class TelegramBot extends Bot { * @param params params in camelCase * @returns Respond form telegram */ - async get(action: `/${string}`, params: P = undefined): Promise { + async get(action: string, params: P = undefined): Promise { this.logger.debug('[request] %s %o', action, params) const response = await this.http.get(action, { params: snakeCase(params || {}) @@ -134,7 +91,7 @@ export class TelegramBot extends Bot { * @param content file stream * @returns Respond form telegram */ - async post(action: `/${string}`, params: P = undefined, field = '', content: Buffer = null, filename = 'file'): Promise { + async post(action: string, params: P = undefined, field = '', content: Buffer = null, filename = 'file'): Promise { this.logger.debug('[request] %s %o', action, params) const payload = new FormData() for (const key in params) { @@ -155,93 +112,6 @@ export class TelegramBot extends Bot { throw new SenderError(params, action, -1, this.selfId) } - private async _sendMessage(chatId: string, content: string) { - const payload: Record = { chatId, caption: '' } - let currAssetType: TLAssetType = null - let lastMsg: Telegram.Message = null - - const segs = segment.parse(content) - let currIdx = 0 - while (currIdx < segs.length && prefixTypes.includes(segs[currIdx].type)) { - if (segs[currIdx].type === 'quote') { - payload.replyToMessage = true - } else if (segs[currIdx].type === 'anonymous') { - if (segs[currIdx].data.ignore === 'false') return null - } else if (segs[currIdx].type === 'markdown') { - payload.parseMode = 'MarkdownV2' - } - // else if (segs[currIdx].type === 'card') {} - ++currIdx - } - - const sendAsset = async () => { - const assetApi: Dict<`/${string}`> = { - photo: '/sendPhoto', - audio: '/sendAudio', - document: '/sendDocument', - video: '/sendVideo', - animation: '/sendAnimation', - } - lastMsg = await this.post(assetApi[currAssetType], ...await maybeFile(payload, currAssetType)) - currAssetType = null - delete payload[currAssetType] - delete payload.replyToMessage - payload.caption = '' - } - - for (const seg of segs.slice(currIdx)) { - switch (seg.type) { - case 'text': - payload.caption += seg.data.content - break - case 'at': { - const atTarget = seg.data.name || seg.data.id || seg.data.role || seg.data.type - if (!atTarget) break - payload.caption += `@${atTarget} ` - break - } - case 'sharp': { - const sharpTarget = seg.data.name || seg.data.id - if (!sharpTarget) break - payload.caption += `#${sharpTarget} ` - break - } - case 'face': - this.logger.info("Telegram don't support face") - break - case 'image': - case 'audio': - case 'video': - case 'file': { - // send previous asset if there is any - if (currAssetType) await sendAsset() - - // handel current asset - const assetUrl = seg.data.url - - if (!assetUrl) { - this.logger.warn('asset segment with no url') - break - } - if (seg.type === 'image') currAssetType = await isGif(assetUrl) ? 'animation' : 'photo' - else if (seg.type === 'file') currAssetType = 'document' - else currAssetType = seg.type - payload[currAssetType] = assetUrl - break - } - default: - this.logger.warn(`Unexpected asset type: ${seg.type}`) - return - } - } - - // if something left in payload - if (currAssetType) await sendAsset() - if (payload.caption) lastMsg = await this.get('/sendMessage', { chatId, text: payload.caption }) - - return lastMsg ? lastMsg.messageId.toString() : null - } - async sendMessage(channelId: string, content: string) { if (!content) return let subtype: string @@ -253,11 +123,19 @@ export class TelegramBot extends Bot { subtype = 'group' chatId = channelId } + const session = this.createSession({ content, channelId, guildId: channelId }) if (await this.app.serial(session, 'before-send', session)) return - session.messageId = await this._sendMessage(chatId, session.content) - this.app.emit(session, 'send', session) - return session.messageId + + const send = Sender.from(this, chatId) + const results = await send(session.content) + + for (const id of results) { + session.messageId = id + this.app.emit(session, 'send', session) + } + + return results } async sendPrivateMessage(userId: string, content: string) { diff --git a/plugins/adapter/telegram/src/sender.ts b/plugins/adapter/telegram/src/sender.ts new file mode 100644 index 0000000000..92b02629d1 --- /dev/null +++ b/plugins/adapter/telegram/src/sender.ts @@ -0,0 +1,155 @@ +import { createReadStream } from 'fs' +import { Dict, Logger, segment } from 'koishi' +import * as Telegram from './types' +import AggregateError from 'es-aggregate-error' +import fileType from 'file-type' +import { TelegramBot } from '.' + +const logger = new Logger('telegram') + +const prefixTypes = ['quote', 'card', 'anonymous', 'markdown'] + +type TLAssetType = + | 'photo' + | 'audio' + | 'document' + | 'video' + | 'animation' + +async function maybeFile(payload: Dict, field: TLAssetType): Promise<[any, string?, Buffer?, string?]> { + if (!payload[field]) return [payload] + let content + let filename = 'file' + const [schema, data] = payload[field].split('://') + if (schema === 'file') { + content = createReadStream(data) + delete payload[field] + } else if (schema === 'base64') { + content = Buffer.from(data, 'base64') + delete payload[field] + } + // add file extension for base64 document (general file) + if (field === 'document' && schema === 'base64') { + const type = await fileType.fromBuffer(Buffer.from(data, 'base64')) + if (!type) { + logger.warn('Can not infer file mime') + } else filename = `file.${type.ext}` + } + return [payload, field, content, filename] +} + +async function isGif(url: string) { + if (url.toLowerCase().endsWith('.gif')) return true + const [schema, data] = url.split('://') + if (schema === 'base64') { + const type = await fileType.fromBuffer(Buffer.from(data, 'base64')) + if (!type) { + logger.warn('Can not infer file mime') + } else if (type.ext === 'gif') return true + } + return false +} + +const assetApi: Dict = { + photo: '/sendPhoto', + audio: '/sendAudio', + document: '/sendDocument', + video: '/sendVideo', + animation: '/sendAnimation', +} + +export class Sender { + errors: Error[] = [] + results: Telegram.Message[] = [] + + currAssetType: TLAssetType = null + payload: Dict + + constructor(private bot: TelegramBot, private chatId: string) { + this.payload = { chatId, caption: '' } + } + + static from(bot: TelegramBot, chatId: string) { + const sender = new Sender(bot, chatId) + return sender.sendMessage.bind(sender) + } + + sendAsset = async () => { + this.results.push(await this.bot.post(assetApi[this.currAssetType], ...await maybeFile(this.payload, this.currAssetType))) + this.currAssetType = null + delete this.payload[this.currAssetType] + delete this.payload.replyToMessage + this.payload.caption = '' + } + + async sendMessage(content: string) { + const segs = segment.parse(content) + let currIdx = 0 + while (currIdx < segs.length && prefixTypes.includes(segs[currIdx].type)) { + if (segs[currIdx].type === 'quote') { + this.payload.replyToMessage = true + } else if (segs[currIdx].type === 'anonymous') { + if (segs[currIdx].data.ignore === 'false') return null + } else if (segs[currIdx].type === 'markdown') { + this.payload.parseMode = 'MarkdownV2' + } + // else if (segs[currIdx].type === 'card') {} + ++currIdx + } + + for (const seg of segs.slice(currIdx)) { + switch (seg.type) { + case 'text': + this.payload.caption += seg.data.content + break + case 'at': { + const atTarget = seg.data.name || seg.data.id || seg.data.role || seg.data.type + if (!atTarget) break + this.payload.caption += `@${atTarget} ` + break + } + case 'sharp': { + const sharpTarget = seg.data.name || seg.data.id + if (!sharpTarget) break + this.payload.caption += `#${sharpTarget} ` + break + } + case 'face': + logger.warn("Telegram don't support face") + break + case 'image': + case 'audio': + case 'video': + case 'file': { + // send previous asset if there is any + if (this.currAssetType) await this.sendAsset() + + // handel current asset + const assetUrl = seg.data.url + + if (!assetUrl) { + logger.warn('asset segment with no url') + break + } + if (seg.type === 'image') this.currAssetType = await isGif(assetUrl) ? 'animation' : 'photo' + else if (seg.type === 'file') this.currAssetType = 'document' + else this.currAssetType = seg.type + this.payload[this.currAssetType] = assetUrl + break + } + default: + logger.warn(`Unexpected asset type: ${seg.type}`) + return + } + } + + // if something left in payload + if (this.currAssetType) await this.sendAsset() + if (this.payload.caption) { + this.results.push(await this.bot.get('/sendMessage', { chatId: this.chatId, text: this.payload.caption })) + } + + if (!this.errors.length) return this.results.map(result => '' + result.messageId) + return new AggregateError(this.errors) + } +} From f0c3dd7502660dc0b44f2143ad68d46d2fe78320 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 14:23:02 +0800 Subject: [PATCH 27/34] feat(discord): allow disable privileged intents --- plugins/adapter/discord/package.json | 2 +- plugins/adapter/discord/src/utils.ts | 13 ++++++++++++- plugins/adapter/discord/src/ws.ts | 18 +++++++++++++----- plugins/adapter/kaiheila/src/bot.ts | 2 +- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/plugins/adapter/discord/package.json b/plugins/adapter/discord/package.json index 5a99fcea40..fc704917b0 100644 --- a/plugins/adapter/discord/package.json +++ b/plugins/adapter/discord/package.json @@ -37,7 +37,7 @@ "axios": "^0.21.4" }, "dependencies": { - "es-aggregate-error": "^1.0.5", + "es-aggregate-error": "^1.0.7", "file-type": "^16.5.3", "form-data": "^4.0.0", "ws": "^8.2.1" diff --git a/plugins/adapter/discord/src/utils.ts b/plugins/adapter/discord/src/utils.ts index 60f45ad30f..46fd907b65 100644 --- a/plugins/adapter/discord/src/utils.ts +++ b/plugins/adapter/discord/src/utils.ts @@ -3,9 +3,20 @@ import { Adapter, App, Bot, Schema, segment, Session } from 'koishi' import { DiscordBot } from './bot' import * as DC from './types' -export interface AdapterConfig extends Adapter.WebSocketClient.Config, App.Config.Request {} +interface PrivilegedIntents { + members?: boolean +} + +export interface AdapterConfig extends Adapter.WebSocketClient.Config, App.Config.Request { + intents?: PrivilegedIntents +} export const AdapterConfig: Schema = Schema.intersect([ + Schema.object({ + intents: Schema.object({ + members: Schema.boolean(), + }), + }), App.Config.Request, Adapter.WebSocketClient.Config, ]) diff --git a/plugins/adapter/discord/src/ws.ts b/plugins/adapter/discord/src/ws.ts index 8a110e6b65..93d6d8b1ec 100644 --- a/plugins/adapter/discord/src/ws.ts +++ b/plugins/adapter/discord/src/ws.ts @@ -29,6 +29,18 @@ export default class WebSocketClient extends Adapter.WebSocketClient { await this._sendSeparate(handle, chain, useMarkdown) } - return session.messageId + return [session.messageId] } async sendPrivateMessage(targetId: string, content: string) { From 32f40c2375e448132c29487c3e092447510c4b6b Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 19:34:00 +0800 Subject: [PATCH 28/34] feat(core): session.send() returning string[] --- docs/guide/message/middleware.md | 2 +- docs/guide/message/session.md | 4 ++-- packages/core/src/internal/help.ts | 3 +-- packages/core/src/session.ts | 15 +++++++++------ plugins/adapter/telegram/src/sender.ts | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/guide/message/middleware.md b/docs/guide/message/middleware.md index f1f99c8e36..15b0c9842a 100644 --- a/docs/guide/message/middleware.md +++ b/docs/guide/message/middleware.md @@ -116,7 +116,7 @@ let message = '' // 当前消息 ctx.middleware((session, next) => { if (session.content === message) { times += 1 - if (times === 3) return session.send(message) + if (times === 3) return message } else { times = 0 message = session.content diff --git a/docs/guide/message/session.md b/docs/guide/message/session.md index 253aa4671f..72fb957de6 100644 --- a/docs/guide/message/session.md +++ b/docs/guide/message/session.md @@ -142,11 +142,11 @@ export default { await session.send('请输入用户名:') const name = await session.prompt() -if (!name) return session.send('输入超时。') +if (!name) return '输入超时。' // 执行后续操作 await ctx.database.setUser(session.platform, session.userId, { name }) -return session.send(`${name},请多指教!`) +return `${name},请多指教!` ``` 你可以给这个方法传入一个 `timeout` 参数,或使用 `delay.prompt` 配置项,来作为等待的时间。 diff --git a/packages/core/src/internal/help.ts b/packages/core/src/internal/help.ts index d2a3049d46..9f02292382 100644 --- a/packages/core/src/internal/help.ts +++ b/packages/core/src/internal/help.ts @@ -73,8 +73,7 @@ export default function help(ctx: Context, config: HelpConfig = {}) { prefix: template('internal.help-suggestion-prefix'), suffix: template('internal.help-suggestion-suffix'), async apply(suggestion) { - const output = await showHelp(app._commands.get(suggestion), this as any, options) - return session.send(output) + return showHelp(app._commands.get(suggestion), this as any, options) }, }) return diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index be9c013745..e6d455c893 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -2,7 +2,7 @@ import { distance } from 'fastest-levenshtein' import { User, Channel } from './database' import { TableType, Tables } from './orm' import { Command } from './command' -import { contain, observe, Logger, defineProperty, Random, template, remove, noop, segment } from '@koishijs/utils' +import { Awaitable, contain, observe, Logger, defineProperty, Random, template, remove, segment } from '@koishijs/utils' import { Argv } from './parser' import { Middleware, Next } from './context' import { App } from './app' @@ -161,9 +161,12 @@ export class Session((error) => { + logger.warn(error) + return [] + }) } cancelQueued(delay = this.app.options.delay.cancel) { @@ -406,7 +409,7 @@ export class Session { const result = await next(callback) - if (result) return this.send(result) + if (result) await this.send(result) } let suggestions: string[], minDistance = Infinity @@ -445,7 +448,7 @@ export interface SuggestOptions { prefix?: string suffix: string minSimilarity?: number - apply: (this: Session, suggestion: string, next: Next) => void + apply: (this: Session, suggestion: string, next: Next) => Awaitable } export function getSessionId(session: Session) { diff --git a/plugins/adapter/telegram/src/sender.ts b/plugins/adapter/telegram/src/sender.ts index 92b02629d1..aca374fb3e 100644 --- a/plugins/adapter/telegram/src/sender.ts +++ b/plugins/adapter/telegram/src/sender.ts @@ -150,6 +150,6 @@ export class Sender { } if (!this.errors.length) return this.results.map(result => '' + result.messageId) - return new AggregateError(this.errors) + throw new AggregateError(this.errors) } } From 26e20a811660acbf8f18ff6ab8a889f7ff4f0a4a Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 19:40:21 +0800 Subject: [PATCH 29/34] fix(teach+github): migrate session api --- plugins/github/src/server.ts | 7 ++++-- plugins/teach/src/index.ts | 4 ++-- plugins/teach/src/search.ts | 8 +++---- plugins/teach/src/update.ts | 43 +++++++++++++++++++++++++++--------- plugins/teach/src/utils.ts | 21 ------------------ 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/plugins/github/src/server.ts b/plugins/github/src/server.ts index c32ccf6c49..4b779abe89 100644 --- a/plugins/github/src/server.ts +++ b/plugins/github/src/server.ts @@ -125,8 +125,11 @@ export class GitHub extends Service { async authorize(session: Session, message: string) { await session.send(message) const name = await session.prompt(this.config.promptTimeout) - if (!name) return session.send('输入超时。') - await session.execute({ name: 'github.authorize', args: [name] }) + if (name) { + await session.execute({ name: 'github.authorize', args: [name] }) + } else { + await session.send('输入超时。') + } } async request(method: Method, url: string, session: ReplySession, body?: any, headers?: Dict) { diff --git a/plugins/teach/src/index.ts b/plugins/teach/src/index.ts index bb943136bf..62f94591bf 100644 --- a/plugins/teach/src/index.ts +++ b/plugins/teach/src/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-irregular-whitespace */ -import { Context, Schema, Time } from 'koishi' +import { Awaitable, Context, Schema, Time } from 'koishi' import { Dialogue } from './utils' // features @@ -34,7 +34,7 @@ export * from './plugins/writer' declare module 'koishi' { interface EventMap { 'dialogue/validate'(argv: Dialogue.Argv): void | string - 'dialogue/execute'(argv: Dialogue.Argv): void | Promise + 'dialogue/execute'(argv: Dialogue.Argv): Awaitable } interface Modules { diff --git a/plugins/teach/src/search.ts b/plugins/teach/src/search.ts index d934d4ad1f..b9600c6c92 100644 --- a/plugins/teach/src/search.ts +++ b/plugins/teach/src/search.ts @@ -160,7 +160,7 @@ async function showSearch(argv: Dialogue.Argv) { const { itemsPerPage = 30, mergeThreshold = 5 } = argv.config const test: DialogueTest = { question, answer, regexp, original } - if (app.bail('dialogue/before-search', argv, test)) return + if (app.bail('dialogue/before-search', argv, test)) return '' const dialogues = await argv.app.teach.get(test) if (pipe) { @@ -184,11 +184,11 @@ async function showSearch(argv: Dialogue.Argv) { if (!options.regexp) { const suffix = options.regexp !== false ? ',请尝试使用正则表达式匹配' : '' if (!original) { - if (!dialogues.length) return session.send(`没有搜索到回答“${answer}”${suffix}。`) + if (!dialogues.length) return `没有搜索到回答“${answer}”${suffix}。` const output = dialogues.map(d => `${formatPrefix(argv, d)}${d.original}`) return sendResult(`回答“${answer}”的问题如下`, output) } else if (!answer) { - if (!dialogues.length) return session.send(`没有搜索到问题“${original}”${suffix}。`) + if (!dialogues.length) return `没有搜索到问题“${original}”${suffix}。` const output = formatAnswers(argv, dialogues) const state = app.getSessionState(session) state.isSearch = true @@ -197,7 +197,7 @@ async function showSearch(argv: Dialogue.Argv) { const total = await getTotalWeight(app, state) return sendResult(`问题“${original}”的回答如下`, output, dialogues.length > 1 ? `实际触发概率:${+Math.min(total, 1).toFixed(3)}` : '') } else { - if (!dialogues.length) return session.send(`没有搜索到问答“${original}”“${answer}”${suffix}。`) + if (!dialogues.length) return `没有搜索到问答“${original}”“${answer}”${suffix}。` const output = [dialogues.map(d => d.id).join(', ')] return sendResult(`“${original}”“${answer}”匹配的回答如下`, output) } diff --git a/plugins/teach/src/update.ts b/plugins/teach/src/update.ts index 393a8425d8..f85936debe 100644 --- a/plugins/teach/src/update.ts +++ b/plugins/teach/src/update.ts @@ -1,12 +1,12 @@ import { Context, difference, deduplicate, sleep, pick, Time, Awaitable } from 'koishi' -import { Dialogue, prepareTargets, sendResult, split, RE_DIALOGUES, isPositiveInteger } from './utils' +import { Dialogue, prepareTargets, split, RE_DIALOGUES, isPositiveInteger } from './utils' import { getDetails, formatDetails, formatAnswer, formatQuestionAnswers } from './search' declare module 'koishi' { interface EventMap { 'dialogue/before-modify'(argv: Dialogue.Argv): Awaitable 'dialogue/modify'(argv: Dialogue.Argv, dialogue: Dialogue): void - 'dialogue/after-modify'(argv: Dialogue.Argv): Awaitable + 'dialogue/after-modify'(argv: Dialogue.Argv): void 'dialogue/before-detail'(argv: Dialogue.Argv): Awaitable 'dialogue/detail'(dialogue: Dialogue, output: string[], argv: Dialogue.Argv): Awaitable } @@ -39,7 +39,7 @@ export default function apply(ctx: Context) { return update(argv) } catch (err) { ctx.logger('teach').warn(err) - return argv.session.send(`${revert ? '回退' : remove ? '删除' : '修改'}问答时出现问题。`) + return `${revert ? '回退' : remove ? '删除' : '修改'}问答时出现问题。` } }) @@ -60,7 +60,7 @@ export default function apply(ctx: Context) { return true }) - if (!dialogues.length) return session.send('没有搜索到满足条件的教学操作。') + if (!dialogues.length) return '没有搜索到满足条件的教学操作。' return options.review ? review(dialogues, argv) : revert(dialogues, argv) }, true) @@ -108,15 +108,15 @@ function review(dialogues: Dialogue[], argv: Dialogue.Argv) { return `${formatDetails(d, details)}${questionType}:${original},${answerType}:${formatAnswer(answer, argv.config)}` }) output.unshift('近期执行的教学操作有:') - return session.send(output.join('\n')) + return output.join('\n') } async function revert(dialogues: Dialogue[], argv: Dialogue.Argv) { try { - return argv.session.send(await argv.app.teach.revert(dialogues, argv)) + return await argv.app.teach.revert(dialogues, argv) } catch (err) { argv.app.logger('teach').warn(err) - return argv.session.send('回退问答中出现问题。') + return '回退问答中出现问题。' } } @@ -127,7 +127,7 @@ export async function update(argv: Dialogue.Argv) { options.modify = !review && !search && (Object.keys(options).length || args.length) if (!options.modify && !search && target.length > maxPreviews) { - return session.send(`一次最多同时预览 ${maxPreviews} 个问答。`) + return `一次最多同时预览 ${maxPreviews} 个问答。` } argv.uneditable = [] @@ -139,7 +139,7 @@ export async function update(argv: Dialogue.Argv) { argv.dialogueMap = Object.fromEntries(dialogues.map(d => [d.id, { ...d }])) if (search) { - return session.send(formatQuestionAnswers(argv, dialogues).join('\n')) + return formatQuestionAnswers(argv, dialogues).join('\n') } const actualIds = argv.dialogues.map(d => d.id) @@ -156,7 +156,7 @@ export async function update(argv: Dialogue.Argv) { if (index) await sleep(previewDelay) await session.send(output.join('\n')) } - return + return '' } const targets = prepareTargets(argv) @@ -225,7 +225,7 @@ export async function create(argv: Dialogue.Argv) { const dialogue = { flag: 0 } as Dialogue if (app.bail('dialogue/permit', argv, dialogue)) { - return argv.session.send('该问答因权限过低无法添加。') + return '该问答因权限过低无法添加。' } try { @@ -241,3 +241,24 @@ export async function create(argv: Dialogue.Argv) { throw err } } + +export function sendResult(argv: Dialogue.Argv, prefix?: string, suffix?: string) { + const { options, uneditable, unknown, skipped, updated, target, config } = argv + const { remove, revert, create } = options + const output = [] + if (prefix) output.push(prefix) + if (updated.length) { + output.push(create ? `修改了已存在的问答,编号为 ${updated.join(', ')}。` : `问答 ${updated.join(', ')} 已成功修改。`) + } + if (skipped.length) { + output.push(create ? `问答已存在,编号为 ${target.join(', ')},如要修改请尝试使用 ${config.prefix}${skipped.join(',')} 指令。` : `问答 ${skipped.join(', ')} 没有发生改动。`) + } + if (uneditable.length) { + output.push(`问答 ${uneditable.join(', ')} 因权限过低无法${revert ? '回退' : remove ? '删除' : '修改'}。`) + } + if (unknown.length) { + output.push(`${revert ? '最近无人修改过' : '没有搜索到'}编号为 ${unknown.join(', ')} 的问答。`) + } + if (suffix) output.push(suffix) + return output.join('\n') +} diff --git a/plugins/teach/src/utils.ts b/plugins/teach/src/utils.ts index 6f1eed60d5..abdf2850f3 100644 --- a/plugins/teach/src/utils.ts +++ b/plugins/teach/src/utils.ts @@ -80,27 +80,6 @@ export namespace Dialogue { } } -export function sendResult(argv: Dialogue.Argv, prefix?: string, suffix?: string) { - const { session, options, uneditable, unknown, skipped, updated, target, config } = argv - const { remove, revert, create } = options - const output = [] - if (prefix) output.push(prefix) - if (updated.length) { - output.push(create ? `修改了已存在的问答,编号为 ${updated.join(', ')}。` : `问答 ${updated.join(', ')} 已成功修改。`) - } - if (skipped.length) { - output.push(create ? `问答已存在,编号为 ${target.join(', ')},如要修改请尝试使用 ${config.prefix}${skipped.join(',')} 指令。` : `问答 ${skipped.join(', ')} 没有发生改动。`) - } - if (uneditable.length) { - output.push(`问答 ${uneditable.join(', ')} 因权限过低无法${revert ? '回退' : remove ? '删除' : '修改'}。`) - } - if (unknown.length) { - output.push(`${revert ? '最近无人修改过' : '没有搜索到'}编号为 ${unknown.join(', ')} 的问答。`) - } - if (suffix) output.push(suffix) - return session.send(output.join('\n')) -} - export function split(source: string) { if (!source) return [] return source.split(',').flatMap((value) => { From 8066b5394cdfc294468a8eee21de4e990b315532 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 22:19:56 +0800 Subject: [PATCH 30/34] fix(core): remove weird undefined for text args, fix #439 --- packages/core/src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/parser.ts b/packages/core/src/parser.ts index f12dad9863..4ec3f22477 100644 --- a/packages/core/src/parser.ts +++ b/packages/core/src/parser.ts @@ -115,7 +115,7 @@ export namespace Argv { stringify(argv: Argv) { const output = argv.tokens.reduce((prev, token) => { - if (token.quoted) prev += leftQuotes[rightQuotes.indexOf(token.terminator[0])] + if (token.quoted) prev += leftQuotes[rightQuotes.indexOf(token.terminator[0])] || '' return prev + token.content + token.terminator }, '') if (argv.rest && !rightQuotes.includes(output[output.length - 1]) || argv.initiator) { From 75b514c4df15309899a1cbb29ad1fe36327d2520 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 22:40:08 +0800 Subject: [PATCH 31/34] fix(eval): using regexp for shortcut, fix #326 --- plugins/eval/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/eval/src/index.ts b/plugins/eval/src/index.ts index dff0387579..15ae50b39b 100644 --- a/plugins/eval/src/index.ts +++ b/plugins/eval/src/index.ts @@ -1,4 +1,4 @@ -import { Context, Command, Argv, segment, Logger, defineProperty, noop, Awaitable } from 'koishi' +import { Context, Command, Argv, segment, Logger, defineProperty, noop, Awaitable, escapeRegExp } from 'koishi' import { EvalWorker, Trap, EvalConfig, Config } from './main' import { resolve } from 'path' import { load } from 'js-yaml' @@ -149,8 +149,8 @@ export function apply(ctx: Context, config: Config = {}) { }) if (prefix) { - command.shortcut(prefix, { fuzzy: true }) - command.shortcut(prefix + prefix[prefix.length - 1], { fuzzy: true, options: { slient: true } }) + command.shortcut(new RegExp(`^${escapeRegExp(prefix)} (.+)$`), { args: ['$1'] }) + command.shortcut(new RegExp(`^${escapeRegExp(prefix + prefix[prefix.length - 1])} (.+)$`), { args: ['$1'], options: { slient: true } }) } Argv.interpolate('${', '}', (raw) => { From f57bd409a93fa0f64fba2273d4a5951dc986842f Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 22:52:15 +0800 Subject: [PATCH 32/34] feat(puppeteer): move jsx support to new plugin --- plugins/puppeteer/build/compile.ts | 5 ----- plugins/puppeteer/package.json | 10 ++-------- plugins/puppeteer/src/index.ts | 25 ------------------------- plugins/puppeteer/src/worker.ts | 18 ------------------ 4 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 plugins/puppeteer/build/compile.ts delete mode 100644 plugins/puppeteer/src/worker.ts diff --git a/plugins/puppeteer/build/compile.ts b/plugins/puppeteer/build/compile.ts deleted file mode 100644 index 830c79f151..0000000000 --- a/plugins/puppeteer/build/compile.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { defineBuild } from '../../../build' - -export = defineBuild(async (base, { entryPoints }) => { - entryPoints.push(base + '/src/worker.ts') -}) diff --git a/plugins/puppeteer/package.json b/plugins/puppeteer/package.json index c4f6ac14ee..5842631252 100644 --- a/plugins/puppeteer/package.json +++ b/plugins/puppeteer/package.json @@ -29,11 +29,7 @@ "puppeteer" ], "devDependencies": { - "@types/pngjs": "^6.0.1", - "@types/react": "*", - "@types/react-dom": "*", - "@koishijs/plugin-eval": "^4.0.0-rc.2", - "@koishijs/plugin-mock": "^1.0.0" + "@types/pngjs": "^6.0.1" }, "peerDependencies": { "koishi": "^4.0.0-rc.3" @@ -41,8 +37,6 @@ "dependencies": { "chrome-finder": "^1.0.7", "pngjs": "^6.0.0", - "puppeteer-core": "^10.2.0", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "puppeteer-core": "^10.2.0" } } diff --git a/plugins/puppeteer/src/index.ts b/plugins/puppeteer/src/index.ts index e4831cee81..df4ba50bcc 100644 --- a/plugins/puppeteer/src/index.ts +++ b/plugins/puppeteer/src/index.ts @@ -63,7 +63,6 @@ export interface Config { idleTimeout?: number maxSize?: number protocols?: string[] - bodyStyle?: Record } export const Config = Schema.object({ @@ -149,10 +148,6 @@ export const defaultConfig: Config = { height: 600, deviceScaleFactor: 2, }, - bodyStyle: { - display: 'inline-block', - padding: '0.25rem 0.375rem', - }, } Context.service('puppeteer') @@ -259,24 +254,4 @@ export function apply(ctx: Context, config: Config = {}) { return '截图失败。' }).finally(() => page.close()) }) - - ctx1.using(['worker'], (ctx) => { - ctx.worker.config.loaderConfig.jsxFactory = 'jsxFactory' - ctx.worker.config.loaderConfig.jsxFragment = 'jsxFragment' - ctx.worker.config.setupFiles['puppeteer.ts'] = resolve(__dirname, 'worker') - - ctx.before('eval/send', (content) => { - return segment.transformAsync(content, { - async fragment({ content }) { - const style = Object - .entries(config.bodyStyle) - .map(([key, value]) => `${hyphenate(key)}: ${value};`) - .join('') - return await ctx.puppeteer.render(` - ${content} - `, async (page, next) => next(await page.$('body'))) - }, - }) - }) - }) } diff --git a/plugins/puppeteer/src/worker.ts b/plugins/puppeteer/src/worker.ts deleted file mode 100644 index bb35fc6e15..0000000000 --- a/plugins/puppeteer/src/worker.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { internal, config } from '@koishijs/plugin-eval/lib/worker' -import { createElement, Fragment } from 'react' -import { renderToStaticMarkup } from 'react-dom/server' -import { segment } from 'koishi' -import { inspect } from 'util' - -function wrapFactory any>(func: F) { - return (...args: Parameters) => { - const node = Object.create(func.apply(null, args)) - node[inspect.custom] = function () { - return segment('fragment', { content: renderToStaticMarkup(this) }) - } - return node - } -} - -internal.setGlobal(config.loaderConfig.jsxFactory, wrapFactory(createElement)) -internal.setGlobal(config.loaderConfig.jsxFragment, Fragment) From 2dd94ecf2f3226c3c08fe42be3f55a040bf701a3 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Fri, 14 Jan 2022 23:42:42 +0800 Subject: [PATCH 33/34] chore: bump versions --- packages/cli/package.json | 4 ++-- packages/core/package.json | 4 ++-- packages/create/package.json | 2 +- packages/create/template/package.json | 16 ++++++++-------- packages/koishi/package.json | 6 +++--- packages/test-utils/package.json | 2 +- packages/ui-core/package.json | 2 +- packages/ui-playground/package.json | 2 +- packages/utils/package.json | 2 +- plugins/a11y/admin/package.json | 2 +- plugins/a11y/bind/package.json | 2 +- plugins/a11y/callme/package.json | 2 +- plugins/a11y/command-utils/package.json | 2 +- plugins/a11y/rate-limit/package.json | 2 +- plugins/a11y/schedule/package.json | 4 ++-- plugins/a11y/sudo/package.json | 2 +- plugins/a11y/switch/package.json | 2 +- plugins/a11y/verifier/package.json | 2 +- plugins/adapter/discord/package.json | 4 ++-- plugins/adapter/kaiheila/package.json | 4 ++-- plugins/adapter/onebot/package.json | 4 ++-- plugins/adapter/qqguild/package.json | 4 ++-- plugins/adapter/telegram/package.json | 4 ++-- plugins/assets/git/package.json | 4 ++-- plugins/assets/local/package.json | 2 +- plugins/assets/remote/package.json | 2 +- plugins/assets/s3/package.json | 2 +- plugins/cache/lru/package.json | 2 +- plugins/cache/redis/package.json | 2 +- plugins/common/broadcast/package.json | 2 +- plugins/common/echo/package.json | 2 +- plugins/common/feedback/package.json | 4 ++-- plugins/common/forward/package.json | 4 ++-- plugins/common/recall/package.json | 2 +- plugins/common/repeater/package.json | 2 +- plugins/common/respondent/package.json | 2 +- plugins/database/level/package.json | 2 +- plugins/database/memory/package.json | 2 +- plugins/database/mongo/package.json | 2 +- plugins/database/mysql/package.json | 2 +- plugins/database/orm-utils/package.json | 2 +- plugins/database/sql-utils/package.json | 2 +- plugins/database/sqlite/package.json | 2 +- plugins/eval/package.json | 4 ++-- plugins/frontend/builder/package.json | 2 +- plugins/frontend/chat/package.json | 6 +++--- plugins/frontend/commands/client/index.ts | 2 +- plugins/frontend/commands/package.json | 6 +++--- plugins/frontend/console/package.json | 4 ++-- plugins/frontend/manager/package.json | 8 ++++---- plugins/frontend/status/package.json | 8 ++++---- plugins/github/package.json | 6 +++--- plugins/mock/package.json | 2 +- plugins/puppeteer/package.json | 4 ++-- plugins/puppeteer/src/index.ts | 6 ++++-- plugins/teach/package.json | 8 ++++---- 56 files changed, 97 insertions(+), 95 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 870fe811b8..a8ec56e9b3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/cli", "description": "CLI for Koishi", - "version": "4.0.0-rc.1", + "version": "4.0.0", "main": "index.js", "typings": "index.d.ts", "engines": { @@ -42,7 +42,7 @@ "chokidar": "^3.5.2", "js-yaml": "^4.1.0", "kleur": "^4.1.4", - "koishi": "^4.0.0-rc.3", + "koishi": "^4.0.0", "prompts": "^2.4.1" } } diff --git a/packages/core/package.json b/packages/core/package.json index 3bd6278495..97716f53b3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/core", "description": "Core features for Koishi", - "version": "4.0.0-rc.3", + "version": "4.0.0", "main": "lib/node.js", "module": "lib/browser.js", "typings": "lib/index.d.ts", @@ -37,7 +37,7 @@ "chai-shape": "^1.0.0" }, "dependencies": { - "@koishijs/utils": "^5.0.0-rc.0", + "@koishijs/utils": "^5.0.0", "fastest-levenshtein": "^1.0.12", "schemastery": "^2.1.3" } diff --git a/packages/create/package.json b/packages/create/package.json index 9dcb6e655f..87eb36dbb1 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -1,7 +1,7 @@ { "name": "create-koishi", "description": "Node runtime for Koishi", - "version": "4.1.1", + "version": "4.1.2", "main": "lib/index.js", "engines": { "node": ">=12.0.0" diff --git a/packages/create/template/package.json b/packages/create/template/package.json index f516d89272..3617f662e0 100644 --- a/packages/create/template/package.json +++ b/packages/create/template/package.json @@ -12,13 +12,13 @@ "typescript": "^4.4.4" }, "dependencies": { - "@koishijs/cli": "^4.0.0-rc.1", - "@koishijs/plugin-adapter-discord": "^2.0.0-rc.1", - "@koishijs/plugin-adapter-onebot": "^4.0.0-rc.1", - "@koishijs/plugin-adapter-telegram": "^2.0.0-rc.0", - "@koishijs/plugin-console": "^1.0.0-rc.2", - "@koishijs/plugin-manager": "^1.0.0-rc.2", - "@koishijs/plugin-status": "^5.0.0-rc.3", - "koishi": "^4.0.0-rc.3" + "@koishijs/cli": "^4.0.0", + "@koishijs/plugin-adapter-discord": "^2.0.0", + "@koishijs/plugin-adapter-onebot": "^4.0.0", + "@koishijs/plugin-adapter-telegram": "^2.0.0", + "@koishijs/plugin-console": "^1.0.0", + "@koishijs/plugin-manager": "^1.0.0", + "@koishijs/plugin-status": "^5.0.0", + "koishi": "^4.0.0" } } diff --git a/packages/koishi/package.json b/packages/koishi/package.json index f385695176..b443e28990 100644 --- a/packages/koishi/package.json +++ b/packages/koishi/package.json @@ -1,7 +1,7 @@ { "name": "koishi", "description": "A QQ bot framework based on CQHTTP", - "version": "4.0.0-rc.3", + "version": "4.0.0", "main": "lib/index.js", "typings": "lib/node.d.ts", "engines": { @@ -33,8 +33,8 @@ }, "dependencies": { "@koa/router": "^10.1.1", - "@koishijs/core": "^4.0.0-rc.3", - "@koishijs/utils": "^5.0.0-rc.0", + "@koishijs/core": "^4.0.0", + "@koishijs/utils": "^5.0.0", "@types/koa": "*", "@types/koa__router": "*", "@types/ws": "^7.4.7", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 2cbe0a78a0..23ab1de4bb 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -39,7 +39,7 @@ }, "peerDependencies": { "@koishijs/plugin-mock": "^1.0.0", - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "chai": "^4.3.4", diff --git a/packages/ui-core/package.json b/packages/ui-core/package.json index 7d6687d6b7..ad097c94d8 100644 --- a/packages/ui-core/package.json +++ b/packages/ui-core/package.json @@ -21,6 +21,6 @@ "vue": "^3.2.21" }, "dependencies": { - "@koishijs/core": "^4.0.0-rc.3" + "@koishijs/core": "^4.0.0" } } diff --git a/packages/ui-playground/package.json b/packages/ui-playground/package.json index 1c8d9c6bac..33c51f9eae 100644 --- a/packages/ui-playground/package.json +++ b/packages/ui-playground/package.json @@ -21,7 +21,7 @@ "vue": "^3.2.21" }, "dependencies": { - "@koishijs/core": "^4.0.0-rc.3", + "@koishijs/core": "^4.0.0", "@vueuse/core": "^6.8.0", "monaco-editor": "^0.27.0" } diff --git a/packages/utils/package.json b/packages/utils/package.json index cdc1b684f9..813e37f3e1 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/utils", "description": "Utilities for Koishi", - "version": "5.0.0-rc.0", + "version": "5.0.0", "main": "lib/node.js", "module": "lib/browser.js", "typings": "lib/index.d.ts", diff --git a/plugins/a11y/admin/package.json b/plugins/a11y/admin/package.json index cc490e3a3a..7a6a857c15 100644 --- a/plugins/a11y/admin/package.json +++ b/plugins/a11y/admin/package.json @@ -32,7 +32,7 @@ "required:database" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/a11y/bind/package.json b/plugins/a11y/bind/package.json index f5c6a28bb6..8cbc93e5ad 100644 --- a/plugins/a11y/bind/package.json +++ b/plugins/a11y/bind/package.json @@ -31,7 +31,7 @@ "bind" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-database-memory": "^1.0.0", diff --git a/plugins/a11y/callme/package.json b/plugins/a11y/callme/package.json index 268eef2e2b..b0b8bb6ef8 100644 --- a/plugins/a11y/callme/package.json +++ b/plugins/a11y/callme/package.json @@ -31,7 +31,7 @@ "callme" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-database-memory": "^1.0.0", diff --git a/plugins/a11y/command-utils/package.json b/plugins/a11y/command-utils/package.json index 98ed0cd990..23454c66bc 100644 --- a/plugins/a11y/command-utils/package.json +++ b/plugins/a11y/command-utils/package.json @@ -30,7 +30,7 @@ "command" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/a11y/rate-limit/package.json b/plugins/a11y/rate-limit/package.json index 71aba63749..ff711f4f22 100644 --- a/plugins/a11y/rate-limit/package.json +++ b/plugins/a11y/rate-limit/package.json @@ -31,7 +31,7 @@ "rate-limit" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/a11y/schedule/package.json b/plugins/a11y/schedule/package.json index a221611427..f469268cd7 100644 --- a/plugins/a11y/schedule/package.json +++ b/plugins/a11y/schedule/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-schedule", "description": "Create scheduled tasks for Koishi", - "version": "4.0.0-rc.0", + "version": "4.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -34,6 +34,6 @@ "@koishijs/plugin-mock": "^1.0.0" }, "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" } } diff --git a/plugins/a11y/sudo/package.json b/plugins/a11y/sudo/package.json index 206d1dfd06..b2664e4313 100644 --- a/plugins/a11y/sudo/package.json +++ b/plugins/a11y/sudo/package.json @@ -31,7 +31,7 @@ "sudo" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-database-memory": "^1.0.0", diff --git a/plugins/a11y/switch/package.json b/plugins/a11y/switch/package.json index a4174fe851..e00ce8b81b 100644 --- a/plugins/a11y/switch/package.json +++ b/plugins/a11y/switch/package.json @@ -33,7 +33,7 @@ "required:database" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-database-memory": "^1.0.0", diff --git a/plugins/a11y/verifier/package.json b/plugins/a11y/verifier/package.json index 6695cb0367..18b5220ff1 100644 --- a/plugins/a11y/verifier/package.json +++ b/plugins/a11y/verifier/package.json @@ -31,7 +31,7 @@ "verifier" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/adapter/discord/package.json b/plugins/adapter/discord/package.json index fc704917b0..aaffb9e832 100644 --- a/plugins/adapter/discord/package.json +++ b/plugins/adapter/discord/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-adapter-discord", "description": "Discord adapter for Koishi", - "version": "2.0.0-rc.1", + "version": "2.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -28,7 +28,7 @@ "koishi" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0", diff --git a/plugins/adapter/kaiheila/package.json b/plugins/adapter/kaiheila/package.json index b5316f81aa..6516703c0f 100644 --- a/plugins/adapter/kaiheila/package.json +++ b/plugins/adapter/kaiheila/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-adapter-kaiheila", "description": "Kaiheila adapter for Koishi", - "version": "2.0.0-rc.0", + "version": "2.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -24,7 +24,7 @@ "koishi" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/adapter/onebot/package.json b/plugins/adapter/onebot/package.json index 709563da80..e63a1e40fd 100644 --- a/plugins/adapter/onebot/package.json +++ b/plugins/adapter/onebot/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-adapter-onebot", "description": "CQHTTP adapter for Koishi", - "version": "4.0.0-rc.1", + "version": "4.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -27,7 +27,7 @@ "koishi" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0", diff --git a/plugins/adapter/qqguild/package.json b/plugins/adapter/qqguild/package.json index ca6edfd148..c29338d550 100644 --- a/plugins/adapter/qqguild/package.json +++ b/plugins/adapter/qqguild/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-adapter-qqguild", "description": "QQ guild adapter for Koishi", - "version": "1.0.0-rc.0", + "version": "1.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -28,7 +28,7 @@ "koishi" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "@qq-guild-sdk/core": "^1.1.0", diff --git a/plugins/adapter/telegram/package.json b/plugins/adapter/telegram/package.json index de7f62b169..f708e6761a 100644 --- a/plugins/adapter/telegram/package.json +++ b/plugins/adapter/telegram/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-adapter-telegram", "description": "Telegram adapter for Koishi", - "version": "2.0.0-rc.0", + "version": "2.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -28,7 +28,7 @@ "koishi" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@types/es-aggregate-error": "^1.0.2", diff --git a/plugins/assets/git/package.json b/plugins/assets/git/package.json index c0ffed1b80..b974edeea3 100644 --- a/plugins/assets/git/package.json +++ b/plugins/assets/git/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-assets-git", "description": "A git assets provider for Koishi", - "version": "1.0.0-rc.2", + "version": "1.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -33,7 +33,7 @@ "service:assets" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "simple-git": "^2.45.1" diff --git a/plugins/assets/local/package.json b/plugins/assets/local/package.json index 8140df3f4a..a873df138b 100644 --- a/plugins/assets/local/package.json +++ b/plugins/assets/local/package.json @@ -30,6 +30,6 @@ "service:assets" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" } } diff --git a/plugins/assets/remote/package.json b/plugins/assets/remote/package.json index 416d9261bd..86d0d93a2e 100644 --- a/plugins/assets/remote/package.json +++ b/plugins/assets/remote/package.json @@ -30,6 +30,6 @@ "service:assets" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" } } diff --git a/plugins/assets/s3/package.json b/plugins/assets/s3/package.json index 255553bdd9..bf5b09d8cd 100644 --- a/plugins/assets/s3/package.json +++ b/plugins/assets/s3/package.json @@ -34,7 +34,7 @@ "service:assets" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "@aws-sdk/client-s3": "^3.20.0" diff --git a/plugins/cache/lru/package.json b/plugins/cache/lru/package.json index 4fe69e8d3a..72dea7d111 100644 --- a/plugins/cache/lru/package.json +++ b/plugins/cache/lru/package.json @@ -31,7 +31,7 @@ "lru" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "lru-cache": "^6.0.0" diff --git a/plugins/cache/redis/package.json b/plugins/cache/redis/package.json index c9807d4b59..9918986c78 100644 --- a/plugins/cache/redis/package.json +++ b/plugins/cache/redis/package.json @@ -35,7 +35,7 @@ "lru" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "generic-pool": "^3.8.2", diff --git a/plugins/common/broadcast/package.json b/plugins/common/broadcast/package.json index 9822bf33b9..ef621dc0e6 100644 --- a/plugins/common/broadcast/package.json +++ b/plugins/common/broadcast/package.json @@ -31,7 +31,7 @@ "broadcast" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-database-memory": "^1.0.0", diff --git a/plugins/common/echo/package.json b/plugins/common/echo/package.json index 3295346a68..5a8faf8771 100644 --- a/plugins/common/echo/package.json +++ b/plugins/common/echo/package.json @@ -31,7 +31,7 @@ "echo" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/common/feedback/package.json b/plugins/common/feedback/package.json index c478cc4a9c..c7c96507fd 100644 --- a/plugins/common/feedback/package.json +++ b/plugins/common/feedback/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-feedback", "description": "Echo plugin for Koishi", - "version": "1.0.1", + "version": "1.0.2", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -31,7 +31,7 @@ "feedback" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/common/forward/package.json b/plugins/common/forward/package.json index 886b965333..3cd01ee718 100644 --- a/plugins/common/forward/package.json +++ b/plugins/common/forward/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-forward", "description": "Forward plugin for Koishi", - "version": "1.0.0", + "version": "1.0.1", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -33,7 +33,7 @@ "optional:database" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/common/recall/package.json b/plugins/common/recall/package.json index bb6f53984d..ad539a07e1 100644 --- a/plugins/common/recall/package.json +++ b/plugins/common/recall/package.json @@ -31,7 +31,7 @@ "recall" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/common/repeater/package.json b/plugins/common/repeater/package.json index ebacb752ca..1dd78c728c 100644 --- a/plugins/common/repeater/package.json +++ b/plugins/common/repeater/package.json @@ -31,7 +31,7 @@ "repeater" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/common/respondent/package.json b/plugins/common/respondent/package.json index 4b487cadab..bc0a629a2f 100644 --- a/plugins/common/respondent/package.json +++ b/plugins/common/respondent/package.json @@ -31,7 +31,7 @@ "optional:database" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0" diff --git a/plugins/database/level/package.json b/plugins/database/level/package.json index defb8f5188..9692affc06 100644 --- a/plugins/database/level/package.json +++ b/plugins/database/level/package.json @@ -31,7 +31,7 @@ "leveldb" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0", diff --git a/plugins/database/memory/package.json b/plugins/database/memory/package.json index 482813dc99..84f371c0d2 100644 --- a/plugins/database/memory/package.json +++ b/plugins/database/memory/package.json @@ -30,7 +30,7 @@ "server" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0", diff --git a/plugins/database/mongo/package.json b/plugins/database/mongo/package.json index 3ba63a8c74..cf2cead416 100644 --- a/plugins/database/mongo/package.json +++ b/plugins/database/mongo/package.json @@ -36,7 +36,7 @@ "@koishijs/test-utils": "^8.0.0-rc.1" }, "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "@koishijs/orm-utils": "^1.0.0", diff --git a/plugins/database/mysql/package.json b/plugins/database/mysql/package.json index 0fbb886733..46665f27b1 100644 --- a/plugins/database/mysql/package.json +++ b/plugins/database/mysql/package.json @@ -35,7 +35,7 @@ "@koishijs/test-utils": "^8.0.0-rc.1" }, "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "@koishijs/orm-utils": "^1.0.0", diff --git a/plugins/database/orm-utils/package.json b/plugins/database/orm-utils/package.json index 307e84781b..8ab49eff32 100644 --- a/plugins/database/orm-utils/package.json +++ b/plugins/database/orm-utils/package.json @@ -31,6 +31,6 @@ "utilities" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" } } diff --git a/plugins/database/sql-utils/package.json b/plugins/database/sql-utils/package.json index 7c1196da7d..7f8a82eaae 100644 --- a/plugins/database/sql-utils/package.json +++ b/plugins/database/sql-utils/package.json @@ -31,7 +31,7 @@ "utilities" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": {}, "dependencies": {} diff --git a/plugins/database/sqlite/package.json b/plugins/database/sqlite/package.json index 9843858465..eb4665f39a 100644 --- a/plugins/database/sqlite/package.json +++ b/plugins/database/sqlite/package.json @@ -32,7 +32,7 @@ "sqlite" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "@koishijs/orm-utils": "^1.0.0", diff --git a/plugins/eval/package.json b/plugins/eval/package.json index b0dcbf0c4c..83d04e5747 100644 --- a/plugins/eval/package.json +++ b/plugins/eval/package.json @@ -1,6 +1,6 @@ { "name": "@koishijs/plugin-eval", - "version": "4.0.0-rc.2", + "version": "4.0.0", "description": "Execute JavaScript and create addons in Koishi", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -33,7 +33,7 @@ "code" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "optionalDependencies": { "@babel/core": "^7.15.5", diff --git a/plugins/frontend/builder/package.json b/plugins/frontend/builder/package.json index 3e34621e1d..27f1300e44 100644 --- a/plugins/frontend/builder/package.json +++ b/plugins/frontend/builder/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/console-builder", "description": "Building utilities for @koishijs/plugin-console", - "version": "1.0.0-beta.0", + "version": "1.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ diff --git a/plugins/frontend/chat/package.json b/plugins/frontend/chat/package.json index 7129d0f6c5..1756cf66f6 100644 --- a/plugins/frontend/chat/package.json +++ b/plugins/frontend/chat/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-chat", "description": "Display and respond to messages for Koishi", - "version": "1.0.0-rc.1", + "version": "1.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -30,9 +30,9 @@ "server" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { - "@koishijs/plugin-console": "^1.0.0-rc.2" + "@koishijs/plugin-console": "^1.0.0" } } diff --git a/plugins/frontend/commands/client/index.ts b/plugins/frontend/commands/client/index.ts index e2b735f907..0a13c84c2a 100644 --- a/plugins/frontend/commands/client/index.ts +++ b/plugins/frontend/commands/client/index.ts @@ -4,7 +4,7 @@ import Commands from './commands.vue' registerPage({ path: '/commands', - name: '指令', + name: '指令管理', icon: 'tools', order: 500, fields: ['commands'], diff --git a/plugins/frontend/commands/package.json b/plugins/frontend/commands/package.json index 84fcc23a12..ae5cec8afe 100644 --- a/plugins/frontend/commands/package.json +++ b/plugins/frontend/commands/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-commands", "description": "Override command config for Koishi", - "version": "1.0.0", + "version": "1.0.1", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -31,8 +31,8 @@ "command" ], "peerDependencies": { - "@koishijs/plugin-console": "^1.0.0-rc.2", - "koishi": "^4.0.0-rc.3" + "@koishijs/plugin-console": "^1.0.0", + "koishi": "^4.0.0" }, "devDependencies": { "@koishijs/plugin-mock": "^1.0.0", diff --git a/plugins/frontend/console/package.json b/plugins/frontend/console/package.json index 469b9d8b19..7040ee6a24 100644 --- a/plugins/frontend/console/package.json +++ b/plugins/frontend/console/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-console", "description": "A web user interface for Koishi", - "version": "1.0.0-rc.2", + "version": "1.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -30,7 +30,7 @@ "webui" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { "@fortawesome/fontawesome-free": "^5.15.4", diff --git a/plugins/frontend/manager/package.json b/plugins/frontend/manager/package.json index 92b22c32ef..f278f9113c 100644 --- a/plugins/frontend/manager/package.json +++ b/plugins/frontend/manager/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-manager", "description": "Manage your bots and plugins with console", - "version": "1.0.0-rc.2", + "version": "1.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -32,11 +32,11 @@ "server" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { - "@koishijs/cli": "^4.0.0-rc.1", - "@koishijs/plugin-console": "^1.0.0-rc.2", + "@koishijs/cli": "^4.0.0", + "@koishijs/plugin-console": "^1.0.0", "@octokit/openapi-types": "^10.6.4", "@types/cross-spawn": "^6.0.2", "@types/throttle-debounce": "^2.1.0" diff --git a/plugins/frontend/status/package.json b/plugins/frontend/status/package.json index db38364b94..9c984d228b 100644 --- a/plugins/frontend/status/package.json +++ b/plugins/frontend/status/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-status", "description": "Status view for Koishi", - "version": "5.0.0-rc.3", + "version": "5.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -30,11 +30,11 @@ "server" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { - "@koishijs/cli": "^4.0.0-rc.1", - "@koishijs/plugin-console": "^1.0.0-rc.2", + "@koishijs/cli": "^4.0.0", + "@koishijs/plugin-console": "^1.0.0", "ansi_up": "^5.0.1" }, "dependencies": { diff --git a/plugins/github/package.json b/plugins/github/package.json index ba035ae695..24688389a4 100644 --- a/plugins/github/package.json +++ b/plugins/github/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-github", "description": "GitHub toolkit for Koishi", - "version": "4.0.0-rc.1", + "version": "4.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -32,12 +32,12 @@ ], "devDependencies": { "@types/marked": "^3.0.0", - "@koishijs/plugin-puppeteer": "^3.0.0-rc.0", + "@koishijs/plugin-puppeteer": "^3.0.0", "@koishijs/plugin-database-memory": "^1.0.0", "@koishijs/plugin-mock": "^1.0.0" }, "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "@octokit/webhooks-types": "^4.4.0", diff --git a/plugins/mock/package.json b/plugins/mock/package.json index 690afb9836..dd4c4fa055 100644 --- a/plugins/mock/package.json +++ b/plugins/mock/package.json @@ -34,6 +34,6 @@ "mock" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" } } diff --git a/plugins/puppeteer/package.json b/plugins/puppeteer/package.json index 5842631252..034f5607c7 100644 --- a/plugins/puppeteer/package.json +++ b/plugins/puppeteer/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-puppeteer", "description": "Take Screenshots in Koishi", - "version": "3.0.0-rc.0", + "version": "3.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "files": [ @@ -32,7 +32,7 @@ "@types/pngjs": "^6.0.1" }, "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "dependencies": { "chrome-finder": "^1.0.7", diff --git a/plugins/puppeteer/src/index.ts b/plugins/puppeteer/src/index.ts index df4ba50bcc..b24fd72543 100644 --- a/plugins/puppeteer/src/index.ts +++ b/plugins/puppeteer/src/index.ts @@ -159,8 +159,10 @@ export function apply(ctx: Context, config: Config = {}) { const { defaultViewport } = config.browser ctx.on('ready', async () => { - ctx.puppeteer = new Puppeteer(ctx, config) - await ctx.puppeteer.launch().catch((error) => { + const puppeteer = new Puppeteer(ctx, config) + await puppeteer.launch().then(() => { + ctx.puppeteer = puppeteer + }, (error) => { logger.error(error) ctx.dispose() }) diff --git a/plugins/teach/package.json b/plugins/teach/package.json index a115545b01..187747561c 100644 --- a/plugins/teach/package.json +++ b/plugins/teach/package.json @@ -1,7 +1,7 @@ { "name": "@koishijs/plugin-teach", "description": "Teach plugin for Koishi", - "version": "3.0.0-rc.3", + "version": "3.0.0", "main": "lib/index.js", "typings": "lib/index.d.ts", "engines": { @@ -36,12 +36,12 @@ "optional:assets" ], "peerDependencies": { - "koishi": "^4.0.0-rc.3" + "koishi": "^4.0.0" }, "devDependencies": { - "@koishijs/plugin-console": "^1.0.0-rc.2", + "@koishijs/plugin-console": "^1.0.0", "@koishijs/plugin-database-memory": "^1.0.0", - "@koishijs/plugin-status": "^5.0.0-rc.3", + "@koishijs/plugin-status": "^5.0.0", "@koishijs/plugin-mock": "^1.0.0" }, "dependencies": { From c83a54720c1e982b61e5e2c2552fb5509b06eb0f Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sat, 15 Jan 2022 00:09:55 +0800 Subject: [PATCH 34/34] feat(puppeteer): enhance service experience --- plugins/puppeteer/src/index.ts | 221 ++++------------------------ plugins/puppeteer/src/screenshot.ts | 136 +++++++++++++++++ 2 files changed, 161 insertions(+), 196 deletions(-) create mode 100644 plugins/puppeteer/src/screenshot.ts diff --git a/plugins/puppeteer/src/index.ts b/plugins/puppeteer/src/index.ts index b24fd72543..10105d25db 100644 --- a/plugins/puppeteer/src/index.ts +++ b/plugins/puppeteer/src/index.ts @@ -1,38 +1,10 @@ -import puppeteer, { Browser, ElementHandle, Page, Shooter, Viewport } from 'puppeteer-core' -import { Context, Logger, hyphenate, noop, segment, Schema, Time } from 'koishi' -import { PNG } from 'pngjs' -import { resolve } from 'path' -import {} from '@koishijs/plugin-eval' +import puppeteer, { Browser, ElementHandle, Page } from 'puppeteer-core' +import { Context, segment, Schema, Service, Logger } from 'koishi' import { SVG, SVGOptions } from './svg' +import * as screenshot from './screenshot' export * from './svg' -// workaround puppeteer typings downgrade -declare module 'puppeteer-core/lib/types' { - interface Base64ScreenshotOptions extends ScreenshotOptions { - encoding: 'base64' - } - - interface BinaryScreenshotOptions extends ScreenshotOptions { - encoding?: 'binary' - } - - interface Shooter { - screenshot(options?: Base64ScreenshotOptions): Promise - screenshot(options?: BinaryScreenshotOptions): Promise - } - - interface Page extends Shooter { - screenshot(options?: Base64ScreenshotOptions): Promise - screenshot(options?: BinaryScreenshotOptions): Promise - } - - interface ElementHandle extends Shooter { - screenshot(options?: Base64ScreenshotOptions): Promise - screenshot(options?: BinaryScreenshotOptions): Promise - } -} - declare module 'koishi' { namespace Context { interface Services { @@ -40,71 +12,59 @@ declare module 'koishi' { } } - interface Modules { - puppeteer: typeof import('.') - } - interface EventMap { - 'puppeteer/start'(): void 'puppeteer/validate'(url: string): string } } type LaunchOptions = Parameters[0] +const LaunchOptions = Schema.object({ + executablePath: Schema.string().description('Chromium 可执行文件的路径。缺省时将自动从系统中寻找。'), + viewPort: Schema.object({ + width: Schema.number().description('默认的视图宽度。').default(800), + height: Schema.number().description('默认的视图高度。').default(600), + deviceScaleFactor: Schema.number().description('默认的设备缩放比率。').default(2), + }), +}).description('浏览器设置') + type RenderCallback = (page: Page, next: (handle?: ElementHandle) => Promise) => Promise export const name = 'puppeteer' export interface Config { browser?: LaunchOptions - renderViewport?: Partial - loadTimeout?: number - idleTimeout?: number - maxSize?: number - protocols?: string[] + screenshot?: screenshot.Config } export const Config = Schema.object({ - browser: Schema.object({ - executablePath: Schema.string().description('Chromium 可执行文件的路径。缺省时将自动从系统中寻找。'), - viewPort: Schema.object({ - width: Schema.number().description('默认的视图宽度。').default(800), - height: Schema.number().description('默认的视图高度。').default(600), - deviceScaleFactor: Schema.number().description('默认的设备缩放比率。').default(2), - }), - }).description('浏览器设置'), - maxSize: Schema.number().description('单张图片的最大尺寸,单位为字节。当截图尺寸超过这个值时会自动截取图片顶部的一段进行发送。').default(1000000), - loadTimeout: Schema.number().description('加载页面的最长时间。当一个页面等待时间超过这个值时,如果此页面主体已经加载完成,则会发送一条提示消息“正在加载中,请稍等片刻”并继续等待加载;否则会直接提示“无法打开页面”并终止加载。').default(Time.second * 10), - idleTimeout: Schema.number().description('等待页面空闲的最长时间。当一个页面等待时间超过这个值时,将停止进一步的加载并立即发送截图。').default(Time.second * 30), + browser: LaunchOptions, + screenshot: screenshot.Config, }) -enum Status { close, opening, open, closing } +const logger = new Logger('puppeteer') + +Context.service('puppeteer') -export class Puppeteer { - status = Status.close +export default class Puppeteer extends Service { browser: Browser - private promise: Promise - constructor(private context: Context, public config: Config) { + constructor(ctx: Context, public config: Config) { + super(ctx, 'puppeteer') if (!config.browser.executablePath) { const findChrome = require('chrome-finder') logger.debug('chrome executable found at %c', config.browser.executablePath = findChrome()) } + ctx.plugin(screenshot, this.config.screenshot) } - launch = async () => { - this.status = Status.opening - this.browser = await (this.promise = puppeteer.launch(this.config.browser)) - this.status = Status.open + async start() { + this.browser = await puppeteer.launch(this.config.browser) logger.debug('browser launched') - this.context.emit('puppeteer/start') } - close = async () => { - this.status = Status.closing + async stop() { await this.browser?.close() - this.status = Status.close } page = () => this.browser.newPage() @@ -112,17 +72,7 @@ export class Puppeteer { svg = (options?: SVGOptions) => new SVG(options) render = async (content: string, callback?: RenderCallback) => { - if (this.status === Status.opening) { - await this.promise - } else if (this.status !== Status.open) { - throw new Error('browser instance is not running') - } - const page = await this.page() - await page.setViewport({ - ...this.config.browser.defaultViewport, - ...this.config.renderViewport, - }) if (content) await page.setContent(content) callback ||= async (_, next) => page.$('body').then(next) @@ -136,124 +86,3 @@ export class Puppeteer { return output } } - -export const defaultConfig: Config = { - browser: {}, - loadTimeout: 10000, // 10s - idleTimeout: 30000, // 30s - maxSize: 1000000, // 1MB - protocols: ['http', 'https'], - renderViewport: { - width: 800, - height: 600, - deviceScaleFactor: 2, - }, -} - -Context.service('puppeteer') - -const logger = new Logger('puppeteer') - -export function apply(ctx: Context, config: Config = {}) { - config = { ...defaultConfig, ...config } - const { defaultViewport } = config.browser - - ctx.on('ready', async () => { - const puppeteer = new Puppeteer(ctx, config) - await puppeteer.launch().then(() => { - ctx.puppeteer = puppeteer - }, (error) => { - logger.error(error) - ctx.dispose() - }) - }) - - ctx.on('dispose', async () => { - await ctx.puppeteer?.close() - delete ctx.puppeteer - }) - - const ctx1 = ctx.intersect(sess => !!sess.app.puppeteer) - ctx1.command('shot [selector:rawtext]', '网页截图', { authority: 2 }) - .alias('screenshot') - .option('full', '-f 对整个可滚动区域截图') - .option('viewport', '-v 指定视口') - .action(async ({ session, options }, url, selector) => { - if (!url) return '请输入网址。' - const scheme = /^(\w+):\/\//.exec(url) - if (!scheme) { - url = 'http://' + url - } else if (!config.protocols.includes(scheme[1])) { - return '请输入正确的网址。' - } - - const result = ctx.bail('puppeteer/validate', url) - if (typeof result === 'string') return result - - let loaded = false - const page = await ctx.puppeteer.page() - page.on('load', () => loaded = true) - - try { - if (options.viewport) { - const viewport = options.viewport.split('x') - const width = +viewport[0] - const height = +viewport[1] - if (width !== defaultViewport?.width || height !== defaultViewport?.height) { - await page.setViewport({ width, height }) - } - } - - await new Promise((resolve, reject) => { - logger.debug(`navigating to ${url}`) - const _resolve = () => { - clearTimeout(timer) - resolve() - } - - page.goto(url, { - waitUntil: 'networkidle0', - timeout: config.idleTimeout, - }).then(_resolve, () => { - return loaded ? _resolve() : reject(new Error('navigation timeout')) - }) - - const timer = setTimeout(() => { - return loaded - ? session.send('正在加载中,请稍等片刻~') - : reject(new Error('navigation timeout')) - }, config.loadTimeout) - }) - } catch (error) { - page.close() - logger.debug(error) - return '无法打开页面。' - } - - const shooter: Shooter = selector ? await page.$(selector) : page - if (!shooter) return '找不到满足该选择器的元素。' - - return shooter.screenshot({ - fullPage: options.full, - }).then(async (buffer) => { - if (buffer.byteLength > config.maxSize) { - await new Promise((resolve, reject) => { - const png = new PNG() - png.parse(buffer, (error, data) => { - return error ? reject(error) : resolve(data) - }) - }).then((data) => { - const width = data.width - const height = data.height * config.maxSize / buffer.byteLength - const png = new PNG({ width, height }) - data.bitblt(png, 0, 0, width, height, 0, 0) - buffer = PNG.sync.write(png) - }).catch(noop) - } - return segment.image(buffer) - }, (error) => { - logger.debug(error) - return '截图失败。' - }).finally(() => page.close()) - }) -} diff --git a/plugins/puppeteer/src/screenshot.ts b/plugins/puppeteer/src/screenshot.ts new file mode 100644 index 0000000000..6579de0b3d --- /dev/null +++ b/plugins/puppeteer/src/screenshot.ts @@ -0,0 +1,136 @@ +import { Shooter } from 'puppeteer-core' +import { Context, Logger, noop, Schema, segment, Time } from 'koishi' +import { PNG } from 'pngjs' + +// workaround puppeteer typings downgrade +declare module 'puppeteer-core/lib/types' { + interface Base64ScreenshotOptions extends ScreenshotOptions { + encoding: 'base64' + } + + interface BinaryScreenshotOptions extends ScreenshotOptions { + encoding?: 'binary' + } + + interface Shooter { + screenshot(options?: Base64ScreenshotOptions): Promise + screenshot(options?: BinaryScreenshotOptions): Promise + } + + interface Page extends Shooter { + screenshot(options?: Base64ScreenshotOptions): Promise + screenshot(options?: BinaryScreenshotOptions): Promise + } + + interface ElementHandle extends Shooter { + screenshot(options?: Base64ScreenshotOptions): Promise + screenshot(options?: BinaryScreenshotOptions): Promise + } +} + +const logger = new Logger('puppeteer') + +export const name = 'screenshot' +export const using = ['puppeteer'] as const + +export interface Config { + loadTimeout?: number + idleTimeout?: number + maxSize?: number + protocols?: string[] +} + +export const Config: Schema = Schema.object({ + protocols: Schema.array(Schema.string()).description('允许的协议列表。').default(['http', 'https']), + maxSize: Schema.number().description('单张图片的最大尺寸,单位为字节。当截图尺寸超过这个值时会自动截取图片顶部的一段进行发送。').default(1000000), + loadTimeout: Schema.number().description('加载页面的最长时间。当一个页面等待时间超过这个值时,如果此页面主体已经加载完成,则会发送一条提示消息“正在加载中,请稍等片刻”并继续等待加载;否则会直接提示“无法打开页面”并终止加载。').default(Time.second * 5), + idleTimeout: Schema.number().description('等待页面空闲的最长时间。当一个页面等待时间超过这个值时,将停止进一步的加载并立即发送截图。').default(Time.second * 30), +}).description('截图设置') + +export function apply(ctx: Context, config: Config) { + const { defaultViewport } = ctx.puppeteer.config.browser + const { protocols, maxSize, loadTimeout, idleTimeout } = config + + ctx.command('shot [selector:rawtext]', '网页截图', { authority: 2 }) + .alias('screenshot') + .option('full', '-f 对整个可滚动区域截图') + .option('viewport', '-v 指定视口') + .action(async ({ session, options }, url, selector) => { + if (!url) return '请输入网址。' + const scheme = /^(\w+):\/\//.exec(url) + if (!scheme) { + url = 'http://' + url + } else if (!protocols.includes(scheme[1])) { + return '请输入正确的网址。' + } + + const result = ctx.bail('puppeteer/validate', url) + if (typeof result === 'string') return result + + let loaded = false + const page = await ctx.puppeteer.page() + page.on('load', () => loaded = true) + + try { + if (options.viewport) { + const viewport = options.viewport.split('x') + const width = +viewport[0] + const height = +viewport[1] + if (width !== defaultViewport?.width || height !== defaultViewport?.height) { + await page.setViewport({ width, height }) + } + } + + await new Promise((resolve, reject) => { + logger.debug(`navigating to ${url}`) + const _resolve = () => { + clearTimeout(timer) + resolve() + } + + page.goto(url, { + waitUntil: 'networkidle0', + timeout: idleTimeout, + }).then(_resolve, () => { + return loaded ? _resolve() : reject(new Error('navigation timeout')) + }) + + const timer = setTimeout(() => { + return loaded + ? session.send('正在加载中,请稍等片刻~') + : reject(new Error('navigation timeout')) + }, loadTimeout) + }) + } catch (error) { + page.close() + logger.debug(error) + return '无法打开页面。' + } + + const shooter: Shooter = selector ? await page.$(selector) : page + if (!shooter) return '找不到满足该选择器的元素。' + + return shooter.screenshot({ + fullPage: options.full, + }).then(async (buffer) => { + if (buffer.byteLength > maxSize) { + await new Promise((resolve, reject) => { + const png = new PNG() + png.parse(buffer, (error, data) => { + return error ? reject(error) : resolve(data) + }) + }).then((data) => { + const width = data.width + const height = data.height * maxSize / buffer.byteLength + const png = new PNG({ width, height }) + data.bitblt(png, 0, 0, width, height, 0, 0) + buffer = PNG.sync.write(png) + }).catch(noop) + } + return segment.image(buffer) + }, (error) => { + logger.debug(error) + return '截图失败。' + }).finally(() => page.close()) + }) +}