Compare commits

..

91 Commits

Author SHA1 Message Date
linyuchen
54179cb686 fix: can't get qq of the at member 2024-03-16 03:28:43 +08:00
linyuchen
c9a5ee69cf Merge pull request #130 from super1207/main 2024-03-15 21:17:25 +08:00
super1207
e348103e84 新增设置头像的api,set_qq_avatar 2024-03-15 20:45:22 +08:00
linyuchen
fccb0852aa feat: 新增主动获取被过滤的加群通知 2024-03-15 18:54:56 +08:00
linyuchen
b3ea8fbc0c Merge branch 'config-api' into dev
# Conflicts:
#	src/onebot11/action/index.ts
2024-03-15 17:42:15 +08:00
linyuchen
ee483dd0cc Merge branch 'dev' 2024-03-15 17:28:56 +08:00
linyuchen
ed681b8adf feat: 群文件上传事件
feat: 群文件上传接口
2024-03-15 17:28:32 +08:00
linyuchen
dcd4533eb3 Merge branch 'dev' 2024-03-15 14:37:31 +08:00
linyuchen
178c32053b fix: 转发消息id时顺序不对
fix: 以文件名发送文件失败
2024-03-15 14:37:05 +08:00
linyuchen
49ba276f5d fix: 收到的文件没有删干净 2024-03-15 11:41:37 +08:00
linyuchen
2bfe9e236b fix: 独立窗口下撤回消息重复上报 2024-03-15 11:41:11 +08:00
linyuchen
05fd258afd feat: config api 2024-03-13 21:51:17 +08:00
linyuchen
3b3098e017 docs: update plugin description 2024-03-13 11:30:13 +08:00
linyuchen
ddf9eed3a5 docs: update readme 2024-03-13 10:00:14 +08:00
linyuchen
712f0a8256 refactor: auto delete db memory cache 2024-03-13 09:11:58 +08:00
linyuchen
a93220f9d2 Merge branch 'main' into dev
# Conflicts:
#	manifest.json
2024-03-13 08:44:13 +08:00
linyuchen
253cee7458 chore: ver 3.14.1 2024-03-13 04:23:18 +08:00
linyuchen
82f9a4c63f Merge branch 'short-video' 2024-03-13 04:22:25 +08:00
linyuchen
de6c8a5558 feat: send video by videoElement 2024-03-13 04:22:11 +08:00
linyuchen
c75337b8cb fix: 开启独立窗口后重复上报 2024-03-13 01:40:44 +08:00
linyuchen
2b796e33fe test: try to send video element 2024-03-13 01:20:50 +08:00
linyuchen
175307d980 docs: update readme 2024-03-12 09:18:39 +08:00
linyuchen
993f8a9e8f docs: update readme 2024-03-12 09:07:47 +08:00
linyuchen
0130b8f6f7 fix: plugin icon 2024-03-12 08:35:41 +08:00
linyuchen
ba482d492f fix: plugin icon 2024-03-12 08:32:51 +08:00
linyuchen
6e71cd6064 feat: group whole ban event 2024-03-11 11:55:10 +08:00
linyuchen
83dc1abd4a refactor: optimize json parser 2024-03-11 11:44:17 +08:00
linyuchen
4cabb9696e refactor: custom json parser 2024-03-11 10:46:58 +08:00
linyuchen
75883e9cae refactor: refactor new group member event
feat: group ban event
2024-03-11 09:55:50 +08:00
linyuchen
eeadaa12e9 fix: group notify db cache 2024-03-10 22:44:33 +08:00
linyuchen
192736c8be docs: add friend link 2024-03-10 10:05:13 +08:00
linyuchen
586fbb6518 refactor: host input component add attribute parameter 2024-03-10 10:04:09 +08:00
linyuchen
0a42e2df5b Merge remote-tracking branch 'origin/main' 2024-03-10 00:04:37 +08:00
linyuchen
97a637f0c6 chore: ver 3.13.10 2024-03-10 00:04:09 +08:00
linyuchen
3f10b7a002 fix: message real_id use int32 2024-03-10 00:03:37 +08:00
linyuchen
f638e48260 fix: 消息重复入库导致message_id每次都是+2 2024-03-10 00:03:18 +08:00
linyuchen
354ee389bc feat: clean cache api 2024-03-09 22:58:21 +08:00
linyuchen
7188946d7a refactor: try to refactor forward msg 2024-03-09 22:30:16 +08:00
linyuchen
53055e9eab Merge pull request #124 from super1207/main
fix cqcode encode
2024-03-09 22:24:24 +08:00
super1207
7bdb84b11b fix cqcode format 2024-03-09 15:04:09 +08:00
linyuchen
c906bcf7ea docs: update todo list 2024-03-09 12:53:16 +08:00
linyuchen
cdc82562a3 docs: update readme 2024-03-09 12:49:35 +08:00
linyuchen
c34ce8ce0c docs: update readme 2024-03-09 12:40:52 +08:00
linyuchen
d1c94754ee fix: send forward msg too fast 2024-03-09 09:59:39 +08:00
linyuchen
2626555c51 fix: check pic and ptt size 2024-03-09 02:43:58 +08:00
linyuchen
5ff6ceec6d chore: ver 3.13.8 2024-03-09 02:38:05 +08:00
linyuchen
17af156451 fix: update msg seqId 2024-03-09 02:34:21 +08:00
linyuchen
c3c9e74832 fix: image cache url 2024-03-08 23:52:35 +08:00
linyuchen
0480208738 feat: file cache db 2024-03-08 23:27:20 +08:00
linyuchen
62eefbdb69 fix: sent pic message url 2024-03-08 22:16:05 +08:00
linyuchen
566537cbe3 fix: 构建转发消息的文件没有自动删除 2024-03-07 20:19:00 +08:00
linyuchen
ed831ae4cd fix: 网络下载文件大小异常提示 2024-03-07 19:07:00 +08:00
linyuchen
501031b39b fix: 网络视频后缀识别
fix: 网络文件下载失败的错误提示
2024-03-07 18:43:47 +08:00
linyuchen
7bfb3f2003 fix: 私聊带@报错,现已过滤私聊的at消息 2024-03-07 17:37:57 +08:00
linyuchen
ba8ed36c6a fix: can not send reply msg
fix: send like return error
2024-03-07 15:34:09 +08:00
linyuchen
55d046b4f9 fix: reply msg id 2024-03-07 14:18:24 +08:00
linyuchen
ac417cedd3 fix: reply msg id 2024-03-07 10:43:17 +08:00
linyuchen
ade509f26d perf: 优化数据库缓存 2024-03-07 09:00:22 +08:00
linyuchen
a66c1a9779 Merge remote-tracking branch 'origin/main' 2024-03-06 23:33:35 +08:00
linyuchen
239cf18887 fix: init db failed 2024-03-06 23:33:19 +08:00
linyuchen
936554cca2 chore: ver 3.13.2 2024-03-06 22:57:36 +08:00
linyuchen
e1d47f55bf fix: hotfix db initialize failed 2024-03-06 22:55:33 +08:00
linyuchen
cc8e8f108b docs: update readme, file msg add new field name 2024-03-06 22:02:11 +08:00
linyuchen
e6d36dc6c3 fix: send video filename
fix: send msg don't return message_id on Linux
2024-03-06 21:14:13 +08:00
linyuchen
aedc8cfc91 Merge branch 'main' into dev 2024-03-06 20:30:59 +08:00
linyuchen
00c80bf181 feat: send file api add name field 2024-03-06 20:30:26 +08:00
linyuchen
72890a8b59 Merge pull request #111 from lgc2333/patch-1
fix typo
2024-03-06 19:48:53 +08:00
student_2333
3b2577bcad fix typo 2024-03-06 19:46:15 +08:00
linyuchen
063b2460f8 feat: some link on setting ui 2024-03-06 11:44:02 +08:00
linyuchen
9427377f30 Merge remote-tracking branch 'origin/main' 2024-03-06 11:24:58 +08:00
linyuchen
ecff16050a feat: history msg db cache
test: try send music card
2024-03-06 11:24:37 +08:00
手瓜一十雪
873cc6d6a5 docs: update readme 2024-03-05 22:26:57 +08:00
手瓜一十雪
eeb429048b docs: update readme 2024-03-05 22:26:09 +08:00
linyuchen
f240f28ea6 docs: update readme 2024-03-05 11:53:42 +08:00
Misa Liu
66ca936148 style: Finishing code 2024-02-28 00:16:04 +08:00
Misa Liu
5088112864 feat: Now clean_cache API can delete cache files 2024-02-28 00:16:04 +08:00
Misa Liu
91075e192b feat: Add getFileCacheInfo to NTQQApi 2024-02-28 00:16:03 +08:00
Misa Liu
11108bc13f fix: Fix type 2024-02-28 00:16:03 +08:00
Misa Liu
3ec1134204 feat: Add clean_cache API to OneBot adapter 2024-02-28 00:16:02 +08:00
Misa Liu
de41dab846 fix: Fix a typo 2024-02-28 00:16:02 +08:00
Misa Liu
ededfe0f8c fix: Delete specific IPC channel for cache related APIs 2024-02-28 00:16:02 +08:00
Misa Liu
6548876c74 fix: Use a specific IPC channel for cache related API 2024-02-28 00:16:01 +08:00
Misa Liu
839fd7f1ab feat: Add addCacheScannedPaths to NTQQApi 2024-02-28 00:16:01 +08:00
Misa Liu
2f9cd8ba19 feat: Add getDesktopTmpPath to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
85d648622d feat: Add getHotUpdateCachePath to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
f08c816286 feat: Add clearCache to NTQQApi 2024-02-28 00:16:00 +08:00
Misa Liu
6c267044f0 fix: Use a specific IPC channel for cache related API 2024-02-28 00:15:59 +08:00
Misa Liu
b548fd3f0e feat: Add getCacheSessingPathList to NTQQApi 2024-02-28 00:15:59 +08:00
Misa Liu
f110c2d3df style: Fix typo 2024-02-28 00:15:58 +08:00
Misa Liu
f521873ba7 feat: Add scanCache to NTQQApi 2024-02-28 00:15:58 +08:00
Misa Liu
afe0ff89a7 feat: Add chat cache scan & clear to NTQQApi 2024-02-28 00:15:58 +08:00
46 changed files with 2428 additions and 1021 deletions

176
README.md
View File

