Compare commits

...

59 Commits

Author SHA1 Message Date
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
34 changed files with 1540 additions and 799 deletions

167
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,8 +29,11 @@ wget -O fastboot.sh https://cdn.jsdelivr.net/gh/MliKiowa/llonebot-docker/fastboo
- [x] 群管理功能,禁言、踢人,改群名片等 - [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息 - [x] 视频消息
- [x] 文件消息 - [x] 文件消息
- [ ] 音乐卡片
- [ ] 无头模式 - [ ] 无头模式
- [ ] 群禁言事件上报
- [ ] 优化加群成功事件上报
- [ ] 清理缓存api
- [ ] 框架对接文档
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>

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 }

View File

@@ -1,10 +1,10 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.11.2", "name": "LLOneBot v3.13.10",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi", "description": "LiteLoaderQQNT的OneBotApi",
"version": "3.11.2", "version": "3.13.10",
"thumbnail": "./icon.png", "thumbnail": "./icon.png",
"authors": [ "authors": [
{ {

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

@@ -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",

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 {log} from "./utils";
export const selfInfo: SelfInfo = { export const selfInfo: SelfInfo = {
uid: '', uid: '',
@@ -18,48 +21,41 @@ 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 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(qq: string, uid: string = ""): Promise<Friend | undefined> {
const existMsg = msgHistory[msg.msgId] let filterKey = uid ? "uid" : "uin"
if (existMsg) { let filterValue = uid ? uid : qq
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
} }
@@ -71,22 +67,25 @@ export async function getGroupMember(groupQQ: string | number, memberQQ: string
} }
const group = await getGroup(groupQQ) const group = await getGroup(groupQQ)
if (group) { if (group) {
let filterFunc: (member: GroupMember) => boolean const filterKey = memberQQ ? "uin" : "uid"
if (memberQQ) { const filterValue = memberQQ ? memberQQ : memberUid
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 +95,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) {

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

@@ -0,0 +1,208 @@
import {Level} from "level";
import {RawMessage} from "../ntqqapi/types";
import {DATA_DIR, log} from "./utils";
import {selfInfo} from "./data";
import {FileCache} from "./types";
class DBUtil {
private readonly DB_KEY_PREFIX_MSG_ID = "msg_id_";
private readonly DB_KEY_PREFIX_MSG_SHORT_ID = "msg_short_id_";
private readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_";
private readonly DB_KEY_PREFIX_FILE = "file_";
private db: Level;
public cache: Record<string, RawMessage | string | FileCache> = {} // <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()
setInterval(() => {
this.cache = {}
}, 1000 * 60 * 10)
}
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
}
async getMsgByShortId(shortMsgId: number): Promise<RawMessage> {
const shortMsgIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + shortMsgId;
if (this.cache[shortMsgIdKey]) {
return this.cache[shortMsgIdKey] as RawMessage
}
const longId = await this.db.get(shortMsgIdKey);
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
return msg
}
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
}
const data = await this.db.get(longIdKey)
const msg = JSON.parse(data)
this.addCache(msg)
return msg
}
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
}
const longId = await this.db.get(seqIdKey);
const msg = await this.getMsgByLongId(longId)
this.addCache(msg)
return msg
}
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);
// log("新增消息记录", msg.msgId)
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;
try {
this.db.put(shortIdKey, msg.msgId).then();
this.db.put(longIdKey, JSON.stringify(msg)).then();
try {
await this.db.get(seqIdKey)
} catch (e) {
// log("新的seqId", seqIdKey)
this.db.put(seqIdKey, msg.msgId).then();
}
if (!this.cache[seqIdKey]) {
this.cache[seqIdKey] = msg;
}
// log(`消息入库 ${seqIdKey}: ${msg.msgId}, ${shortMsgId}: ${msg.msgId}`);
} catch (e) {
// log("addMsg db error", e.stack.toString());
}
return shortMsgId
}
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();
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();
try {
await this.db.get(seqIdKey)
} catch (e) {
this.db.put(seqIdKey, msg.msgId).then();
// 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++;
await this.db.put(key, this.currentShortId.toString());
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;
await this.db.put(key, JSON.stringify(cacheDBData));
}
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) {
}
}
}
export const dbUtil = new DBUtil();

