Compare commits

..

64 Commits

Author SHA1 Message Date
linyuchen
5044d24ee1 feat: go-cqhttp api get_stranger_info
feat: api send_like
fix: some image of message use base64 instead of http url
2024-02-21 22:19:02 +08:00
linyuchen
7664e746b4 feat: some go-cqhttp feature 2024-02-21 17:17:15 +08:00
linyuchen
ebea755731 perf: log long string 2024-02-21 16:43:10 +08:00
linyuchen
e4508ea5c7 feat: go-cqhttp api send_private_forward_msg & send_group_forward_msg 2024-02-21 16:36:40 +08:00
linyuchen
5ef221608c chore: GitHub zip action 2024-02-21 06:06:08 +08:00
linyuchen
6b2a45e087 chore: GitHub zip action 2024-02-21 05:55:49 +08:00
linyuchen
03d4a68c33 feat: post image http url 2024-02-21 05:48:37 +08:00
linyuchen
0f84e82d74 fix: reverse ws support koishi 2024-02-21 05:22:07 +08:00
linyuchen
0f4d8f3fe2 fix: send temp msg
fix: multi forward msg
2024-02-21 05:11:13 +08:00
linyuchen
be7b68ec4e fix: old http port config 2024-02-21 03:47:14 +08:00
linyuchen
103e0b43f8 fix: reverse ws restart 2024-02-21 03:30:04 +08:00
linyuchen
f092fad2f4 fix: get method params parse 2024-02-20 23:08:42 +08:00
linyuchen
c4e54fa259 feat: auto encode silk 2024-02-20 22:39:24 +08:00
linyuchen
0e4de038ca merge v3.4.0 2024-02-20 16:12:46 +08:00
linyuchen
ed48a76c33 Merge branch 'v3.4.0' into dev
# Conflicts:
#	src/common/utils.ts
#	src/global.d.ts
#	src/main/ipcsend.ts
#	src/main/main.ts
#	src/ntqqapi/hook.ts
#	src/onebot11/action/SendMsg.ts
#	src/onebot11/action/TestForwdMsg.ts
#	src/onebot11/action/types.ts
#	src/onebot11/server.ts
#	src/preload.ts
2024-02-20 16:09:15 +08:00
linyuchen
0545bcfdab refactor: function getConfig add cache param 2024-02-20 15:51:55 +08:00
linyuchen
a4301f0b55 Merge remote-tracking branch 'origin/v3.4.0' into v3.4.0
# Conflicts:
#	src/common/config.ts
2024-02-20 15:46:41 +08:00
linyuchen
e34e8c2768 Merge pull request #56 from disymayufei/main
临时修复配置文件的问题
2024-02-20 15:41:13 +08:00
linyuchen
dce65a295f Merge remote-tracking branch 'origin/v3.4.0' into v3.4.0
# Conflicts:
#	src/common/config.ts
2024-02-20 15:38:48 +08:00
linyuchen
f9b97543d9 refactor: default config 2024-02-20 03:29:28 +08:00
linyuchen
c1dd309b21 refactor: base server & setting ui 2024-02-20 03:25:16 +08:00
Disy
20399dc369 fix: get config return null ref 2024-02-20 00:10:46 +08:00
linyuchen
4e4ccf4935 Merge pull request #54 from disymayufei/main
增加配置文件的内存缓存机制
2024-02-19 23:17:11 +08:00
Disy
6e97044437 feat: cache config 2024-02-19 23:05:44 +08:00
Disy
5cf9a6e942 Merge pull request #1 from disymayufei/dev-1
合并开发分支
2024-02-19 22:56:26 +08:00
linyuchen
5094ba724a Merge pull request #53 from disymayufei/dev-1
补充支持基本的正向和反向Websocket
2024-02-19 21:54:27 +08:00
linyuchen
1938eef746 fix: send multi forward msg 2024-02-19 21:48:50 +08:00
Disy
82e3ca113d chore: change app version 2024-02-19 18:31:10 +08:00
Disy
acb1ec3871 feat: Asynchronous connect reverse websocket 2024-02-19 13:54:09 +08:00
Disy
9b0f2d0983 chore: Conflict resolution 2024-02-19 13:31:57 +08:00
Disy
d1eef6759c Merge branch 'linyuchen:main' into dev-1 2024-02-18 10:05:27 +08:00
Disy
6219f4ec95 Merge branch 'dev' into dev-1 2024-02-17 23:50:05 +08:00
linyuchen
9b8b9a203c fix: group member_count & member_max_count 2024-02-17 23:38:18 +08:00
linyuchen
e5edfd78eb docs: update readme 2024-02-17 20:19:09 +08:00
linyuchen
ee4206c33d docs: update readme 2024-02-17 20:10:08 +08:00
linyuchen
42d6f1528a Merge branch 'dev'
# Conflicts:
#	manifest.json
#	src/common/data.ts
2024-02-17 20:07:13 +08:00
linyuchen
1a1d673c8c feat: face msg
feat: recall notice
2024-02-17 20:06:17 +08:00
linyuchen
06ad92b846 ver: 3.2.2 2024-02-17 01:44:21 +08:00
linyuchen
df5968ccc1 Merge pull request #47 from YuChuXi/patch-1
fix get_group_info
2024-02-17 01:43:12 +08:00
linyuchen
ba387b40ca 暂存 2024-02-17 01:42:14 +08:00
YuChuXi
e554d805b5 修东西
fix: get_group_info和get_group_list都返回群列表
2024-02-17 01:39:06 +08:00
linyuchen
d54111ce94 fix: ws url token parse 2024-02-16 22:48:43 +08:00
Disy
018ec07082 feat: support reverse websocket 2024-02-16 22:34:12 +08:00
linyuchen
4f9682289c feat: api /get_version_info
feat: api /can_send_image
feat: api /can_send_record
feat: ws heart & lifecycle
2024-02-16 21:32:37 +08:00
linyuchen
963aad1510 fix: some id(int and string) compatibility 2024-02-16 15:54:07 +08:00
linyuchen
0eeba1d29e fix: remove ws welcome 2024-02-16 10:34:56 +08:00
linyuchen
97200f427d docs: update readme 2024-02-16 00:52:19 +08:00
linyuchen
ef4443d080 feat: Websocket Server
feat: change port not need restart
2024-02-16 00:47:04 +08:00
Disy
f02b0bdcad Merge branch 'main' of https://github.com/disymayufei/LiteLoaderQQNT-OneBotApi 2024-02-15 22:27:01 +08:00
Disy
72b1c906f7 fix: Notification event not effective 2024-02-15 22:26:53 +08:00
Disy
53d30ed7ea Merge branch 'linyuchen:main' into main 2024-02-15 21:48:05 +08:00
Disy
8f48d1d4ca feat: 预添加群成员变动事件 2024-02-15 21:47:16 +08:00
linyuchen
a7d75f84cb fix: send voice msg 2024-02-15 18:43:29 +08:00
Disy
c875cfda15 feat: add websocket support 2024-02-14 22:11:07 +08:00
linyuchen
9bb69058c2 fix: group msg subtype: normal 2024-02-14 12:58:29 +08:00
linyuchen
89971dd2e4 docs: update README 2024-02-14 01:38:46 +08:00
linyuchen
aea67db27c fix: report self sent message_id 2024-02-14 01:35:48 +08:00
linyuchen
c4b45f8298 ver: 3.0.5 2024-02-14 01:02:48 +08:00
linyuchen
1a77abfc62 fix: 发送回复消息多了个@符号 2024-02-14 01:01:54 +08:00
linyuchen
eb32ecb79b perf: 去掉多余日志 2024-02-14 00:37:53 +08:00
linyuchen
ccf91f4a94 fix: 消息重复上报 2024-02-14 00:35:36 +08:00
linyuchen
282b2a0da0 fix: message_id过长导致koishi对接失败
perf: 初始化卡顿优化
2024-02-13 21:17:16 +08:00
linyuchen
b28b812396 fix: file://中有中文无法正确解析 2024-02-13 19:56:02 +08:00
linyuchen
1936671cb3 fix: self nickname
fix: @member msg report
fix: send file:// on Windows
ver: 3.0.2
2024-02-13 18:37:01 +08:00
71 changed files with 3212 additions and 968 deletions

View File