@@ -1,173 +1,25 @@
# LLOneBot API # LLOneBot API
LiteLoaderQQNTOneBot11协议插件 LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG群<https://t.me/+nLZEnpne-pQ1OWFl>
*注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档*
*V3之后不再需要LLAPI*
## 安装方法 ## 安装方法
### Linux 容器化快速安装
执行以下任意脚本按照提示设置NoVnc密码即可运行脚本问题与异常参考 [llonebot-docker](https://github.com/MliKiowa/llonebot-docker) 项目。 <https://llonebot.github.io/zh-CN/guide/getting-started>
```bash ## 设置界面
curl https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboot.sh -o fastboot.sh & chmod +x fastboot.sh & sudo sh fastboot.sh
```
```bash
wget -O fastboot.sh https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboot.sh & chmod +x fastboot.sh & sudo sh fastboot.sh
```
### 通用手动安装方法 <img src="./doc/image/setting.png" width="500px" alt="图片名称"/>
1.安装[LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) ## HTTP 调用示例
2.安装本项目插件[OneBotApi](https://github.com/linyuchen/LiteLoaderQQNT-OneBotApi/releases/), 注意本插件2.0以下的版本不支持LiteLoader 1.0.0及以上版本
*关于插件的安装方法: 下载后解压复制到插件目录*
*插件目录:`LiteLoaderQQNT/plugins`*
安装后的目录结构如下
```
├── plugins
│ ├── LLOneBot
│ │ └── main/
│ │ └── preload/
│ │ └── renderer/
│ │ └── manifest.json
│ │ └── node_modules/...
```
### 使用termux安装
见<https://github.com/LLOneBot/llonebot-termux>
## 支持的API
目前支持的协议
- [x] http调用api
- [x] http事件上报
- [x] 正向websocket
- [x] 反向websocket
主要功能:
- [x] 发送好友消息
- [x] 发送群消息
- [x] 获取好友列表
- [x] 获取群列表
- [x] 获取群成员列表
- [x] 撤回消息
- [x] 处理添加好友请求
- [x] 处理加群请求
- [x] 退群
- [x] 上报好友消息
- [x] 上报添加好友请求
- [x] 上报群消息
- [x] 上报好友、群消息撤回
- [x] 上报加群请求
- [x] 上报群员人数变动(尚不支持识别群员人数变动原因)
- [x] 设置群管理员
- [x] 群禁言/全体禁言
- [x] 群踢人
- [x] 群改群成员名片
- [x] 修改群名
消息格式支持:
- [x] cq码
- [x] 文字
- [x] 表情
- [x] 图片
- [x] 引用消息
- [x] @群成员
- [x] 语音(支持mp3、wav等多种音频格式直接发送)
- [x] json消息(只上报)
- [x] 转发消息记录(目前只能发不能收)
- [x] 视频(上报时暂时只有个空的file)
- [x] 文件(上报时暂时只有个空的file), type为file, data为{file: uri}, 发送时uri支持http://, file://, base64://
```
{
"type": "file",
"data": {
"file": "file:///D:/1.txt"
}
}
```
- [ ] 发送音乐卡片
- [ ] 红包(没有计划支持)
- [ ] xml (没有计划支持)
## 示例
![](doc/image/example.jpg) ![](doc/image/example.jpg)
## 一些坑 ## 支持的 api 和功能详情
<details> <https://llonebot.github.io/zh-CN/develop/api>
<summary>下载了插件但是没有看到在NTQQ中生效</summary>
<br/>
检查是否下载的是插件release的版本如果是源码的话需要自行编译。依然不生效请查阅<a href="https://liteloaderqqnt.github.io/guide/plugins.html">LiteLoaderQQNT的文档</a>
</details>
<br/>
<details>
<summary>调用接口报404</summary>
<br/>
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口
-
</details>
<br/>
<details>
<summary>发送不了图片和语音</summary>
<br/>
检查当前操作用户是否有LiteLoaderQQNT/data/LLOneBot的写入权限如Windows把QQ上安装到C盘有可能会出现无权限导致发送失败
</details>
<br/>
<details>
<summary>QQ变得很卡</summary>
<br/>
这是你的群特别多导致的,因为启动后会批量获取群成员列表,获取完之后就正常了
</details>
<br/>
## 支持的onebot v11 api:
- [x] get_login_info
- [x] send_msg
- [x] send_group_msg
- [x] send_private_msg
- [x] delete_msg
- [x] get_group_list
- [x] get_group_info
- [x] get_group_member_list
- [x] get_group_member_info
- [x] get_friend_list
- [x] set_friend_add_request
- [x] get_msg
- [x] send_like
- [x] set_group_add_request
- [x] set_group_leave
- [x] set_group_kick
- [x] set_group_ban
- [x] set_group_whole_ban
- [x] set_group_kick
- [x] set_group_admin
- [x] set_group_card
- [x] set_group_name
- [x] get_version_info
- [x] get_status
- [x] can_send_image
- [x] can_send_record
- [x] get_image
- [x] get_record
### 支持的go-cqhtp api:
- [x] send_private_forward_msg
- [x] send_group_forward_msg
- [x] get_stranger_info
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用 - [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
@@ -177,14 +29,24 @@ wget -O fastboot.sh https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboo
- [x] 群管理功能,禁言、踢人,改群名片等 - [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息 - [x] 视频消息
- [x] 文件消息 - [x] 文件消息
- [ ] 音乐卡片 - [x] 群禁言事件上报
- [x] 优化加群成功事件上报
- [x] 清理缓存api
- [ ] 无头模式 - [ ] 无头模式
- [ ] 框架对接文档
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>
## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
## 鸣谢 ## 鸣谢
* [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) * [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) * [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI)
* chronocat * [chronocat](https://github.com/chrononeko/chronocat/)
* [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) * [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
## 友链
* [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架

BIN
doc/image/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,7 +1,9 @@
import cp from 'vite-plugin-cp'; import cp from 'vite-plugin-cp';
import "./scripts/gen-version" import "./scripts/gen-version"
const external = ["silk-wasm", "ws"]; const external = ["silk-wasm", "ws",
"level", "classic-level", "abstract-level", "level-supports", "level-transcoder",
"module-error", "catering", "node-gyp-build"];
function genCpModule(module: string) { function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false } return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }
@@ -26,7 +28,9 @@ let config = {
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg' './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg'
}, },
}, },
plugins: [cp({ targets: [...external.map(genCpModule), { src: './manifest.json', dest: 'dist' }] })] plugins: [cp({ targets: [...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' }, {src: './icon.jpg', dest: 'dist' }]
})]
}, },
preload: { preload: {
// vite config options // vite config options

BIN
icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,11 +1,11 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.11.2", "name": "LLOneBot v3.15.1",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi", "description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新",
"version": "3.11.2", "version": "3.15.1",
"thumbnail": "./icon.png", "icon": "./icon.jpg",
"authors": [ "authors": [
{ {
"name": "linyuchen", "name": "linyuchen",

384
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1",
"silk-wasm": "^3.2.3", "silk-wasm": "^3.2.3",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
@@ -985,16 +986,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": { "node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "13.24.0", "version": "13.24.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/globals/-/globals-13.24.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/globals/-/globals-13.24.0.tgz",
@@ -1010,18 +1001,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@eslint/eslintrc/node_modules/type-fest": { "node_modules/@eslint/eslintrc/node_modules/type-fest": {
"version": "0.20.2", "version": "0.20.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/type-fest/-/type-fest-0.20.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/type-fest/-/type-fest-0.20.2.tgz",
@@ -1057,28 +1036,6 @@
"node": ">=10.10.0" "node": ">=10.10.0"
} }
}, },
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@humanwhocodes/module-importer": { "node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -1892,6 +1849,23 @@
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true "dev": true
}, },
"node_modules/abstract-level": {
"version": "1.0.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/abstract-level/-/abstract-level-1.0.4.tgz",
"integrity": "sha512-eUP/6pbXBkMbXFdx4IH2fVgvB7M0JvR7/lIL33zcs0IBcwjdzSSl31TOJsaCzmKSSDF9h8QYSOJux4Nd4YJqFg==",
"dependencies": {
"buffer": "^6.0.3",
"catering": "^2.1.0",
"is-buffer": "^2.0.5",
"level-supports": "^4.0.0",
"level-transcoder": "^1.0.1",
"module-error": "^1.0.1",
"queue-microtask": "^1.2.3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -2157,6 +2131,25 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@@ -2221,6 +2214,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/browser-level": {
"version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/browser-level/-/browser-level-1.0.1.tgz",
"integrity": "sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ==",
"dependencies": {
"abstract-level": "^1.0.2",
"catering": "^2.1.1",
"module-error": "^1.0.2",
"run-parallel-limit": "^1.1.0"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.23.0", "version": "4.23.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/browserslist/-/browserslist-4.23.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/browserslist/-/browserslist-4.23.0.tgz",
@@ -2253,6 +2257,29 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-crc32": { "node_modules/buffer-crc32": {
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -2397,6 +2424,14 @@
} }
] ]
}, },
"node_modules/catering": {
"version": "2.1.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/catering/-/catering-2.1.1.tgz",
"integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==",
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/chalk/-/chalk-4.1.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/chalk/-/chalk-4.1.2.tgz",
@@ -2425,6 +2460,22 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/classic-level": {
"version": "1.4.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/classic-level/-/classic-level-1.4.1.tgz",
"integrity": "sha512-qGx/KJl3bvtOHrGau2WklEZuXhS3zme+jf+fsu6Ej7W7IP/C49v7KNlWIsT1jZu0YnfzSIYDGcEWpCa1wKGWXQ==",
"hasInstallScript": true,
"dependencies": {
"abstract-level": "^1.0.2",
"catering": "^2.1.0",
"module-error": "^1.0.1",
"napi-macros": "^2.2.2",
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/clone-response": { "node_modules/clone-response": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
@@ -3130,16 +3181,6 @@
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
} }
}, },
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-import/node_modules/debug": { "node_modules/eslint-plugin-import/node_modules/debug": {
"version": "3.2.7", "version": "3.2.7",
"resolved": "https://mirrors.cloud.tencent.com/npm/debug/-/debug-3.2.7.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/debug/-/debug-3.2.7.tgz",
@@ -3161,18 +3202,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eslint-plugin-import/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-plugin-n": { "node_modules/eslint-plugin-n": {
"version": "16.6.2", "version": "16.6.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz",
@@ -3201,16 +3230,6 @@
"eslint": ">=7.0.0" "eslint": ">=7.0.0"
} }
}, },
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-n/node_modules/globals": { "node_modules/eslint-plugin-n/node_modules/globals": {
"version": "13.24.0", "version": "13.24.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/globals/-/globals-13.24.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/globals/-/globals-13.24.0.tgz",
@@ -3226,18 +3245,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/eslint-plugin-n/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-plugin-n/node_modules/semver": { "node_modules/eslint-plugin-n/node_modules/semver": {
"version": "7.6.0", "version": "7.6.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/semver/-/semver-7.6.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/semver/-/semver-7.6.0.tgz",
@@ -3289,16 +3296,6 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint/node_modules/eslint-scope": { "node_modules/eslint/node_modules/eslint-scope": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/eslint-scope/-/eslint-scope-7.2.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -3382,18 +3379,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint/node_modules/p-limit": { "node_modules/eslint/node_modules/p-limit": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/p-limit/-/p-limit-3.1.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/p-limit/-/p-limit-3.1.0.tgz",
@@ -3917,6 +3902,26 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -4319,6 +4324,28 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-buffer": {
"version": "2.0.5",
"resolved": "https://mirrors.cloud.tencent.com/npm/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"engines": {
"node": ">=4"
}
},
"node_modules/is-builtin-module": { "node_modules/is-builtin-module": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/is-builtin-module/-/is-builtin-module-3.2.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
@@ -4623,6 +4650,43 @@
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
}, },
"node_modules/level": {
"version": "8.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/level/-/level-8.0.1.tgz",
"integrity": "sha512-oPBGkheysuw7DmzFQYyFe8NAia5jFLAgEnkgWnK3OXAuJr8qFT+xBQIwokAZPME2bhPFzS8hlYcL16m8UZrtwQ==",
"dependencies": {
"abstract-level": "^1.0.4",
"browser-level": "^1.0.1",
"classic-level": "^1.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/level"
}
},
"node_modules/level-supports": {
"version": "4.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/level-supports/-/level-supports-4.0.1.tgz",
"integrity": "sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==",
"engines": {
"node": ">=12"
}
},
"node_modules/level-transcoder": {
"version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/level-transcoder/-/level-transcoder-1.0.1.tgz",
"integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==",
"dependencies": {
"buffer": "^6.0.3",
"module-error": "^1.0.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/levn/-/levn-0.4.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/levn/-/levn-0.4.1.tgz",
@@ -4768,6 +4832,28 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimatch/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimist/-/minimist-1.2.8.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/minimist/-/minimist-1.2.8.tgz",
@@ -4777,6 +4863,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/module-error": {
"version": "1.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/module-error/-/module-error-1.0.2.tgz",
"integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==",
"engines": {
"node": ">=10"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -4801,6 +4895,11 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/napi-macros": {
"version": "2.2.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/napi-macros/-/napi-macros-2.2.2.tgz",
"integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g=="
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -5178,7 +5277,6 @@
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -5343,48 +5441,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/roarr": { "node_modules/roarr": {
"version": "2.15.4", "version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@@ -5458,6 +5514,28 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/run-parallel-limit": {
"version": "1.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz",
"integrity": "sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-array-concat": { "node_modules/safe-array-concat": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/safe-array-concat/-/safe-array-concat-1.1.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/safe-array-concat/-/safe-array-concat-1.1.0.tgz",

View File

@@ -9,7 +9,7 @@
"build-mac": "npm run build && npm run deploy-mac", "build-mac": "npm run build && npm run deploy-mac",
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/", "deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/",
"build-win": "npm run build && npm run deploy-win", "build-win": "npm run build && npm run deploy-win",
"deploy-win": "cmd /c \"xcopy /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"" "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\""
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@@ -17,6 +17,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1",
"silk-wasm": "^3.2.3", "silk-wasm": "^3.2.3",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",

17
scripts/test/test_db.ts Normal file
View File

@@ -0,0 +1,17 @@
import {Level} from "level"
const db = new Level(process.env["level_db_path"], {valueEncoding: 'json'});
async function getGroupNotify() {
let keys = await db.keys().all();
let result = []
for (const key of keys) {
// console.log(key)
if (key.startsWith("group_notify_")) {
result.push(key)
}
}
return result
}
getGroupNotify().then(console.log)

View File

@@ -4,6 +4,8 @@ import {mergeNewProperties} from "./utils";
export const HOOK_LOG = false; export const HOOK_LOG = false;
export const ALLOW_SEND_TEMP_MSG = false;
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string; private readonly configPath: string;
private config: Config | null = null; private config: Config | null = null;

View File

@@ -9,6 +9,9 @@ import {
type SelfInfo type SelfInfo
} from '../ntqqapi/types' } from '../ntqqapi/types'
import {type FileCache, type LLOneBotError} from './types' import {type FileCache, type LLOneBotError} from './types'
import {dbUtil} from "./db";
import {raw} from "express";
import {isNumeric, log} from "./utils";
export const selfInfo: SelfInfo = { export const selfInfo: SelfInfo = {
uid: '', uid: '',
@@ -18,75 +21,68 @@ export const selfInfo: SelfInfo = {
} }
export let groups: Group[] = [] export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
export let msgHistory: Record<string, RawMessage> = {} // msgId: RawMessage
export let groupNotifies: Map<string, GroupNotify> = new Map<string, GroupNotify>()
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>() export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
otherError: '' otherError: ''
} }
let globalMsgId = Math.floor(Date.now() / 1000)
export const fileCache = new Map<string, FileCache>()
export function addHistoryMsg(msg: RawMessage): boolean { export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
const existMsg = msgHistory[msg.msgId] let filterKey = isNumeric(uinOrUid) ? "uin" : "uid"
if (existMsg) { let filterValue = uinOrUid
Object.assign(existMsg, msg) let friend = friends.find(friend => friend[filterKey] === filterValue.toString())
msg.msgShortId = existMsg.msgShortId // if (!friend) {
return false // try {
} // friends = (await NTQQApi.getFriends(true))
msg.msgShortId = ++globalMsgId // friend = friends.find(friend => friend[filterKey] === filterValue.toString())
msgHistory[msg.msgId] = msg // } catch (e) {
return true // // log("刷新好友列表失败", e.stack.toString())
} // }
// }
export function getHistoryMsgByShortId(shortId: number | string) {
// log("getHistoryMsgByShortId", shortId, Object.values(msgHistory).map(m=>m.msgShortId))
return Object.values(msgHistory).find(msg => msg.msgShortId.toString() == shortId.toString())
}
export async function getFriend(qq: string): Promise<Friend | undefined> {
let friend = friends.find(friend => friend.uin === qq)
if (!friend){
friends = (await NTQQApi.getFriends(true))
friend = friends.find(friend => friend.uin === qq)
}
return friend return friend
} }
export async function getGroup(qq: string): Promise<Group | undefined> { export async function getGroup(qq: string): Promise<Group | undefined> {
let group = groups.find(group => group.groupCode === qq) let group = groups.find(group => group.groupCode === qq.toString())
if (!group){ if (!group) {
groups = await NTQQApi.getGroups(true); try {
group = groups.find(group => group.groupCode === qq) const _groups = await NTQQApi.getGroups(true);
group = _groups.find(group => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
} }
return group return group
} }
export async function getGroupMember(groupQQ: string | number, memberQQ: string | number, memberUid: string = null) { export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString() groupQQ = groupQQ.toString()
if (memberQQ) { memberUinOrUid = memberUinOrUid.toString()
memberQQ = memberQQ.toString()
}
const group = await getGroup(groupQQ) const group = await getGroup(groupQQ)
if (group) { if (group) {
let filterFunc: (member: GroupMember) => boolean const filterKey = isNumeric(memberUinOrUid) ? "uin" : "uid"
if (memberQQ) { const filterValue = memberUinOrUid
filterFunc = member => member.uin === memberQQ let filterFunc: (member: GroupMember) => boolean = member => member[filterKey] === filterValue
} else if (memberUid) {
filterFunc = member => member.uid === memberUid
}
let member = group.members?.find(filterFunc) let member = group.members?.find(filterFunc)
if (!member) { if (!member) {
const _members = await NTQQApi.getGroupMembers(groupQQ) try {
if (_members.length > 0) { const _members = await NTQQApi.getGroupMembers(groupQQ)
group.members = _members if (_members.length > 0) {
group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
} }
member = group.members?.find(filterFunc) member = group.members?.find(filterFunc)
} }
return member return member
} }
return null
} }
export async function refreshGroupMembers(groupQQ: string) { export async function refreshGroupMembers(groupQQ: string) {
@@ -96,10 +92,6 @@ export async function refreshGroupMembers(groupQQ: string) {
} }
} }
export function getHistoryMsgBySeq(seq: string) {
return Object.values(msgHistory).find(msg => msg.msgSeq === seq)
}
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号 export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) { export function getUidByUin(uin: string) {

263
src/common/db.ts Normal file
View File

@@ -0,0 +1,263 @@
import {Level} from "level";
import {type GroupNotify, RawMessage} from "../ntqqapi/types";
import {DATA_DIR, log} from "./utils";
import {selfInfo} from "./data";
import {FileCache} from "./types";
class DBUtil {
public readonly DB_KEY_PREFIX_MSG_ID = "msg_id_";
public readonly DB_KEY_PREFIX_MSG_SHORT_ID = "msg_short_id_";
public readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_";
public readonly DB_KEY_PREFIX_FILE = "file_";
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = "group_notify_";
public db: Level;
public cache: Record<string, RawMessage | string | FileCache | GroupNotify> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number;
/*
* 数据库结构
* msg_id_101231230999: {} // 长id: RawMessage
* msg_short_id_1: 101231230999 // 短id: 长id
* msg_seq_id_1: 101231230999 // 序列id: 长id
* file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */
constructor() {
let initCount = 0;
new Promise((resolve, reject) => {
const initDB = () => {
initCount++;
// if (initCount > 50) {
// return reject("init db fail")
// }
try {
if (!selfInfo.uin) {
setTimeout(initDB, 300);
return
}
const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}`;
this.db = new Level(DB_PATH, {valueEncoding: 'json'});
console.log("llonebot init db success")
resolve(null)
} catch (e) {
console.log("init db fail", e.stack.toString())
setTimeout(initDB, 300);
}
}
initDB();
}).then()
const expiredMilliSecond = 1000 * 60 * 60;
setInterval(() => {
// this.cache = {}
// 清理时间较久的缓存
const now = Date.now()
for (let key in this.cache) {
let message: RawMessage = this.cache[key] as RawMessage;
if (message?.msgTime){
if ((now - (parseInt(message.msgTime) * 1000)) > expiredMilliSecond) {
delete this.cache[key]
// log("clear cache", key, message.msgTime);
}
}
}
}, expiredMilliSecond)
}
private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq
this.cache[longIdKey] = this.cache[shortIdKey] = msg
}
public clearCache() {
this.cache = {}
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId;
if (this.cache[shortMsgIdKey]) {
// log("getMsgByShortId cache", shortMsgIdKey, this.cache[shortMsgIdKey])
return this.cache[shortMsgIdKey] as RawMessage
}
try {
const longId = await this.db.get(shortMsgIdKey);
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
return msg
} catch (e) {
log("getMsgByShortId db error", e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage> {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + longId;
if (this.cache[longIdKey]) {
return this.cache[longIdKey] as RawMessage
}
try {
const data = await this.db.get(longIdKey)
const msg = JSON.parse(data)
this.addCache(msg)
return msg
} catch (e) {
// log("getMsgByLongId db error", e.stack.toString())
}
}
async getMsgBySeqId(seqId: string): Promise<RawMessage> {
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + seqId;
if (this.cache[seqIdKey]) {
return this.cache[seqIdKey] as RawMessage
}
try {
const longId = await this.db.get(seqIdKey);
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
return msg
} catch (e) {
log("getMsgBySeqId db error", e.stack.toString())
}
}
async addMsg(msg: RawMessage) {
// 有则更新,无则添加
// log("addMsg", msg.msgId, msg.msgSeq, msg.msgShortId);
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
// log("addMsg getMsgByLongId error", e.stack.toString())
}
}
if (existMsg) {
// log("消息已存在", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId)
this.updateMsg(msg).then()
return existMsg.msgShortId
}
this.addCache(msg);
const shortMsgId = await this.genMsgShortId();
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId;
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq;
msg.msgShortId = shortMsgId;
// log("新增消息记录", msg.msgId)
this.db.put(shortIdKey, msg.msgId).then().catch();
this.db.put(longIdKey, JSON.stringify(msg)).then().catch();
try {
await this.db.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then().catch();
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg;
}
return shortMsgId
// log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`);
}
async updateMsg(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
let existMsg = this.cache[longIdKey] as RawMessage
if (!existMsg) {
try {
existMsg = await this.getMsgByLongId(msg.msgId)
} catch (e) {
existMsg = msg
}
}
Object.assign(existMsg, msg)
this.db.put(longIdKey, JSON.stringify(existMsg)).then().catch();
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + existMsg.msgShortId;
const seqIdKey = this.DB_KEY_PREFIX_MSG_SEQ_ID + msg.msgSeq;
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = existMsg;
}
this.db.put(shortIdKey, msg.msgId).then().catch();
try {
await this.db.get(seqIdKey)
} catch (e) {
this.db.put(seqIdKey, msg.msgId).then().catch();
// log("更新seqId error", e.stack, seqIdKey);
}
// log("更新消息", existMsg.msgSeq, existMsg.msgShortId, existMsg.msgId);
}
private async genMsgShortId(): Promise<number> {
const key = "msg_current_short_id";
if (this.currentShortId === undefined) {
try {
let id: string = await this.db.get(key);
this.currentShortId = parseInt(id);
} catch (e) {
this.currentShortId = -2147483640
}
}
this.currentShortId++;
this.db.put(key, this.currentShortId.toString()).then().catch();
return this.currentShortId;
}
async addFileCache(fileName: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_FILE + fileName;
if (this.cache[key]) {
return
}
let cacheDBData = {...data}
delete cacheDBData['downloadFunc']
this.cache[fileName] = data;
try {
await this.db.put(key, JSON.stringify(cacheDBData));
} catch (e) {
log("addFileCache db error", e.stack.toString())
}
}
async getFileCache(fileName: string): Promise<FileCache | undefined> {
const key = this.DB_KEY_PREFIX_FILE + fileName;
if (this.cache[key]) {
return this.cache[key] as FileCache
}
try {
let data = await this.db.get(key);
return JSON.parse(data);
} catch (e) {
// log("getFileCache db error", e.stack.toString())
}
}
async addGroupNotify(notify: GroupNotify) {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + notify.seq;
let existNotify = this.cache[key] as GroupNotify
if (existNotify) {
return
}
this.cache[key] = notify;
this.db.put(key, JSON.stringify(notify)).then().catch();
}
async getGroupNotify(seq: string): Promise<GroupNotify | undefined> {
const key = this.DB_KEY_PREFIX_GROUP_NOTIFY + seq;
if (this.cache[key]) {
return this.cache[key] as GroupNotify
}
try {
let data = await this.db.get(key);
return JSON.parse(data);
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}
}
}
export const dbUtil = new DBUtil();

View File

@@ -11,8 +11,21 @@ export abstract class HttpServerBase {
constructor() { constructor() {
this.expressAPP = express(); this.expressAPP = express();
this.expressAPP.use(express.urlencoded({extended: true, limit: "500mb"})); this.expressAPP.use(express.urlencoded({extended: true, limit: "5000mb"}));
this.expressAPP.use(json({limit: "500mb"})); this.expressAPP.use((req, res, next) => {
// 兼容处理没有带content-type的请求
// log("req.headers['content-type']", req.headers['content-type'])
req.headers['content-type'] = 'application/json';
const originalJson = express.json({limit: "5000mb"});
// 调用原始的express.json()处理器
originalJson(req, res, (err) => {
if (err) {
log("Error parsing JSON:", err);
return res.status(400).send("Invalid JSON");
}
next();
});
});
} }
authorize(req: Request, res: Response, next: () => void) { authorize(req: Request, res: Response, next: () => void) {

View File

@@ -4,13 +4,14 @@ import {ConfigUtil} from "./config";
import util from "util"; import util from "util";
import {encode, getDuration, isWav} from "silk-wasm"; import {encode, getDuration, isWav} from "silk-wasm";
import fs from 'fs'; import fs from 'fs';
import * as crypto from 'crypto';
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import ffmpeg from "fluent-ffmpeg" import ffmpeg from "fluent-ffmpeg"
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export function getConfigUtil() { export function getConfigUtil() {
const configFilePath = path.join(CONFIG_DIR, `config_${selfInfo.uin}.json`) const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }
@@ -31,6 +32,11 @@ function truncateString(obj: any, maxLength = 500) {
return obj; return obj;
} }
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function log(...msg: any[]) { export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) { if (!getConfigUtil().getConfig().log) {
return //console.log(...msg); return //console.log(...msg);
@@ -55,7 +61,7 @@ export function log(...msg: any[]) {
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n` logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg); // sendLog(...msg);
// console.log(msg) // console.log(msg)
fs.appendFile(path.join(CONFIG_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => { fs.appendFile(path.join(DATA_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
}) })
} }
@@ -192,7 +198,7 @@ export async function encodeSilk(filePath: string) {
try { try {
const fileName = path.basename(filePath); const fileName = path.basename(filePath);
const pttPath = path.join(CONFIG_DIR, uuidv4()); const pttPath = path.join(DATA_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") { if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`) log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath); const _isWav = await isWavFile(filePath);
@@ -205,7 +211,7 @@ export async function encodeSilk(filePath: string) {
if (ffmpegPath) { if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath); ffmpeg.setFfmpegPath(ffmpegPath);
} }
ffmpeg(filePath).toFormat("wav").on('end', function () { ffmpeg(filePath).toFormat("wav").audioChannels(2).on('end', function () {
log('wav转换完成'); log('wav转换完成');
}) })
.on('error', function (err) { .on('error', function (err) {
@@ -220,11 +226,11 @@ export async function encodeSilk(filePath: string) {
}) })
} }
// const sampleRate = await getAudioSampleRate(filePath) || 0; // const sampleRate = await getAudioSampleRate(filePath) || 0;
// log("音频采样率", sampleRate)
const pcm = fs.readFileSync(filePath); const pcm = fs.readFileSync(filePath);
const silk = await encode(pcm, 0); const silk = await encode(pcm, 0);
fs.writeFileSync(pttPath, silk.data); fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => { fs.unlink(wavPath, (err) => { });
});
log(`语音文件${filePath}转换成功!`, pttPath) log(`语音文件${filePath}转换成功!`, pttPath)
return { return {
converted: true, converted: true,
@@ -234,9 +240,9 @@ export async function encodeSilk(filePath: string) {
} else { } else {
const pcm = fs.readFileSync(filePath); const pcm = fs.readFileSync(filePath);
let duration = 0; let duration = 0;
try{ try {
duration = getDuration(pcm); duration = getDuration(pcm);
}catch (e) { } catch (e) {
log("获取语音文件时长失败", filePath, e.stack) log("获取语音文件时长失败", filePath, e.stack)
duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s
duration = Math.floor(duration) duration = Math.floor(duration)
@@ -256,6 +262,81 @@ export async function encodeSilk(filePath: string) {
} }
} }
export async function getVideoInfo(filePath: string) {
const size = fs.statSync(filePath).size;
return new Promise<{ width: number, height: number, time: number, format: string, size: number, filePath: string }>((resolve, reject) => {
ffmpeg(filePath).ffprobe( (err, metadata) => {
if (err) {
reject(err);
} else {
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
if (videoStream) {
console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`);
} else {
console.log('未找到视频流信息。');
}
resolve({
width: videoStream.width, height: videoStream.height,
time: parseInt(videoStream.duration),
format: metadata.format.format_name,
size,
filePath
});
}
});
})
}
export async function encodeMp4(filePath: string) {
let videoInfo = await getVideoInfo(filePath);
log("视频信息", videoInfo)
if (videoInfo.format.indexOf("mp4") === -1) {
log("视频需要转换为MP4格式", filePath)
// 转成mp4
const newPath: string = await new Promise<string>((resolve, reject) => {
const newPath = filePath + ".mp4"
ffmpeg(filePath)
.toFormat('mp4')
.on('error', (err) => {
reject(`转换视频格式失败: ${err.message}`);
})
.on('end', () => {
log('视频转换为MP4格式完成');
resolve(newPath); // 返回转换后的文件路径
})
.save(newPath);
});
return await getVideoInfo(newPath)
}
return videoInfo
}
export function isNull(value: any) { export function isNull(value: any) {
return value === undefined || value === null; return value === undefined || value === null;
} }
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}

View File

@@ -11,38 +11,31 @@ import {
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
} from "../common/channels"; } from "../common/channels";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {checkFfmpeg, CONFIG_DIR, getConfigUtil, log} from "../common/utils"; import {checkFfmpeg, DATA_DIR, getConfigUtil, log} from "../common/utils";
import { import {
addHistoryMsg,
friendRequests, friendRequests,
getFriend,
getGroup, getGroup,
getGroupMember, getGroupMember,
groupNotifies,
llonebotError, llonebotError,
msgHistory, refreshGroupMembers, refreshGroupMembers,
selfInfo selfInfo
} from "../common/data"; } from "../common/data";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook"; import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor"; import {OB11Constructor} from "../onebot11/constructor";
import {NTQQApi} from "../ntqqapi/ntcall"; import {NTQQApi} from "../ntqqapi/ntcall";
import { import {ChatType, FriendRequestNotify, GroupNotifies, GroupNotifyTypes, RawMessage} from "../ntqqapi/types";
ChatType,
FriendRequestNotify,
GroupMember,
GroupNotifies,
GroupNotifyTypes,
RawMessage
} from "../ntqqapi/types";
import {ob11HTTPServer} from "../onebot11/server/http"; import {ob11HTTPServer} from "../onebot11/server/http";
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent";
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent";
import {postOB11Event} from "../onebot11/server/postOB11Event"; import {postOB11Event} from "../onebot11/server/postOB11Event";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent"; import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest"; import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest";
import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest"; import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest";
import * as path from "node:path"; import * as path from "node:path";
import {dbUtil} from "../common/db";
import {setConfig} from "./setConfig";
let running = false; let running = false;
@@ -82,8 +75,8 @@ function onLoad() {
return "" return ""
} }
}) })
if (!fs.existsSync(CONFIG_DIR)) { if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(CONFIG_DIR, {recursive: true}); fs.mkdirSync(DATA_DIR, {recursive: true});
} }
ipcMain.handle(CHANNEL_ERROR, (event, arg) => { ipcMain.handle(CHANNEL_ERROR, (event, arg) => {
return llonebotError; return llonebotError;
@@ -92,75 +85,23 @@ function onLoad() {
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
return config; return config;
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event, arg: Config) => { ipcMain.on(CHANNEL_SET_CONFIG, (event, config: Config) => {
let oldConfig = getConfigUtil().getConfig(); setConfig(config).then();
getConfigUtil().setConfig(arg)
if (arg.ob11.httpPort != oldConfig.ob11.httpPort && arg.ob11.enableHttp) {
ob11HTTPServer.restart(arg.ob11.httpPort);
}
// 判断是否启用或关闭HTTP服务
if (!arg.ob11.enableHttp) {
ob11HTTPServer.stop();
} else {
ob11HTTPServer.start(arg.ob11.httpPort);
}
// 正向ws端口变化重启服务
if (arg.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(arg.ob11.wsPort);
}
// 判断是否启用或关闭正向ws
if (arg.ob11.enableWs != oldConfig.ob11.enableWs) {
if (arg.ob11.enableWs) {
ob11WebsocketServer.start(arg.ob11.wsPort);
} else {
ob11WebsocketServer.stop();
}
}
// 判断是否启用或关闭反向ws
if (arg.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (arg.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
} else {
ob11ReverseWebsockets.stop();
}
}
if (arg.ob11.enableWsReverse) {
// 判断反向ws地址有变化
if (arg.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
ob11ReverseWebsockets.restart();
} else {
for (const newHost of arg.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
ob11ReverseWebsockets.restart();
break;
}
}
}
}
// 检查ffmpeg
if (arg.ffmpeg) {
checkFfmpeg(arg.ffmpeg).then(success => {
if (success) {
llonebotError.ffmpegError = ''
}
})
}
}) })
ipcMain.on(CHANNEL_LOG, (event, arg) => { ipcMain.on(CHANNEL_LOG, (event, arg) => {
log(arg); log(arg);
}) })
function postReceiveMsg(msgList: RawMessage[]) { async function postReceiveMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) { for (let message of msgList) {
// log("收到新消息", message)
message.msgShortId = msgHistory[message.msgId]?.msgShortId // log("收到新消息", message.msgId, message.msgSeq)
if (!message.msgShortId) { // if (message.senderUin !== selfInfo.uin){
addHistoryMsg(message); message.msgShortId = await dbUtil.addMsg(message);
} // }
OB11Constructor.message(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
msg.raw = message; msg.raw = message;
@@ -171,27 +112,35 @@ function onLoad() {
} }
postOB11Event(msg); postOB11Event(msg);
// log("post msg", msg) // log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString())); }).catch(e => log("constructMessage error: ", e.stack.toString()));
OB11Constructor.GroupEvent(message).then(groupEvent => {
if (groupEvent) {
// log("post group event", groupEvent);
postOB11Event(groupEvent);
}
})
} }
} }
async function startReceiveHook() { async function startReceiveHook() {
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, async (payload) => {
try { try {
postReceiveMsg(payload.msgList); await postReceiveMsg(payload.msgList);
} catch (e) { } catch (e) {
log("report message error: ", e.toString()); log("report message error: ", e.stack.toString());
} }
}) })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("message update", message.sendStatus, message) // log("message update", message.sendStatus, message.msgId, message.msgSeq)
if (message.recallTime != "0") { if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断
// 撤回消息上报 // 撤回消息上报
const oriMessage = msgHistory[message.msgId] const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) { if (!oriMessage) {
continue continue
} }
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then();
if (message.chatType == ChatType.friend) { if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId); const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId);
postOB11Event(friendRecallEvent); postOB11Event(friendRecallEvent);
@@ -199,7 +148,7 @@ function onLoad() {
let operatorId = message.senderUin let operatorId = message.senderUin
for (const element of message.elements) { for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, null, operatorUid) const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin operatorId = operator.uin
} }
const groupRecallEvent = new OB11GroupRecallNoticeEvent( const groupRecallEvent = new OB11GroupRecallNoticeEvent(
@@ -211,21 +160,22 @@ function onLoad() {
postOB11Event(groupRecallEvent); postOB11Event(groupRecallEvent);
} }
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue continue
} }
addHistoryMsg(message) dbUtil.updateMsg(message).then();
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, async (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig(); const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) { if (!reportSelfMessage) {
return return
} }
// log("reportSelfMessage", payload) // log("reportSelfMessage", payload)
try { try {
postReceiveMsg([payload.msgRecord]); await postReceiveMsg([payload.msgRecord]);
} catch (e) { } catch (e) {
log("report self message error: ", e.toString()); log("report self message error: ", e.stack.toString());
} }
}) })
registerReceiveHook<{ registerReceiveHook<{
@@ -245,30 +195,29 @@ function onLoad() {
const notifies = notify.notifies.slice(0, payload.unreadCount) const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload); // log("获取群通知详情完成", notifies, payload);
try {
for (const notify of notifies) { for (const notify of notifies) {
try {
notify.time = Date.now(); notify.time = Date.now();
const notifyTime = parseInt(notify.seq) / 1000 // const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`); // log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
if (notifyTime < startTime) { // if (notifyTime < startTime) {
continue; // continue;
} // }
let existNotify = groupNotifies[notify.seq]; let existNotify = await dbUtil.getGroupNotify(notify.seq);
if (existNotify) { if (existNotify) {
if (Date.now() - existNotify.time < 3000) { continue
continue
}
} }
log("收到群通知", notify); log("收到群通知", notify);
groupNotifies[notify.seq] = notify; await dbUtil.addGroupNotify(notify);
const member1 = await getGroupMember(notify.group.groupCode, null, notify.user1.uid); // let member2: GroupMember;
refreshGroupMembers(notify.group.groupCode).then() // if (notify.user2.uid) {
let member2: GroupMember; // member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
if (notify.user2.uid) { // }
member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
}
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) { if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) {
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid);
log("有管理员变动通知"); log("有管理员变动通知");
refreshGroupMembers(notify.group.groupCode).then()
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent() let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode); groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode);
log("开始获取变动的管理员") log("开始获取变动的管理员")
@@ -281,8 +230,9 @@ function onLoad() {
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode)); log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
} }
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT) { } else if (notify.type == GroupNotifyTypes.MEMBER_EXIT) {
log("有成员退出通知"); // log("有成员退出通知");
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin)) // const member1 = await getGroupMember(notify.group.groupCode, null, notify.user1.uid);
// let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin))
// postEvent(groupDecreaseEvent, true); // postEvent(groupDecreaseEvent, true);
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) { } else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log("有加群请求"); log("有加群请求");
@@ -300,20 +250,24 @@ function onLoad() {
groupRequestEvent.flag = notify.seq; groupRequestEvent.flag = notify.seq;
postOB11Event(groupRequestEvent); postOB11Event(groupRequestEvent);
} else if (notify.type == GroupNotifyTypes.INVITE_ME) { } else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log("收到邀请我加群通知")
let groupInviteEvent = new OB11GroupRequestEvent(); let groupInviteEvent = new OB11GroupRequestEvent();
groupInviteEvent.group_id = parseInt(notify.group.groupCode); groupInviteEvent.group_id = parseInt(notify.group.groupCode);
let user_id = (await NTQQApi.getUserDetailInfo(notify.user2.uid))?.uin let user_id = (await getFriend(notify.user2.uid))?.uin
if (!user_id) {
user_id = (await NTQQApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id); groupInviteEvent.user_id = parseInt(user_id);
groupInviteEvent.sub_type = "invite"; groupInviteEvent.sub_type = "invite";
groupInviteEvent.flag = notify.seq; groupInviteEvent.flag = notify.seq;
postOB11Event(groupInviteEvent); postOB11Event(groupInviteEvent);
} }
} catch (e) {
log("解析群通知失败", e.stack.toString());
} }
} catch (e) {
log("解析群通知失败", e.stack);
} }
}
else if (payload.doubt){ } else if (payload.doubt) {
// 可能有群管理员变动 // 可能有群管理员变动
} }
}) })
@@ -341,6 +295,7 @@ function onLoad() {
let startTime = 0; let startTime = 0;
async function start() { async function start() {
log("llonebot pid", process.pid)
startTime = Date.now(); startTime = Date.now();
startReceiveHook().then(); startReceiveHook().then();
NTQQApi.getGroups(true).then() NTQQApi.getGroups(true).then()
@@ -407,6 +362,10 @@ function onLoad() {
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) {
return
}
log("window create", window.webContents.getURL().toString())
try { try {
hookNTQQApiCall(window); hookNTQQApiCall(window);
hookNTQQApiReceive(window); hookNTQQApiReceive(window);

63
src/main/setConfig.ts Normal file
View File

@@ -0,0 +1,63 @@
import {Config} from "../common/types";
import {checkFfmpeg, getConfigUtil} from "../common/utils";
import {ob11HTTPServer} from "../onebot11/server/http";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {llonebotError} from "../common/data";
export async function setConfig(config: Config) {
let oldConfig = getConfigUtil().getConfig();
getConfigUtil().setConfig(config)
if (config.ob11.httpPort != oldConfig.ob11.httpPort && config.ob11.enableHttp) {
ob11HTTPServer.restart(config.ob11.httpPort);
}
// 判断是否启用或关闭HTTP服务
if (!config.ob11.enableHttp) {
ob11HTTPServer.stop();
} else {
ob11HTTPServer.start(config.ob11.httpPort);
}
// 正向ws端口变化重启服务
if (config.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(config.ob11.wsPort);
}
// 判断是否启用或关闭正向ws
if (config.ob11.enableWs != oldConfig.ob11.enableWs) {
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
} else {
ob11WebsocketServer.stop();
}
}
// 判断是否启用或关闭反向ws
if (config.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
} else {
ob11ReverseWebsockets.stop();
}
}
if (config.ob11.enableWsReverse) {
// 判断反向ws地址有变化
if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
ob11ReverseWebsockets.restart();
} else {
for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
ob11ReverseWebsockets.restart();
break;
}
}
}
}
// 检查ffmpeg
if (config.ffmpeg) {
checkFfmpeg(config.ffmpeg).then(success => {
if (success) {
llonebotError.ffmpegError = ''
}
})
}
}