View File

@@ -7,10 +7,10 @@ import fs from 'fs';
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)
} }
@@ -55,7 +55,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 +192,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);

View File

@@ -11,15 +11,13 @@ 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, getFriend,
friendRequests,
getGroup, getGroup,
getGroupMember, getGroupMember,
groupNotifies, groupNotifies,
llonebotError, llonebotError, refreshGroupMembers,
msgHistory, 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";
@@ -43,6 +41,7 @@ import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecrease
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";
let running = false; let running = false;
@@ -82,8 +81,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;
@@ -153,14 +152,14 @@ function onLoad() {
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) // log("收到新消息", message.msgId, message.msgSeq)
message.msgShortId = msgHistory[message.msgId]?.msgShortId // if (message.senderUin !== selfInfo.uin){
if (!message.msgShortId) { message.msgShortId = await dbUtil.addMsg(message);
addHistoryMsg(message); // }
}
OB11Constructor.message(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
msg.raw = message; msg.raw = message;
@@ -171,27 +170,29 @@ 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()));
} }
} }
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") {
// 撤回消息上报 // 撤回消息上报
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);
@@ -211,21 +212,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,14 +247,15 @@ 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 = groupNotifies[notify.seq];
if (existNotify) { if (existNotify) {
if (Date.now() - existNotify.time < 3000) { if (Date.now() - existNotify.time < 3000) {
@@ -261,14 +264,14 @@ function onLoad() {
} }
log("收到群通知", notify); log("收到群通知", notify);
groupNotifies[notify.seq] = notify; groupNotifies[notify.seq] = 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, null, 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 +284,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 +304,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) {
// 可能有群管理员变动 // 可能有群管理员变动
} }
}) })

View File

@@ -1,6 +1,6 @@
import { import {
AtType, AtType,
ElementType, PicType, ElementType, PicType, SendArkElement,
SendFaceElement, SendFaceElement,
SendFileElement, SendFileElement,
SendPicElement, SendPicElement,
@@ -57,6 +57,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,19 +84,22 @@ export class SendMsgElementConstructor {
}; };
} }
static async file(filePath: string, isVideo: boolean = false): Promise<SendFileElement> { static async file(filePath: string, showPreview: boolean = false, fileName: string = ""): Promise<SendFileElement> {
let picHeight = 0; let picHeight = 0;
let picWidth = 0; let picWidth = 0;
if (isVideo) { if (showPreview) {
picHeight = 1024; picHeight = 1024;
picWidth = 768; picWidth = 768;
} }
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE); const {md5, fileName: _fileName, path, fileSize} = await NTQQApi.uploadFile(filePath, ElementType.FILE);
if (fileSize === 0){
throw "文件异常大小为0";
}
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, picHeight,
@@ -104,14 +110,17 @@ export class SendMsgElementConstructor {
return element; return element;
} }
static video(filePath: string): Promise<SendFileElement> { static video(filePath: string, fileName: string=""): Promise<SendFileElement> {
return SendMsgElementConstructor.file(filePath, true); return SendMsgElementConstructor.file(filePath, true, fileName);
} }
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, () => {
}); });
@@ -150,4 +159,12 @@ export class SendMsgElementConstructor {
} }
} }
} }
static ark(data: any): SendArkElement {
return {
elementType: ElementType.ARK,
elementId: "",
arkElement: data
}
}
} }

View File