@@ -26,7 +26,8 @@ jobs:
run: |
sudo apt install zip -y
cp manifest.json ./dist/manifest.json
zip LLOneBot.zip ./dist/* -j
cd ./dist/
zip -r ../LLOneBot.zip ./*
- name: publish
uses: ncipollo/release-action@v1

View File

@@ -1,9 +1,11 @@
# LLOneBot API
将NTQQLiteLoaderAPI封装成OneBot11标准的API
LiteLoaderQQNT的OneBot11协议插件
*注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档*
*V3之后不再需要LLAPI*
## 安装方法
1.安装[LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
@@ -16,7 +18,11 @@
## 支持的API
目前支持http协议POST方法不支持websocket事件上报也是http协议
目前支持协议
- [x] http调用api
- [x] http事件上报
- [x] 正向websocket
- [x] 反向websocket
主要功能:
- [x] 发送好友消息
@@ -27,16 +33,19 @@
- [x] 撤回消息
- [x] 上报好友消息
- [x] 上报群消息
- [x] 上报好友、群消息撤回
消息格式支持:
- [x] cq码
- [x] 文字
- [x] 表情
- [x] 图片
- [x] 引用消息
- [x] @群成员
- [x] 语音
- [x] 语音(支持mp3、wav等多种音频格式直接发送)
- [x] json消息(只上报)
- [x] 转发消息记录(目前只能发不能收)
- [ ] 红包
- [ ] 转发消息记录
- [ ] xml
支持的api:
@@ -46,10 +55,19 @@
- [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] get_msg
- [x] get_version_info
- [x] get_status
- [x] can_send_image
- [x] can_send_record
支持的go-cqhtp api:
- [x] send_private_forward_msg
- [x] send_group_forward_msg
## 示例
@@ -67,7 +85,7 @@
<details>
<summary>调用接口报404</summary>
<br/>
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口并且所有接口都只支持POST方法调用GET方法会报404
目前没有支持全部的onebot规范接口请检查是否调用了不支持的接口
</details>
<br/>
@@ -78,13 +96,6 @@
</details>
<br/>
<details>
<summary>不支持cq码</summary>
<br/>
cq码已经过时了没有支持的打算(主要是我不用这玩意儿,加上我懒)
</details>
<br/>
<details>
<summary>QQ变得很卡</summary>
<br/>
@@ -94,11 +105,10 @@
## TODO
- [ ] 转发消息记录
- [ ] 好友点赞api
- [ ] 支持websocket等个有缘人提PR实现
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录
- [ ] 好友点赞api
## onebot11文档
<https://11.onebot.dev/>

View File

@@ -1,31 +1,33 @@
{
"manifest_version": 4,
"type": "extension",
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi",
"version": "3.0.1",
"thumbnail": "./icon.png",
"authors": [{
"name": "linyuchen",
"link": "https://github.com/linyuchen"
}],
"repository": {
"manifest_version": 4,
"type": "extension",
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi",
"version": "3.6.0",
"thumbnail": "./icon.png",
"authors": [
{
"name": "linyuchen",
"link": "https://github.com/linyuchen"
}
],
"repository": {
"repo": "linyuchen/LiteLoaderQQNT-OneBotApi",
"branch": "main",
"release": {
"tag": "latest",
"name": "LLOneBot.zip"
}
},
"platform": [
"win32",
"linux",
"darwin"
],
"injects": {
"renderer": "./renderer.js",
"main": "./main.js",
"preload": "./preload.js"
"tag": "latest",
"name": "LLOneBot.zip"
}
},
"platform": [
"win32",
"linux",
"darwin"
],
"injects": {
"renderer": "./renderer.js",
"main": "./main.js",
"preload": "./preload.js"
}
}

604
package-lock.json generated
View File

@@ -12,13 +12,20 @@
"dependencies": {
"express": "^4.18.2",
"json-bigint": "^1.0.0",
"uuid": "^9.0.1"
"music-metadata": "^8.1.4",
"silk-wasm": "^3.2.3",
"uuid": "^9.0.1",
"ws": "^8.16.0"
},
"devDependencies": {
"@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20",
"@types/node": "^20.11.19",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"webpack": "^5.89.0",
@@ -2064,6 +2071,58 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://mirrors.cloud.tencent.com/npm/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://mirrors.cloud.tencent.com/npm/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://mirrors.cloud.tencent.com/npm/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
"integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"node_modules/@types/body-parser": {
"version": "1.19.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/body-parser/-/body-parser-1.19.4.tgz",
@@ -2152,9 +2211,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.8.9",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.8.9.tgz",
"integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==",
"version": "20.11.19",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -2199,6 +2258,15 @@
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
@@ -2884,6 +2952,83 @@
"resolved": "https://mirrors.cloud.tencent.com/npm/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/copy-webpack-plugin": {
"version": "12.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz",
"integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==",
"dev": true,
"dependencies": {
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.1",
"globby": "^14.0.0",
"normalize-path": "^3.0.0",
"schema-utils": "^4.2.0",
"serialize-javascript": "^6.0.2"
},
"engines": {
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.1.0"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/copy-webpack-plugin/node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/schema-utils/-/schema-utils-4.2.0.tgz",
"integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/core-js-compat": {
"version": "3.33.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/core-js-compat/-/core-js-compat-3.33.2.tgz",
@@ -2897,6 +3042,24 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3129,6 +3292,34 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.4"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3144,6 +3335,31 @@
"node": ">= 4.9.1"
}
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/file-type": {
"version": "18.7.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/file-type/-/file-type-18.7.0.tgz",
"integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==",
"dependencies": {
"readable-web-to-node-stream": "^3.0.2",
"strtok3": "^7.0.0",
"token-types": "^5.0.1"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -3344,12 +3560,44 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
},
"node_modules/globby": {
"version": "14.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/globby/-/globby-14.0.1.tgz",
"integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==",
"dev": true,
"dependencies": {
"@sindresorhus/merge-streams": "^2.1.0",
"fast-glob": "^3.3.2",
"ignore": "^5.2.4",
"path-type": "^5.0.0",
"slash": "^5.1.0",
"unicorn-magic": "^0.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/gopd/-/gopd-1.0.1.tgz",
@@ -3446,6 +3694,34 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"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/ignore": {
"version": "5.3.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/ignore/-/ignore-5.3.1.tgz",
"integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/import-local": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
@@ -3499,6 +3775,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -3662,6 +3959,15 @@
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/methods/-/methods-1.1.2.tgz",
@@ -3718,6 +4024,56 @@
"resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/music-metadata": {
"version": "8.1.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/music-metadata/-/music-metadata-8.1.4.tgz",
"integrity": "sha512-q9mw2qeESeJY69cXtdaum/YJstDimpP+mwZnb801iq20JpyY75v6uzcp6VfVXZDixpD2f9yWneJtA0TgSEypxA==",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"content-type": "^1.0.5",
"debug": "^4.3.4",
"file-type": "^18.2.1",
"media-typer": "^1.1.0",
"strtok3": "^7.0.0",
"token-types": "^5.0.1"
},
"engines": {
"node": "^14.13.1 || >=16.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/music-metadata/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/music-metadata/node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/music-metadata/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/negotiator/-/negotiator-0.6.3.tgz",
@@ -3732,12 +4088,33 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/node-gyp-build": {
"version": "4.8.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/node-gyp-build/-/node-gyp-build-4.8.0.tgz",
"integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==",
"optional": true,
"peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"dev": true
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -3830,6 +4207,30 @@
"resolved": "https://mirrors.cloud.tencent.com/npm/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/path-type": {
"version": "5.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/path-type/-/path-type-5.0.0.tgz",
"integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/peek-readable": {
"version": "5.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/peek-readable/-/peek-readable-5.0.0.tgz",
"integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==",
"engines": {
"node": ">=14.16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.0.0.tgz",
@@ -3895,6 +4296,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"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/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -3926,6 +4347,34 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
"dependencies": {
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/rechoir": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@@ -4050,6 +4499,39 @@
"node": ">=8"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"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-buffer": {
"version": "5.2.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -4136,9 +4618,9 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serialize-javascript": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
"version": "6.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
@@ -4223,6 +4705,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/silk-wasm": {
"version": "3.2.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/silk-wasm/-/silk-wasm-3.2.3.tgz",
"integrity": "sha512-zZ3hgMpiPR6cFnKvCPgPpCwx6n5RoJCbEGIFlge2kAxAmgzBTf0b2F2xIPG5W4obUhQPQXXTTH074eGZJK01xw=="
},
"node_modules/slash": {
"version": "5.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -4250,6 +4749,30 @@
"node": ">= 0.8"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strtok3": {
"version": "7.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/strtok3/-/strtok3-7.0.0.tgz",
"integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^5.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -4358,6 +4881,22 @@
"node": ">=0.6"
}
},
"node_modules/token-types": {
"version": "5.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/token-types/-/token-types-5.0.1.tgz",
"integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/ts-loader": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz",
@@ -4458,6 +4997,18 @@
"node": ">=4"
}
},
"node_modules/unicorn-magic": {
"version": "0.1.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
"integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/unpipe/-/unpipe-1.0.0.tgz",
@@ -4505,6 +5056,25 @@
"punycode": "^2.1.0"
}
},
"node_modules/utf-8-validate": {
"version": "6.0.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/utf-8-validate/-/utf-8-validate-6.0.3.tgz",
"integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -4691,6 +5261,26 @@
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"dev": true
},
"node_modules/ws": {
"version": "8.16.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@@ -5,13 +5,13 @@
"main": "dist/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"postinstall": "ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install electron --no-save",
"postinstall": "cross-env ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save",
"build": "npm run build-main && npm run build-preload && npm run build-renderer",
"build-main": "webpack --config webpack.main.config.js",
"build-preload": "webpack --config webpack.preload.config.js",
"build-renderer": "webpack --config webpack.renderer.config.js",
"build-mac": "npm run build && cp manifest.json dist/ && npm run deploy-mac",
"deploy-mac": "cp dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOnebot/",
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOnebot/",
"build-win": "npm run build && cp manifest.json dist/ && npm run deploy-win",
"deploy-win": "cmd /c \"copy dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOnebot\\\""
},
@@ -20,13 +20,20 @@
"dependencies": {
"express": "^4.18.2",
"json-bigint": "^1.0.0",
"uuid": "^9.0.1"
"music-metadata": "^8.1.4",
"silk-wasm": "^3.2.3",
"uuid": "^9.0.1",
"ws": "^8.16.0"
},
"devDependencies": {
"@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20",
"@types/node": "^20.11.19",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"webpack": "^5.89.0",

View File

@@ -1,3 +1,5 @@
import {Peer} from "../ntqqapi/ntcall";
export const CHANNEL_GET_CONFIG = "llonebot_get_config"
export const CHANNEL_SET_CONFIG = "llonebot_set_config"
export const CHANNEL_LOG = "llonebot_log"
export const CHANNEL_LOG = "llonebot_log"

View File

@@ -1,28 +1,79 @@
import {Config} from "./types";
import fs from "fs";
import {Config, OB11Config} from "./types";
import {mergeNewProperties} from "./utils";
const fs = require("fs")
export class ConfigUtil{
configPath: string;
export class ConfigUtil {
private readonly configPath: string;
private config: Config | null = null;
constructor(configPath: string) {
this.configPath = configPath;
}
getConfig(): Config{
getConfig(cache=true) {
if (this.config && cache) {
return this.config;
}
return this.reloadConfig();
}
reloadConfig(): Config {
let ob11Default: OB11Config = {
httpPort: 3000,
httpHosts: [],
wsPort: 3001,
wsHosts: [],
enableHttp: true,
enableHttpPost: true,
enableWs: true,
enableWsReverse: false
}
let defaultConfig: Config = {
ob11: ob11Default,
heartInterval: 60000,
token: "",
enableLocalFile2Url: false,
debug: false,
log: false,
reportSelfMessage: false
};
if (!fs.existsSync(this.configPath)) {
return {port:3000, hosts: ["http://192.168.1.2:5000/"]}
this.config = defaultConfig;
return this.config;
} else {
const data = fs.readFileSync(this.configPath, "utf-8");
let jsonData =JSON.parse(data);
if (!jsonData.hosts){
jsonData.hosts = []
let jsonData: Config = defaultConfig;
try {
jsonData = JSON.parse(data)
} catch (e) {
this.config = defaultConfig;
return this.config;
}
return jsonData;
mergeNewProperties(defaultConfig, jsonData);
this.checkOldConfig(jsonData.ob11, jsonData, "httpPort", "http");
this.checkOldConfig(jsonData.ob11, jsonData, "httpHosts", "hosts");
this.checkOldConfig(jsonData.ob11, jsonData, "wsPort", "wsPort");
// console.log("get config", jsonData);
this.config = jsonData;
return this.config;
}
}
setConfig(config: Config){
setConfig(config: Config) {
this.config = config;
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8")
}
private checkOldConfig(currentConfig: Config | OB11Config,
oldConfig: Config | OB11Config,
currentKey: string, oldKey: string) {
// 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey];
if (oldValue) {
currentConfig[currentKey] = oldValue;
delete oldConfig[oldKey];
}
}
}

View File

@@ -1,10 +1,30 @@
import { NTQQApi } from '../ntqqapi/ntcall';
import { Friend, Group, RawMessage, SelfInfo } from "../ntqqapi/types";
import {NTQQApi} from '../ntqqapi/ntcall';
import {Friend, Group, GroupMember, RawMessage, SelfInfo} from "../ntqqapi/types";
export let groups: Group[] = []
export let friends: Friend[] = []
export let msgHistory: Record<string, RawMessage> = {} // msgId: RawMessage
let globalMsgId = Date.now()
export function addHistoryMsg(msg: RawMessage): boolean{
let existMsg = msgHistory[msg.msgId]
if (existMsg){
Object.assign(existMsg, msg)
msg.msgShortId = existMsg.msgShortId;
return false
}
msg.msgShortId = ++globalMsgId
msgHistory[msg.msgId] = msg
return true
}
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){
@@ -23,16 +43,23 @@ export async function getGroup(qq: string): Promise<Group | undefined> {
return group
}
export async function getGroupMember(groupQQ: string, memberQQ: string) {
export async function getGroupMember(groupQQ: string, memberQQ: string=null, memberUid: string=null) {
const group = await getGroup(groupQQ)
if (group) {
let member = group.members?.find(member => member.uin === memberQQ)
let filterFunc: (member: GroupMember) => boolean
if (memberQQ){
filterFunc = member => member.uin === memberQQ
}
else if (memberUid){
filterFunc = member => member.uid === memberUid
}
let member = group.members?.find(filterFunc)
if (!member){
const _members = await NTQQApi.getGroupMembers(groupQQ)
if (_members.length){
group.members = _members
}
member = group.members?.find(member => member.uin === memberQQ)
member = group.members?.find(filterFunc)
}
return member
}
@@ -50,12 +77,14 @@ export function getHistoryMsgBySeq(seq: string) {
}
export let uidMaps:Record<string, Friend> = {} // 一串加密的字符串(uid) -> qq号
export let uidMaps:Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getStrangerByUin(uin: string) {
export function getUidByUin(uin: string) {
for (const key in uidMaps) {
if (uidMaps[key].uin === uin) {
return uidMaps[key];
if (uidMaps[key] === uin) {
return key;
}
}
}
}
export const version = "v3.6.0"

110
src/common/server/http.ts Normal file
View File

@@ -0,0 +1,110 @@
import express, {Express, Request, Response} from "express";
import {getConfigUtil, log} from "../utils";
import http from "http";
const JSONbig = require('json-bigint')({storeAsString: true});
type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase {
name: string = "LLOneBot";
private readonly expressAPP: Express;
private server: http.Server = null;
constructor() {
this.expressAPP = express();
this.expressAPP.use(express.urlencoded({extended: true, limit: "500mb"}));
this.expressAPP.use((req, res, next) => {
let data = '';
req.on('data', chunk => {
data += chunk.toString();
});
req.on('end', () => {
if (data) {
try {
// log("receive raw", data)
req.body = JSONbig.parse(data);
} catch (e) {
return next(e);
}
}
next();
});
});
}
authorize(req: Request, res: Response, next: () => void) {
let serverToken = getConfigUtil().getConfig().token;
let clientToken = ""
const authHeader = req.get("authorization")
if (authHeader) {
clientToken = authHeader.split("Bearer ").pop()
log("receive http header token", clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
clientToken = req.query.access_token[0].toString();
} else {
clientToken = req.query.access_token.toString();
}
log("receive http url token", clientToken)
}
if (serverToken && clientToken != serverToken) {
return res.status(403).send(JSON.stringify({message: 'token verify failed!'}));
}
next();
};
start(port: number) {
this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`${this.name}已启动`);
})
this.listen(port);
}
stop() {
if (this.server){
this.server.close()
this.server = null;
}
}
restart(port: number){
this.stop()
this.start(port)
}
abstract handleFailed(res: Response, payload: any, err: any): void
registerRouter(method: "post" | "get" | string, url: string, handler: RegisterHandler) {
if (!url.startsWith("/")) {
url = "/" + url
}
if (!this.expressAPP[method]){
const err = `${this.name} register router failed${method} not exist`;
log(err);
throw err;
}
this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => {
let payload = req.body;
if (method == "get"){
payload = req.query
}
log("收到http请求", url, payload);
try{
res.send(await handler(res, payload))
}catch (e) {
this.handleFailed(res, payload, e.stack.toString())
}
});
}
protected listen(port: number) {
this.server = this.expressAPP.listen(port, "0.0.0.0", () => {
const info = `${this.name} started 0.0.0.0:${port}`
console.log(info);
log(info);
});
}
}

View File

@@ -0,0 +1,95 @@
import {Server, WebSocket} from "ws";
import {getConfigUtil, log} from "../utils";
import urlParse from "url";
import {IncomingMessage} from "node:http";
class WebsocketClientBase {
private wsClient: WebSocket
constructor() {
}
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg);
}
}
onMessage(msg: string){
}
}
export class WebsocketServerBase {
private ws: Server = null;
constructor() {
console.log(`llonebot websocket service started`)
}
start(port: number) {
this.ws = new Server({port});
this.ws.on("connection", (wsClient, req)=>{
const url = req.url.split("?").shift()
this.authorize(wsClient, req);
this.onConnect(wsClient, url, req);
wsClient.on("message", async (msg)=>{
this.onMessage(wsClient, url, msg.toString())
})
})
}
stop() {
this.ws.close((err) => {
log("ws server close failed!", err)
});
this.ws = null;
}
restart(port: number){
this.stop();
this.start(port);
}
authorize(wsClient: WebSocket, req) {
let token = getConfigUtil().getConfig().token;
const url = req.url.split("?").shift();
log("ws connect", url)
let clientToken: string = ""
const authHeader = req.headers['authorization'];
if (authHeader) {
clientToken = authHeader.split("Bearer ").pop()
log("receive ws header token", clientToken);
} else {
const parsedUrl = urlParse.parse(req.url, true);
const urlToken = parsedUrl.query.access_token;
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log("receive ws url token", clientToken);
}
}
if (token && clientToken != token) {
this.authorizeFailed(wsClient)
return wsClient.close()
}
}
authorizeFailed(wsClient: WebSocket) {
}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
}
onMessage(wsClient: WebSocket, url: string, msg: string) {
}
sendHeart() {
}
}

View File

@@ -1,9 +1,20 @@
export interface OB11Config {
httpPort: number
httpHosts: string[]
wsPort: number
wsHosts: string[]
enableHttp?: boolean
enableHttpPost?: boolean
enableWs?: boolean
enableWsReverse?: boolean
}
export interface Config {
port: number
hosts: string[]
enableBase64?: boolean
ob11: OB11Config
token?: string
heartInterval?: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean
reportSelfMessage?: boolean
log?: boolean
}
}

View File

@@ -2,9 +2,9 @@ import * as path from "path";
import {selfInfo} from "./data";
import {ConfigUtil} from "./config";
import util from "util";
import { sendLog } from '../main/ipcsend';
const fs = require('fs');
import {encode, getDuration} from "silk-wasm";
import fs from 'fs';
import {v4 as uuidv4} from "uuid";
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
@@ -13,8 +13,25 @@ export function getConfigUtil() {
return new ConfigUtil(configFilePath)
}
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log){
if (!getConfigUtil().getConfig().log) {
return
}
let currentDateTime = new Date().toLocaleString();
@@ -25,18 +42,19 @@ export function log(...msg: any[]) {
const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg){
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object"){
logMsg += JSON.stringify(msgItem) + " ";
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n`
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(CONFIG_DIR , `llonebot-${currentDate}.log`), logMsg, (err: any) => {
fs.appendFile(path.join(CONFIG_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
})
}
@@ -49,9 +67,13 @@ export function isGIF(path: string) {
return buffer.toString() === 'GIF8'
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number=3000): Promise<void> {
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
@@ -69,7 +91,7 @@ export function checkFileReceived(path: string, timeout: number=3000): Promise<v
});
}
export async function file2base64(path: string){
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
let result = {
err: "",
@@ -94,3 +116,81 @@ export async function file2base64(path: string){
}
return result;
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key];
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]);
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key];
}
}
});
}
export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) {
// 定义要读取的字节数
const bytesToRead = 7;
try {
const buffer = fs.readFileSync(filePath, {
encoding: null,
flag: "r",
});
const fileHeader = buffer.toString("hex", 0, bytesToRead);
return fileHeader;
} catch (err) {
console.error("读取文件错误:", err);
return;
}
}
async function getAudioSampleRate(filePath: string) {
try {
const mm = await import('music-metadata');
const metadata = await mm.parseFile(filePath);
log(`${filePath}采样率`, metadata.format.sampleRate);
return metadata.format.sampleRate;
} catch (error) {
log(`${filePath}采样率获取失败`, error.stack);
// console.error(error);
}
}
try {
const fileName = path.basename(filePath);
const pcm = fs.readFileSync(filePath);
const pttPath = path.join(CONFIG_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换`)
const sampleRate = await getAudioSampleRate(filePath) || 44100;
const silk = await encode(pcm, sampleRate);
fs.writeFileSync(pttPath, silk.data);
log(`语音文件${filePath}转换成功!`)
return {
converted: true,
path: pttPath,
duration: silk.duration,
};
} else {
const duration = getDuration(pcm);
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}

7
src/global.d.ts vendored
View File

@@ -1,11 +1,6 @@
import { Config } from "./common/types";
import {LLOneBot} from "./preload";
declare var llonebot: {
log(data: any): void,
setConfig(config: Config):void;
getConfig():Promise<Config>;
};
declare global {
interface Window {

View File

@@ -1,5 +1,4 @@
import {webContents} from 'electron';
import { CHANNEL_LOG } from '../common/channels';
function sendIPCMsg(channel: string, ...data: any) {
@@ -12,7 +11,3 @@ function sendIPCMsg(channel: string, ...data: any) {
}
}
}
export function sendLog(...args){
sendIPCMsg(CHANNEL_LOG, ...args)
}

View File

@@ -1,25 +1,23 @@
// 运行在 Electron 主进程 下的插件入口
import * as path from "path";
import { BrowserWindow, ipcMain } from 'electron';
import * as util from 'util';
import {BrowserWindow, ipcMain} from 'electron';
import fs from 'fs';
import {Config} from "../common/types";
import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "../common/channels";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {CONFIG_DIR, getConfigUtil, log} from "../common/utils";
import {addHistoryMsg, getGroupMember, msgHistory, selfInfo} from "../common/data";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor";
import {NTQQApi} from "../ntqqapi/ntcall";
import {ChatType, RawMessage} from "../ntqqapi/types";
import {ob11HTTPServer} from "../onebot11/server/http";
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent";
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent";
import {postEvent} from "../onebot11/server/postevent";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {EventType} from "../onebot11/event/OB11BaseEvent";
import { Config } from "../common/types";
import {
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SET_CONFIG,
} from "../common/channels";
import { ConfigUtil } from "../common/config";
import { postMsg, startExpress } from "../onebot11/server";
import { CONFIG_DIR, getConfigUtil, log } from "../common/utils";
import { friends, groups, msgHistory, selfInfo } from "../common/data";
import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook";
import { OB11Constructor } from "../onebot11/constructor";
import { NTQQApi } from "../ntqqapi/ntcall";
import { Group, RawMessage, SelfInfo } from "../ntqqapi/types";
const fs = require('fs');
let running = false;
@@ -27,10 +25,6 @@ let running = false;
// 加载插件时触发
function onLoad() {
log("llonebot main onLoad");
// const config_dir = browserWindow.LiteLoader.plugins["LLOneBot"].path.data;
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, {recursive: true});
}
@@ -38,117 +32,190 @@ function onLoad() {
return getConfigUtil().getConfig()
})
ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => {
let oldConfig = getConfigUtil().getConfig();
getConfigUtil().setConfig(arg)
if (arg.ob11.httpPort != oldConfig.ob11.httpPort && arg.ob11.enableHttp) {
ob11HTTPServer.restart(arg.ob11.httpPort);
}
// 判断是否启用或关闭HTTP服务
if (!arg.ob11.enableHttp) {
ob11HTTPServer.stop();
} else {
ob11HTTPServer.start(arg.ob11.httpPort);
}
// 正向ws端口变化重启服务
if (arg.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(arg.ob11.wsPort);
}
// 判断是否启用或关闭正向ws
if (arg.ob11.enableWs != oldConfig.ob11.enableWs) {
if (arg.ob11.enableWs) {
ob11WebsocketServer.start(arg.ob11.wsPort);
} else {
ob11WebsocketServer.stop();
}
}
// 判断是否启用或关闭反向ws
if (arg.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (arg.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
} else {
ob11ReverseWebsockets.stop();
}
}
if (arg.ob11.enableWsReverse) {
// 判断反向ws地址有变化
if (arg.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
ob11ReverseWebsockets.restart();
} else {
for (const newHost of arg.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
ob11ReverseWebsockets.restart();
break;
}
}
}
}
})
ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => {
log(arg)
log(arg);
})
function postRawMsg(msgList: RawMessage[]) {
function postReceiveMsg(msgList: RawMessage[]) {
const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (const message of msgList) {
for (let message of msgList) {
// log("收到新消息", message)
message.msgShortId = msgHistory[message.msgId]?.msgShortId
if (!message.msgShortId) {
addHistoryMsg(message);
}
OB11Constructor.message(message).then((msg) => {
if (debug) {
msg.raw = message;
}
if (msg.user_id == selfInfo.uin && !reportSelfMessage) {
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
postMsg(msg);
postEvent(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString()));
}
}
function start() {
async function start() {
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try {
postRawMsg(payload.msgList);
postReceiveMsg(payload.msgList);
} catch (e) {
log("report message error: ", e.toString())
log("report message error: ", e.toString());
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, async (payload) => {
for (const message of payload.msgList) {
// log("message update", message.sendStatus, message)
if (message.recallTime != "0") {
// 撤回消息上报
const oriMessage = msgHistory[message.msgId]
if (!oriMessage) {
continue
}
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId);
postEvent(friendRecallEvent);
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, null, operatorUid)
operatorId = operator.uin
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId
)
postEvent(groupRecallEvent);
}
continue
}
addHistoryMsg(message)
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig()
const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) {
return
}
log("reportSelfMessage", payload)
// log("reportSelfMessage", payload)
try {
postRawMsg([payload.msgRecord]);
postReceiveMsg([payload.msgRecord]);
} catch (e) {
log("report self message error: ", e.toString())
log("report self message error: ", e.toString());
}
})
startExpress(getConfigUtil().getConfig().port)
NTQQApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) {
try {
ob11HTTPServer.start(config.ob11.httpPort)
} catch (e) {
log("http server start failed", e);
}
}
if (config.ob11.enableWs){
ob11WebsocketServer.start(config.ob11.wsPort);
}
if (config.ob11.enableWsReverse){
ob11ReverseWebsockets.start();
}
log("LLOneBot start")
}
async function getSelfInfo() {
const init = async () => {
try {
const _ = await NTQQApi.getSelfInfo()
Object.assign(selfInfo, _)
selfInfo.nick = selfInfo.uin
log("get self simple info", _)
const _ = await NTQQApi.getSelfInfo();
Object.assign(selfInfo, _);
selfInfo.nick = selfInfo.uin;
log("get self simple info", _);
} catch (e) {
log("retry get self info")
log("retry get self info");
}
if (selfInfo.uin) {
try {
const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid))
const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid));
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick
selfInfo.nick = userInfo.nick;
} else {
return setTimeout(() => {
getSelfInfo().then()
}, 100)
return setTimeout(init, 1000);
}
} catch (e) {
log("get self nickname failed", e.toString())
log("get self nickname failed", e.toString());
return setTimeout(init, 1000);
}
start();
// try {
// friends.push(...(await NTQQApi.getFriends(true)))
// log("get friends", friends)
// let _groups: Group[] = []
// for(let i=0; i++; i<3){
// try{
// _groups = await NTQQApi.getGroups(true)
// log("get groups sucess", _groups)
// break
// } catch(e) {
// log("get groups failed", e)
// }
// }
// for (let g of _groups) {
// g.members = (await NTQQApi.getGroupMembers(g.groupCode))
// log("group members", g.members)
// groups.push(g)
// }
// } catch (e) {
// log("!!!初始化失败", e.stack.toString())
// }
start().then();
} else {
setTimeout(() => {
getSelfInfo().then()
}, 100)
setTimeout(init, 1000)
}
}
getSelfInfo().then()
setTimeout(init, 1000);
}
// 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) {
try {
hookNTQQApiCall(window);
hookNTQQApiReceive(window);
} catch (e) {
log("llonebot hook error: ", e.toString())
log("LLOneBot hook error: ", e.toString())
}
}

View File

@@ -1,5 +1,15 @@
import {ElementType, SendPicElement, SendPttElement, SendReplyElement, SendTextElement, AtType} from "./types";
import {
AtType,
ElementType,
SendFaceElement,
SendPicElement,
SendPttElement,
SendReplyElement,
SendTextElement
} from "./types";
import {NTQQApi} from "./ntcall";
import {encodeSilk, log} from "../common/utils";
import fs from "fs";
export class SendMsgElementConstructor {
@@ -44,7 +54,7 @@ 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);
const imageSize = await NTQQApi.getImageSize(picPath);
const picElement = {
@@ -70,8 +80,13 @@ export class SendMsgElementConstructor {
};
}
static async ptt(pttPath: string):Promise<SendPttElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(pttPath);
static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath);
if (converted){
fs.unlink(silkPath, ()=>{});
}
return {
elementType: ElementType.PTT,
elementId: "",
@@ -80,7 +95,8 @@ export class SendMsgElementConstructor {
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration / 1000,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
@@ -94,4 +110,15 @@ export class SendMsgElementConstructor {
}
};
}
static face(faceId: number): SendFaceElement {
return {
elementType: ElementType.FACE,
elementId: "",
faceElement: {
faceIndex: faceId,
faceType: 1
}
}
}
}

View File

@@ -1,24 +1,27 @@
import {BrowserWindow} from 'electron';
import {getConfigUtil, log} from "../common/utils";
import {log, sleep} from "../common/utils";
import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import { Group, User } from "./types";
import { RawMessage } from "./types";
import {friends, groups, msgHistory} from "../common/data";
import { v4 as uuidv4 } from 'uuid';
import {Group, RawMessage, User} from "./types";
import {addHistoryMsg, friends, groups, msgHistory} from "../common/data";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {v4 as uuidv4} from "uuid"
import {postEvent} from "../onebot11/server/postevent";
export let hookApiCallbacks: Record<string, (apiReturn: any)=>void>={}
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export enum ReceiveCmd {
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
NEW_MSG = "nodeIKernelMsgListener/onRecvMsg",
SELF_SEND_MSG = "nodeIKernelMsgListener/onAddSendMsg",
USER_INFO = "nodeIKernelProfileListener/onProfileDetailInfoChanged",
USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
GROUPS = "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX = "onGroupListUpdate",
FRIENDS = "onBuddyListChange"
FRIENDS = "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete"
}
interface NTQQApiReturnData<PayloadType=unknown> extends Array<any> {
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: {
"type": "request",
"eventName": NTQQApiClass,
@@ -34,7 +37,7 @@ interface NTQQApiReturnData<PayloadType=unknown> extends Array<any> {
let receiveHooks: Array<{
method: ReceiveCmd,
hookFunc: (payload: any) => void,
hookFunc: ((payload: any) => void | Promise<void>)
id: string
}> = []
@@ -50,8 +53,11 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => {
try {
hook.hookFunc(receiveData.payload);
}catch (e) {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then()
}
} catch (e) {
log("hook error", e, receiveData.payload)
}
}).then()
@@ -59,10 +65,10 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
}
}
}
if (args[0]?.callbackId){
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]){
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]);
@@ -75,6 +81,24 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
window.webContents.send = patchSend;
}
export function hookNTQQApiCall(window: BrowserWindow) {
// 监听调用NTQQApi
let webContents = window.webContents as any;
const ipc_message_proxy = webContents._events["-ipc-message"]?.[0] || webContents._events["-ipc-message"];
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
log("call NTQQ api", thisArg, args);
return target.apply(thisArg, args);
},
});
// if (webContents._events["-ipc-message"]?.[0]) {
// webContents._events["-ipc-message"][0] = proxyIpcMsg;
// } else {
// webContents._events["-ipc-message"] = proxyIpcMsg;
// }
}
export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string {
const id = uuidv4()
receiveHooks.push({
@@ -85,70 +109,144 @@ export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (
return id;
}
export function removeReceiveHook(id: string){
const index = receiveHooks.findIndex(h=>h.id === id)
export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex(h => h.id === id)
receiveHooks.splice(index, 1);
}
async function updateGroups(_groups: Group[]){
for(let group of _groups){
let existGroup = groups.find(g=>g.groupCode == group.groupCode)
if (!existGroup){
// log("update group")
let _membeers = await NTQQApi.getGroupMembers(group.groupCode)
if (_membeers){
group.members = _membeers
}
log("update group members", group.members)
async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) {
let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) {
Object.assign(existGroup, group);
}
else{
group.members = [...existGroup.members]
else {
groups.push(group);
existGroup = group;
}
if (needUpdate) {
const members = await NTQQApi.getGroupMembers(group.groupCode);
if (members) {
existGroup.members = members;
}
}
}
groups.length = 0;
groups.push(..._groups)
}
registerReceiveHook<{groupList: Group[]}>(ReceiveCmd.GROUPS, (payload)=>updateGroups(payload.groupList).then())
registerReceiveHook<{groupList: Group[]}>(ReceiveCmd.GROUPS_UNIX, (payload)=>updateGroups(payload.groupList).then())
registerReceiveHook<{data:{categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[]}[]}>(ReceiveCmd.FRIENDS, payload=>{
friends.length = 0
for (const fData of payload.data) {
friends.push(...fData.buddyList)
async function processGroupEvent(payload) {
try {
const newGroupList = payload.groupList;
for (const group of newGroupList) {
let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) {
if (existGroup.memberCount > group.memberCount) {
const oldMembers = existGroup.members;
await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
group.members = newMembers;
const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度
for (const member of newMembers) {
newMembersSet.add(member.uin);
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) {
postEvent(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin)));
break;
}
}
}
else if (existGroup.memberCount < group.memberCount) {
const oldMembers = existGroup.members;
const oldMembersSet = new Set<string>();
for (const member of oldMembers) {
oldMembersSet.add(member.uin);
}
await sleep(200);
const newMembers = await NTQQApi.getGroupMembers(group.groupCode);
group.members = newMembers;
for (const member of newMembers) {
if (!oldMembersSet.has(member.uin)) {
postEvent(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin)));
break;
}
}
}
}
}
updateGroups(newGroupList, false).then();
}
catch (e) {
updateGroups(payload.groupList).then();
console.log(e);
}
}
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => {
if (payload.updateType != 2) {
updateGroups(payload.groupList).then();
}
else {
if (process.platform == "win32") {
processGroupEvent(payload).then();
}
}
})
// registerReceiveHook<any>(ReceiveCmd.USER_INFO, (payload)=>{
// log("user info", payload);
// })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, (payload) => {
for (const message of payload.msgList) {
msgHistory[message.msgId] = message;
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => {
if (payload.updateType != 2) {
updateGroups(payload.groupList).then();
}
else {
if (process.platform != "win32") {
processGroupEvent(payload).then();
}
}
})
registerReceiveHook<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
}>(ReceiveCmd.FRIENDS, payload => {
for (const fData of payload.data) {
const _friends = fData.buddyList;
for (let friend of _friends) {
let existFriend = friends.find(f => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
} else {
Object.assign(existFriend, friend)
}
}
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
for (const message of payload.msgList) {
log("收到新消息push到历史记录", message)
if (!msgHistory[message.msgId]){
msgHistory[message.msgId] = message
}
else{
Object.assign(msgHistory[message.msgId], message)
}
// log("收到新消息push到历史记录", message)
addHistoryMsg(message)
}
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 peerUid = message.peerUid;
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
const sendCallback = sendMessagePool[peerUid];
if (sendCallback){
try{
if (sendCallback) {
try {
sendCallback(message);
}catch(e){
} catch (e) {
log("receive self msg error", e.stack)
}
}

View File

@@ -1,13 +1,10 @@
import { ipcMain } from "electron";
import { v4 as uuidv4 } from "uuid";
import { ReceiveCmd, hookApiCallbacks, registerReceiveHook, removeReceiveHook } from "./hook";
import { log } from "../common/utils";
import { ChatType, Friend, PicElement, SelfInfo, User } from "./types";
import { Group } from "./types";
import { GroupMember } from "./types";
import { RawMessage } from "./types";
import { SendMessageElement } from "./types";
import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log} from "../common/utils";
import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types";
import * as fs from "fs";
import {addHistoryMsg, msgHistory, selfInfo} from "../common/data";
import {v4 as uuidv4} from "uuid"
interface IPCReceiveEvent {
eventName: string
@@ -29,7 +26,6 @@ export enum NTQQApiClass {
export enum NTQQApiMethod {
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelBuddyService/getBuddyList",
GROUPS = "nodeIKernelGroupService/getGroupList",
@@ -44,7 +40,8 @@ export enum NTQQApiMethod {
MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia"
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment" // 合并转发
}
enum NTQQApiChannel {
@@ -64,8 +61,26 @@ enum CallBackType {
METHOD
}
interface NTQQApiParams {
methodName: NTQQApiMethod,
className?: NTQQApiClass,
channel?: NTQQApiChannel,
args?: unknown[],
cbCmd?: ReceiveCmd | null,
cmdCB?: (payload: any) => boolean;
timeoutSecond?: number,
}
function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClass, methodName: NTQQApiMethod, args: unknown[] = [], cbCmd: ReceiveCmd | null = null, timeout = 5) {
function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let {
className, methodName, channel, args,
cbCmd, timeoutSecond: timeout,
cmdCB
} = params;
className = className ?? NTQQApiClass.NT_API;
channel = channel ?? NTQQApiChannel.IPC_UP_2;
args = args ?? [];
timeout = timeout ?? 5;
const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => {
@@ -78,20 +93,26 @@ function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClas
success = true
resolve(r)
};
}
else {
} else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result.result == 0) {
if (result?.result == 0 || result === undefined) {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
log(methodName, "second callback", cbCmd, payload);
removeReceiveHook(hookId);
success = true
resolve(payload);
if (cmdCB) {
if (cmdCB(payload)) {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
} else {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
})
}
else {
} else {
success = true
reject(`ntqq api call failed, ${result.errMsg}`);
}
@@ -104,16 +125,18 @@ function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClas
reject(`ntqq api timeout ${channel}, ${className}, ${methodName}`)
}
}, _timeout)
const eventName = className + "-" + channel[channel.length - 1];
const apiArgs = [methodName, ...args]
ipcMain.emit(
channel,
{},
{ type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1] },
[methodName, ...args],
{type: 'request', callbackId: uuid, eventName},
apiArgs
)
})
}
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
interface GeneralCallResult {
@@ -125,31 +148,49 @@ interface GeneralCallResult {
export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static likeFriend(uid: string, count = 1) {
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND, [{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null])
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
}, null]
})
}
static getSelfInfo() {
return callNTQQApi<SelfInfo>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.GLOBAL_DATA, NTQQApiMethod.SELF_INFO, [], null, 2)
return callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ info: User }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO,
[{ force: true, uids: [uid] }, undefined], ReceiveCmd.USER_INFO)
return result.info
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{force: true, uids: [uid]}, undefined],
cbCmd: ReceiveCmd.USER_INFO
})
return result.profiles.get(uid)
}
static async getFriends(forced = false) {
const data = await callNTQQApi<{ data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: Friend[] }[] }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.FRIENDS, [{ force_update: forced }, undefined], ReceiveCmd.FRIENDS)
const data = await callNTQQApi<{
data: {
categoryId: number,
categroyName: string,
categroyMbCount: number,
buddyList: Friend[]
}[]
}>(
{
methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmd.FRIENDS
})
let _friends: Friend[] = [];
for (const fData of data.data) {
_friends.push(...fData.buddyList)
@@ -162,30 +203,44 @@ export class NTQQApi {
if (process.platform != "win32") {
cbCmd = ReceiveCmd.GROUPS_UNIX
}
const result = await callNTQQApi<{ updateType: number, groupList: Group[] }>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUPS, [{ force_update: forced }, undefined], cbCmd)
const result = await callNTQQApi<{
updateType: number,
groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000) {
const sceneId = await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBER_SCENE, [{
groupCode: groupQQ,
scene: "groupMemberList_MainWindow"
}])
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [{
groupCode: groupQQ,
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId);
try {
const result = await callNTQQApi<{result:{infos: any}}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBERS,
[{
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId: sceneId,
num: num
},
null
])
]
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
let values = result.result.infos.values()
values = Array.from(values) as GroupMember[]
let members = Array.from(values) as GroupMember[]
for (const member of members) {
// uidMaps[member.uid] = member.uin;
}
// log(uidMaps);
// log("members info", values);
return values
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
return []
@@ -193,48 +248,66 @@ export class NTQQApi {
}
static getFileType(filePath: string) {
return callNTQQApi<{
ext: string
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_TYPE, [filePath])
return callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath]
})
}
static getFileMd5(filePath: string) {
return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_MD5, [filePath])
return callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_MD5,
args: [filePath]
})
}
static copyFile(filePath: string, destPath: string) {
return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_COPY, [{ fromPath: filePath, toPath: destPath }])
return callNTQQApi<string>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_COPY, args: [{
fromPath: filePath,
toPath: destPath
}]
})
}
static getImageSize(filePath: string) {
return callNTQQApi<{
width: number,
height: number
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.IMAGE_SIZE, [filePath])
return callNTQQApi<{ width: number, height: number }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath]
})
}
static getFileSize(filePath: string) {
return callNTQQApi<number>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_SIZE, [filePath])
return callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath]
})
}
// 上传文件到QQ的文件夹
static async uploadFile(filePath: string) {
const md5 = await NTQQApi.getFileMd5(filePath);
const fileName = `${md5}.${(await NTQQApi.getFileType(filePath)).ext}`;
const mediaPath = await callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.MEDIA_FILE_PATH, [{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: 2,
elementSubType: 0,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ""
}
}])
let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) {
ext = "." + ext
} else {
ext = ""
}
const fileName = `${md5}${ext}`;
const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: 2,
elementSubType: 0,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ""
}
}]
})
log("media path", mediaPath)
await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath);
@@ -246,9 +319,9 @@ export class NTQQApi {
}
}
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string){
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string) {
// 用于下载收到的消息中的图片等
if (fs.existsSync(sourcePath)){
if (fs.existsSync(sourcePath)) {
return sourcePath
}
const apiParams = [
@@ -265,24 +338,40 @@ export class NTQQApi {
},
undefined,
]
await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.DOWNLOAD_MEDIA, apiParams)
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE,
cmdCB:(payload: {notifyInfo: {filePath: string}})=>{
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath);
return payload.notifyInfo.filePath == sourcePath;
}})
return sourcePath
}
static recallMsg(peer: Peer, msgIds: string[]) {
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.RECALL_MSG, [{ peer, msgIds }, null])
return callNTQQApi({
methodName: NTQQApiMethod.RECALL_MSG, args: [{
peer,
msgIds
}, null]
})
}
static sendMsg(peer: Peer, msgElements: SendMessageElement[]) {
const sendTimeout = 10 * 1000
static sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false, timeout = 10000) {
const sendTimeout = timeout
return new Promise<RawMessage>((resolve, reject) => {
const peerUid = peer.peerUid;
let usingTime = 0;
let success = false;
let isTimeout = false;
const checkSuccess = () => {
if (!success) {
sendMessagePool[peerUid] = null;
isTimeout = true;
reject("发送超时")
}
}
@@ -292,30 +381,99 @@ export class NTQQApi {
let lastSending = sendMessagePool[peerUid]
if (sendTimeout < usingTime) {
sendMessagePool[peerUid] = null;
isTimeout = true;
reject("发送超时")
}
if (!!lastSending) {
// log("有正在发送的消息,等待中...")
usingTime += 100;
setTimeout(checkLastSend, 100);
}
else {
usingTime += 500;
setTimeout(checkLastSend, 500);
} else {
log("可以进行发送消息,设置发送成功回调", sendMessagePool)
sendMessagePool[peerUid] = (rawMessage: RawMessage) => {
success = true;
sendMessagePool[peerUid] = null;
resolve(rawMessage);
const checkSendComplete = () => {
if (isTimeout) {
return reject("发送超时")
}
if (msgHistory[rawMessage.msgId]?.sendStatus == 2) {
success = true;
resolve(rawMessage);
} else {
setTimeout(checkSendComplete, 500)
}
}
if (waitComplete) {
checkSendComplete();
} else {
success = true;
resolve(rawMessage);
}
}
}
}
checkLastSend()
callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.SEND_MSG, [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]).then()
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
})
}
static multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
let msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: "LLOneBot"}
})
const apiArgs = [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
return new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject("转发消息超时");
}
}, 5000)
registerReceiveHook(ReceiveCmd.SELF_SEND_MSG, (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord;
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
// log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData);
if (forwardData.app != "com.tencent.multimsg") {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true;
addHistoryMsg(msg)
resolve(msg);
log("转发消息成功:", payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
}
}

View File

@@ -7,13 +7,14 @@ export interface User {
remark?: string
}
export interface SelfInfo extends User{
export interface SelfInfo extends User {
}
export interface Friend extends User{}
export interface Friend extends User {
}
export interface Group{
export interface Group {
groupCode: string,
maxMember: number,
memberCount: number,
@@ -62,6 +63,7 @@ export enum ElementType {
TEXT = 1,
PIC = 2,
PTT = 4,
FACE = 6,
REPLY = 7,
}
@@ -76,6 +78,7 @@ export interface SendTextElement {
atNtUid: string,
}
}
export interface SendPttElement {
elementType: ElementType.PTT,
elementId: "",
@@ -127,7 +130,13 @@ export interface SendReplyElement {
}
}
export type SendMessageElement = SendTextElement | SendPttElement | SendPicElement | SendReplyElement
export interface SendFaceElement {
elementType: ElementType.FACE,
elementId: "",
faceElement: FaceElement
}
export type SendMessageElement = SendTextElement | SendPttElement | SendPicElement | SendReplyElement | SendFaceElement
export enum AtType {
notAt = 0,
@@ -140,6 +149,7 @@ export enum ChatType {
group = 2,
temp = 100
}
export interface PttElement {
canConvert2Text: boolean;
duration: number; // 秒数
@@ -180,16 +190,36 @@ export interface PicElement {
fileUuid: string;
}
export interface GrayTipElement {
revokeElement: {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
}
}
export interface FaceElement {
faceIndex: number,
faceType: 1
}
export interface RawMessage {
msgId: string;
msgShortId?: number; // 自己维护的消息id
msgTime: string;
msgSeq: string;
senderUin: string; // 发送者QQ号
senderUid: string;
senderUin?: string; // 发送者QQ号
peerUid: string; // 群号 或者 QQ uid
peerUin: string; // 群号 或者 发送者QQ号
sendNickName: string;
sendMemberName?: string; // 发送者群名片
chatType: ChatType;
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string; // 撤回时间, "0"是没有撤回
elements: {
elementId: string,
replyElement: {
@@ -207,17 +237,7 @@ export interface RawMessage {
picElement: PicElement;
pttElement: PttElement;
arkElement: ArkElement;
grayTipElement: GrayTipElement;
faceElement: FaceElement;
}[];
}
export interface MessageElement {
raw: RawMessage;
peer: any;
sender: {
uid: string; // 一串加密的字符串
memberName: string;
nickname: string;
};
}

View File

@@ -0,0 +1,44 @@
import {ActionName, BaseCheckResult} from "./types"
import {OB11Response} from "./utils"
import {OB11Return} from "../types";
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
valid: true,
}
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload);
if (!result.valid) {
return OB11Response.error(result.message, 400);
}
try {
const resData = await this._handle(payload);
return OB11Response.ok(resData);
} catch (e) {
return OB11Response.error(e.toString(), 200);
}
}
public async websocketHandle(payload: PayloadType, echo: string): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload)
if (!result.valid) {
return OB11Response.error(result.message, 1400)
}
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo);
} catch (e) {
return OB11Response.error(e.toString(), 1200, echo)
}
}
protected async _handle(payload: PayloadType): Promise<ReturnDataType> {
throw `pleas override ${this.actionName} _handle`;
}
}
export default BaseAction

View File

@@ -0,0 +1,10 @@
import {ActionName} from "./types";
import CanSendRecord from "./CanSendRecord";
interface ReturnType{
yes: boolean
}
export default class CanSendImage extends CanSendRecord{
actionName = ActionName.CanSendImage
}

View File

@@ -0,0 +1,16 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
interface ReturnType{
yes: boolean
}
export default class CanSendRecord extends BaseAction<any, ReturnType>{
actionName = ActionName.CanSendRecord
protected async _handle(payload): Promise<ReturnType>{
return {
yes: true
}
}
}

View File

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

View File

@@ -1,8 +1,8 @@
import { OB11User } from '../types';
import { OB11Constructor } from "../constructor";
import { friends } from "../../common/data";
import {OB11User} from '../types';
import {OB11Constructor} from "../constructor";
import {friends} from "../../common/data";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import {ActionName} from "./types";
class GetFriendList extends BaseAction<null, OB11User[]> {

View File

@@ -1,24 +1,24 @@
import { OB11Group } from '../types';
import { getGroup, groups } from "../../common/data";
import { OB11Constructor } from "../constructor";
import {OB11Group} from '../types';
import {getGroup} from "../../common/data";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import {ActionName} from "./types";
interface PayloadType {
group_id: number
}
class GetGroupInfo extends BaseAction<PayloadType, OB11Group[]> {
class GetGroupInfo extends BaseAction<PayloadType, OB11Group> {
actionName = ActionName.GetGroupInfo
protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString())
if (group) {
return OB11Constructor.groups(groups)
return OB11Constructor.group(group)
} else {
throw `${payload.group_id}不存在`
}
}
}
export default GetGroupInfo
export default GetGroupInfo

View File

@@ -1,9 +1,8 @@
import { OB11Group } from '../types';
import { OB11Constructor } from "../constructor";
import { groups } from "../../common/data";
import {OB11Group} from '../types';
import {OB11Constructor} from "../constructor";
import {groups} from "../../common/data";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import {ActionName} from "./types";
class GetGroupList extends BaseAction<null, OB11Group[]> {

View File

@@ -1,8 +1,8 @@
import { OB11GroupMember } from '../types';
import { getGroupMember } from "../../common/data";
import { OB11Constructor } from "../constructor";
import {OB11GroupMember} from '../types';
import {getGroupMember} from "../../common/data";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import {ActionName} from "./types";
export interface PayloadType {

View File

@@ -1,9 +1,9 @@
import { OB11GroupMember } from '../types';
import { getGroup } from "../../common/data";
import { NTQQApi } from "../../ntqqapi/ntcall";
import { OB11Constructor } from "../constructor";
import {OB11GroupMember} from '../types';
import {getGroup} from "../../common/data";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import {ActionName} from "./types";
export interface PayloadType {
group_id: number

View File

@@ -1,8 +1,8 @@
import { OB11User } from '../types';
import { OB11Constructor } from "../constructor";
import { selfInfo } from "../../common/data";
import {OB11User} from '../types';
import {OB11Constructor} from "../constructor";
import {selfInfo} from "../../common/data";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import {ActionName} from "./types";
class GetLoginInfo extends BaseAction<null, OB11User> {

View File

@@ -1,13 +1,12 @@
import { msgHistory } from "../../common/data";
import { OB11Message } from '../types';
import { OB11Constructor } from "../constructor";
import { log } from "../../common/utils";
import {getHistoryMsgByShortId} from "../../common/data";
import {OB11Message} from '../types';
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
import {ActionName} from "./types";
export interface PayloadType {
message_id: string
message_id: number
}
export type ReturnDataType = OB11Message
@@ -17,7 +16,10 @@ class GetMsg extends BaseAction<PayloadType, OB11Message> {
protected async _handle(payload: PayloadType){
// log("history msg ids", Object.keys(msgHistory));
const msg = msgHistory[payload.message_id.toString()]
if (!payload.message_id){
throw("参数message_id不能为空")
}
const msg = getHistoryMsgByShortId(payload.message_id)
if (msg) {
const msgData = await OB11Constructor.message(msg);
return msgData

View File

@@ -0,0 +1,14 @@
import BaseAction from "./BaseAction";
import {OB11Status} from "../types";
import {ActionName} from "./types";
export default class GetStatus extends BaseAction<any, OB11Status> {
actionName = ActionName.GetStatus
protected async _handle(payload: any): Promise<OB11Status> {
return {
online: null,
good: true
}
}
}

View File

@@ -0,0 +1,15 @@
import BaseAction from "./BaseAction";
import {OB11Version} from "../types";
import {version} from "../../common/data";
import {ActionName} from "./types";
export default class GetVersionInfo extends BaseAction<any, OB11Version>{
actionName = ActionName.GetVersionInfo
protected async _handle(payload: any): Promise<OB11Version> {
return {
app_name: "LLOneBot",
protocol_version: "v11",
app_version: version
}
}
}

View File

@@ -1,5 +1,5 @@
import SendMsg from "./SendMsg";
import { ActionName } from "./types";
import {ActionName} from "./types";
class SendGroupMsg extends SendMsg{

View File

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

View File

@@ -0,0 +1,272 @@
import {AtType, ChatType, Group, SendMessageElement} from "../../ntqqapi/types";
import {addHistoryMsg, friends, getGroup, getHistoryMsgByShortId, getUidByUin, selfInfo,} from "../../common/data";
import {OB11MessageData, OB11MessageDataType, OB11MessageMixType, OB11MessageNode, OB11PostSendMsg} from '../types';
import {NTQQApi, Peer} from "../../ntqqapi/ntcall";
import {SendMsgElementConstructor} from "../../ntqqapi/constructor";
import {uri2local} from "../utils";
import BaseAction from "./BaseAction";
import {ActionName, BaseCheckResult} from "./types";
import * as fs from "fs";
import {log} from "../../common/utils";
import {v4 as uuidv4} from "uuid"
import {parseCQCode} from "../cqcode";
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (let msg of sendMsgList) {
if (msg["type"] && msg["data"]) {
let type = msg["type"];
let data = msg["data"];
if (type === "text" && !data["text"]) {
return 400;
} else if (["image", "voice", "record"].includes(type)) {
if (!data["file"]) {
return 400;
} else {
if (checkUri(data["file"])) {
return 200;
} else {
return 400;
}
}
} else if (type === "at" && !data["qq"]) {
return 400;
} else if (type === "reply" && !data["id"]) {
return 400;
}
} else {
return 400
}
}
return 200;
}
export interface ReturnDataType {
message_id: number
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = this.convertMessage2List(payload.message);
const fmNum = this.forwardMsgNum(payload)
if (fmNum && fmNum != messages.length) {
return {
valid: false,
message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素"
}
}
return {
valid: true,
}
}
protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = {
chatType: ChatType.friend,
peerUid: ""
}
let group: Group | undefined = undefined;
if (payload?.group_id) {
group = await getGroup(payload.group_id.toString())
if (!group) {
throw (`${payload.group_id}不存在`)
}
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group.groupCode
} else if (payload?.user_id) {
const friend = friends.find(f => f.uin == payload.user_id.toString())
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid
} else {
peer.chatType = ChatType.temp
const tempUserUid = getUidByUin(payload.user_id.toString())
if (!tempUserUid) {
throw (`找不到私聊对象${payload.user_id}`)
}
// peer.name = tempUser.nickName
peer.peerUid = tempUserUid;
}
}
const messages = this.convertMessage2List(payload.message);
if (this.forwardMsgNum(payload)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw ("发送转发消息失败 " + e.toString())
}
}
// log("send msg:", peer, sendElements)
const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group)
try {
const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles)
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw (e.toString())
}
}
protected convertMessage2List(message: OB11MessageMixType) {
if (typeof message === "string") {
// message = [{
// type: OB11MessageDataType.text,
// data: {
// text: message
// }
// }] as OB11MessageData[]
message = parseCQCode(message.toString())
} else if (!Array.isArray(message)) {
message = [message]
}
return message;
}
private forwardMsgNum(payload: OB11PostSendMsg): number {
if (Array.isArray(payload.message)) {
return payload.message.filter(msg => msg.type == OB11MessageDataType.node).length
}
return 0
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) {
const selfPeer: Peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
let nodeIds: string[] = []
for (const messageNode of messageNodes){
// 一个node表示一个人的消息
let nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
let nodeMsg = getHistoryMsgByShortId(nodeId);
if (nodeMsg){
nodeIds.push(nodeMsg.msgId);
}
} else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const {
sendElements,
deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group);
log("开始生成转发节点", sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
nodeIds.push(nodeMsg.msgId)
log("转发节点生成成功", nodeMsg.msgId);
} catch (e) {
log("生效转发消息节点失败", e)
}
}
}
// 开发转发
try {
return await NTQQApi.multiForwardMsg(selfPeer, destPeer, nodeIds)
} catch (e) {
log("forward failed", e)
return null;
}
}
private async createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) {
let sendElements: SendMessageElement[] = []
let deleteAfterSentFiles: string[] = []
for (let sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue
}
switch (sendMsg.type) {
case OB11MessageDataType.text: {
const text = sendMsg.data?.text;
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
}
break;
case OB11MessageDataType.at: {
let atQQ = sendMsg.data?.qq;
if (atQQ) {
atQQ = atQQ.toString()
if (atQQ === "all") {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, "全体成员"))
} else {
const atMember = group?.members.find(m => m.uin == atQQ)
if (atMember) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick))
}
}
}
}
break;
case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id;
if (replyMsgId) {
replyMsgId = replyMsgId.toString()
const replyMsg = getHistoryMsgByShortId(replyMsgId)
if (replyMsg) {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin))
}
}
}
break;
case OB11MessageDataType.face: {
const faceId = sendMsg.data?.id
if (faceId) {
sendElements.push(SendMsgElementConstructor.face(parseInt(faceId)))
}
}
break;
case OB11MessageDataType.image:
case OB11MessageDataType.voice: {
const file = sendMsg.data?.file
if (file) {
const {path, isLocal} = (await uri2local(uuidv4(), file))
if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(await SendMsgElementConstructor.pic(path))
} else {
sendElements.push(await SendMsgElementConstructor.ptt(path))
}
}
}
}
break;
}
}
return {
sendElements,
deleteAfterSentFiles
}
}
private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = false) {
if (!sendElements.length) {
throw ("消息体无法解析")
}
const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete, 20000);
addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))
return returnMsg
}
}
export default SendMsg

View File

@@ -1,5 +1,5 @@
import SendMsg from "./SendMsg";
import { ActionName } from "./types";
import {ActionName} from "./types";
class SendPrivateMsg extends SendMsg {
actionName = ActionName.SendPrivateMsg

View File

@@ -0,0 +1,24 @@
import BaseAction from "../BaseAction";
import {OB11GroupMember, OB11User} from "../../types";
import {friends, getFriend, getGroupMember, groups} from "../../../common/data";
import {OB11Constructor} from "../../constructor";
import {ActionName} from "../types";
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{user_id: number}, OB11User>{
actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString()
const friend = await getFriend(user_id)
if (friend){
return OB11Constructor.friend(friend);
}
for(const group of groups){
const member = await getGroupMember(group.groupCode, user_id)
if (member){
return OB11Constructor.groupMember(group.groupCode, member) as OB11User
}
}
throw ("查无此人")
}
}

View File

@@ -0,0 +1,15 @@
import SendMsg, {ReturnDataType} from "../SendMsg";
import {OB11MessageMixType, OB11PostSendMsg} from "../../types";
import {ActionName, BaseCheckResult} from "../types";
export class GoCQHTTPSendGroupForwardMsg extends SendMsg{
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
protected async check(payload: OB11PostSendMsg){
payload.message = this.convertMessage2List(payload.messages);
return super.check(payload);
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendGroupForwardMsg{
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
}

View File

@@ -0,0 +1,50 @@
import GetMsg from './GetMsg'
import GetLoginInfo from './GetLoginInfo'
import GetFriendList from './GetFriendList'
import GetGroupList from './GetGroupList'
import GetGroupInfo from './GetGroupInfo'
import GetGroupMemberList from './GetGroupMemberList'
import GetGroupMemberInfo from './GetGroupMemberInfo'
import SendGroupMsg from './SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg";
import BaseAction from "./BaseAction";
import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage";
import GetStatus from "./GetStatus";
import {GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg";
import GoCQHTTPGetStrangerInfo from "./go-cqhttp/GetStrangerInfo";
import SendLike from "./SendLike";
export const actionHandlers = [
new SendLike(),
new GetMsg(),
new GetLoginInfo(),
new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg(),
new GetVersionInfo(),
new CanSendRecord(),
new CanSendImage(),
new GetStatus(),
//以下为go-cqhttp api
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo()
]
function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>();
for (const action of actionHandlers) {
actionMap.set(action.actionName, action);
}
return actionMap
}
export const actionMap = initActionMap();

View File

@@ -11,7 +11,9 @@ export interface InvalidCheckResult {
[k: string | number]: any
}
export enum ActionName{
export enum ActionName {
TestForwardMsg = "test_forward_msg",
SendLike = "send_like",
GetLoginInfo = "get_login_info",
GetFriendList = "get_friend_list",
GetGroupInfo = "get_group_info",
@@ -22,5 +24,13 @@ export enum ActionName{
SendMsg = "send_msg",
SendGroupMsg = "send_group_msg",
SendPrivateMsg = "send_private_msg",
DeleteMsg = "delete_msg"
DeleteMsg = "delete_msg",
GetVersionInfo = "get_version_info",
GetStatus = "get_status",
CanSendRecord = "can_send_record",
CanSendImage = "can_send_image",
// 以下为go-cqhttp api
GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg",
GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg",
GoCQHTTP_GetStrangerInfo = "get_stranger_info"
}

View File

@@ -0,0 +1,30 @@
import {OB11Return} from '../types';
export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {
return {
status: status,
retcode: retcode,
data: data,
message: message,
wording: message,
echo: ""
}
}
static ok<T>(data: T, echo: string = "") {
let res = OB11Response.res<T>(data, "ok", 0)
if (echo) {
res.echo = echo;
}
return res;
}
static error(err: string, retcode: number, echo: string = "") {
let res = OB11Response.res(null, "failed", retcode, err)
if (echo) {
res.echo = echo;
}
return res;
}
}

View File

@@ -1,31 +0,0 @@
import {ActionName, BaseCheckResult} from "./types"
import { OB11Response } from "./utils"
import { OB11Return } from "../types";
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return {
valid: true,
}
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload)
if (!result.valid) {
return OB11Response.error(result.message)
}
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData)
}catch (e) {
return OB11Response.error(e.toString())
}
}
protected async _handle(payload: PayloadType): Promise<ReturnDataType> {
throw `pleas override ${this.actionName} _handle`
}
}
export default BaseAction

View File

@@ -1,127 +0,0 @@
import { AtType, ChatType, Group } from "../../ntqqapi/types";
import { friends, getGroup, getStrangerByUin, msgHistory } from "../../common/data";
import { OB11MessageData, OB11MessageDataType, OB11PostSendMsg } from '../types';
import { NTQQApi } from "../../ntqqapi/ntcall";
import { Peer } from "../../ntqqapi/ntcall";
import { SendMessageElement } from "../../ntqqapi/types";
import { SendMsgElementConstructor } from "../../ntqqapi/constructor";
import { uri2local } from "../utils";
import { v4 as uuid4 } from 'uuid';
import { log } from "../../common/utils";
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
export interface ReturnDataType {
message_id: string
}
class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
protected async _handle(payload: OB11PostSendMsg){
const peer: Peer = {
chatType: ChatType.friend,
peerUid: ""
}
let group: Group | undefined = undefined;
if (payload?.group_id) {
group = await getGroup(payload.group_id.toString())
if (!group) {
throw (`${payload.group_id}不存在`)
}
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group.groupCode
}
else if (payload?.user_id) {
const friend = friends.find(f => f.uin == payload.user_id.toString())
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid
}
else {
peer.chatType = ChatType.temp
const tempUser = getStrangerByUin(payload.user_id.toString())
if (!tempUser) {
throw(`找不到私聊对象${payload.user_id}`)
}
// peer.name = tempUser.nickName
peer.peerUid = tempUser.uid
}
}
if (typeof payload.message === "string") {
payload.message = [{
type: OB11MessageDataType.text,
data: {
text: payload.message
}
}] as OB11MessageData[]
}
else if (!Array.isArray(payload.message)) {
payload.message = [payload.message]
}
const sendElements: SendMessageElement[] = []
for (let sendMsg of payload.message) {
switch (sendMsg.type) {
case OB11MessageDataType.text: {
const text = sendMsg.data?.text;
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
} break;
case OB11MessageDataType.at: {
let atQQ = sendMsg.data?.qq;
if (atQQ) {
atQQ = atQQ.toString()
if (atQQ === "all") {
sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, "全体成员"))
}
else {
const atMember = group?.members.find(m => m.uin == atQQ)
if (atMember) {
sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick))
}
}
}
} break;
case OB11MessageDataType.reply: {
let replyMsgId = sendMsg.data.id;
if (replyMsgId) {
replyMsgId = replyMsgId.toString()
const replyMsg = msgHistory[replyMsgId]
if (replyMsg) {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsgId, replyMsg.senderUin, replyMsg.senderUin))
}
}
} break;
case OB11MessageDataType.image: {
const file = sendMsg.data?.file
if (file) {
const picPath = await (await uri2local(uuid4(), file)).path
if (picPath) {
sendElements.push(await SendMsgElementConstructor.pic(picPath))
}
}
} break;
case OB11MessageDataType.voice: {
const file = sendMsg.data?.file
if (file) {
const voicePath = await (await uri2local(uuid4(), file)).path
if (voicePath) {
sendElements.push(await SendMsgElementConstructor.ptt(voicePath))
}
}
}
}
}
// log("send msg:", peer, sendElements)
try {
const returnMsg = await NTQQApi.sendMsg(peer, sendElements)
return { message_id: returnMsg.msgId }
} catch (e) {
throw(e.toString())
}
}
}
export default SendMsg

View File

@@ -1,20 +0,0 @@
import GetMsg from './GetMsg'
import GetLoginInfo from './GetLoginInfo'
import GetFriendList from './GetFriendList'
import GetGroupList from './GetGroupList'
import GetGroupInfo from './GetGroupInfo'
import GetGroupMemberList from './GetGroupMemberList'
import GetGroupMemberInfo from './GetGroupMemberInfo'
import SendGroupMsg from './SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg";
export const actionHandlers = [
new GetMsg(),
new GetLoginInfo(),
new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg()
]

View File

@@ -1,18 +0,0 @@
import { OB11Return } from '../types';
export class OB11Response {
static res<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> {
return {
status: status,
retcode: status,
data: data,
message: message
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(data)
}
static error(err: string) {
return OB11Response.res(null, -1, err)
}
}

View File

@@ -1,23 +1,43 @@
import {OB11MessageDataType, OB11GroupMemberRole, OB11Message, OB11MessageData, OB11Group, OB11GroupMember, OB11User} from "./types";
import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/types';
import { getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo } from '../common/data';
import {
OB11Group,
OB11GroupMember,
OB11GroupMemberRole,
OB11Message,
OB11MessageData,
OB11MessageDataType,
OB11User
} from "./types";
import {
AtType,
ChatType,
Friend,
Group,
GroupMember,
IMAGE_HTTP_HOST,
RawMessage,
SelfInfo,
User
} from '../ntqqapi/types';
import {getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo} from '../common/data';
import {file2base64, getConfigUtil, log} from "../common/utils";
import { NTQQApi } from "../ntqqapi/ntcall";
import {NTQQApi} from "../ntqqapi/ntcall";
import {EventType} from "./event/OB11BaseEvent";
export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> {
const {enableBase64} = getConfigUtil().getConfig()
const {enableLocalFile2Url} = getConfigUtil().getConfig()
const message_type = msg.chatType == ChatType.group ? "group" : "private";
const resMsg: OB11Message = {
self_id: selfInfo.uin,
user_id: msg.senderUin,
self_id: parseInt(selfInfo.uin),
user_id: parseInt(msg.senderUin),
time: parseInt(msg.msgTime) || 0,
message_id: msg.msgId,
message_id: msg.msgShortId,
real_id: msg.msgId,
message_type: msg.chatType == ChatType.group ? "group" : "private",
sender: {
user_id: msg.senderUin,
user_id: parseInt(msg.senderUin),
nickname: msg.sendNickName,
card: msg.sendMemberName || "",
},
@@ -25,13 +45,15 @@ export class OB11Constructor {
font: 14,
sub_type: "friend",
message: [],
post_type: "message",
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
}
if (msg.chatType == ChatType.group) {
resMsg.group_id = msg.peerUin
resMsg.sub_type = "normal" // 这里go-cqhttp是group而onebot11标准是normal, 蛋疼
resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin);
if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role);
resMsg.sender.nickname = member.nick
}
} else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = "friend"
@@ -44,7 +66,7 @@ export class OB11Constructor {
}
for (let element of msg.elements) {
let message_data: any = {
let message_data: OB11MessageData | any = {
data: {},
type: "unknown"
}
@@ -54,39 +76,54 @@ export class OB11Constructor {
message_data["data"]["mention"] = "all"
message_data["data"]["qq"] = "all"
} else {
let atUid = element.textElement.atNtUid
let atQQ = element.textElement.atUid
// let atMember = await getGroupMember(msg.peerUin, uid)
message_data["data"]["mention"] = atQQ
message_data["data"]["qq"] = atQQ
if (!atQQ || atQQ === "0") {
const atMember = await getGroupMember(msg.peerUin, null, atUid)
if (atMember) {
atQQ = atMember.uin
}
}
if (atQQ) {
message_data["data"]["mention"] = atQQ
message_data["data"]["qq"] = atQQ
}
}
} else if (element.textElement) {
message_data["type"] = "text"
message_data["data"]["text"] = element.textElement.content
let text= element.textElement.content
if (!text.trim()){
continue;
}
message_data["data"]["text"] = text
if (text){
resMsg.raw_message += text
}
} else if (element.picElement) {
message_data["type"] = "image"
message_data["data"]["file_id"] = element.picElement.fileUuid
message_data["data"]["path"] = element.picElement.sourcePath
message_data["data"]["file"] = element.picElement.sourcePath
message_data["data"]["http_file"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl
try {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.picElement.thumbPath.get(0), element.picElement.sourcePath)
}catch (e) {
message_data["data"]["http_file"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl
} catch (e) {
}
} else if (element.replyElement) {
message_data["type"] = "reply"
const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq)
if (replyMsg) {
message_data["data"]["id"] = replyMsg.msgId
}
else{
message_data["data"]["id"] = replyMsg.msgShortId
} else {
continue
}
} else if (element.pttElement) {
message_data["type"] = OB11MessageDataType.voice;
message_data["data"]["file"] = element.pttElement.filePath
message_data["data"]["file_id"] = element.pttElement.fileUuid
// console.log("收到语音消息", message.raw.msgId, message.peer, element.pttElement)
// log("收到语音消息", msg)
// window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
// console.log("语音转文字结果", text);
// }).catch(err => {
@@ -95,20 +132,24 @@ export class OB11Constructor {
} else if (element.arkElement) {
message_data["type"] = OB11MessageDataType.json;
message_data["data"]["data"] = element.arkElement.bytesData;
} else if (element.faceElement) {
message_data["type"] = OB11MessageDataType.face;
message_data["data"]["id"] = element.faceElement.faceIndex.toString();
}
if (message_data.data.http_file){
message_data.data.file = message_data.data.http_file
}
else if (message_data.data.file) {
if (message_data.data.file) {
let filePath: string = message_data.data.file;
message_data.data.file = "file://" + filePath
if (enableBase64) {
// filePath = filePath.replace("\\Ori\\", "\\Thumb\\")
let {err, data} = await file2base64(filePath);
if (err) {
console.log("文件转base64失败", err)
if (!enableLocalFile2Url) {
message_data.data.file = "file://" + filePath
} else { // 不使用本地路径
if (message_data.data.http_file && !message_data.data.http_file.startsWith(IMAGE_HTTP_HOST + "/download")) {
message_data.data.file = message_data.data.http_file
} else {
message_data.data.file = "base64://" + data
let {err, data} = await file2base64(filePath);
if (err) {
log("文件转base64失败", filePath, err)
} else {
message_data.data.file = "base64://" + data
}
}
}
}
@@ -116,36 +157,27 @@ export class OB11Constructor {
resMsg.message.push(message_data);
}
}
// if (msgHistory.length > 10000) {
// msgHistory.splice(0, 100)
// }
// msgHistory.push(message)
// if (!reportSelfMessage && onebot_message_data["user_id"] == self_qq) {
// console.log("开启了不上传自己发送的消息,进行拦截 ", onebot_message_data);
// } else {
// console.log("发送上传消息给ipc main", onebot_message_data);
// window.llonebot.postData(onebot_message_data);
// }
resMsg.raw_message = resMsg.raw_message.trim();
return resMsg;
}
static friend(friend: User): OB11User{
static friend(friend: User): OB11User {
return {
user_id: friend.uin,
user_id: parseInt(friend.uin),
nickname: friend.nick,
remark: friend.remark
}
}
static selfInfo(selfInfo: SelfInfo): OB11User{
static selfInfo(selfInfo: SelfInfo): OB11User {
return {
user_id: selfInfo.uin,
user_id: parseInt(selfInfo.uin),
nickname: selfInfo.nick
}
}
static friends(friends: User[]): OB11User[]{
static friends(friends: User[]): OB11User[] {
return friends.map(OB11Constructor.friend)
}
@@ -157,28 +189,30 @@ export class OB11Constructor {
}[role]
}
static groupMember(group_id: string, member: GroupMember): OB11GroupMember{
static groupMember(group_id: string, member: GroupMember): OB11GroupMember {
return {
group_id,
user_id: member.uin,
group_id: parseInt(group_id),
user_id: parseInt(member.uin),
nickname: member.nick,
card: member.cardName
}
}
static groupMembers(group: Group): OB11GroupMember[]{
static groupMembers(group: Group): OB11GroupMember[] {
log("construct ob11 group members", group)
return group.members.map(m=>OB11Constructor.groupMember(group.groupCode, m))
return group.members.map(m => OB11Constructor.groupMember(group.groupCode, m))
}
static group(group: Group): OB11Group{
static group(group: Group): OB11Group {
return {
group_id: group.groupCode,
group_name: group.groupName
group_id: parseInt(group.groupCode),
group_name: group.groupName,
member_count: group.memberCount,
max_member_count: group.maxMember
}
}
static groups(groups: Group[]): OB11Group[]{
static groups(groups: Group[]): OB11Group[] {
return groups.map(OB11Constructor.group)
}
}

49
src/onebot11/cqcode.ts Normal file
View File

@@ -0,0 +1,49 @@
import {OB11MessageData} from "./types";
const pattern = /\[CQ:(\w+)((,\w+=[^,\]]*)*)\]/
function unescape(source: string) {
return String(source)
.replace(/&#91;/g, '[')
.replace(/&#93;/g, ']')
.replace(/&#44;/g, ',')
.replace(/&amp;/g, '&')
}
function from(source: string) {
const capture = pattern.exec(source)
if (!capture) return null
const [, type, attrs] = capture
const data: Record<string, any> = {}
attrs && attrs.slice(1).split(',').forEach((str) => {
const index = str.indexOf('=')
data[str.slice(0, index)] = unescape(str.slice(index + 1))
})
return {type, data, capture}
}
function h(type: string, data: any) {
return {
type,
data,
}
}
export function parseCQCode(source: string): OB11MessageData[] {
const elements: any[] = []
let result: ReturnType<typeof from>
while ((result = from(source))) {
const {type, data, capture} = result
if (capture.index) {
elements.push(h('text', {text: unescape(source.slice(0, capture.index))}))
}
elements.push(h(type, data))
source = source.slice(capture.index + capture[0].length)
}
if (source) elements.push(h('text', {text: unescape(source)}))
return elements
}
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
// const result = parseCQCode("好好好")
// console.log(JSON.stringify(result))

View File

@@ -0,0 +1,16 @@
import {selfInfo} from "../../common/data";
export enum EventType {
META = "meta_event",
REQUEST = "request",
NOTICE = "notice",
MESSAGE = "message",
MESSAGE_SENT = "message_sent",
}
export abstract class OB11BaseEvent {
time = new Date().getTime();
self_id = parseInt(selfInfo.uin);
post_type: EventType;
}

View File

@@ -0,0 +1,5 @@
import {EventType, OB11BaseEvent} from "../OB11BaseEvent";
export abstract class OB11BaseMessageEvent extends OB11BaseEvent {
post_type = EventType.MESSAGE;
}

View File

@@ -0,0 +1,6 @@
import {EventType, OB11BaseEvent} from "../OB11BaseEvent";
export abstract class OB11BaseMetaEvent extends OB11BaseEvent {
post_type = EventType.META;
meta_event_type: string;
}

View File

@@ -0,0 +1,21 @@
import {OB11BaseMetaEvent} from "./OB11BaseMetaEvent";
interface HeartbeatStatus {
online: boolean | null,
good: boolean
}
export class OB11HeartbeatEvent extends OB11BaseMetaEvent {
meta_event_type = "heartbeat";
status: HeartbeatStatus;
interval: number;
public constructor(isOnline: boolean | null, isGood: boolean, interval: number) {
super();
this.interval = interval;
this.status = {
online: isOnline,
good: isGood
}
}
}

View File

@@ -0,0 +1,17 @@
import {OB11BaseMetaEvent} from "./OB11BaseMetaEvent";
export enum LifeCycleSubType {
ENABLE = "enable",
DISABLE = "disable",
CONNECT = "connect"
}
export class OB11LifeCycleEvent extends OB11BaseMetaEvent {
meta_event_type = "lifecycle";
sub_type: LifeCycleSubType;
public constructor(subType: LifeCycleSubType) {
super();
this.sub_type = subType;
}
}

View File

@@ -0,0 +1,5 @@
import {EventType, OB11BaseEvent} from "../OB11BaseEvent";
export abstract class OB11BaseNoticeEvent extends OB11BaseEvent {
post_type = EventType.NOTICE;
}

View File

@@ -0,0 +1,13 @@
import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent";
export class OB11FriendRecallNoticeEvent extends OB11BaseNoticeEvent {
notice_type = "friend_recall"
user_id: number
message_id: number
public constructor(userId: number, messageId: number) {
super();
this.user_id = userId;
this.message_id = messageId;
}
}

View File

@@ -0,0 +1,6 @@
import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent";
export class OB11GroupAdminNoticeEvent extends OB11BaseNoticeEvent {
notice_type = "group_admin"
sub_type: string // "set" | "unset"
}

View File

@@ -0,0 +1,14 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = "group_decrease";
sub_type = "leave"; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operate_id: number;
constructor(groupId: number, userId: number) {
super();
this.group_id = groupId;
this.operate_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被踢出的,还是自己主动退出的
this.user_id = userId;
}
}

View File

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

View File

@@ -0,0 +1,6 @@
import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent";
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number;
user_id: number;
}

View File

@@ -0,0 +1,15 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent {
notice_type = "group_recall"
operator_id: number
message_id: number
constructor(groupId: number, userId: number, operatorId: number, messageId: number) {
super();
this.group_id = groupId;
this.user_id = userId;
this.operator_id = operatorId;
this.message_id = messageId;
}
}

View File

@@ -1,161 +0,0 @@
import { getConfigUtil, log } from "../common/utils";
const express = require("express");
import { Request } from 'express';
import { Response } from 'express';
const JSONbig = require('json-bigint')({ storeAsString: true });
import { selfInfo } from "../common/data";
import { OB11Message, OB11Return, OB11MessageData } from './types';
import { actionHandlers } from "./actions";
// @SiberianHusky 2021-08-15
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (let msg of sendMsgList) {
if (msg["type"] && msg["data"]) {
let type = msg["type"];
let data = msg["data"];
if (type === "text" && !data["text"]) {
return 400;
} else if (["image", "voice", "record"].includes(type)) {
if (!data["file"]) {
return 400;
} else {
if (checkUri(data["file"])) {
return 200;
} else {
return 400;
}
}
} else if (type === "at" && !data["qq"]) {
return 400;
} else if (type === "reply" && !data["id"]) {
return 400;
}
} else {
return 400
}
}
return 200;
}
// ==end==
class OB11Response {
static res<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> {
return {
status: status,
retcode: status,
data: data,
message: message
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(data)
}
static error(err: string) {
return OB11Response.res(null, -1, err)
}
}
const expressAPP = express();
expressAPP.use(express.urlencoded({ extended: true, limit: "500mb" }));
expressAPP.use((req, res, next) => {
let data = '';
req.on('data', chunk => {
data += chunk.toString();
});
req.on('end', () => {
if (data) {
try {
// log("receive raw", data)
req.body = JSONbig.parse(data);
} catch (e) {
return next(e);
}
}
next();
});
});
// expressAPP.use(express.json({
// limit: '500mb',
// verify: (req: any, res: any, buf: any, encoding: any) => {
// req.rawBody = buf;
// }
// }));
export function startExpress(port: number) {
expressAPP.get('/', (req: Request, res: Response) => {
res.send('llonebot已启动');
})
expressAPP.listen(port, "0.0.0.0", () => {
console.log(`llonebot started 0.0.0.0:${port}`);
});
}
export function postMsg(msg: OB11Message) {
const { reportSelfMessage } = getConfigUtil().getConfig()
if (!reportSelfMessage) {
if (msg.user_id == selfInfo.uin) {
return
}
}
for (const host of getConfigUtil().getConfig().hosts) {
fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
},
body: JSON.stringify(msg)
}).then((res: any) => {
log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => {
log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg));
});
}
}
let routers: Record<string, (payload: any) => Promise<OB11Return<any>>> = {};
function registerRouter(action: string, handle: (payload: any) => Promise<any>) {
let url = action.toString()
if (!action.startsWith("/")) {
url = "/" + action
}
async function _handle(res: Response, payload: any) {
log("receive post data", url, payload)
try {
const result = await handle(payload)
res.send(result)
}
catch (e) {
log(e.stack);
res.send(OB11Response.error(e.stack.toString()))
}
}
expressAPP.post(url, (req: Request, res: Response) => {
_handle(res, req.body).then()
});
expressAPP.get(url, (req: Request, res: Response) => {
_handle(res, req.query as any).then()
});
routers[url] = handle
}
for (const action of actionHandlers) {
registerRouter(action.actionName, (payload) => action.handle(payload))
}

View File

@@ -0,0 +1,26 @@
import {Response} from "express";
import {getConfigUtil} from "../../common/utils";
import {OB11Response} from "../action/utils";
import {HttpServerBase} from "../../common/server/http";
import {actionHandlers} from "../action";
class OB11HTTPServer extends HttpServerBase {
name = "OneBot V11 server"
handleFailed(res: Response, payload: any, e: any) {
res.send(OB11Response.error(e.stack.toString(), 200))
}
protected listen(port: number) {
if (getConfigUtil().getConfig().ob11.enableHttp) {
super.listen(port);
}
}
}
export const ob11HTTPServer = new OB11HTTPServer();
for (const action of actionHandlers) {
for(const method of ["post", "get"]){
ob11HTTPServer.registerRouter(method, action.actionName, (res, payload) => action.handle(payload))
}
}

View File

@@ -0,0 +1,57 @@
import {getConfigUtil, log} from "../../common/utils";
import {OB11Message} from "../types";
import {selfInfo} from "../../common/data";
import {OB11BaseMetaEvent} from "../event/meta/OB11BaseMetaEvent";
import {OB11BaseNoticeEvent} from "../event/notice/OB11BaseNoticeEvent";
import * as websocket from "ws";
import {wsReply} from "./ws/reply";
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
const eventWSList: websocket.WebSocket[] = [];
export function registerWsEventSender(ws: websocket.WebSocket) {
eventWSList.push(ws);
}
export function unregisterWsEventSender(ws: websocket.WebSocket) {
let index = eventWSList.indexOf(ws);
if (index !== -1) {
eventWSList.splice(index, 1);
}
}
export function postWsEvent(event: PostEventType) {
for (const ws of eventWSList) {
new Promise(() => {
wsReply(ws, event);
}).then()
}
}
export function postEvent(msg: PostEventType) {
const config = getConfigUtil().getConfig();
// 判断msg是否是event
if (!config.reportSelfMessage) {
if ((msg as OB11Message).user_id.toString() == selfInfo.uin) {
return
}
}
if (config.ob11.enableHttpPost) {
for (const host of config.ob11.httpHosts) {
fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
},
body: JSON.stringify(msg)
}).then((res: any) => {
log(`新消息事件HTTP上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => {
log(`新消息事件HTTP上报失败: ${host} ` + err + JSON.stringify(msg));
});
}
}
postWsEvent(msg);
}

View File

@@ -0,0 +1,137 @@
import {getConfigUtil, log} from "../../../common/utils";
import * as WebSocket from "ws";
import {selfInfo} from "../../../common/data";
import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent";
import {ActionName} from "../../action/types";
import {OB11Response} from "../../action/utils";
import BaseAction from "../../action/BaseAction";
import {actionMap} from "../../action";
import {registerWsEventSender, unregisterWsEventSender} from "../postevent";
import {wsReply} from "./reply";
export let rwsList: ReverseWebsocket[] = [];
export class ReverseWebsocket {
public websocket: WebSocket.WebSocket;
public url: string;
private running: boolean = false;
public constructor(url: string) {
this.url = url;
this.running = true;
this.connect();
}
public stop() {
this.running = false;
this.websocket.close();
}
public onopen() {
wsReply(this.websocket, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT));
}
public async onmessage(msg: string) {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}}
let echo = ""
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
log("收到反向Websocket消息", receiveData)
} catch (e) {
return wsReply(this.websocket, OB11Response.error("json解析失败请检查数据格式", 1400, echo))
}
const action: BaseAction<any, any> = actionMap.get(receiveData.action);
if (!action) {
return wsReply(this.websocket, OB11Response.error("不支持的api " + receiveData.action, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(receiveData.params, echo);
wsReply(this.websocket, handleResult)
} catch (e) {
wsReply(this.websocket, OB11Response.error(`api处理出错:${e}`, 1200, echo))
}
}
public onclose = function () {
log("反向ws断开", this.url);
unregisterWsEventSender(this.websocket);
if (this.running) {
this.reconnect();
}
}
public send(msg: string) {
if (this.websocket && this.websocket.readyState == WebSocket.OPEN) {
this.websocket.send(msg);
}
}
private reconnect() {
setTimeout(() => {
this.connect();
}, 3000); // TODO: 重连间隔在配置文件中实现
}
private connect() {
const {token} = getConfigUtil().getConfig()
this.websocket = new WebSocket.WebSocket(this.url, {
handshakeTimeout: 2000,
perMessageDeflate: false,
headers: {
'X-Self-ID': selfInfo.uin,
'Authorization': `Bearer ${token}`,
'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段
}
});
registerWsEventSender(this.websocket);
log("Trying to connect to the websocket server: " + this.url);
this.websocket.on("open", () => {
log("Connected to the websocket server: " + this.url);
this.onopen();
});
this.websocket.on("message", async (data) => {
await this.onmessage(data.toString());
});
this.websocket.on("error", log);
this.websocket.on("close", () => {
log("The websocket connection: " + this.url + " closed, trying reconnecting...");
this.onclose();
});
}
}
class OB11ReverseWebsockets {
start() {
for (const url of getConfigUtil().getConfig().ob11.wsHosts) {
log("开始连接反向ws", url)
new Promise(() => {
try {
rwsList.push(new ReverseWebsocket(url));
} catch (e) {
log(e.stack);
}
}).then();
}
}
stop() {
for (let rws of rwsList) {
rws.stop();
}
}
restart() {
this.stop();
this.start();
}
}
export const ob11ReverseWebsockets = new OB11ReverseWebsockets();

View File

@@ -0,0 +1,73 @@
import {WebSocket} from "ws";
import {getConfigUtil, log} from "../../../common/utils";
import {actionMap} from "../../action";
import {OB11Response} from "../../action/utils";
import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postevent";
import {ActionName} from "../../action/types";
import BaseAction from "../../action/BaseAction";
import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent";
import {OB11HeartbeatEvent} from "../../event/meta/OB11HeartbeatEvent";
import {WebsocketServerBase} from "../../../common/server/websocket";
import {IncomingMessage} from "node:http";
import {wsReply} from "./reply";
let heartbeatRunning = false;
class OB11WebsocketServer extends WebsocketServerBase {
authorizeFailed(wsClient: WebSocket) {
wsClient.send(JSON.stringify(OB11Response.res(null, "failed", 1403, "token验证失败")))
}
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: string) {
const action: BaseAction<any, any> = actionMap.get(actionName);
if (!action) {
return wsReply(wsClient, OB11Response.error("不支持的api " + actionName, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(params, echo);
wsReply(wsClient, handleResult)
} catch (e) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e}`, 1200, echo))
}
}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
if (url == "/api" || url == "/api/" || url == "/") {
wsClient.on("message", async (msg) => {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}}
let echo = ""
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
log("收到正向Websocket消息", receiveData);
} catch (e) {
return wsReply(wsClient, OB11Response.error("json解析失败请检查数据格式", 1400, echo))
}
this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then()
})
}
if (url == "/event" || url == "/event/" || url == "/") {
registerWsEventSender(wsClient);
log("event上报ws客户端已连接")
try {
wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT))
} catch (e) {
log("发送生命周期失败", e)
}
const {heartInterval} = getConfigUtil().getConfig();
const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(true, true, heartInterval));
}, heartInterval); // 心跳包
wsClient.on("close", () => {
log("event上报ws客户端已断开")
clearInterval(wsClientInterval);
unregisterWsEventSender(wsClient);
})
}
}
}
export const ob11WebsocketServer = new OB11WebsocketServer()

View File

@@ -0,0 +1,18 @@
import * as websocket from "ws";
import {OB11Response} from "../../action/utils";
import {PostEventType} from "../postevent";
import {log} from "../../../common/utils";
export function wsReply(wsClient: websocket.WebSocket, data: OB11Response | PostEventType) {
try {
let packet = Object.assign({
}, data);
if (!packet["echo"]){
delete packet["echo"];
}
wsClient.send(JSON.stringify(packet))
log("ws 消息上报", wsClient.url || "", data)
} catch (e) {
log("websocket 回复失败", e)
}
}

View File

@@ -1,27 +1,27 @@
import { AtType } from "../ntqqapi/types";
import { RawMessage } from "../ntqqapi/types";
import {AtType, RawMessage} from "../ntqqapi/types";
import {EventType} from "./event/OB11BaseEvent";
export interface OB11User{
user_id: string;
export interface OB11User {
user_id: number;
nickname: string;
remark?: string
}
export enum OB11UserSex{
export enum OB11UserSex {
male = "male",
female = "female",
unknown = "unknown"
}
export enum OB11GroupMemberRole{
export enum OB11GroupMemberRole {
owner = "owner",
admin = "admin",
member = "member",
}
export interface OB11GroupMember {
group_id: string
user_id: string
group_id: number
user_id: number
nickname: string
card?: string
sex?: OB11UserSex
@@ -33,15 +33,15 @@ export interface OB11GroupMember {
title?: string
}
export interface OB11Group{
group_id: string
export interface OB11Group {
group_id: number
group_name: string
member_count?: number
max_member_count?: number
}
interface OB11Sender {
user_id: string,
user_id: number,
nickname: string,
sex?: OB11UserSex,
age?: number,
@@ -56,87 +56,121 @@ export enum OB11MessageType {
}
export interface OB11Message {
self_id?: string,
self_id?: number,
time: number,
message_id: string,
message_id: number,
real_id: string,
user_id: string,
group_id?: string,
user_id: number,
group_id?: number,
message_type: "private" | "group",
sub_type?: "friend" | "group" | "other",
sub_type?: "friend" | "group" | "normal",
sender: OB11Sender,
message: OB11MessageData[],
raw_message: string,
font: number,
post_type?: "message",
post_type?: EventType,
raw?: RawMessage
}
export type OB11ApiName =
"send_msg"
| "send_private_msg"
| "send_group_msg"
| "get_group_list"
| "get_group_info"
| "get_friend_list"
| "delete_msg"
| "get_login_info"
| "get_group_member_list"
| "get_group_member_info"
| "get_msg"
export interface OB11Return<DataType> {
status: number
status: string
retcode: number
data: DataType
message: string
message: string,
echo?: string, // ws调用api才有此字段
wording?: string, // go-cqhttp字段错误信息
}
export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{}
export enum OB11MessageDataType {
text = "text",
image = "image",
voice = "record",
at = "at",
reply = "reply",
json = "json"
json = "json",
face = "face",
node = "node" // 合并转发消息
}
export type OB11MessageData = {
export interface OB11MessageText {
type: OB11MessageDataType.text,
content: string,
data?: {
data: {
text: string, // 纯文本
}
} | {
type: "image" | "voice" | "record",
file: string, // 本地路径
data?: {
file: string // 本地路径
}
} | {
type: OB11MessageDataType.at,
atType?: AtType,
content?: string,
atUid?: string,
atNtUid?: string,
data?: {
qq: string // at的qq号
}
} | {
type: OB11MessageDataType.reply,
msgId: string,
msgSeq: string,
senderUin: string,
}
interface OB11MessageFileBase {
data: {
id: string,
file: string,
http_file?: string;
}
}
export interface OB11MessageImage extends OB11MessageFileBase {
type: OB11MessageDataType.image
}
export interface OB11MessageRecord extends OB11MessageFileBase {
type: OB11MessageDataType.voice
}
export interface OB11MessageAt {
type: OB11MessageDataType.at
data: {
qq: string | "all"
}
}
export interface OB11MessageReply {
type: OB11MessageDataType.reply
data: {
id: string
}
}
export interface OB11MessageFace {
type: OB11MessageDataType.face
data: {
id: string
}
}
export type OB11MessageMixType = OB11MessageData[] | string | OB11MessageData;
export interface OB11MessageNode {
type: OB11MessageDataType.node
data: {
id?: string
user_id?: number
nickname: string
content: OB11MessageMixType
}
}
export type OB11MessageData =
OB11MessageText |
OB11MessageFace |
OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord |
OB11MessageNode
export interface OB11PostSendMsg {
message_type?: "private" | "group"
user_id: string,
group_id?: string,
message: OB11MessageData[] | string | OB11MessageData;
}
message: OB11MessageMixType;
messages?: OB11MessageMixType; // 兼容 go-cqhttp
}
export interface OB11Version {
app_name: "LLOneBot"
app_version: string
protocol_version: "v11"
}
export interface OB11Status {
online: boolean | null,
good: boolean
}

View File

@@ -1,11 +1,18 @@
import { CONFIG_DIR, isGIF } from "../common/utils";
import {CONFIG_DIR, isGIF} from "../common/utils";
import * as path from 'path';
import { NTQQApi } from '../ntqqapi/ntcall';
import {OB11MessageData} from "./types";
const fs = require("fs").promises;
export async function uri2local(fileName: string, uri: string){
let filePath = path.join(CONFIG_DIR, fileName)
let url = new URL(uri);
let res = {
success: false,
errMsg: "",
path: "",
isLocal: false
}
if (url.protocol == "base64:") {
// base64转成文件
let base64Data = uri.split("base64://")[1]
@@ -13,51 +20,44 @@ export async function uri2local(fileName: string, uri: string){
const buffer = Buffer.from(base64Data, 'base64');
await fs.writeFile(filePath, buffer);
} catch (e: any) {
return {
success: false,
errMsg: `base64文件下载失败,` + e.toString(),
path: ""
}
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件
let res = await fetch(url)
if (!res.ok) {
return {
success: false,
errMsg: `${url}下载失败,` + res.statusText,
path: ""
}
let fetchRes = await fetch(url)
if (!fetchRes.ok) {
res.errMsg = `${url}下载失败,` + fetchRes.statusText
return res
}
let blob = await res.blob();
let blob = await fetchRes.blob();
let buffer = await blob.arrayBuffer();
try {
await fs.writeFile(filePath, Buffer.from(buffer));
} catch (e: any) {
return {
success: false,
errMsg: `${url}下载失败,` + e.toString(),
path: ""
}
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else if (url.protocol === "file:"){
await fs.copyFile(url.pathname, filePath);
// filePath = (await NTQQApi.uploadFile(url.pathname)).path;
// await fs.copyFile(url.pathname, filePath);
let pathname = decodeURIComponent(url.pathname)
if (process.platform === "win32"){
filePath = pathname.slice(1)
}
else{
filePath = pathname
}
res.isLocal = true
}
else{
return {
success: false,
errMsg: `不支持的file协议,` + url.protocol,
path: ""
}
res.errMsg = `不支持的file协议,` + url.protocol
return res
}
if (isGIF(filePath)) {
if (isGIF(filePath) && !res.isLocal) {
await fs.rename(filePath, filePath + ".gif");
filePath += ".gif";
}
return {
success: true,
errMsg: "",
path: filePath
};
res.success = true
res.path = filePath
return res
}

View File

@@ -1,25 +1,23 @@
// Electron 主进程 与 渲染进程 交互的桥梁
import {Config} from "./common/types";
import {
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SET_CONFIG,
} from "./common/channels";
import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "./common/channels";
const {contextBridge} = require("electron");
const {ipcRenderer} = require('electron');
// 在window对象下导出只读对象
contextBridge.exposeInMainWorld("llonebot", {
const llonebot = {
log: (data: any) => {
ipcRenderer.send(CHANNEL_LOG, data);
},
setConfig: (config: Config)=>{
setConfig: (config: Config) => {
ipcRenderer.send(CHANNEL_SET_CONFIG, config);
},
getConfig: async () => {
return ipcRenderer.invoke(CHANNEL_GET_CONFIG);
},
});
}
export type LLOneBot = typeof llonebot;
// 在window对象下导出只读对象
contextBridge.exposeInMainWorld("llonebot", llonebot);

View File

@@ -5,49 +5,113 @@
async function onSettingWindowCreated(view: Element) {
window.llonebot.log("setting window created");
let config = await window.llonebot.getConfig()
const httpClass = "http";
const httpPostClass = "http-post";
const wsClass = "ws";
const reverseWSClass = "reverse-ws";
function creatHostEleStr(host: string) {
function createHttpHostEleStr(host: string) {
let eleStr = `
<setting-item data-direction="row" class="hostItem vertical-list-item">
<h2>事件上报地址(http)</h2>
<input class="host input-text" type="text" value="${host}"
<setting-item data-direction="row" class="hostItem vertical-list-item ${httpPostClass}">
<h2>HTTP事件上报地址(http)</h2>
<input class="httpHost input-text" type="text" value="${host}"
style="width:60%;padding: 5px"
placeholder="如果localhost上报失败试试局域网ip"/>
placeholder="如:http://127.0.0.1:8080/onebot/v11/http"/>
</setting-item>
`
return eleStr
}
let hostsEleStr = ""
for (const host of config.hosts) {
hostsEleStr += creatHostEleStr(host);
function createWsHostEleStr(host: string) {
let eleStr = `
<setting-item data-direction="row" class="hostItem vertical-list-item ${reverseWSClass}">
<h2>反向websocket地址:</h2>
<input class="wsHost input-text" type="text" value="${host}"
style="width:60%;padding: 5px"
placeholder="如: ws://127.0.0.1:5410/onebot"/>
</setting-item>
`
return eleStr
}
let httpHostsEleStr = ""
for (const host of config.ob11.httpHosts) {
httpHostsEleStr += createHttpHostEleStr(host);
}
let wsHostsEleStr = ""
for (const host of config.ob11.wsHosts) {
wsHostsEleStr += createWsHostEleStr(host);
}
let html = `
<div class="config_view llonebot">
<setting-section>
<setting-panel>
<setting-list class="wrap">
<setting-item class="vertical-list-item" data-direction="row">
<setting-text>监听端口</setting-text>
<input id="port" type="number" value="${config.port}"/>
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>启用HTTP服务</div>
</div>
<setting-switch id="http" ${config.ob11.enableHttp ? "is-active" : ""}></setting-switch>
</setting-item>
<div>
<button id="addHost" class="q-button">添加上报地址</button>
<setting-item class="vertical-list-item ${httpClass}" data-direction="row" style="display: ${config.ob11.enableHttp ? '' : 'none'}">
<setting-text>HTTP监听端口</setting-text>
<input id="httpPort" type="number" value="${config.ob11.httpPort}"/>
</setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>启用HTTP事件上报</div>
</div>
<setting-switch id="httpPost" ${config.ob11.enableHttpPost ? "is-active" : ""}></setting-switch>
</setting-item>
<div class="${httpPostClass}" style="display: ${config.ob11.enableHttpPost ? '' : 'none'}">
<div >
<button id="addHttpHost" class="q-button">添加HTTP POST上报地址</button>
</div>
<div id="httpHostItems">
${httpHostsEleStr}
</div>
</div>
<div id="hostItems">
${hostsEleStr}
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>启用正向Websocket协议</div>
</div>
<setting-switch id="websocket" ${config.ob11.enableWs ? "is-active" : ""}></setting-switch>
</setting-item>
<setting-item class="vertical-list-item ${wsClass}" data-direction="row" style="display: ${config.ob11.enableWs ? '' : 'none'}">
<setting-text>正向Websocket监听端口</setting-text>
<input id="wsPort" type="number" value="${config.ob11.wsPort}"/>
</setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>启用反向Websocket协议</div>
</div>
<setting-switch id="websocketReverse" ${config.ob11.enableWsReverse ? "is-active" : ""}></setting-switch>
</setting-item>
<div class="${reverseWSClass}" style="display: ${config.ob11.enableWsReverse ? '' : 'none'}">
<div>
<button id="addWsHost" class="q-button">添加反向Websocket地址</button>
</div>
<div id="wsHostItems">
${wsHostsEleStr}
</div>
</div>
<button id="save" class="q-button">保存(监听端口重启QQ后生效)</button>
<setting-item class="vertical-list-item" data-direction="row">
<setting-text>Access Token</setting-text>
<input id="token" type="text" placeholder="可为空" value="${config.token}"/>
</setting-item>
<button id="save" class="q-button">保存</button>
</setting-list>
</setting-panel>
<setting-panel>
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>上报文件进行base64编码</div>
<div class="tips">开启,上报文件将以本地路径形式发送</div>
<div>上报文件不采用本地路径</div>
<div class="tips">开启,上报文件(图片语音等)为http链接或base64编码</div>
</div>
<setting-switch id="switchBase64" ${config.enableBase64 ? "is-active" : ""}></setting-switch>
<setting-switch id="switchFileUrl" ${config.enableLocalFile2Url ? "is-active" : ""}></setting-switch>
</setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
@@ -59,14 +123,14 @@ async function onSettingWindowCreated(view: Element) {
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>上报自身消息</div>
<div class="tips">开启后上报自己发出的消息</div>
<div class="tips">慎用,不然会自己和自己聊个不停</div>
</div>
<setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? "is-active" : ""}></setting-switch>
</setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>日志</div>
<div class="tips">日志目录:${window.LiteLoader.plugins["LLOneBot"].path.data}</div>
<div class="tips">目录:${window.LiteLoader.plugins["LLOneBot"].path.data}</div>
</div>
<setting-switch id="log" ${config.log ? "is-active" : ""}></setting-switch>
</setting-item>
@@ -92,51 +156,93 @@ async function onSettingWindowCreated(view: Element) {
const doc = parser.parseFromString(html, "text/html");
function addHostEle(initValue: string = "") {
let addressDoc = parser.parseFromString(creatHostEleStr(initValue), "text/html");
let addressEle = addressDoc.querySelector("setting-item")
let hostItemsEle = document.getElementById("hostItems");
function addHostEle(type: string, initValue: string = "") {
let addressEle, hostItemsEle;
if (type === "ws") {
let addressDoc = parser.parseFromString(createWsHostEleStr(initValue), "text/html");
addressEle = addressDoc.querySelector("setting-item")
hostItemsEle = document.getElementById("wsHostItems");
} else {
let addressDoc = parser.parseFromString(createHttpHostEleStr(initValue), "text/html");
addressEle = addressDoc.querySelector("setting-item")
hostItemsEle = document.getElementById("httpHostItems");
}
hostItemsEle.appendChild(addressEle);
}
doc.getElementById("addHost").addEventListener("click", () => addHostEle())
doc.getElementById("addHttpHost").addEventListener("click", () => addHostEle("http"))
doc.getElementById("addWsHost").addEventListener("click", () => addHostEle("ws"))
function switchClick(eleId: string, configKey: string) {
function switchClick(eleId: string, configKey: string, _config=null) {
if (!_config){
_config = config
}
doc.getElementById(eleId)?.addEventListener("click", (e) => {
const switchEle = e.target as HTMLInputElement
if (config[configKey]) {
config[configKey] = false
if (_config[configKey]) {
_config[configKey] = false
switchEle.removeAttribute("is-active")
} else {
config[configKey] = true
_config[configKey] = true
switchEle.setAttribute("is-active", "")
}
// 妈蛋手动操作DOM越写越麻烦要不用vue算了
const keyClassMap = {
"enableHttp": httpClass,
"enableHttpPost": httpPostClass,
"enableWs": wsClass,
"enableWsReverse": reverseWSClass,
}
for (let e of document.getElementsByClassName(keyClassMap[configKey])) {
e["style"].display = _config[configKey] ? "" : "none"
}
window.llonebot.setConfig(config)
})
}
switchClick("http", "enableHttp", config.ob11);
switchClick("httpPost", "enableHttpPost", config.ob11);
switchClick("websocket", "enableWs", config.ob11);
switchClick("websocketReverse", "enableWsReverse", config.ob11);
switchClick("debug", "debug");
switchClick("switchBase64", "enableBase64");
switchClick("switchFileUrl", "enableLocalFile2Url");
switchClick("reportSelfMessage", "reportSelfMessage");
switchClick("log", "log");
doc.getElementById("save")?.addEventListener("click",
() => {
const portEle: HTMLInputElement = document.getElementById("port") as HTMLInputElement
const hostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("host") as HTMLCollectionOf<HTMLInputElement>;
// const port = doc.querySelector("input[type=number]")?.value
// const host = doc.querySelector("input[type=text]")?.value
const httpPortEle: HTMLInputElement = document.getElementById("httpPort") as HTMLInputElement;
const httpHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("httpHost") as HTMLCollectionOf<HTMLInputElement>;
const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement;
const wsHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("wsHost") as HTMLCollectionOf<HTMLInputElement>;
const tokenEle = document.getElementById("token") as HTMLInputElement;
// 获取端口和host
const port = portEle.value
let hosts: string[] = [];
for (const hostEle of hostEles) {
if (hostEle.value) {
hosts.push(hostEle.value);
}
const httpPort = httpPortEle.value
let httpHosts: string[] = [];
for (const hostEle of httpHostEles) {
const value = hostEle.value.trim();
value && httpHosts.push(value);
}
config.port = parseInt(port);
config.hosts = hosts;
const wsPort = wsPortEle.value;
const token = tokenEle.value.trim();
let wsHosts: string[] = [];
for (const hostEle of wsHostEles) {
const value = hostEle.value.trim();
value && wsHosts.push(value);
}
config.ob11.httpPort = parseInt(httpPort);
config.ob11.httpHosts = httpHosts;
config.ob11.wsPort = parseInt(wsPort);
config.ob11.wsHosts = wsHosts;
config.token = token;
window.llonebot.setConfig(config);
alert("保存成功");
})
@@ -146,7 +252,21 @@ async function onSettingWindowCreated(view: Element) {
view.appendChild(node);
});
}
function init() {
let hash = location.hash;
if (hash === "#/blank") {
return;
}
}
if (location.hash === "#/blank") {
(window as any).navigation.addEventListener("navigatesuccess", init, {once: true});
} else {
init();
}

View File

@@ -8,6 +8,7 @@
"allowJs": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
// "sourceMap": true
},
"include": ["src/*"],
"exclude": [

View File

@@ -1,8 +1,14 @@
// import path from "path";
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
const ignoreModules = [
"silk-wasm", "electron"
];
const copyModules = ["silk-wasm"]
let config = {
// target: 'node',
entry: {
// main: './src/main.ts',
@@ -15,11 +21,10 @@ module.exports = {
// libraryTarget: "commonjs2",
// chunkFormat: "commonjs",
},
externals: [
// "express",
"electron", "fs"],
externals: ignoreModules,
experiments: {
// outputModule: true
// asyncWebAssembly: true
},
resolve: {
extensions: ['.js', '.ts']
@@ -33,7 +38,6 @@ module.exports = {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
}
}
},
@@ -46,8 +50,7 @@ module.exports = {
// configFile: 'src/tsconfig.json'
}
}
}
]
}]
},
optimization: {
minimize: false,
@@ -56,5 +59,18 @@ module.exports = {
extractComments: false,
}),
],
}
}
},
plugins: [
new CopyPlugin({
patterns: copyModules.map(m=>{
m = `node_modules/${m}`
return {
from: m,
to: m
}
})
}),
], // devtool: 'source-map',
}
module.exports = config