View File

@@ -1,16 +1,20 @@
import { import {
AtType, AtType,
ElementType, PicType, ElementType,
PicType,
SendArkElement,
SendFaceElement, SendFaceElement,
SendFileElement, SendFileElement,
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
SendReplyElement, SendReplyElement,
SendTextElement SendTextElement,
SendVideoElement
} from "./types"; } from "./types";
import {NTQQApi} from "./ntcall"; import {NTQQApi} from "./ntcall";
import {encodeSilk, isGIF} from "../common/utils"; import {calculateFileMD5, encodeSilk, getVideoInfo, isGIF, log, sleep} from "../common/utils";
import * as fs from "node:fs"; import {promises as fs} from "node:fs";
import ffmpeg from "fluent-ffmpeg"
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
@@ -57,6 +61,9 @@ export class SendMsgElementConstructor {
static async pic(picPath: string): Promise<SendPicElement> { static async pic(picPath: string): Promise<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath, ElementType.PIC);
if (fileSize === 0) {
throw "文件异常大小为0";
}
const imageSize = await NTQQApi.getImageSize(picPath); const imageSize = await NTQQApi.getImageSize(picPath);
const picElement = { const picElement = {
md5HexStr: md5, md5HexStr: md5,
@@ -81,40 +88,101 @@ export class SendMsgElementConstructor {
}; };
} }
static async file(filePath: string, isVideo: boolean = false): Promise<SendFileElement> { static async file(filePath: string, fileName: string = ""): Promise<SendFileElement> {
let picHeight = 0; const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE);
let picWidth = 0; if (fileSize === 0) {
if (isVideo) { throw "文件异常大小为0";
picHeight = 1024;
picWidth = 768;
} }
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE);
let element: SendFileElement = { let element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: "", elementId: "",
fileElement: { fileElement: {
fileName, fileName: fileName || _fileName,
"filePath": path, "filePath": path,
"fileSize": (fileSize).toString(), "fileSize": (fileSize).toString(),
picHeight,
picWidth
} }
} }
return element; return element;
} }
static video(filePath: string): Promise<SendFileElement> { static async video(filePath: string, fileName: string = ""): Promise<SendVideoElement> {
return SendMsgElementConstructor.file(filePath, true); let {fileName: _fileName, path, fileSize, md5} = await NTQQApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw "文件异常大小为0";
}
// const videoInfo = await encodeMp4(path);
// path = videoInfo.filePath
// md5 = videoInfo.md5;
// fileSize = videoInfo.size;
// log("上传视频", md5, path, fileSize, fileName || _fileName)
const pathLib = require("path");
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumb = pathLib.dirname(thumb)
// log("thumb 目录", thumb)
const videoInfo = await getVideoInfo(path);
log("视频信息", videoInfo)
const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`
ffmpeg(filePath)
.on("end", () => {
})
.on("error", (err) => {
reject(err);
})
.screenshots({
timestamps: [0],
filename: thumbFileName,
folder: thumb,
size: videoInfo.width + "x" + videoInfo.height
}).on("end", () => {
resolve(pathLib.join(thumb, thumbFileName));
});
})
let thumbPath = new Map()
const _thumbPath = await createThumb;
const thumbSize = (await fs.stat(_thumbPath)).size;
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath);
let element: SendVideoElement = {
elementType: ElementType.VIDEO,
elementId: "",
videoElement: {
fileName: fileName || _fileName,
filePath: path,
videoMd5: md5,
thumbMd5,
fileTime: videoInfo.time,
thumbPath: thumbPath,
thumbSize,
thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height,
fileSize: "" + fileSize,
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// sourceVideoCodecFormat: 2
}
}
return element;
} }
static async ptt(pttPath: string): Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath); const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration); // log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT);
if (fileSize === 0) {
throw "文件异常大小为0";
}
if (converted) { if (converted) {
fs.unlink(silkPath, () => { fs.unlink(silkPath).then();
});
} }
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,
@@ -150,4 +218,12 @@ export class SendMsgElementConstructor {
} }
} }
} }
static ark(data: any): SendArkElement {
return {
elementType: ElementType.ARK,
elementId: "",
arkElement: data
}
}
} }

View File

@@ -1,71 +1,74 @@
import {type BrowserWindow} from 'electron' import {BrowserWindow} from 'electron';
import {getConfigUtil, log, sleep} from '../common/utils' import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApi, type NTQQApiClass, sendMessagePool} from './ntcall' import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import {type Group, type RawMessage, type User} from './types' import {Group, RawMessage, User} from "./types";
import {addHistoryMsg, friends, groups, msgHistory, selfInfo, tempGroupCodeMap} from '../common/data' import {friends, groups, selfInfo, tempGroupCodeMap} from "../common/data";
import {OB11GroupDecreaseEvent} from '../onebot11/event/notice/OB11GroupDecreaseEvent' import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupIncreaseEvent} from '../onebot11/event/notice/OB11GroupIncreaseEvent' import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {v4 as uuidv4} from 'uuid' import {v4 as uuidv4} from "uuid"
import {postOB11Event} from '../onebot11/server/postOB11Event' import {postOB11Event} from "../onebot11/server/postOB11Event";
import {HOOK_LOG} from '../common/config' import {HOOK_LOG} from "../common/config";
import fs from 'fs' import fs from "fs";
import {dbUtil} from "../common/db";
export const hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export enum ReceiveCmd { export enum ReceiveCmd {
UPDATE_MSG = 'nodeIKernelMsgListener/onMsgInfoListUpdate', UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
NEW_MSG = 'nodeIKernelMsgListener/onRecvMsg', NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = 'nodeIKernelMsgListener/onAddSendMsg', SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO = 'nodeIKernelProfileListener/onProfileSimpleChanged', USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
USER_DETAIL_INFO = 'nodeIKernelProfileListener/onProfileDetailInfoChanged', USER_DETAIL_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged",
GROUPS = 'nodeIKernelGroupListener/onGroupListUpdate', GROUPS = "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX = 'onGroupListUpdate', GROUPS_UNIX = "onGroupListUpdate",
FRIENDS = 'onBuddyListChange', FRIENDS = "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaDownloadComplete', MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated', UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
GROUP_NOTIFY = 'nodeIKernelGroupListener/onGroupSingleScreenNotifies', GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies",
FRIEND_REQUEST = 'nodeIKernelBuddyListener/onBuddyReqChange', FRIEND_REQUEST = "nodeIKernelBuddyListener/onBuddyReqChange",
SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS = 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH = "nodeIKernelStorageCleanListener/onFinishScan",
MEDIA_UPLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaUploadComplete",
} }
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> { interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: { 0: {
'type': 'request' "type": "request",
'eventName': NTQQApiClass "eventName": NTQQApiClass,
'callbackId'?: string "callbackId"?: string
} },
1: 1:
Array<{ {
cmdName: ReceiveCmd cmdName: ReceiveCmd,
cmdType: 'event' cmdType: "event",
payload: PayloadType payload: PayloadType
}> }[]
} }
const receiveHooks: Array<{ let receiveHooks: Array<{
method: ReceiveCmd method: ReceiveCmd,
hookFunc: ((payload: any) => void | Promise<void>) hookFunc: ((payload: any) => void | Promise<void>)
id: string id: string
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow) { export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send const originalSend = window.webContents.send;
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args)) HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args))
if (args?.[1] instanceof Array) { if (args?.[1] instanceof Array) {
for (const receiveData of args?.[1]) { for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) // log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (const hook of receiveHooks) { for (let hook of receiveHooks) {
if (hook.method === ntQQApiMethodName) { if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
const _ = hook.hookFunc(receiveData.payload) let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === 'AsyncFunction') { if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then() (_ as Promise<void>).then()
} }
} catch (e) { } catch (e) {
log('hook error', e, receiveData.payload) log("hook error", e, receiveData.payload)
} }
}).then() }).then()
} }
@@ -74,35 +77,35 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
} }
if (args[0]?.callbackId) { if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args) // log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]) { if (hookApiCallbacks[callbackId]) {
// log("callback found") // log("callback found")
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]) hookApiCallbacks[callbackId](args[1]);
}).then() }).then()
delete hookApiCallbacks[callbackId] delete hookApiCallbacks[callbackId];
} }
} }
return originalSend.call(window.webContents, channel, ...args) return originalSend.call(window.webContents, channel, ...args);
} }
window.webContents.send = patchSend window.webContents.send = patchSend;
} }
export function hookNTQQApiCall(window: BrowserWindow) { export function hookNTQQApiCall(window: BrowserWindow) {
// 监听调用NTQQApi // 监听调用NTQQApi
const webContents = window.webContents as any let webContents = window.webContents as any;
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message'] const ipc_message_proxy = webContents._events["-ipc-message"]?.[0] || webContents._events["-ipc-message"];
const proxyIpcMsg = new Proxy(ipc_message_proxy, { const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
HOOK_LOG && log('call NTQQ api', thisArg, args) HOOK_LOG && log("call NTQQ api", thisArg, args);
return target.apply(thisArg, args) return target.apply(thisArg, args);
} },
}) });
if (webContents._events['-ipc-message']?.[0]) { if (webContents._events["-ipc-message"]?.[0]) {
webContents._events['-ipc-message'][0] = proxyIpcMsg webContents._events["-ipc-message"][0] = proxyIpcMsg;
} else { } else {
webContents._events['-ipc-message'] = proxyIpcMsg webContents._events["-ipc-message"] = proxyIpcMsg;
} }
} }
@@ -113,29 +116,29 @@ export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (
hookFunc, hookFunc,
id id
}) })
return id return id;
} }
export function removeReceiveHook(id: string) { export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex(h => h.id === id) const index = receiveHooks.findIndex(h => h.id === id)
receiveHooks.splice(index, 1) receiveHooks.splice(index, 1);
} }
async function updateGroups(_groups: Group[], needUpdate: boolean = true) { async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (const group of _groups) { for (let group of _groups) {
let existGroup = groups.find(g => g.groupCode == group.groupCode) let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) { if (existGroup) {
Object.assign(existGroup, group) Object.assign(existGroup, group);
} else { } else {
groups.push(group) groups.push(group);
existGroup = group existGroup = group;
} }
if (needUpdate) { if (needUpdate) {
const members = await NTQQApi.getGroupMembers(group.groupCode) const members = await NTQQApi.getGroupMembers(group.groupCode);
if (members) { if (members) {
existGroup.members = members existGroup.members = members;
} }
} }
} }
@@ -143,83 +146,68 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
async function processGroupEvent(payload) { async function processGroupEvent(payload) {
try { try {
const newGroupList = payload.groupList const newGroupList = payload.groupList;
for (const group of newGroupList) { for (const group of newGroupList) {
const existGroup = groups.find(g => g.groupCode == group.groupCode) let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) { if (existGroup) {
if (existGroup.memberCount > group.memberCount) { if (existGroup.memberCount > group.memberCount) {
const oldMembers = existGroup.members const oldMembers = existGroup.members;
await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时 await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQApi.getGroupMembers(group.groupCode) const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
group.members = newMembers group.members = newMembers;
const newMembersSet = new Set<string>() // 建立索引降低时间复杂度 const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度
for (const member of newMembers) { for (const member of newMembers) {
newMembersSet.add(member.uin) newMembersSet.add(member.uin);
} }
for (const member of oldMembers) { for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) { if (!newMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))) postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)));
break break;
}
}
} else if (existGroup.memberCount < group.memberCount) {
const oldMembers = existGroup.members
const oldMembersSet = new Set<string>()
for (const member of oldMembers) {
oldMembersSet.add(member.uin)
}
await sleep(200)
const newMembers = await NTQQApi.getGroupMembers(group.groupCode)
group.members = newMembers
for (const member of newMembers) {
if (!oldMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin)))
break
} }
} }
} }
} }
} }
updateGroups(newGroupList, false).then() updateGroups(newGroupList, false).then();
} catch (e) { } catch (e) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then();
console.log(e) console.log(e);
} }
} }
// 群列表变动
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => { registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => {
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then();
} else { } else {
if (process.platform == 'win32') { if (process.platform == "win32") {
processGroupEvent(payload).then() processGroupEvent(payload).then();
} }
} }
}) })
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => { registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => {
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then() updateGroups(payload.groupList).then();
} else { } else {
if (process.platform != 'win32') { if (process.platform != "win32") {
processGroupEvent(payload).then() processGroupEvent(payload).then();
} }
} }
}) })
// 好友列表变动
registerReceiveHook<{ registerReceiveHook<{
data: Array<{ categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }> data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
}>(ReceiveCmd.FRIENDS, payload => { }>(ReceiveCmd.FRIENDS, payload => {
for (const fData of payload.data) { for (const fData of payload.data) {
const _friends = fData.buddyList const _friends = fData.buddyList;
for (const friend of _friends) { for (let friend of _friends) {
const existFriend = friends.find(f => f.uin == friend.uin) let existFriend = friends.find(f => f.uin == friend.uin)
if (!existFriend) { if (!existFriend) {
friends.push(friend) friends.push(friend)
} else { } else {
@@ -229,20 +217,26 @@ registerReceiveHook<{
} }
}) })
registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) => { // 新消息
const {autoDeleteFile, autoDeleteFileSecond} = getConfigUtil().getConfig() registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
const {autoDeleteFile} = getConfigUtil().getConfig();
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message) // log("收到新消息push到历史记录", message.msgId)
addHistoryMsg(message) // dbUtil.addMsg(message).then()
// 清理文件 // 清理文件
if (!autoDeleteFile) {
continue
}
for (const msgElement of message.elements) { for (const msgElement of message.elements) {
setTimeout(() => { setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath const pttPath = msgElement.pttElement?.filePath
const pathList = [picPath, pttPath] const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) { if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath)) pathList.push(...Object.values(msgElement.picElement.thumbPath))
} }
@@ -250,33 +244,32 @@ registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) =>
if (aioOpGrayTipElement){ if (aioOpGrayTipElement){
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat; tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat;
} }
// log("需要清理的文件", pathList); // log("需要清理的文件", pathList);
for (const path of pathList) { for (const path of pathList) {
if (path) { if (path) {
fs.unlink(picPath, () => { fs.unlink(picPath, () => {
log('删除文件成功', path) log("删除文件成功", path)
}) });
} }
} }
}, autoDeleteFileSecond * 1000) }, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
} }
} }
const msgIds = Object.keys(msgHistory)
if (msgIds.length > 30000) {
delete msgHistory[msgIds.sort()[0]]
}
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRecord}) => {
const message = msgRecord const message = msgRecord;
const peerUid = message.peerUid const peerUid = message.peerUid;
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid] const sendCallback = sendMessagePool[peerUid]
if (sendCallback) { if (sendCallback) {
try { try {
sendCallback(message) sendCallback(message);
} catch (e) { } catch (e) {
log('receive self msg error', e.stack) log("receive self msg error", e.stack)
} }
} }
}) })

View File

@@ -1,25 +1,30 @@
import {ipcMain} from 'electron' import {BrowserWindow, ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from './hook' import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log, sleep} from '../common/utils' import {log, sleep} from "../common/utils";
import { import {
type ChatType, ChatType,
ElementType, ElementType,
type Friend, Friend,
type FriendRequest, FriendRequest,
type Group, GroupMember, Group,
type GroupMemberRole, GroupMember,
type GroupNotifies, GroupMemberRole,
type GroupNotify, GroupNotifies,
type GroupRequestOperateTypes, GroupNotify,
type RawMessage, GroupRequestOperateTypes,
type SelfInfo, RawMessage,
type SendMessageElement, SelfInfo,
type User SendMessageElement,
} from './types' User,
import * as fs from 'node:fs' CacheScanResult,
import {addHistoryMsg, friendRequests, groupNotifies, msgHistory, selfInfo} from '../common/data' ChatCacheList, ChatCacheListItemBasic,
import {v4 as uuidv4} from 'uuid' CacheFileList, CacheFileListItem, CacheFileType,
import path from 'path' } from "./types";
import * as fs from "fs";
import {friendRequests, selfInfo, uidMaps} from "../common/data";
import {v4 as uuidv4} from "uuid"
import path from "path";
import {dbUtil} from "../common/db";
interface IPCReceiveEvent { interface IPCReceiveEvent {
eventName: string eventName: string
@@ -34,88 +39,111 @@ export type IPCReceiveDetail = [
] ]
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = 'ns-ntApi', NT_API = "ns-ntApi",
FS_API = 'ns-FsApi', FS_API = "ns-FsApi",
GLOBAL_DATA = 'ns-GlobalDataApi' OS_API = "ns-OsApi",
WINDOW_API = "ns-WindowApi",
HOTUPDATE_API = "ns-HotUpdateApi",
BUSINESS_API = "ns-BusinessApi",
GLOBAL_DATA = "ns-GlobalDataApi"
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike', SET_HEADER = "nodeIKernelProfileService/setHeader",
SELF_INFO = 'fetchAuthData', LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
FRIENDS = 'nodeIKernelBuddyService/getBuddyList', SELF_INFO = "fetchAuthData",
GROUPS = 'nodeIKernelGroupService/getGroupList', FRIENDS = "nodeIKernelBuddyService/getBuddyList",
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene', GROUPS = "nodeIKernelGroupService/getGroupList",
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList', GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene",
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo', GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo', USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
FILE_TYPE = 'getFileType', USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
FILE_MD5 = 'getFileMd5', FILE_TYPE = "getFileType",
FILE_COPY = 'copyFile', FILE_MD5 = "getFileMd5",
IMAGE_SIZE = 'getImageSizeFromPath', FILE_COPY = "copyFile",
FILE_SIZE = 'getFileSize', IMAGE_SIZE = "getImageSizeFromPath",
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild', FILE_SIZE = "getFileSize",
RECALL_MSG = 'nodeIKernelMsgService/recallMsg', MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
SEND_MSG = 'nodeIKernelMsgService/sendMsg', RECALL_MSG = "nodeIKernelMsgService/recallMsg",
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia', SEND_MSG = "nodeIKernelMsgService/sendMsg",
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发 DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies', FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment",
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify', MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup', GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange" // READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest', HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest",
KICK_MEMBER = 'nodeIKernelGroupService/kickMember', KICK_MEMBER = "nodeIKernelGroupService/kickMember",
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp', MUTE_MEMBER = "nodeIKernelGroupService/setMemberShutUp",
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp', MUTE_GROUP = "nodeIKernelGroupService/setGroupShutUp",
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName', SET_MEMBER_CARD = "nodeIKernelGroupService/modifyMemberCardName",
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole', SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole",
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin', PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin",
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName', SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName",
SET_GROUP_TITLE = "nodeIKernelGroupService/modifyMemberSpecialTitle",
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader'
} }
enum NTQQApiChannel { enum NTQQApiChannel {
IPC_UP_2 = 'IPC_UP_2', IPC_UP_2 = "IPC_UP_2",
IPC_UP_3 = 'IPC_UP_3', IPC_UP_3 = "IPC_UP_3",
IPC_UP_1 = 'IPC_UP_1', IPC_UP_1 = "IPC_UP_1",
} }
export interface Peer { export interface Peer {
chatType: ChatType chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串 peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: '' guildId?: ""
} }
interface NTQQApiParams { interface NTQQApiParams {
methodName: NTQQApiMethod | string methodName: NTQQApiMethod | string,
className?: NTQQApiClass className?: NTQQApiClass,
channel?: NTQQApiChannel channel?: NTQQApiChannel,
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[] args?: unknown[],
cbCmd?: ReceiveCmd | null cbCmd?: ReceiveCmd | null,
cmdCB?: (payload: any) => boolean cmdCB?: (payload: any) => boolean;
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean, // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number timeoutSecond?: number,
} }
async function callNTQQApi<ReturnType>(params: NTQQApiParams) { function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let { let {
className, methodName, channel, args, className, methodName, channel, args,
cbCmd, timeoutSecond: timeout, cbCmd, timeoutSecond: timeout,
classNameIsRegister, cmdCB, afterFirstCmd classNameIsRegister, cmdCB, afterFirstCmd
} = params } = params;
className = className ?? NTQQApiClass.NT_API className = className ?? NTQQApiClass.NT_API;
channel = channel ?? NTQQApiChannel.IPC_UP_2 channel = channel ?? NTQQApiChannel.IPC_UP_2;
args = args ?? [] args = args ?? [];
timeout = timeout ?? 5 timeout = timeout ?? 5;
afterFirstCmd = afterFirstCmd ?? true afterFirstCmd = afterFirstCmd ?? true;
const uuid = uuidv4() const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid) // log("callNTQQApi", channel, className, methodName, args, uuid)
return await new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) // log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000 const _timeout = timeout * 1000
let success = false let success = false
let eventName = className + '-' + channel[channel.length - 1] let eventName = className + "-" + channel[channel.length - 1];
if (classNameIsRegister) { if (classNameIsRegister) {
eventName += '-register' eventName += "-register";
} }
const apiArgs = [methodName, ...args] const apiArgs = [methodName, ...args]
if (!cbCmd) { if (!cbCmd) {
@@ -123,40 +151,40 @@ async function callNTQQApi<ReturnType>(params: NTQQApiParams) {
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true success = true
resolve(r) resolve(r)
} };
} else { } else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB); // log(methodName, "second callback", cbCmd, payload, cmdCB);
if (cmdCB) { if (!!cmdCB) {
if (cmdCB(payload)) { if (cmdCB(payload)) {
removeReceiveHook(hookId) removeReceiveHook(hookId);
success = true success = true
resolve(payload) resolve(payload);
} }
} else { } else {
removeReceiveHook(hookId) removeReceiveHook(hookId);
success = true success = true
resolve(payload) resolve(payload);
} }
}) })
} }
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback();
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result) log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) { if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback();
} else { } else {
success = true success = true
reject(`ntqq api call failed, ${result.errMsg}`) reject(`ntqq api call failed, ${result.errMsg}`);
} }
} }
} }
setTimeout(() => { setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName) // log("ntqq api timeout", success, channel, className, methodName)
if (!success) { if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs) log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs);
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`) reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
} }
}, _timeout) }, _timeout)
@@ -170,15 +198,23 @@ async function callNTQQApi<ReturnType>(params: NTQQApiParams) {
}) })
} }
export const sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
interface GeneralCallResult { interface GeneralCallResult {
result: number // 0: success result: number, // 0: success
errMsg: string errMsg: string
} }
export class NTQQApi { export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND) static async setHeader(path: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_HEADER,
args: [path]
})
}
static async likeFriend(uid: string, count = 1) { static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND, methodName: NTQQApiMethod.LIKE_FRIEND,
@@ -196,9 +232,7 @@ export class NTQQApi {
static async getSelfInfo() { static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({ return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA, className: NTQQApiClass.GLOBAL_DATA,
// channel: NTQQApiChannel.IPC_UP_3, methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
methodName: NTQQApiMethod.SELF_INFO,
timeoutSecond: 2
}) })
} }
@@ -228,24 +262,28 @@ export class NTQQApi {
null null
] ]
}) })
return result.info const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
} }
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: Array<{ data: {
categoryId: number categoryId: number,
categroyName: string categroyName: string,
categroyMbCount: number categroyMbCount: number,
buddyList: Friend[] buddyList: Friend[]
}> }[]
}>( }>(
{ {
methodName: NTQQApiMethod.FRIENDS, methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined], args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmd.FRIENDS cbCmd: ReceiveCmd.FRIENDS
}) })
const _friends: Friend[] = [] let _friends: Friend[] = [];
for (const fData of data.data) { for (const fData of data.data) {
_friends.push(...fData.buddyList) _friends.push(...fData.buddyList)
} }
@@ -254,11 +292,11 @@ export class NTQQApi {
static async getGroups(forced = false) { static async getGroups(forced = false) {
let cbCmd = ReceiveCmd.GROUPS let cbCmd = ReceiveCmd.GROUPS
if (process.platform != 'win32') { if (process.platform != "win32") {
cbCmd = ReceiveCmd.GROUPS_UNIX cbCmd = ReceiveCmd.GROUPS_UNIX
} }
const result = await callNTQQApi<{ const result = await callNTQQApi<{
updateType: number updateType: number,
groupList: Group[] groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) }>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList return result.groupList
@@ -269,7 +307,7 @@ export class NTQQApi {
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [{ args: [{
groupCode: groupQQ, groupCode: groupQQ,
scene: 'groupMemberList_MainWindow' scene: "groupMemberList_MainWindow"
}] }]
}) })
// log("get group member sceneId", sceneId); // log("get group member sceneId", sceneId);
@@ -279,8 +317,8 @@ export class NTQQApi {
}>({ }>({
methodName: NTQQApiMethod.GROUP_MEMBERS, methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{ args: [{
sceneId, sceneId: sceneId,
num num: num
}, },
null null
] ]
@@ -290,7 +328,7 @@ export class NTQQApi {
const members: GroupMember[] = Array.from(values) const members: GroupMember[] = Array.from(values)
for (const member of members) { for (const member of members) {
// uidMaps[member.uid] = member.uin; uidMaps[member.uid] = member.uin;
} }
// log(uidMaps); // log(uidMaps);
// log("members info", values); // log("members info", values);
@@ -341,35 +379,35 @@ export class NTQQApi {
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) {
const md5 = await NTQQApi.getFileMd5(filePath) const md5 = await NTQQApi.getFileMd5(filePath);
let ext = (await NTQQApi.getFileType(filePath))?.ext let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) { if (ext) {
ext = '.' + ext ext = "." + ext
} else { } else {
ext = '' ext = ""
} }
let fileName = `${path.basename(filePath)}` let fileName = `${path.basename(filePath)}`;
if (!fileName.includes('.')) { if (fileName.indexOf(".") === -1) {
fileName += ext fileName += ext;
} }
const mediaPath = await callNTQQApi<string>({ const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH, methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{ args: [{
path_info: { path_info: {
md5HexStr: md5, md5HexStr: md5,
fileName, fileName: fileName,
elementType, elementType: elementType,
elementSubType: 0, elementSubType: 0,
thumbSize: 0, thumbSize: 0,
needCreate: true, needCreate: true,
downloadType: 1, downloadType: 1,
file_uuid: '' file_uuid: ""
} }
}] }]
}) })
log('media path', mediaPath) log("media path", mediaPath)
await NTQQApi.copyFile(filePath, mediaPath) await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath) const fileSize = await NTQQApi.getFileSize(filePath);
return { return {
md5, md5,
fileName, fileName,
@@ -386,16 +424,16 @@ export class NTQQApi {
const apiParams = [ const apiParams = [
{ {
getReq: { getReq: {
msgId, msgId: msgId,
chatType, chatType: chatType,
peerUid, peerUid: peerUid,
elementId, elementId: elementId,
thumbSize: 0, thumbSize: 0,
downloadType: 1, downloadType: 1,
filePath: thumbPath filePath: thumbPath,
} },
}, },
undefined undefined,
] ]
// log("需要下载media", sourcePath); // log("需要下载media", sourcePath);
await callNTQQApi({ await callNTQQApi({
@@ -404,7 +442,7 @@ export class NTQQApi {
cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string } }) => { cmdCB: (payload: { notifyInfo: { filePath: string } }) => {
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath); // log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath);
return payload.notifyInfo.filePath == sourcePath return payload.notifyInfo.filePath == sourcePath;
} }
}) })
return sourcePath return sourcePath
@@ -420,60 +458,83 @@ export class NTQQApi {
}) })
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false, timeout = 10000) { static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
const peerUid = peer.peerUid const peerUid = peer.peerUid
// 等待上一个相同的peer发送完 // 等待上一个相同的peer发送完
let checkLastSendUsingTime = 0 let checkLastSendUsingTime = 0;
const waitLastSend = async () => { const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) { if (checkLastSendUsingTime > timeout) {
throw ('发送超时') throw ("发送超时")
} }
const lastSending = sendMessagePool[peer.peerUid] let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) { if (lastSending) {
// log("有正在发送的消息,等待中...") // log("有正在发送的消息,等待中...")
await sleep(500) await sleep(500);
checkLastSendUsingTime += 500 checkLastSendUsingTime += 500;
return await waitLastSend() return await waitLastSend();
} else { } else {
return;
} }
} }
await waitLastSend() await waitLastSend();
let sentMessage: RawMessage = null let sentMessage: RawMessage = null;
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid] delete sendMessagePool[peerUid];
sentMessage = rawMessage sentMessage = rawMessage;
} }
let checkSendCompleteUsingTime = 0 let checkSendCompleteUsingTime = 0;
const checkSendComplete = async (): Promise<RawMessage> => { const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage && msgHistory[sentMessage.msgId]?.sendStatus == 2) { if (sentMessage) {
// log(`给${peerUid}发送消息成功`) if (waitComplete) {
return sentMessage if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) {
} else { return sentMessage
checkSendCompleteUsingTime += 500 }
if (checkSendCompleteUsingTime > timeout) { } else {
throw ('发送超时') return sentMessage
} }
await sleep(500) // log(`给${peerUid}发送消息成功`)
return await checkSendComplete()
} }
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw ('发送超时')
}
await sleep(500)
return await checkSendComplete()
} }
callNTQQApi({ callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG, methodName: NTQQApiMethod.SEND_MSG,
args: [{ args: [{
msgId: '0', msgId: "0",
peer, peer, msgElements,
msgElements, msgAttributeInfos: new Map(),
msgAttributeInfos: new Map()
}, null] }, null]
}).then() }).then()
return await checkSendComplete() return await checkSendComplete()
} }
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [
destPeer
],
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
})
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const msgInfos = msgIds.map(id => { const msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: selfInfo.nick} return {msgId: id, senderShowName: selfInfo.nick}
@@ -486,16 +547,16 @@ export class NTQQApi {
commentElements: [], commentElements: [],
msgAttributeInfos: new Map() msgAttributeInfos: new Map()
}, },
null null,
] ]
return await new Promise<RawMessage>((resolve, reject) => { return await new Promise<RawMessage>((resolve, reject) => {
let complete = false let complete = false
setTimeout(() => { setTimeout(() => {
if (!complete) { if (!complete) {
reject('转发消息超时') reject("转发消息超时");
} }
}, 5000) }, 5000)
registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, (payload: { msgRecord: RawMessage }) => { registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord const msg = payload.msgRecord
// 需要判断它是转发的消息,并且识别到是当前转发的这一条 // 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find(ele => ele.arkElement) const arkElement = msg.elements.find(ele => ele.arkElement)
@@ -509,7 +570,7 @@ export class NTQQApi {
} }
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true complete = true
addHistoryMsg(msg) await dbUtil.addMsg(msg)
resolve(msg) resolve(msg)
log('转发消息成功:', payload) log('转发消息成功:', payload)
} }
@@ -518,10 +579,10 @@ export class NTQQApi {
methodName: NTQQApiMethod.MULTI_FORWARD_MSG, methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs args: apiArgs
}).then(result => { }).then(result => {
log('转发消息结果:', result, apiArgs) log("转发消息结果:", result, apiArgs)
if (result.result !== 0) { if (result.result !== 0) {
complete = true complete = true;
reject('转发消息失败,' + JSON.stringify(result)) reject("转发消息失败," + JSON.stringify(result));
} }
}) })
}) })
@@ -532,56 +593,79 @@ export class NTQQApi {
// 加群通知,退出通知,需要管理员权限 // 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({ callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY, methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true classNameIsRegister: true,
}).then() }).then()
return await callNTQQApi<GroupNotifies>({ return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE, methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmd.GROUP_NOTIFY, cbCmd: ReceiveCmd.GROUP_NOTIFY,
afterFirstCmd: false, afterFirstCmd: false,
args: [ args: [
{doubt: false, startSeq: '', number: 14}, {"doubt": false, "startSeq": "", "number": 14},
null null
] ]
});
}
static async getGroupIgnoreNotifies() {
await NTQQApi.getGroupNotifies();
const result = callNTQQApi<GroupNotifies>({
className: NTQQApiClass.WINDOW_API,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW,
cbCmd: ReceiveCmd.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
"GroupNotifyFilterWindow"
]
}) })
// 关闭窗口
setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())
if (w.webContents.getURL().indexOf("#/notify-filter/") != -1) {
w.close();
}
}
}, 2000);
return result;
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = groupNotifies[seq] const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) { if (!notify) {
throw `${seq}对应的加群通知不存在` throw `${seq}对应的加群通知不存在`
} }
delete groupNotifies[seq] // delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [ args: [
{ {
doubt: false, "doubt": false,
operateMsg: { "operateMsg": {
operateType, // 2 拒绝 "operateType": operateType, // 2 拒绝
targetMsg: { "targetMsg": {
seq, // 通知序列号 "seq": seq, // 通知序列号
type: notify.type, "type": notify.type,
groupCode: notify.group.groupCode, "groupCode": notify.group.groupCode,
postscript: reason "postscript": reason
} }
} }
}, },
null null
] ]
}) });
} }
static async quitGroup(groupQQ: string) { static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({ await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP, methodName: NTQQApiMethod.QUIT_GROUP,
args: [ args: [
{groupCode: groupQQ}, {"groupCode": groupQQ},
null null
] ]
}) })
} }
static async handleFriendRequest(sourceId: number, accept: boolean) { static async handleFriendRequest(sourceId: number, accept: boolean,) {
const request: FriendRequest = friendRequests[sourceId] const request: FriendRequest = friendRequests[sourceId]
if (!request) { if (!request) {
throw `sourceId ${sourceId}, 对应的好友请求不存在` throw `sourceId ${sourceId}, 对应的好友请求不存在`
@@ -590,16 +674,16 @@ export class NTQQApi {
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [ args: [
{ {
approvalInfo: { "approvalInfo": {
friendUid: request.friendUid, "friendUid": request.friendUid,
reqTime: request.reqTime, "reqTime": request.reqTime,
accept accept
} }
} }
] ]
}) })
delete friendRequests[sourceId] delete friendRequests[sourceId];
return result return result;
} }
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
@@ -611,7 +695,7 @@ export class NTQQApi {
groupCode: groupQQ, groupCode: groupQQ,
kickUids, kickUids,
refuseForever, refuseForever,
kickReason kickReason,
} }
] ]
} }
@@ -626,7 +710,7 @@ export class NTQQApi {
args: [ args: [
{ {
groupCode: groupQQ, groupCode: groupQQ,
memList memList,
} }
] ]
} }
@@ -683,7 +767,143 @@ export class NTQQApi {
}) })
} }
static async call(className: NTQQApiClass, cmdName: string, args: any[],) {
return await callNTQQApi<GeneralCallResult>({
className,
methodName: cmdName,
args: [
...args,
]
})
}
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title
}, null
]
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) { static publishGroupBulletin(groupQQ: string, title: string, content: string) {
} }
static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [{
isSilent
}, null]
});
}
static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [{
pathMap: {...pathMap},
}, null]
});
}
static scanCache() {
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then();
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
});
}
static getHotUpdateCachePath() {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE
});
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP
});
}
static getCacheSessionPathList() {
return callNTQQApi<{
key: string,
value: string
}[]>({
className: NTQQApiClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION,
});
}
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [{
keys: cacheKeys
}, null]
});
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET,
args: [{
chatType: type,
pageSize,
order: 1,
pageIndex
}, null]
}).then(list => res(list))
.catch(e => rej(e));
});
}
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : {fileType: fileType};
return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET,
args: [{
fileType: fileType,
restart: true,
pageSize: pageSize,
order: 1,
lastRecord: _lastRecord,
}, null]
})
}
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{
chats,
fileKeys
}, null]
});
}
static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [{
path:filePath
}, null],
timeoutSecond: 10 // 10秒不一定够
});
}
} }