@@ -1,71 +1,73 @@
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",
} }
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 +76,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 +115,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 +145,84 @@ 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) { } else if (existGroup.memberCount < group.memberCount) {
const oldMembers = existGroup.members const oldMembers = existGroup.members;
const oldMembersSet = new Set<string>() const oldMembersSet = new Set<string>();
for (const member of oldMembers) { for (const member of oldMembers) {
oldMembersSet.add(member.uin) oldMembersSet.add(member.uin);
} }
await sleep(200) await sleep(200);
const newMembers = await NTQQApi.getGroupMembers(group.groupCode) const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
group.members = newMembers group.members = newMembers;
for (const member of newMembers) { for (const member of newMembers) {
if (!oldMembersSet.has(member.uin)) { if (!oldMembersSet.has(member.uin)) {
postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin))) postOB11Event(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin)));
break 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,11 +232,11 @@ registerReceiveHook<{
} }
}) })
registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
const {autoDeleteFile, autoDeleteFileSecond} = getConfigUtil().getConfig() const {autoDeleteFile} = getConfigUtil().getConfig();
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) { if (!autoDeleteFile) {
continue continue
@@ -254,29 +257,27 @@ registerReceiveHook<{ msgList: RawMessage[] }>(ReceiveCmd.NEW_MSG, (payload) =>
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) }, 60 * 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 {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, groupNotifies, 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,104 @@ 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",
HOTUPDATE_API = "ns-HotUpdateApi",
BUSINESS_API = "ns-BusinessApi",
GLOBAL_DATA = "ns-GlobalDataApi"
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike', LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
SELF_INFO = 'fetchAuthData', SELF_INFO = "fetchAuthData",
FRIENDS = 'nodeIKernelBuddyService/getBuddyList', FRIENDS = "nodeIKernelBuddyService/getBuddyList",
GROUPS = 'nodeIKernelGroupService/getGroupList', GROUPS = "nodeIKernelGroupService/getGroupList",
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene', GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene",
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList', GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo', USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo', USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
FILE_TYPE = 'getFileType', FILE_TYPE = "getFileType",
FILE_MD5 = 'getFileMd5', FILE_MD5 = "getFileMd5",
FILE_COPY = 'copyFile', FILE_COPY = "copyFile",
IMAGE_SIZE = 'getImageSizeFromPath', IMAGE_SIZE = "getImageSizeFromPath",
FILE_SIZE = 'getFileSize', FILE_SIZE = "getFileSize",
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild', MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = 'nodeIKernelMsgService/recallMsg', RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = 'nodeIKernelMsgService/sendMsg', SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia', DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发 FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment",
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies', MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify', GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup', 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",
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',
} }
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 +144,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,13 +191,15 @@ 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 likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static async likeFriend(uid: string, count = 1) { static async likeFriend(uid: string, count = 1) {
@@ -196,9 +219,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
}) })
} }
@@ -233,19 +254,19 @@ export class NTQQApi {
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 +275,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 +290,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 +300,8 @@ export class NTQQApi {
}>({ }>({
methodName: NTQQApiMethod.GROUP_MEMBERS, methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{ args: [{
sceneId, sceneId: sceneId,
num num: num
}, },
null null
] ]
@@ -290,7 +311,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 +362,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 +407,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 +425,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 +441,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) {
throw ('发送超时')
} }
await sleep(500) else{
return await checkSendComplete() return sentMessage
}
// log(`给${peerUid}发送消息成功`)
} }
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 +530,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 +553,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 +562,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 +576,56 @@ 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 handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = groupNotifies[seq] const notify: GroupNotify = groupNotifies[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 +634,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 +655,7 @@ export class NTQQApi {
groupCode: groupQQ, groupCode: groupQQ,
kickUids, kickUids,
refuseForever, refuseForever,
kickReason kickReason,
} }
] ]
} }
@@ -626,7 +670,7 @@ export class NTQQApi {
args: [ args: [
{ {
groupCode: groupQQ, groupCode: groupQQ,
memList memList,
} }
] ]
} }
@@ -686,4 +730,107 @@ export class NTQQApi {
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]
});
}
}

View File