View File

@@ -71,8 +71,10 @@ export enum ElementType {
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
PTT = 4, PTT = 4,
VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
ARK = 10,
} }
export interface SendTextElement { export interface SendTextElement {
@@ -111,6 +113,7 @@ export enum PicType {
gif = 2000, gif = 2000,
jpg = 1000 jpg = 1000
} }
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC, elementType: ElementType.PIC,
elementId: "", elementId: "",
@@ -165,13 +168,25 @@ export interface FileElement {
} }
export interface SendFileElement { export interface SendFileElement {
"elementType": ElementType.FILE, elementType: ElementType.FILE
"elementId": "", elementId: "",
"fileElement": FileElement fileElement: FileElement
}
export interface SendVideoElement {
elementType: ElementType.VIDEO
elementId: "",
videoElement: VideoElement
}
export interface SendArkElement {
elementType: ElementType.ARK,
elementId: "",
arkElement: ArkElement
} }
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendFileElement SendPicElement | SendReplyElement | SendFaceElement | SendFileElement | SendVideoElement | SendArkElement
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
@@ -210,6 +225,8 @@ export interface PttElement {
export interface ArkElement { export interface ArkElement {
bytesData: string; bytesData: string;
linkInfo: null,
subElementType: null
} }
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn" export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn"
@@ -223,6 +240,7 @@ export interface PicElement {
fileSize: number; fileSize: number;
fileName: string; fileName: string;
fileUuid: string; fileUuid: string;
md5HexStr?: string;
} }
export interface GrayTipElement { export interface GrayTipElement {
@@ -234,7 +252,8 @@ export interface GrayTipElement {
operatorMemRemark?: string; operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语 wording: string; // 自定义的撤回提示语
} }
aioOpGrayTipElement: TipAioOpGrayTipElement aioOpGrayTipElement: TipAioOpGrayTipElement,
groupElement: TipGroupElement
} }
export interface FaceElement { export interface FaceElement {
@@ -245,34 +264,78 @@ export interface FaceElement {
export interface VideoElement { export interface VideoElement {
"filePath": string, "filePath": string,
"fileName": string, "fileName": string,
"videoMd5": string, "videoMd5"?: string,
"thumbMd5": string "thumbMd5"?: string
"fileTime": 87, // second "fileTime"?: number, // second
"thumbSize": 314235, // byte "thumbSize"?: number, // byte
"fileFormat": 2, // 2表示mp4 "fileFormat"?: number, // 2表示mp4
"fileSize": string, // byte "fileSize"?: string, // byte
"thumbWidth": number, "thumbWidth"?: number,
"thumbHeight": number, "thumbHeight"?: number,
"busiType": 0, // 未知 "busiType"?: 0, // 未知
"subBusiType": 0, // 未知 "subBusiType"?: 0, // 未知
"thumbPath": Map<number, any>, "thumbPath"?: Map<number, any>,
"transferStatus": 0, // 未知 "transferStatus"?: 0, // 未知
"progress": 0, // 下载进度? "progress"?: 0, // 下载进度?
"invalidState": 0, // 未知 "invalidState"?: 0, // 未知
"fileUuid": string, // 可以用于下载链接? "fileUuid"?: string, // 可以用于下载链接?
"fileSubId": "", "fileSubId"?: "",
"fileBizId": null, "fileBizId"?: null,
"originVideoMd5": "", "originVideoMd5"?: "",
"import_rich_media_context": null, "import_rich_media_context"?: null,
"sourceVideoCodecFormat": 0 "sourceVideoCodecFormat"?: number
} }
export interface TipAioOpGrayTipElement{ export interface TipAioOpGrayTipElement { // 这是什么提示来着?
operateType: number, operateType: number,
peerUid: string, peerUid: string,
fromGrpCodeOfTmpChat: string, fromGrpCodeOfTmpChat: string,
} }
export enum TipGroupElementType {
memberIncrease = 1,
ban = 8
}
export interface TipGroupElement {
"type": TipGroupElementType, // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0, // 暂时不知
"groupName": string, // 暂时获取不到
"memberUid": string,
"memberNick": string,
"memberRemark": string,
"adminUid": string, // 同意加群的管理员uid
"adminNick": string,
"adminRemark": string,
"createGroup": null,
"memberAdd"?: {
"showType": 1,
"otherAdd": null,
"otherAddByOtherQRCode": null,
"otherAddByYourQRCode": null,
"youAddByOtherQRCode": null,
"otherInviteOther": null,
"otherInviteYou": null,
"youInviteOther": null
},
"shutUp"?: {
"curTime": string,
"duration": string, // 禁言时间,秒
"admin": {
"uid": string,
"card": string,
"name": string,
"role": GroupMemberRole
},
"member": {
"uid": string
"card": string,
"name": string,
"role": GroupMemberRole
}
}
}
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string;
@@ -329,11 +392,17 @@ export interface GroupNotifies {
notifies: GroupNotify[], notifies: GroupNotify[],
} }
export enum GroupNotifyStatus {
IGNORE = 0,
WAIT_HANDLE = 1,
APPROVE = 2,
REJECT = 3
}
export interface GroupNotify { export interface GroupNotify {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 转成数字再除以1000应该就是时间戳 seq: string, // 唯一标识符,转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes, type: GroupNotifyTypes,
status: 0, // 未知 status: GroupNotifyStatus, // 0是已忽略1是未处理2是已同意
group: { groupCode: string, groupName: string }, group: { groupCode: string, groupName: string },
user1: { uid: string, nickName: string }, // 被设置管理员的人 user1: { uid: string, nickName: string }, // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者 user2: { uid: string, nickName: string }, // 操作者
@@ -369,3 +438,67 @@ export interface FriendRequestNotify {
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }
export interface CacheScanResult {
result: number,
size: [ // 单位为字节
string, // 系统总存储空间
string, // 系统可用存储空间
string, // 系统已用存储空间
string, // QQ总大小
string, // 「聊天与文件」大小
string, // 未知
string, // 「缓存数据」大小
string, // 「其他数据」大小
string, // 未知
]
}
export interface ChatCacheList {
pageCount: number,
infos: ChatCacheListItem[]
}
export interface ChatCacheListItem {
chatType: ChatType,
basicChatCacheInfo: ChatCacheListItemBasic,
guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容
}
export interface ChatCacheListItemBasic {
chatSize: string,
chatTime: string,
uid: string,
uin: string,
remarkName: string,
nickName: string,
chatType?: ChatType,
isChecked?: boolean
}
export enum CacheFileType {
IMAGE = 0,
VIDEO = 1,
AUDIO = 2,
DOCUMENT = 3,
OTHER = 4,
}
export interface CacheFileList {
infos: CacheFileListItem[],
}
export interface CacheFileListItem {
fileSize: string,
fileTime: string,
fileKey: string,
elementId: string,
elementIdStr: string,
fileType: CacheFileType,
path: string,
fileName: string,
senderId: string,
previewPath: string,
senderName: string,
isChecked?: boolean,
}

View File

@@ -1,6 +1,7 @@
import {ActionName, BaseCheckResult} from "./types" import {ActionName, BaseCheckResult} from "./types"
import {OB11Response} from "./utils" import {OB11Response} from "./utils"
import {OB11Return} from "../types"; import {OB11Return} from "../types";
import {log} from "../../common/utils";
class BaseAction<PayloadType, ReturnDataType> { class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName actionName: ActionName
@@ -20,7 +21,8 @@ class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload); const resData = await this._handle(payload);
return OB11Response.ok(resData); return OB11Response.ok(resData);
} catch (e) { } catch (e) {
return OB11Response.error(e.toString(), 200); log("发生错误", e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || "未知错误,可能操作超时", 200);
} }
} }
@@ -33,7 +35,8 @@ class BaseAction<PayloadType, ReturnDataType> {
const resData = await this._handle(payload) const resData = await this._handle(payload)
return OB11Response.ok(resData, echo); return OB11Response.ok(resData, echo);
} catch (e) { } catch (e) {
return OB11Response.error(e.toString(), 1200, echo) log("发生错误", e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
} }
} }

View File

@@ -0,0 +1,105 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {NTQQApi} from "../../ntqqapi/ntcall";
import fs from "fs";
import Path from "path";
import {
ChatType,
ChatCacheListItemBasic,
CacheFileType
} from '../../ntqqapi/types';
import {dbUtil} from "../../common/db";
export default class CleanCache extends BaseAction<void, void> {
actionName = ActionName.CleanCache
protected _handle(): Promise<void> {
return new Promise<void>(async (res, rej) => {
try {
// dbUtil.clearCache();
const cacheFilePaths: string[] = [];
await NTQQApi.setCacheSilentScan(false);
cacheFilePaths.push((await NTQQApi.getHotUpdateCachePath()));
cacheFilePaths.push((await NTQQApi.getDesktopTmpPath()));
(await NTQQApi.getCacheSessionPathList()).forEach(e => cacheFilePaths.push(e.value));
// await NTQQApi.addCacheScannedPaths(); // XXX: 调用就崩溃,原因目前还未知
const cacheScanResult = await NTQQApi.scanCache();
const cacheSize = parseInt(cacheScanResult.size[6]);
if (cacheScanResult.result !== 0) {
throw('Something went wrong while scanning cache. Code: ' + cacheScanResult.result);
}
await NTQQApi.setCacheSilentScan(true);
if (cacheSize > 0 && cacheFilePaths.length > 2) { // 存在缓存文件且大小不为 0 时执行清理动作
// await NTQQApi.clearCache([ 'tmp', 'hotUpdate', ...cacheScanResult ]) // XXX: 也是调用就崩溃,调用 fs 删除得了
deleteCachePath(cacheFilePaths);
}
// 获取聊天记录列表
// NOTE: 以防有人不需要删除聊天记录,暂时先注释掉,日后加个开关
// const privateChatCache = await getCacheList(ChatType.friend); // 私聊消息
// const groupChatCache = await getCacheList(ChatType.group); // 群聊消息
// const chatCacheList = [ ...privateChatCache, ...groupChatCache ];
const chatCacheList: ChatCacheListItemBasic[] = [];
// 获取聊天缓存文件列表
const cacheFileList: string[] = [];
for (const name in CacheFileType) {
if (!isNaN(parseInt(name))) continue;
const fileTypeAny: any = CacheFileType[name];
const fileType: CacheFileType = fileTypeAny;
cacheFileList.push(...(await NTQQApi.getFileCacheInfo(fileType)).infos.map(file => file.fileKey));
}
// 一并清除
await NTQQApi.clearChatCache(chatCacheList, cacheFileList);
res();
} catch(e) {
console.error('清理缓存时发生了错误');
rej(e);
}
});
}
}
function deleteCachePath(pathList: string[]) {
const emptyPath = (path: string) => {
if (!fs.existsSync(path)) return;
const files = fs.readdirSync(path);
files.forEach(file => {
const filePath = Path.resolve(path, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) emptyPath(filePath);
else fs.unlinkSync(filePath);
});
fs.rmdirSync(path);
}
for (const path of pathList) {
emptyPath(path);
}
}
function getCacheList(type: ChatType) { // NOTE: 做这个方法主要是因为目前还不支持针对频道消息的清理
return new Promise<Array<ChatCacheListItemBasic>>((res, rej) => {
NTQQApi.getChatCacheList(type, 1000, 0)
.then(data => {
const list = data.infos.filter(e => e.chatType === type && parseInt(e.basicChatCacheInfo.chatSize) > 0);
const result = list.map(e => {
const result = { ...e.basicChatCacheInfo };
result.chatType = type;
result.isChecked = true;
return result;
});
res(result);
})
.catch(e => rej(e));
});
}