@@ -73,6 +73,7 @@ export enum ElementType {
PTT = 4, PTT = 4,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
ARK = 10,
} }
export interface SendTextElement { export interface SendTextElement {
@@ -165,13 +166,20 @@ export interface FileElement {
} }
export interface SendFileElement { export interface SendFileElement {
"elementType": ElementType.FILE, elementType: ElementType.FILE,
"elementId": "", elementId: "",
"fileElement": FileElement fileElement: FileElement
}
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 | SendArkElement
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
@@ -210,6 +218,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 +233,7 @@ export interface PicElement {
fileSize: number; fileSize: number;
fileName: string; fileName: string;
fileUuid: string; fileUuid: string;
md5HexStr?: string;
} }
export interface GrayTipElement { export interface GrayTipElement {
@@ -273,6 +284,30 @@ export interface TipAioOpGrayTipElement{
fromGrpCodeOfTmpChat: string, fromGrpCodeOfTmpChat: string,
} }
export interface TipGroupElement {
"type": 1, // 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": null
}
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string;
@@ -368,4 +403,68 @@ export interface FriendRequestNotify {
unreadNums: number, unreadNums: number,
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,6 +21,7 @@ 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) {
log("发生错误", e)
return OB11Response.error(e.toString(), 200); return OB11Response.error(e.toString(), 200);
} }
} }
@@ -33,6 +35,7 @@ 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) {
log("发生错误", e)
return OB11Response.error(e.toString(), 1200, echo) return OB11Response.error(e.toString(), 1200, echo)
} }
} }

View File