View File

@@ -1,7 +1,7 @@
import {ActionName} from "./types"; import {ActionName} from "./types";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall"; import {NTQQApi} from "../../ntqqapi/ntcall";
import {getHistoryMsgByShortId} from "../../common/data"; import {dbUtil} from "../../common/db";
interface Payload { interface Payload {
message_id: number message_id: number
@@ -11,7 +11,7 @@ class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg actionName = ActionName.DeleteMsg
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
let msg = getHistoryMsgByShortId(payload.message_id) let msg = await dbUtil.getMsgByShortId(payload.message_id)
await NTQQApi.recallMsg({ await NTQQApi.recallMsg({
chatType: msg.chatType, chatType: msg.chatType,
peerUid: msg.peerUid peerUid: msg.peerUid

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {fileCache} from "../../common/data";
import {getConfigUtil} from "../../common/utils"; import {getConfigUtil} from "../../common/utils";
import fs from "fs/promises"; import fs from "fs/promises";
import {dbUtil} from "../../common/db";
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名 file: string // 文件名
@@ -18,7 +18,7 @@ export interface GetFileResponse {
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = fileCache.get(payload.file) const cache = await dbUtil.getFileCache(payload.file)
const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig() const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig()
if (!cache) { if (!cache) {
throw new Error('file not found') throw new Error('file not found')

View File

@@ -1,8 +1,8 @@
import {getHistoryMsgByShortId} from "../../common/data";
import {OB11Message} from '../types'; import {OB11Message} from '../types';
import {OB11Constructor} from "../constructor"; import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {ActionName} from "./types"; import {ActionName} from "./types";
import {dbUtil} from "../../common/db";
export interface PayloadType { export interface PayloadType {
@@ -19,13 +19,14 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
if (!payload.message_id) { if (!payload.message_id) {
throw ("参数message_id不能为空") throw ("参数message_id不能为空")
} }
const msg = getHistoryMsgByShortId(payload.message_id) let msg = await dbUtil.getMsgByShortId(payload.message_id)
if (msg) { if(!msg) {
const msgData = await OB11Constructor.message(msg); msg = await dbUtil.getMsgByLongId(payload.message_id.toString())
return msgData }
} else { if (!msg){
throw ("消息不存在") throw ("消息不存在")
} }
return await OB11Constructor.message(msg)
} }
} }

View File

@@ -1,9 +1,17 @@
import SendMsg from "./SendMsg"; import SendMsg from "./SendMsg";
import {ActionName} from "./types"; import {ActionName, BaseCheckResult} from "./types";
import {OB11PostSendMsg} from "../types";
import {log} from "../../common/utils";
class SendGroupMsg extends SendMsg { class SendGroupMsg extends SendMsg {
actionName = ActionName.SendGroupMsg actionName = ActionName.SendGroupMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
delete payload.user_id;
payload.message_type = "group"
return super.check(payload);
}
} }
export default SendGroupMsg export default SendGroupMsg

View File

@@ -1,7 +1,8 @@
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {getFriend} from "../../common/data"; import {getFriend, getUidByUin, uidMaps} from "../../common/data";
import {NTQQApi} from "../../ntqqapi/ntcall"; import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types"; import {ActionName} from "./types";
import {log} from "../../common/utils";
interface Payload { interface Payload {
user_id: number, user_id: number,
@@ -12,13 +13,17 @@ export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const qq = payload.user_id.toString(); log("点赞参数", payload)
const friend = await getFriend(qq)
if (!friend) {
throw (`点赞失败,${qq}不是好友`)
}
try { try {
let result = await NTQQApi.likeFriend(friend.uid, parseInt(payload.times.toString()) || 1); const qq = payload.user_id.toString();
const friend = await getFriend(qq)
let uid: string;
if (!friend) {
uid = getUidByUin(qq)
} else {
uid = friend.uid
}
let result = await NTQQApi.likeFriend(uid, parseInt(payload.times?.toString()) || 1);
if (result.result !== 0) { if (result.result !== 0) {
throw result.errMsg throw result.errMsg
} }

View File

@@ -1,22 +1,31 @@
import {AtType, ChatType, Group, RawMessage, SendMessageElement} from "../../ntqqapi/types";
import { import {
addHistoryMsg, AtType,
friends, ChatType,
getGroup, ElementType,
getGroupMember, Group,
getHistoryMsgByShortId, RawMessage,
getUidByUin, SendArkElement,
selfInfo, SendMessageElement
} from "../../common/data"; } from "../../ntqqapi/types";
import {OB11MessageData, OB11MessageDataType, OB11MessageMixType, OB11MessageNode, OB11PostSendMsg} from '../types'; import {friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo,} from "../../common/data";
import {
OB11MessageCustomMusic,
OB11MessageData,
OB11MessageDataType,
OB11MessageMixType,
OB11MessageNode,
OB11PostSendMsg
} from '../types';
import {NTQQApi, Peer} from "../../ntqqapi/ntcall"; import {NTQQApi, Peer} from "../../ntqqapi/ntcall";
import {SendMsgElementConstructor} from "../../ntqqapi/constructor"; import {SendMsgElementConstructor} from "../../ntqqapi/constructor";
import {uri2local} from "../utils"; import {uri2local} from "../utils";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {ActionName, BaseCheckResult} from "./types"; import {ActionName, BaseCheckResult} from "./types";
import * as fs from "node:fs"; import * as fs from "node:fs";
import {log} from "../../common/utils"; import {log, sleep} from "../../common/utils";
import {decodeCQCode} from "../cqcode"; import {decodeCQCode} from "../cqcode";
import {dbUtil} from "../../common/db";
import {ALLOW_SEND_TEMP_MSG} from "../../common/config";
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@@ -62,34 +71,50 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = this.convertMessage2List(payload.message); const messages = this.convertMessage2List(payload.message);
const fmNum = this.forwardMsgNum(payload) const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node)
if (fmNum && fmNum != messages.length) { if (fmNum && fmNum != messages.length) {
return { return {
valid: false, valid: false,
message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素" message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素"
} }
} }
if (payload.group_id && !(await getGroup(payload.group_id))) {
return {
valid: false,
message: `${payload.group_id}不存在`
}
}
if (payload.user_id && payload.message_type !== "group") {
if (!(await getFriend(payload.user_id))) {
if (!ALLOW_SEND_TEMP_MSG) {
return {
valid: false,
message: `不能发送临时消息`
}
}
}
}
return { return {
valid: true, valid: true,
} }
} }
protected async _handle(payload: OB11PostSendMsg) { protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = { const peer: Peer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: "" peerUid: ""
} }
let isTempMsg = false;
let group: Group | undefined = undefined; let group: Group | undefined = undefined;
if (payload?.group_id) { const genGroupPeer = async () => {
group = await getGroup(payload.group_id.toString()) group = await getGroup(payload.group_id.toString())
if (!group) {
throw (`${payload.group_id}不存在`)
}
peer.chatType = ChatType.group peer.chatType = ChatType.group
// peer.name = group.name // peer.name = group.name
peer.peerUid = group.groupCode peer.peerUid = group.groupCode
} else if (payload?.user_id) { }
const genFriendPeer = () => {
const friend = friends.find(f => f.uin == payload.user_id.toString()) const friend = friends.find(f => f.uin == payload.user_id.toString())
if (friend) { if (friend) {
// peer.name = friend.nickName // peer.name = friend.nickName
@@ -101,26 +126,49 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
throw (`找不到私聊对象${payload.user_id}`) throw (`找不到私聊对象${payload.user_id}`)
} }
// peer.name = tempUser.nickName // peer.name = tempUser.nickName
isTempMsg = true;
peer.peerUid = tempUserUid; peer.peerUid = tempUserUid;
} }
} }
if (payload?.group_id && payload.message_type === "group") {
await genGroupPeer()
} else if (payload?.user_id) {
genFriendPeer()
} else if (payload.group_id) {
await genGroupPeer()
} else {
throw ("发送消息参数错误, 请指定group_id或user_id")
}
const messages = this.convertMessage2List(payload.message); const messages = this.convertMessage2List(payload.message);
if (this.forwardMsgNum(payload)) { if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) {
try { try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group) const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return {message_id: returnMsg.msgShortId} return {message_id: returnMsg.msgShortId}
} catch (e) { } catch (e) {
throw ("发送转发消息失败 " + e.toString()) throw ("发送转发消息失败 " + e.toString())
} }
} else {
if (this.getSpecialMsgNum(payload, OB11MessageDataType.music)) {
const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic
if (music) {
const {url, audio, title, content, image} = music.data;
const selfPeer: Peer = {peerUid: selfInfo.uid, chatType: ChatType.friend}
// 搞不定!
// const musicMsg = await this.send(selfPeer, [this.genMusicElement(url, audio, title, content, image)], [], false)
// 转发
// const res = await NTQQApi.forwardMsg(selfPeer, peer, [musicMsg.msgId])
// log("转发音乐消息成功", res);
// return {message_id: musicMsg.msgShortId}
}
}
} }
// log("send msg:", peer, sendElements) // log("send msg:", peer, sendElements)
const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group) const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group)
try { const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles)
const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles) deleteAfterSentFiles.map(f => fs.unlink(f, () => {
return {message_id: returnMsg.msgShortId} }));
} catch (e) { return {message_id: returnMsg.msgShortId}
throw (e.toString())
}
} }
protected convertMessage2List(message: OB11MessageMixType) { protected convertMessage2List(message: OB11MessageMixType) {
@@ -138,29 +186,65 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
return message; return message;
} }
private forwardMsgNum(payload: OB11PostSendMsg): number { private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
if (Array.isArray(payload.message)) { if (Array.isArray(payload.message)) {
return payload.message.filter(msg => msg.type == OB11MessageDataType.node).length return payload.message.filter(msg => msg.type == msgType).length
} }
return 0 return 0
} }
private async cloneMsg(msg: RawMessage): Promise<RawMessage> {
log("克隆的目标消息", msg)
let sendElements: SendMessageElement[] = [];
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement)
// Object.keys(ele).forEach((eleKey) => {
// if (eleKey.endsWith("Element")) {
// }
}
if (sendElements.length === 0) {
log("需要clone的消息无法解析将会忽略掉", msg)
}
log("克隆消息", sendElements)
try {
const nodeMsg = await NTQQApi.sendMsg({
chatType: ChatType.friend,
peerUid: selfInfo.uid
}, sendElements, true);
await sleep(500);
return nodeMsg
} catch (e) {
log(e, "克隆转发消息失败,将忽略本条消息", msg);
}
}
// 返回一个合并转发的消息id // 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) { private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) {
const selfPeer: Peer = {
const selfPeer = {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: selfInfo.uid peerUid: selfInfo.uid
} }
let selfNodeMsgList: RawMessage[] = []; let nodeMsgIds: string[] = []
let originalNodeMsgList: RawMessage[] = []; // 先判断一遍是不是id和自定义混用
let needClone = messageNodes.filter(node => node.data.id).length && messageNodes.filter(node => !node.data.id).length
for (const messageNode of messageNodes) { for (const messageNode of messageNodes) {
// 一个node表示一个人的消息 // 一个node表示一个人的消息
let nodeId = messageNode.data.id; let nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片 // 有nodeId表示一个子转发消息卡片
if (nodeId) { if (nodeId) {
let nodeMsg = getHistoryMsgByShortId(nodeId); let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId));
if (nodeMsg) { if (!needClone) {
originalNodeMsgList.push(nodeMsg); nodeMsgIds.push(nodeMsg.msgId)
} else {
if (nodeMsg.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg)
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId)
}
}
} }
} else { } else {
// 自定义的消息 // 自定义的消息
@@ -171,58 +255,82 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
deleteAfterSentFiles deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group); } = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group);
log("开始生成转发节点", sendElements); log("开始生成转发节点", sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true); let sendElementsSplit: SendMessageElement[][] = []
selfNodeMsgList.push(nodeMsg); let splitIndex = 0;
log("转发节点生成成功", nodeMsg.msgId); for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = []
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++;
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++;
} else {
sendElementsSplit[splitIndex].push(ele)
}
log(sendElementsSplit)
}
// log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await this.send(selfPeer, eles, [], true);
nodeMsgIds.push(nodeMsg.msgId)
await sleep(500);
log("转发节点生成成功", nodeMsg.msgId);
}
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}));
} catch (e) { } catch (e) {
log("生转发消息节点失败", e) log("生转发消息节点失败", e)
} }
} }
} }
let nodeIds: string[] = [] // 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
// 检查是否需要克隆直接引用消息id的节点 let nodeMsgArray: Array<RawMessage> = []
let srcPeer: Peer = null;
let needSendSelf = false; let needSendSelf = false;
if (selfNodeMsgList.length) { for (const [index, msgId] of nodeMsgIds.entries()) {
needSendSelf = true const nodeMsg = await dbUtil.getMsgByLongId(msgId)
} else { if (nodeMsg) {
needSendSelf = !originalNodeMsgList.every((msg, index) => msg.peerUid === originalNodeMsgList[0].peerUid && msg.recallTime.length < 2) nodeMsgArray.push(nodeMsg)
if (!srcPeer) {
srcPeer = {chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid}
} else if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
srcPeer = selfPeer
}
}
} }
log("nodeMsgArray", nodeMsgArray);
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
if (needSendSelf) { if (needSendSelf) {
nodeIds = selfNodeMsgList.map(msg => msg.msgId); log("需要克隆转发消息");
for (const originalNodeMsg of originalNodeMsgList) { for (const [index, msg] of nodeMsgArray.entries()) {
if (originalNodeMsg.peerUid === selfInfo.uid && originalNodeMsg.recallTime.length < 2) { if (msg.peerUid !== selfInfo.uid) {
nodeIds.push(originalNodeMsg.msgId) const cloneMsg = await this.cloneMsg(msg)
} else { // 需要进行克隆 if (cloneMsg) {
let sendElements: SendMessageElement[] = [] nodeMsgIds[index] = cloneMsg.msgId
Object.keys(originalNodeMsg.elements).forEach((eleKey) => {
if (eleKey !== "elementId") {
sendElements.push(originalNodeMsg.elements[eleKey])
}
})
try {
const nodeMsg = await NTQQApi.sendMsg(selfPeer, sendElements, true);
nodeIds.push(nodeMsg.msgId)
log("克隆转发消息成功")
} catch (e) {
log("克隆转发消息失败", e)
} }
} }
} }
} else {
nodeIds = originalNodeMsgList.map(msg => msg.msgId)
}
let srcPeer = selfPeer;
if (!needSendSelf) {
srcPeer = {
chatType: originalNodeMsgList[0].chatType === ChatType.group ? ChatType.group : ChatType.friend,
peerUid: originalNodeMsgList[0].peerUid
}
} }
// elements之间用换行符分隔
// let _sendForwardElements: SendMessageElement[] = []
// for(let i = 0; i < sendForwardElements.length; i++){
// _sendForwardElements.push(sendForwardElements[i])
// _sendForwardElements.push(SendMsgElementConstructor.text("\n\n"))
// }
// const nodeMsg = await NTQQApi.sendMsg(selfPeer, _sendForwardElements, true);
// nodeIds.push(nodeMsg.msgId)
// await sleep(500);
// 开发转发 // 开发转发
try { try {
return await NTQQApi.multiForwardMsg(srcPeer, destPeer, nodeIds) log("开发转发", nodeMsgIds)
return await NTQQApi.multiForwardMsg(srcPeer, destPeer, nodeMsgIds)
} catch (e) { } catch (e) {
log("forward failed", e) log("forward failed", e)
return null; return null;
@@ -245,6 +353,9 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
break; break;
case OB11MessageDataType.at: { case OB11MessageDataType.at: {
if (!group) {
continue
}
let atQQ = sendMsg.data?.qq; let atQQ = sendMsg.data?.qq;
if (atQQ) { if (atQQ) {
atQQ = atQQ.toString() atQQ = atQQ.toString()
@@ -263,8 +374,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
case OB11MessageDataType.reply: { case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id; let replyMsgId = sendMsg.data.id;
if (replyMsgId) { if (replyMsgId) {
replyMsgId = replyMsgId.toString() const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId))
const replyMsg = getHistoryMsgByShortId(replyMsgId)
if (replyMsg) { if (replyMsg) {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin)) sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin))
} }
@@ -278,13 +388,30 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
break; break;
case OB11MessageDataType.image: case OB11MessageDataType.image:
case OB11MessageDataType.file: case OB11MessageDataType.file:
case OB11MessageDataType.video: case OB11MessageDataType.video:
case OB11MessageDataType.voice: { case OB11MessageDataType.voice: {
const file = sendMsg.data?.file let file = sendMsg.data?.file
const payloadFileName = sendMsg.data?.name
if (file) { if (file) {
const {path, isLocal} = (await uri2local(file)) const cache = await dbUtil.getFileCache(file)
if (cache) {
if (fs.existsSync(cache.filePath)) {
file = "file://" + cache.filePath
} else if (cache.downloadFunc) {
await cache.downloadFunc()
file = cache.filePath;
} else if (cache.url) {
file = cache.url
}
log("找到文件缓存", file);
}
const {path, isLocal, fileName, errMsg} = (await uri2local(file))
if (errMsg) {
throw errMsg
}
if (path) { if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件 if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path) deleteAfterSentFiles.push(path)
@@ -295,7 +422,15 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
[OB11MessageDataType.video]: SendMsgElementConstructor.video, [OB11MessageDataType.video]: SendMsgElementConstructor.video,
[OB11MessageDataType.file]: SendMsgElementConstructor.file, [OB11MessageDataType.file]: SendMsgElementConstructor.file,
} }
sendElements.push(await constructorMap[sendMsg.type](path)); if (sendMsg.type === OB11MessageDataType.file) {
log("发送文件", path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName));
} else if (sendMsg.type === OB11MessageDataType.video) {
log("发送视频", path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName));
} else {
sendElements.push(await constructorMap[sendMsg.type](path));
}
} }
} }
} }
@@ -310,16 +445,53 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = false) { private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
if (!sendElements.length) { if (!sendElements.length) {
throw ("消息体无法解析") throw ("消息体无法解析")
} }
const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete, 20000); const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete, 20000);
addHistoryMsg(returnMsg) log("消息发送结果", returnMsg)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => { deleteAfterSentFiles.map(f => fs.unlink(f, () => {
})) }))
return returnMsg return returnMsg
} }
private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement {
const musicJson = {
app: 'com.tencent.structmsg',
config: {
ctime: 1709689928,
forward: 1,
token: '5c1e4905f926dd3a64a4bd3841460351',
type: 'normal'
},
extra: {app_type: 1, appid: 100497308, uin: selfInfo.uin},
meta: {
news: {
action: '',
android_pkg_name: '',
app_type: 1,
appid: 100497308,
ctime: 1709689928,
desc: content || title,
jumpUrl: url,
musicUrl: audio,
preview: image,
source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
source_url: '',
tag: 'QQ音乐',
title: title,
uin: selfInfo.uin,
}
},
prompt: content || title,
ver: '0.0.0.1',
view: 'news'
}
return SendMsgElementConstructor.ark(musicJson)
}
} }
export default SendMsg export default SendMsg

View File

@@ -1,8 +1,14 @@
import SendMsg from "./SendMsg"; import SendMsg from "./SendMsg";
import {ActionName} from "./types"; import {ActionName, BaseCheckResult} from "./types";
import {OB11PostSendMsg} from "../types";
class SendPrivateMsg extends SendMsg { class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg actionName = ActionName.SendPrivateMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
payload.message_type = "private"
return super.check(payload);
}
} }
export default SendPrivateMsg export default SendPrivateMsg

View File

@@ -1,6 +1,5 @@
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import {groupNotifies} from "../../common/data"; import {GroupRequestOperateTypes} from "../../ntqqapi/types";
import {GroupNotify, GroupRequestOperateTypes} from "../../ntqqapi/types";
import {NTQQApi} from "../../ntqqapi/ntcall"; import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types"; import {ActionName} from "./types";
@@ -17,15 +16,10 @@ export default class SetGroupAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString(); const seq = payload.flag.toString();
const notify: GroupNotify = groupNotifies[seq] await NTQQApi.handleGroupRequest(seq,
try { payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
await NTQQApi.handleGroupRequest(seq, payload.reason
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, )
payload.reason
)
} catch (e) {
throw e
}
return null return null
} }
} }

View File

@@ -0,0 +1,37 @@
import BaseAction from "../BaseAction";
import {getGroup} from "../../../common/data";
import {ActionName} from "../types";
import {SendMsgElementConstructor} from "../../../ntqqapi/constructor";
import {ChatType, SendFileElement} from "../../../ntqqapi/types";
import {NTQQApi} from "../../../ntqqapi/ntcall";
import {uri2local} from "../../utils";
import fs from "fs";
interface Payload{
group_id: number
file: string
name: string
folder: string
}
export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id.toString());
if (!group){
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file;
if (fs.existsSync(file)){
file = `file://${file}`
}
const downloadResult = await uri2local(file);
if (downloadResult.errMsg){
throw new Error(downloadResult.errMsg)
}
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
await NTQQApi.sendMsg({chatType: ChatType.group, peerUid: group.groupCode}, [sendFileEle]);
return null
}
}

View File