@@ -0,0 +1,103 @@
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';
export default class CleanCache extends BaseAction<void, void> {
actionName = ActionName.CleanCache
protected _handle(): Promise<void> {
return new Promise<void>(async (res, rej) => {
try {
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,10 +19,9 @@ 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) const msg = await dbUtil.getMsgByShortId(payload.message_id)
if (msg) { if (msg) {
const msgData = await OB11Constructor.message(msg); return await OB11Constructor.message(msg)
return msgData
} else { } else {
throw ("消息不存在") throw ("消息不存在")
} }

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,24 @@
import {AtType, ChatType, Group, RawMessage, SendMessageElement} from "../../ntqqapi/types"; import {AtType, ChatType, Group, RawMessage, SendArkElement, SendMessageElement} from "../../ntqqapi/types";
import {friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo,} from "../../common/data";
import { import {
addHistoryMsg, OB11MessageCustomMusic,
friends, OB11MessageData,
getGroup, OB11MessageDataType,
getGroupMember, OB11MessageMixType,
getHistoryMsgByShortId, OB11MessageNode,
getUidByUin, OB11PostSendMsg
selfInfo, } from '../types';
} from "../../common/data";
import {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";
import {FileCache} from "../../common/types";
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@@ -62,13 +64,29 @@ 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,
} }
@@ -79,17 +97,16 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
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,24 +118,51 @@ 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 { 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} return {message_id: returnMsg.msgShortId}
} catch (e) { } catch (e) {
log("发送消息失败", e.stack.toString())
throw (e.toString()) throw (e.toString())
} }
} }
@@ -138,9 +182,9 @@ 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
} }
@@ -151,14 +195,15 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
chatType: ChatType.friend, chatType: ChatType.friend,
peerUid: selfInfo.uid peerUid: selfInfo.uid
} }
let selfNodeMsgList: RawMessage[] = []; let selfNodeMsgList: RawMessage[] = []; // 自己给自己发的消息
let originalNodeMsgList: RawMessage[] = []; let originalNodeMsgList: RawMessage[] = [];
let sendForwardElements: SendMessageElement[] = []
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 (nodeMsg) {
originalNodeMsgList.push(nodeMsg); originalNodeMsgList.push(nodeMsg);
} }
@@ -171,9 +216,11 @@ 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);
sendForwardElements.push(...sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true); const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
selfNodeMsgList.push(nodeMsg); selfNodeMsgList.push(nodeMsg);
log("转发节点生成成功", nodeMsg.msgId); log("转发节点生成成功", nodeMsg.msgId);
await sleep(500);
} catch (e) { } catch (e) {
log("生效转发消息节点失败", e) log("生效转发消息节点失败", e)
} }
@@ -183,27 +230,28 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
let nodeIds: string[] = [] let nodeIds: string[] = []
// 检查是否需要克隆直接引用消息id的节点 // 检查是否需要克隆直接引用消息id的节点
let needSendSelf = false; let needSendSelf = false;
if (selfNodeMsgList.length) { if (sendForwardElements.length) {
needSendSelf = true needSendSelf = true
} else { } else {
needSendSelf = !originalNodeMsgList.every((msg, index) => msg.peerUid === originalNodeMsgList[0].peerUid && msg.recallTime.length < 2) needSendSelf = !originalNodeMsgList.every((msg, index) => msg.peerUid === originalNodeMsgList[0].peerUid && msg.recallTime.length < 2)
} }
if (needSendSelf) { if (needSendSelf) {
nodeIds = selfNodeMsgList.map(msg => msg.msgId); nodeIds = selfNodeMsgList.map(msg => msg.msgId);
let sendElements: SendMessageElement[] = [];
for (const originalNodeMsg of originalNodeMsgList) { for (const originalNodeMsg of originalNodeMsgList) {
if (originalNodeMsg.peerUid === selfInfo.uid && originalNodeMsg.recallTime.length < 2) { if (originalNodeMsg.peerUid === selfInfo.uid && originalNodeMsg.recallTime.length < 2) {
nodeIds.push(originalNodeMsg.msgId) nodeIds.push(originalNodeMsg.msgId)
} else { // 需要进行克隆 } else { // 需要进行克隆
let sendElements: SendMessageElement[] = []
Object.keys(originalNodeMsg.elements).forEach((eleKey) => { Object.keys(originalNodeMsg.elements).forEach((eleKey) => {
if (eleKey !== "elementId") { if (eleKey !== "elementId") {
sendForwardElements.push(originalNodeMsg.elements[eleKey])
sendElements.push(originalNodeMsg.elements[eleKey]) sendElements.push(originalNodeMsg.elements[eleKey])
} }
}) })
try { try {
const nodeMsg = await NTQQApi.sendMsg(selfPeer, sendElements, true); const nodeMsg = await NTQQApi.sendMsg(selfPeer, sendElements, true);
nodeIds.push(nodeMsg.msgId) nodeIds.push(nodeMsg.msgId)
log("克隆转发消息成功") log("克隆转发消息到节点")
} catch (e) { } catch (e) {
log("克隆转发消息失败", e) log("克隆转发消息失败", e)
} }
@@ -220,6 +268,15 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
peerUid: originalNodeMsgList[0].peerUid 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) return await NTQQApi.multiForwardMsg(srcPeer, destPeer, nodeIds)
@@ -245,6 +302,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 +323,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 +337,29 @@ 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;
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 +370,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, false, 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 +393,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

@@ -31,6 +31,7 @@ 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";
export const actionHandlers = [ export const actionHandlers = [
new Debug(), new Debug(),
@@ -56,6 +57,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(),

View File

@@ -42,6 +42,7 @@ 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",

View File

@@ -9,11 +9,12 @@ import {
OB11UserSex OB11UserSex
} from "./types"; } from "./types";
import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types'; import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types';
import {fileCache, getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo, tempGroupCodeMap} from '../common/data'; import {getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data';
import {getConfigUtil, log} from "../common/utils"; import {getConfigUtil, log} 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";
export class OB11Constructor { export class OB11Constructor {
@@ -26,7 +27,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),
@@ -95,30 +96,44 @@ 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 +142,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 +150,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 +158,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 +166,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 +174,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 => {

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;
@@ -68,4 +75,4 @@ export function encodeCQCode(data: OB11MessageData) {
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]") // const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
// const result = parseCQCode("好好好") // const result = parseCQCode("好好好")
// console.log(JSON.stringify(result)) // console.log(JSON.stringify(result))

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) {
super(); super();
this.group_id = groupId; this.group_id = groupId;
this.operate_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被邀请的,还是主动加入的 this.operator_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被邀请的,还是主动加入的
this.user_id = userId; this.user_id = userId;
} }
} }

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,8 +1,8 @@
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;
@@ -10,11 +10,13 @@ export async function uri2local(uri: string, fileName: string = null) {
if (!fileName) { if (!fileName) {
fileName = uuidv4(); fileName = uuidv4();
} }
let filePath = path.join(CONFIG_DIR, fileName) let filePath = path.join(DATA_DIR, fileName)
let url = new URL(uri); let url = new URL(uri);
let res = { let res = {
success: false, success: false,
errMsg: "", errMsg: "",
fileName: "",
ext: "",
path: "", path: "",
isLocal: false isLocal: false
} }
@@ -31,7 +33,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 +47,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 +73,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 +91,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

@@ -136,9 +136,36 @@ 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) => {
const dom = { const dom = {

View File

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