@@ -31,9 +31,19 @@ import SetGroupCard from "./SetGroupCard";
import GetImage from "./GetImage"; import GetImage from "./GetImage";
import GetRecord from "./GetRecord"; import GetRecord from "./GetRecord";
import GoCQHTTPMarkMsgAsRead from "./MarkMsgAsRead"; import GoCQHTTPMarkMsgAsRead from "./MarkMsgAsRead";
import CleanCache from "./CleanCache";
import GoCQHTTPUploadGroupFile from "./go-cqhttp/UploadGroupFile";
import {GetConfigAction, SetConfigAction} from "./llonebot/Config";
import GetGroupAddRequest from "./llonebot/GetGroupAddRequest";
import SetQQAvatar from './llonebot/SetQQAvatar'
export const actionHandlers = [ export const actionHandlers = [
new Debug(), new Debug(),
new GetConfigAction(),
new SetConfigAction(),
new GetGroupAddRequest(),
new SetQQAvatar(),
// onebot11
new SendLike(), new SendLike(),
new GetMsg(), new GetMsg(),
new GetLoginInfo(), new GetLoginInfo(),
@@ -56,6 +66,7 @@ export const actionHandlers = [
new SetGroupCard(), new SetGroupCard(),
new GetImage(), new GetImage(),
new GetRecord(), new GetRecord(),
new CleanCache(),
//以下为go-cqhttp api //以下为go-cqhttp api
new GoCQHTTPSendGroupForwardMsg(), new GoCQHTTPSendGroupForwardMsg(),
@@ -63,6 +74,7 @@ export const actionHandlers = [
new GoCQHTTPGetStrangerInfo(), new GoCQHTTPGetStrangerInfo(),
new GetGuildList(), new GetGuildList(),
new GoCQHTTPMarkMsgAsRead(), new GoCQHTTPMarkMsgAsRead(),
new GoCQHTTPUploadGroupFile(),
] ]

View File

@@ -0,0 +1,20 @@
import BaseAction from "../BaseAction";
import {Config} from "../../../common/types";
import {getConfigUtil} from "../../../common/utils";
import {ActionName} from "../types";
import {setConfig} from "../../../main/setConfig";
export class GetConfigAction extends BaseAction<null, Config> {
actionName = ActionName.GetConfig
protected async _handle(payload: null): Promise<Config> {
return getConfigUtil().getConfig()
}
}
export class SetConfigAction extends BaseAction<Config, void> {
actionName = ActionName.SetConfig
protected async _handle(payload: Config): Promise<void> {
setConfig(payload).then();
}
}

View File

@@ -0,0 +1,32 @@
import {GroupNotify, GroupNotifyStatus} from "../../../ntqqapi/types";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQApi} from "../../../ntqqapi/ntcall";
import {uidMaps} from "../../../common/data";
import {log} from "../../../common/utils";
interface OB11GroupRequestNotify {
group_id: number,
user_id: number,
flag: string
}
export default class GetGroupAddRequest extends BaseAction<null, OB11GroupRequestNotify[]> {
actionName = ActionName.GetGroupIgnoreAddRequest
protected async _handle(payload: null): Promise<OB11GroupRequestNotify[]> {
const data = await NTQQApi.getGroupIgnoreNotifies()
log(data);
let notifies: GroupNotify[] = data.notifies.filter(notify => notify.status === GroupNotifyStatus.WAIT_HANDLE);
let returnData: OB11GroupRequestNotify[] = []
for (const notify of notifies) {
const uin = uidMaps[notify.user1.uid] || (await NTQQApi.getUserDetailInfo(notify.user1.uid))?.uin
returnData.push({
group_id: parseInt(notify.group.groupCode),
user_id: parseInt(uin),
flag: notify.seq
})
}
return returnData;
}
}

View File

@@ -0,0 +1,44 @@
import BaseAction from "../BaseAction";
import {NTQQApi} from "../../../ntqqapi/ntcall";
import {ActionName} from "../types";
import { uri2local } from "../../utils";
import * as fs from "node:fs";
import { checkFileReceived } from "../../../common/utils";
// import { log } from "../../../common/utils";
interface Payload {
file: string
}
export default class SetAvatar extends BaseAction<Payload, null> {
actionName = ActionName.SetQQAvatar
protected async _handle(payload: Payload): Promise<null> {
const {path, isLocal, errMsg} = (await uri2local(payload.file))
if (errMsg){
throw `头像${payload.file}设置失败,file字段可能格式不正确`
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQApi.setQQAvatar(path)
if (!isLocal){
fs.unlink(path, () => {})
}
if (!ret) {
throw `头像${payload.file}设置失败,api无返回`
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
if (ret['result'] == 1004022) {
throw `头像${payload.file}设置失败,文件可能不是图片格式`
} else if(ret['result'] != 0) {
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`
}
} else {
if (!isLocal){
fs.unlink(path, () => {})
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`
}
return null
}
}

View File

@@ -14,6 +14,10 @@ export interface InvalidCheckResult {
} }
export enum ActionName { export enum ActionName {
GetGroupIgnoreAddRequest = "get_group_ignore_add_request",
SetQQAvatar = "set_qq_avatar",
GetConfig = "get_config",
SetConfig = "set_config",
Debug = "llonebot_debug", Debug = "llonebot_debug",
SendLike = "send_like", SendLike = "send_like",
GetLoginInfo = "get_login_info", GetLoginInfo = "get_login_info",
@@ -42,10 +46,12 @@ export enum ActionName {
SetGroupName = "set_group_name", SetGroupName = "set_group_name",
GetImage = "get_image", GetImage = "get_image",
GetRecord = "get_record", GetRecord = "get_record",
CleanCache = "clean_cache",
// 以下为go-cqhttp api // 以下为go-cqhttp api
GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg", GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg",
GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg", GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg",
GoCQHTTP_GetStrangerInfo = "get_stranger_info", GoCQHTTP_GetStrangerInfo = "get_stranger_info",
GetGuildList = "get_guild_list", GetGuildList = "get_guild_list",
GoCQHTTP_MarkMsgAsRead = "mark_msg_as_read", GoCQHTTP_MarkMsgAsRead = "mark_msg_as_read",
GoCQHTTP_UploadGroupFile = "upload_group_file",
} }

View File

@@ -8,12 +8,27 @@ import {
OB11User, OB11User,
OB11UserSex OB11UserSex
} from "./types"; } from "./types";
import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types'; import {
import {fileCache, getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo, tempGroupCodeMap} from '../common/data'; AtType,
import {getConfigUtil, log} from "../common/utils"; ChatType,
Group,
GroupMember,
IMAGE_HTTP_HOST,
RawMessage,
SelfInfo,
TipGroupElementType,
User
} from '../ntqqapi/types';
import {getFriend, getGroup, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data';
import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApi} from "../ntqqapi/ntcall"; import {NTQQApi} from "../ntqqapi/ntcall";
import {EventType} from "./event/OB11BaseEvent"; import {EventType} from "./event/OB11BaseEvent";
import {encodeCQCode} from "./cqcode"; import {encodeCQCode} from "./cqcode";
import {dbUtil} from "../common/db";
import {OB11GroupIncreaseEvent} from "./event/notice/OB11GroupIncreaseEvent";
import {OB11GroupBanEvent} from "./event/notice/OB11GroupBanEvent";
import {OB11GroupUploadNoticeEvent} from "./event/notice/OB11GroupUploadNoticeEvent";
import {OB11GroupNoticeEvent} from "./event/notice/OB11GroupNoticeEvent";
export class OB11Constructor { export class OB11Constructor {
@@ -26,7 +41,7 @@ export class OB11Constructor {
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin),
time: parseInt(msg.msgTime) || Date.now(), time: parseInt(msg.msgTime) || Date.now(),
message_id: msg.msgShortId, message_id: msg.msgShortId,
real_id: msg.msgId, real_id: msg.msgShortId,
message_type: msg.chatType == ChatType.group ? "group" : "private", message_type: msg.chatType == ChatType.group ? "group" : "private",
sender: { sender: {
user_id: parseInt(msg.senderUin), user_id: parseInt(msg.senderUin),
@@ -76,7 +91,7 @@ export class OB11Constructor {
let atUid = element.textElement.atNtUid let atUid = element.textElement.atNtUid
let atQQ = element.textElement.atUid let atQQ = element.textElement.atUid
if (!atQQ || atQQ === "0") { if (!atQQ || atQQ === "0") {
const atMember = await getGroupMember(msg.peerUin, null, atUid) const atMember = await getGroupMember(msg.peerUin, atUid)
if (atMember) { if (atMember) {
atQQ = atMember.uin atQQ = atMember.uin
} }
@@ -95,30 +110,43 @@ export class OB11Constructor {
message_data["data"]["text"] = text message_data["data"]["text"] = text
} else if (element.replyElement) { } else if (element.replyElement) {
message_data["type"] = "reply" message_data["type"] = "reply"
const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq) // log("收到回复消息", element.replyElement.replayMsgSeq)
if (replyMsg) { try {
message_data["data"]["id"] = replyMsg.msgShortId.toString() const replyMsg = await dbUtil.getMsgBySeqId(element.replyElement.replayMsgSeq)
} else { // log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId)
continue if (replyMsg) {
message_data["data"]["id"] = replyMsg.msgShortId.toString()
} else {
continue
}
} catch (e) {
log("获取不到引用的消息", e.stack, element.replyElement.replayMsgSeq)
} }
} else if (element.picElement) { } else if (element.picElement) {
message_data["type"] = "image" message_data["type"] = "image"
// message_data["data"]["file"] = element.picElement.sourcePath // message_data["data"]["file"] = element.picElement.sourcePath
message_data["data"]["file"] = element.picElement.fileName message_data["data"]["file"] = element.picElement.fileName
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
message_data["data"]["url"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl const url = element.picElement.originImageUrl
const fileMd5 = element.picElement.md5HexStr
if (url) {
message_data["data"]["url"] = IMAGE_HTTP_HOST + url
} else if (fileMd5 && element.picElement.fileUuid.indexOf("_") === -1) { // fileuuid有下划线的是Linux发送的这个url是另外的格式目前尚未得知如何组装
message_data["data"]["url"] = `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${fileMd5.toUpperCase()}/0`
}
// message_data["data"]["file_id"] = element.picElement.fileUuid // message_data["data"]["file_id"] = element.picElement.fileUuid
message_data["data"]["file_size"] = element.picElement.fileSize message_data["data"]["file_size"] = element.picElement.fileSize
fileCache.set(element.picElement.fileName, { dbUtil.addFileCache(element.picElement.fileName, {
fileName: element.picElement.fileName, fileName: element.picElement.fileName,
filePath: element.picElement.sourcePath, filePath: element.picElement.sourcePath,
fileSize: element.picElement.fileSize.toString(), fileSize: element.picElement.fileSize.toString(),
url: IMAGE_HTTP_HOST + element.picElement.originImageUrl, url: message_data["data"]["url"],
downloadFunc: async () => { downloadFunc: async () => {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.picElement.thumbPath.get(0), element.picElement.sourcePath) element.elementId, element.picElement.thumbPath?.get(0) || "", element.picElement.sourcePath)
} }
}) }).then()
// 不在自动下载图片 // 不在自动下载图片
} else if (element.videoElement) { } else if (element.videoElement) {
@@ -127,7 +155,7 @@ export class OB11Constructor {
message_data["data"]["path"] = element.videoElement.filePath message_data["data"]["path"] = element.videoElement.filePath
// message_data["data"]["file_id"] = element.videoElement.fileUuid // message_data["data"]["file_id"] = element.videoElement.fileUuid
message_data["data"]["file_size"] = element.videoElement.fileSize message_data["data"]["file_size"] = element.videoElement.fileSize
fileCache.set(element.videoElement.fileName, { dbUtil.addFileCache(element.videoElement.fileName, {
fileName: element.videoElement.fileName, fileName: element.videoElement.fileName,
filePath: element.videoElement.filePath, filePath: element.videoElement.filePath,
fileSize: element.videoElement.fileSize, fileSize: element.videoElement.fileSize,
@@ -135,7 +163,7 @@ export class OB11Constructor {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.videoElement.thumbPath.get(0), element.videoElement.filePath) element.elementId, element.videoElement.thumbPath.get(0), element.videoElement.filePath)
} }
}) }).then()
// 怎么拿到url呢 // 怎么拿到url呢
} else if (element.fileElement) { } else if (element.fileElement) {
message_data["type"] = OB11MessageDataType.file; message_data["type"] = OB11MessageDataType.file;
@@ -143,7 +171,7 @@ export class OB11Constructor {
// message_data["data"]["path"] = element.fileElement.filePath // message_data["data"]["path"] = element.fileElement.filePath
// message_data["data"]["file_id"] = element.fileElement.fileUuid // message_data["data"]["file_id"] = element.fileElement.fileUuid
message_data["data"]["file_size"] = element.fileElement.fileSize message_data["data"]["file_size"] = element.fileElement.fileSize
fileCache.set(element.fileElement.fileName, { dbUtil.addFileCache(element.fileElement.fileName, {
fileName: element.fileElement.fileName, fileName: element.fileElement.fileName,
filePath: element.fileElement.filePath, filePath: element.fileElement.filePath,
fileSize: element.fileElement.fileSize, fileSize: element.fileElement.fileSize,
@@ -151,7 +179,7 @@ export class OB11Constructor {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, null, element.fileElement.filePath) element.elementId, null, element.fileElement.filePath)
} }
}) }).then()
// 怎么拿到url呢 // 怎么拿到url呢
} else if (element.pttElement) { } else if (element.pttElement) {
message_data["type"] = OB11MessageDataType.voice; message_data["type"] = OB11MessageDataType.voice;
@@ -159,11 +187,11 @@ export class OB11Constructor {
message_data["data"]["path"] = element.pttElement.filePath message_data["data"]["path"] = element.pttElement.filePath
// message_data["data"]["file_id"] = element.pttElement.fileUuid // message_data["data"]["file_id"] = element.pttElement.fileUuid
message_data["data"]["file_size"] = element.pttElement.fileSize message_data["data"]["file_size"] = element.pttElement.fileSize
fileCache.set(element.pttElement.fileName, { dbUtil.addFileCache(element.pttElement.fileName, {
fileName: element.pttElement.fileName, fileName: element.pttElement.fileName,
filePath: element.pttElement.filePath, filePath: element.pttElement.filePath,
fileSize: element.pttElement.fileSize, fileSize: element.pttElement.fileSize,
}) }).then()
// log("收到语音消息", msg) // log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { // window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
@@ -178,7 +206,10 @@ export class OB11Constructor {
message_data["type"] = OB11MessageDataType.face; message_data["type"] = OB11MessageDataType.face;
message_data["data"]["id"] = element.faceElement.faceIndex.toString(); message_data["data"]["id"] = element.faceElement.faceIndex.toString();
} }
// todo: 解析入群grayTipElement
else if (element.grayTipElement?.aioOpGrayTipElement) {
log("收到 group gray tip 消息", element.grayTipElement.aioOpGrayTipElement)
}
// if (message_data.data.file) { // if (message_data.data.file) {
// let filePath: string = message_data.data.file; // let filePath: string = message_data.data.file;
// if (!enableLocalFile2Url) { // if (!enableLocalFile2Url) {
@@ -215,6 +246,60 @@ export class OB11Constructor {
return resMsg; return resMsg;
} }
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent> {
if (msg.chatType !== ChatType.group) {
return;
}
for (let element of msg.elements) {
const groupElement = element.grayTipElement?.groupElement
if (groupElement) {
// log("收到群提示消息", groupElement)
if (groupElement.type == TipGroupElementType.memberIncrease) {
log("收到群成员增加消息", groupElement)
await sleep(1000);
const member = await getGroupMember(msg.peerUid, groupElement.memberUid);
let memberUin = member?.uin;
if (!memberUin) {
memberUin = (await NTQQApi.getUserDetailInfo(groupElement.memberUid)).uin
}
// log("获取新群成员QQ", memberUin)
const adminMember = await getGroupMember(msg.peerUid, groupElement.adminUid);
// log("获取同意新成员入群的管理员", adminMember)
if (memberUin) {
const operatorUin = adminMember?.uin || memberUin
let event = new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(operatorUin));
// log("构造群增加事件", event)
return event;
}
}
else if (groupElement.type === TipGroupElementType.ban) {
log("收到群群员禁言提示", groupElement)
const memberUid = groupElement.shutUp.member.uid
const adminUid = groupElement.shutUp.admin.uid
let memberUin: string = ""
let duration = parseInt(groupElement.shutUp.duration)
let sub_type: "ban" | "lift_ban" = duration > 0 ? "ban" : "lift_ban"
if (memberUid){
memberUin = (await getGroupMember(msg.peerUid, memberUid))?.uin || (await NTQQApi.getUserDetailInfo(memberUid))?.uin
}
else {
memberUin = "0"; // 0表示全员禁言
if (duration > 0) {
duration = -1
}
}
const adminUin = (await getGroupMember(msg.peerUid, adminUid))?.uin || (await NTQQApi.getUserDetailInfo(adminUid))?.uin
if (memberUin && adminUin) {
return new OB11GroupBanEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, sub_type);
}
}
}
else if (element.fileElement){
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {id: element.fileElement.fileName, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize)})
}
}
}
static friend(friend: User): OB11User { static friend(friend: User): OB11User {
return { return {
user_id: parseInt(friend.uin), user_id: parseInt(friend.uin),

View File

@@ -46,15 +46,22 @@ export function decodeCQCode(source: string): OB11MessageData[] {
export function encodeCQCode(data: OB11MessageData) { export function encodeCQCode(data: OB11MessageData) {
const CQCodeEscape = (text: string) => { const CQCodeEscapeText = (text: string) => {
return text.replace(/\[/g, '&#91;') return text.replace(/\&/g, '&amp;')
.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;')
};
const CQCodeEscape = (text: string) => {
return text.replace(/\&/g, '&amp;')
.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;') .replace(/\]/g, '&#93;')
.replace(/\&/g, '&amp;')
.replace(/,/g, '&#44;'); .replace(/,/g, '&#44;');
}; };
if (data.type === 'text') { if (data.type === 'text') {
return CQCodeEscape(data.data.text); return CQCodeEscapeText(data.data.text);
} }
let result = '[CQ:' + data.type; let result = '[CQ:' + data.type;

View File

@@ -0,0 +1,17 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupBanEvent extends OB11GroupNoticeEvent {
notice_type = "group_ban";
operator_id: number;
duration: number;
sub_type: "ban" | "lift_ban";
constructor(groupId: number, userId: number, operatorId: number, duration: number, sub_type: "ban" | "lift_ban") {
super();
this.group_id = groupId;
this.operator_id = operatorId;
this.user_id = userId;
this.duration = duration;
this.sub_type = sub_type;
}
}

View File

@@ -3,12 +3,12 @@ import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent { export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
notice_type = "group_increase"; notice_type = "group_increase";
sub_type = "approve"; // TODO: 实现其他几种子类型的识别 ("approve" | "invite") sub_type = "approve"; // TODO: 实现其他几种子类型的识别 ("approve" | "invite")
operate_id: number; operator_id: number;
constructor(groupId: number, userId: number) { constructor(groupId: number, userId: number, operatorId: number) {
super(); super();
this.group_id = groupId; this.group_id = groupId;
this.operate_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被邀请的,还是主动加入的 this.operator_id = operatorId;
this.user_id = userId; this.user_id = userId;
} }
} }

View File

@@ -0,0 +1,19 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export interface GroupUploadFile{
id: string,
name: string,
size: number
}
export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent {
notice_type = "group_upload"
file: GroupUploadFile
constructor(groupId: number, userId: number, file: GroupUploadFile) {
super();
this.group_id = groupId;
this.user_id = userId;
this.file = file
}
}

View File

@@ -20,8 +20,10 @@ class OB11HTTPServer extends HttpServerBase {
export const ob11HTTPServer = new OB11HTTPServer(); export const ob11HTTPServer = new OB11HTTPServer();
for (const action of actionHandlers) { setTimeout(() => {
for (const method of ["post", "get"]) { for (const action of actionHandlers) {
ob11HTTPServer.registerRouter(method, action.actionName, (res, payload) => action.handle(payload)) for (const method of ["post", "get"]) {
ob11HTTPServer.registerRouter(method, action.actionName, (res, payload) => action.handle(payload))
}
} }
} }, 0)

View File

@@ -28,7 +28,7 @@ class OB11WebsocketServer extends WebsocketServerBase {
let handleResult = await action.websocketHandle(params, echo); let handleResult = await action.websocketHandle(params, echo);
wsReply(wsClient, handleResult) wsReply(wsClient, handleResult)
} catch (e) { } catch (e) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e}`, 1200, echo)) wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo))
} }
} }

View File

@@ -67,7 +67,7 @@ export interface OB11Message {
self_id?: number, self_id?: number,
time: number, time: number,
message_id: number, message_id: number,
real_id: string, real_id: number,
user_id: number, user_id: number,
group_id?: number, group_id?: number,
message_type: "private" | "group", message_type: "private" | "group",
@@ -93,6 +93,7 @@ export interface OB11Return<DataType> {
export enum OB11MessageDataType { export enum OB11MessageDataType {
text = "text", text = "text",
image = "image", image = "image",
music = "music",
video = "video", video = "video",
voice = "record", voice = "record",
file = "file", file = "file",
@@ -100,7 +101,7 @@ export enum OB11MessageDataType {
reply = "reply", reply = "reply",
json = "json", json = "json",
face = "face", face = "face",
node = "node" // 合并转发消息 node = "node", // 合并转发消息
} }
export interface OB11MessageText { export interface OB11MessageText {
@@ -112,6 +113,7 @@ export interface OB11MessageText {
interface OB11MessageFileBase { interface OB11MessageFileBase {
data: { data: {
name?: string;
file: string, file: string,
url?: string; url?: string;
} }
@@ -166,12 +168,24 @@ export interface OB11MessageNode {
} }
} }
export interface OB11MessageCustomMusic{
type: OB11MessageDataType.music
data: {
type: "custom"
url: string,
audio: string,
title: string,
content?: string,
image?: string
}
}
export type OB11MessageData = export type OB11MessageData =
OB11MessageText | OB11MessageText |
OB11MessageFace | OB11MessageFace |
OB11MessageAt | OB11MessageReply | OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo | OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
OB11MessageNode OB11MessageNode | OB11MessageCustomMusic
export interface OB11PostSendMsg { export interface OB11PostSendMsg {
message_type?: "private" | "group" message_type?: "private" | "group"

View File

@@ -1,23 +1,42 @@
import {CONFIG_DIR, isGIF, log} from "../common/utils"; import {DATA_DIR, isGIF, log} from "../common/utils";
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import * as path from 'node:path'; import * as path from 'node:path';
import {fileCache} from "../common/data";
import * as fileType from 'file-type'; import * as fileType from 'file-type';
import {dbUtil} from "../common/db";
const fs = require("fs").promises; const fs = require("fs").promises;
export async function uri2local(uri: string, fileName: string = null) { type Uri2LocalRes = {
if (!fileName) { success: boolean,
fileName = uuidv4(); errMsg: string,
} fileName: string,
let filePath = path.join(CONFIG_DIR, fileName) ext: string,
let url = new URL(uri); path: string,
isLocal: boolean
}
export async function uri2local(uri: string, fileName: string = null) : Promise<Uri2LocalRes>{
let res = { let res = {
success: false, success: false,
errMsg: "", errMsg: "",
fileName: "",
ext: "",
path: "", path: "",
isLocal: false isLocal: false
} }
if (!fileName) {
fileName = uuidv4();
}
let filePath = path.join(DATA_DIR, fileName)
let url = null;
try{
url = new URL(uri);
}catch (e) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
}
// log("uri protocol", url.protocol, uri);
if (url.protocol == "base64:") { if (url.protocol == "base64:") {
// base64转成文件 // base64转成文件
let base64Data = uri.split("base64://")[1] let base64Data = uri.split("base64://")[1]
@@ -31,7 +50,13 @@ export async function uri2local(uri: string, fileName: string = null) {
} }
} else if (url.protocol == "http:" || url.protocol == "https:") { } else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件 // 下载文件
let fetchRes = await fetch(url) let fetchRes: Response;
try{
fetchRes = await fetch(url)
}catch (e) {
res.errMsg = `${url}下载失败`
return res
}
if (!fetchRes.ok) { if (!fetchRes.ok) {
res.errMsg = `${url}下载失败,` + fetchRes.statusText res.errMsg = `${url}下载失败,` + fetchRes.statusText
return res return res
@@ -39,8 +64,16 @@ export async function uri2local(uri: string, fileName: string = null) {
let blob = await fetchRes.blob(); let blob = await fetchRes.blob();
let buffer = await blob.arrayBuffer(); let buffer = await blob.arrayBuffer();
try { try {
fileName = path.parse(url.pathname).name || fileName const pathInfo = path.parse(decodeURIComponent(url.pathname))
filePath = path.join(CONFIG_DIR, uuidv4() + fileName) if (pathInfo.name){
fileName = pathInfo.name
if (pathInfo.ext){
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
res.fileName = fileName
filePath = path.join(DATA_DIR, uuidv4() + fileName)
await fs.writeFile(filePath, Buffer.from(buffer)); await fs.writeFile(filePath, Buffer.from(buffer));
} catch (e: any) { } catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString() res.errMsg = `${url}下载失败,` + e.toString()
@@ -57,7 +90,7 @@ export async function uri2local(uri: string, fileName: string = null) {
filePath = pathname filePath = pathname
} }
} else { } else {
const cache = fileCache.get(uri) const cache = await dbUtil.getFileCache(uri);
if (cache) { if (cache) {
filePath = cache.filePath filePath = cache.filePath
} else { } else {
@@ -75,15 +108,17 @@ export async function uri2local(uri: string, fileName: string = null) {
// await fs.rename(filePath, filePath + ".gif"); // await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif"; // filePath += ".gif";
// } // }
if (!res.isLocal) { if (!res.isLocal && !res.ext) {
try{ try {
const {ext} = await fileType.fileTypeFromFile(filePath) let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
if (ext) { if (ext) {
log("获取文件类型", ext, filePath) log("获取文件类型", ext, filePath)
await fs.rename(filePath, filePath + `.${ext}`) await fs.rename(filePath, filePath + `.${ext}`)
filePath += `.${ext}` filePath += `.${ext}`
} res.fileName += `.${ext}`
}catch (e){ res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack) // log("获取文件类型失败", filePath,e.stack)
} }
} }

View File

@@ -90,7 +90,7 @@ async function onSettingWindowCreated(view: Element) {
], 'ob11.messagePostFormat', config.ob11.messagePostFormat), ], 'ob11.messagePostFormat', config.ob11.messagePostFormat),
), ),
SettingItem( SettingItem(
'ffmpeg 路径', `<span id="config-ffmpeg-path-text">${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}</span>`, 'ffmpeg 路径, 发送语音、视频需要同时保证ffprobe和ffmpeg在一起', `<span id="config-ffmpeg-path-text">${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}</span>`,
SettingButton('选择', 'config-ffmpeg-select'), SettingButton('选择', 'config-ffmpeg-select'),
), ),
SettingItem( SettingItem(
@@ -136,21 +136,47 @@ async function onSettingWindowCreated(view: Element) {
SettingButton('打开', 'config-open-log-path'), SettingButton('打开', 'config-open-log-path'),
), ),
]), ]),
SettingList([
SettingItem(
'GitHub和文档',
`https://github.com/LLOneBot/LLOneBot`,
SettingButton('点个Star', 'open-github'),
),
SettingItem(
'Telegram 群',
`https://t.me/+nLZEnpne-pQ1OWFl`,
SettingButton('进去逛逛', 'open-telegram'),
),
SettingItem(
'QQ 群',
`545402644`,
SettingButton('我要进去', 'open-qq-group'),
),
]),
'</div>', '</div>',
].join(''), "text/html"); ].join(''), "text/html");
// 外链按钮
doc.querySelector('#open-github').addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://github.com/LLOneBot/LLOneBot')
})
doc.querySelector('#open-telegram').addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://t.me/+nLZEnpne-pQ1OWFl')
})
doc.querySelector('#open-qq-group').addEventListener('click', () => {
window.LiteLoader.api.openExternal('https://qm.qq.com/q/bDnHRG38aI')
})
// 生成反向地址列表 // 生成反向地址列表
const buildHostListItem = (type: string, host: string, index: number) => { const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any={}) => {
const dom = { const dom = {
container: document.createElement('setting-item'), container: document.createElement('setting-item'),
input: document.createElement('input'), input: document.createElement('input'),
inputContainer: document.createElement('div'), inputContainer: document.createElement('div'),
deleteBtn: document.createElement('setting-button'), deleteBtn: document.createElement('setting-button'),
}; };
dom.container.classList.add('setting-host-list-item'); dom.container.classList.add('setting-host-list-item');
dom.container.dataset.direction = 'row'; dom.container.dataset.direction = 'row';
Object.assign(dom.input, inputAttrs);
dom.input.classList.add('q-input__inner'); dom.input.classList.add('q-input__inner');
dom.input.type = 'url'; dom.input.type = 'url';
dom.input.value = host; dom.input.value = host;
@@ -173,18 +199,18 @@ async function onSettingWindowCreated(view: Element) {
return dom.container; return dom.container;
}; };
const buildHostList = (hosts: string[], type: string) => { const buildHostList = (hosts: string[], type: string, inputAttr: any={}) => {
const result: HTMLElement[] = []; const result: HTMLElement[] = [];
hosts.forEach((host, index) => { hosts.forEach((host, index) => {
result.push(buildHostListItem(type, host, index)); result.push(buildHostListItem(type, host, index, inputAttr));
}); });
return result; return result;
}; };
const addReverseHost = (type: string, doc: Document = document) => { const addReverseHost = (type: string, doc: Document = document, inputAttr: any={}) => {
const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`); const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`);
hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length)); hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr));
ob11Config[type].push(''); ob11Config[type].push('');
}; };
const initReverseHost = (type: string, doc: Document = document) => { const initReverseHost = (type: string, doc: Document = document) => {
@@ -197,8 +223,8 @@ async function onSettingWindowCreated(view: Element) {
initReverseHost('httpHosts', doc); initReverseHost('httpHosts', doc);
initReverseHost('wsHosts', doc); initReverseHost('wsHosts', doc);
doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts')); doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts', document, {'placeholder': '如http://127.0.0.1:5140/onebot' }));
doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts')); doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts', document, {'placeholder': '如ws://127.0.0.1:5140/onebot' }));
doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => { doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => {
window.llonebot.selectFile() window.llonebot.selectFile()

View File

@@ -1 +1 @@
export const version = "3.11.2" export const version = "3.15.1"