Compare commits

...

45 Commits

Author SHA1 Message Date
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
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
Disy
c875cfda15 feat: add websocket support 2024-02-14 22:11:07 +08:00
66 changed files with 2764 additions and 828 deletions

View File

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

View File

@@ -22,7 +22,7 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] http调用api - [x] http调用api
- [x] http事件上报 - [x] http事件上报
- [x] 正向websocket - [x] 正向websocket
- [ ] 反向websocket - [x] 反向websocket
主要功能: 主要功能:
- [x] 发送好友消息 - [x] 发送好友消息
@@ -33,16 +33,18 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] 撤回消息 - [x] 撤回消息
- [x] 上报好友消息 - [x] 上报好友消息
- [x] 上报群消息 - [x] 上报群消息
- [x] 上报好友、群消息撤回
消息格式支持: 消息格式支持:
- [x] 文字 - [x] 文字
- [x] 表情
- [x] 图片 - [x] 图片
- [x] 引用消息 - [x] 引用消息
- [x] @群成员 - [x] @群成员
- [x] 语音 - [x] 语音(支持mp3、wav等多种音频格式直接发送)
- [x] json消息(只上报) - [x] json消息(只上报)
- [x] 转发消息记录(目前只能发不能收)
- [ ] 红包 - [ ] 红包
- [ ] 转发消息记录
- [ ] xml - [ ] xml
支持的api: 支持的api:
@@ -57,6 +59,10 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] get_group_member_info - [x] get_group_member_info
- [x] get_friend_list - [x] get_friend_list
- [x] get_msg - [x] get_msg
- [x] get_version_info
- [x] get_status
- [x] can_send_image
- [x] can_send_record
## 示例 ## 示例
@@ -102,8 +108,8 @@ LiteLoaderQQNT的OneBot11协议插件
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用 - [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正向websocket - [x] 支持正、反向websocket(感谢@disymayufei的PR
- [ ] 转发消息记录 - [x] 转发消息记录
- [ ] 好友点赞api - [ ] 好友点赞api
## onebot11文档 ## onebot11文档

View File

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

578
package-lock.json generated
View File

@@ -12,19 +12,24 @@
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"json-bigint": "^1.0.0", "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": { "devDependencies": {
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/node": "^20.11.19",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"webpack": "^5.89.0", "webpack": "^5.89.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4"
"ws": "^8.16.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -2066,6 +2071,58 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@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": { "node_modules/@types/body-parser": {
"version": "1.19.4", "version": "1.19.4",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/body-parser/-/body-parser-1.19.4.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/@types/body-parser/-/body-parser-1.19.4.tgz",
@@ -2154,9 +2211,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.8.9", "version": "20.11.19",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.8.9.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@@ -2895,6 +2952,83 @@
"resolved": "https://mirrors.cloud.tencent.com/npm/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" "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": { "node_modules/core-js-compat": {
"version": "3.33.2", "version": "3.33.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/core-js-compat/-/core-js-compat-3.33.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/core-js-compat/-/core-js-compat-3.33.2.tgz",
@@ -2908,6 +3042,24 @@
"url": "https://opencollective.com/core-js" "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": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3140,6 +3292,34 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true "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": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3155,6 +3335,31 @@
"node": ">= 4.9.1" "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": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -3355,12 +3560,44 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/glob-to-regexp": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true "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": { "node_modules/gopd": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/gopd/-/gopd-1.0.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/gopd/-/gopd-1.0.1.tgz",
@@ -3457,6 +3694,34 @@
"node": ">=0.10.0" "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": { "node_modules/import-local": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
@@ -3510,6 +3775,27 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -3673,6 +3959,15 @@
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true "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": { "node_modules/methods": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/methods/-/methods-1.1.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/methods/-/methods-1.1.2.tgz",
@@ -3729,6 +4024,56 @@
"resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.0.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "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": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/negotiator/-/negotiator-0.6.3.tgz",
@@ -3743,12 +4088,33 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "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": { "node_modules/node-releases": {
"version": "2.0.13", "version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"dev": true "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": { "node_modules/object-inspect": {
"version": "1.13.1", "version": "1.13.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/object-inspect/-/object-inspect-1.13.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -3841,6 +4207,30 @@
"resolved": "https://mirrors.cloud.tencent.com/npm/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" "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": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/picocolors/-/picocolors-1.0.0.tgz",
@@ -3906,6 +4296,26 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -3937,6 +4347,34 @@
"node": ">= 0.8" "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": { "node_modules/rechoir": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@@ -4061,6 +4499,39 @@
"node": ">=8" "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": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -4147,9 +4618,9 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
@@ -4234,6 +4705,23 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -4261,6 +4749,30 @@
"node": ">= 0.8" "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": { "node_modules/supports-color": {
"version": "8.1.1", "version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -4369,6 +4881,22 @@
"node": ">=0.6" "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": { "node_modules/ts-loader": {
"version": "9.5.0", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.0.tgz",
@@ -4469,6 +4997,18 @@
"node": ">=4" "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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/unpipe/-/unpipe-1.0.0.tgz",
@@ -4516,6 +5056,25 @@
"punycode": "^2.1.0" "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": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -4706,7 +5265,6 @@
"version": "8.16.0", "version": "8.16.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.16.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"dev": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },

View File

@@ -5,13 +5,13 @@
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "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": "npm run build-main && npm run build-preload && npm run build-renderer",
"build-main": "webpack --config webpack.main.config.js", "build-main": "webpack --config webpack.main.config.js",
"build-preload": "webpack --config webpack.preload.config.js", "build-preload": "webpack --config webpack.preload.config.js",
"build-renderer": "webpack --config webpack.renderer.config.js", "build-renderer": "webpack --config webpack.renderer.config.js",
"build-mac": "npm run build && cp manifest.json dist/ && npm run deploy-mac", "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", "build-win": "npm run build && cp manifest.json dist/ && npm run deploy-win",
"deploy-win": "cmd /c \"copy dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOnebot\\\"" "deploy-win": "cmd /c \"copy dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOnebot\\\""
}, },
@@ -20,18 +20,23 @@
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"json-bigint": "^1.0.0", "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": { "devDependencies": {
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/node": "^20.11.19",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"webpack": "^5.89.0", "webpack": "^5.89.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4"
"ws": "^8.16.0"
} }
} }

View File

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

View File

@@ -1,31 +1,79 @@
import { Config } from "./types"; import fs from "fs";
import {Config, OB11Config} from "./types";
const fs = require("fs") import {mergeNewProperties} from "./utils";
export class ConfigUtil { export class ConfigUtil {
configPath: string; private readonly configPath: string;
private config: Config | null = null;
constructor(configPath: string) { constructor(configPath: string) {
this.configPath = configPath; 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)) { if (!fs.existsSync(this.configPath)) {
return {port: 3000, hosts: ["http://192.168.1.2:5000/"], wsPort: 3001} this.config = defaultConfig;
return this.config;
} else { } else {
const data = fs.readFileSync(this.configPath, "utf-8"); const data = fs.readFileSync(this.configPath, "utf-8");
let jsonData = JSON.parse(data); let jsonData: Config = defaultConfig;
if (!jsonData.hosts) { try {
jsonData.hosts = [] jsonData = JSON.parse(data)
} catch (e) {
this.config = defaultConfig;
return this.config;
} }
if (!jsonData.wsPort){ mergeNewProperties(defaultConfig, jsonData);
jsonData.wsPort = 3001 this.checkOldConfig(jsonData.ob11, jsonData, "httpPort", "http");
} this.checkOldConfig(jsonData.ob11, jsonData, "httpHosts", "hosts");
return jsonData; 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") 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,6 +1,5 @@
import { NTQQApi } from '../ntqqapi/ntcall'; import {NTQQApi} from '../ntqqapi/ntcall';
import { Friend, Group, GroupMember, RawMessage, SelfInfo } from "../ntqqapi/types"; import {Friend, Group, GroupMember, RawMessage, SelfInfo} from "../ntqqapi/types";
import { log } from "./utils";
export let groups: Group[] = [] export let groups: Group[] = []
export let friends: Friend[] = [] export let friends: Friend[] = []
@@ -78,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) { for (const key in uidMaps) {
if (uidMaps[key].uin === uin) { if (uidMaps[key] === uin) {
return uidMaps[key]; return key;
} }
} }
} }
export const version = "v3.4.0"

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

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

View File

@@ -2,9 +2,9 @@ import * as path from "path";
import {selfInfo} from "./data"; import {selfInfo} from "./data";
import {ConfigUtil} from "./config"; import {ConfigUtil} from "./config";
import util from "util"; import util from "util";
import { sendLog } from '../main/ipcsend'; import {encode, getDuration} from "silk-wasm";
import fs from 'fs';
const fs = require('fs'); import {v4 as uuidv4} from "uuid";
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
@@ -14,7 +14,7 @@ export function getConfigUtil() {
} }
export function log(...msg: any[]) { export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log){ if (!getConfigUtil().getConfig().log) {
return return
} }
let currentDateTime = new Date().toLocaleString(); let currentDateTime = new Date().toLocaleString();
@@ -25,18 +25,18 @@ export function log(...msg: any[]) {
const currentDate = `${year}-${month}-${day}`; const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : "" const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = ""; let logMsg = "";
for (let msgItem of msg){ for (let msgItem of msg) {
// 判断是否是对象 // 判断是否是对象
if (typeof msgItem === "object"){ if (typeof msgItem === "object") {
logMsg += JSON.stringify(msgItem) + " "; logMsg += JSON.stringify(msgItem) + " ";
continue; continue;
} }
logMsg += msgItem + " "; logMsg += msgItem + " ";
} }
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n` logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg); // sendLog(...msg);
// console.log(msg) // console.log(msg)
fs.appendFile(path.join(CONFIG_DIR , `llonebot-${currentDate}.log`), logMsg, (err: any) => { fs.appendFile(path.join(CONFIG_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
}) })
} }
@@ -49,9 +49,13 @@ export function isGIF(path: string) {
return buffer.toString() === 'GIF8' 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) => { return new Promise((resolve, reject) => {
const startTime = Date.now(); const startTime = Date.now();
@@ -69,7 +73,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); const readFile = util.promisify(fs.readFile);
let result = { let result = {
err: "", err: "",
@@ -94,3 +98,81 @@ export async function file2base64(path: string){
} }
return result; 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 { declare global {
interface Window { interface Window {

View File

@@ -1,5 +1,4 @@
import {webContents} from 'electron'; import {webContents} from 'electron';
import { CHANNEL_LOG } from '../common/channels';
function sendIPCMsg(channel: string, ...data: any) { 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,24 +1,22 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import * as path from "path"; import {BrowserWindow, ipcMain} from 'electron';
import { BrowserWindow, ipcMain } from 'electron'; import fs from 'fs';
import * as util from 'util'; 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 { Config } from "../common/types";
import {
CHANNEL_GET_CONFIG,
CHANNEL_LOG,
CHANNEL_SET_CONFIG,
} from "../common/channels";
import { postMsg, startHTTPServer, startWSServer } from "../onebot11/server";
import { CONFIG_DIR, getConfigUtil, log } from "../common/utils";
import { addHistoryMsg, 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; let running = false;
@@ -26,10 +24,6 @@ let running = false;
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log("llonebot main onLoad"); log("llonebot main onLoad");
// const config_dir = browserWindow.LiteLoader.plugins["LLOneBot"].path.data;
if (!fs.existsSync(CONFIG_DIR)) { if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, {recursive: true}); fs.mkdirSync(CONFIG_DIR, {recursive: true});
} }
@@ -39,25 +33,62 @@ function onLoad() {
ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => { ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => {
let oldConfig = getConfigUtil().getConfig(); let oldConfig = getConfigUtil().getConfig();
getConfigUtil().setConfig(arg) getConfigUtil().setConfig(arg)
if (arg.port != oldConfig.port){ if (arg.ob11.httpPort != oldConfig.ob11.httpPort && arg.ob11.enableHttp) {
startHTTPServer(arg.port) ob11HTTPServer.restart(arg.ob11.httpPort);
} }
if (arg.wsPort != oldConfig.wsPort){ // 判断是否启用或关闭HTTP服务
startWSServer(arg.wsPort) 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) => { 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(); const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) { for (let message of msgList) {
// log("收到新消息", message)
message.msgShortId = msgHistory[message.msgId]?.msgShortId message.msgShortId = msgHistory[message.msgId]?.msgShortId
if (!message.msgShortId) { if (!message.msgShortId) {
addHistoryMsg(message) addHistoryMsg(message);
} }
OB11Constructor.message(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
@@ -66,77 +97,120 @@ function onLoad() {
if (msg.user_id.toString() == selfInfo.uin && !reportSelfMessage) { if (msg.user_id.toString() == selfInfo.uin && !reportSelfMessage) {
return return
} }
postMsg(msg); postEvent(msg);
// log("post msg", msg) // log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.toString())); }).catch(e => log("constructMessage error: ", e.toString()));
} }
} }
function start() { async function start() {
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try { try {
// log("received msg length", payload.msgList.length); postReceiveMsg(payload.msgList);
postRawMsg(payload.msgList);
} catch (e) { } 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) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig() const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) { if (!reportSelfMessage) {
return return
} }
// log("reportSelfMessage", payload) // log("reportSelfMessage", payload)
try { try {
postRawMsg([payload.msgRecord]); postReceiveMsg([payload.msgRecord]);
} catch (e) { } catch (e) {
log("report self message error: ", e.toString()) log("report self message error: ", e.toString());
} }
}) })
NTQQApi.getGroups(true).then() NTQQApi.getGroups(true).then()
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
startHTTPServer(config.port) if (config.ob11.enableHttp) {
startWSServer(config.wsPort) 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") log("LLOneBot start")
} }
const init = async () => { const init = async () => {
try { try {
const _ = await NTQQApi.getSelfInfo() const _ = await NTQQApi.getSelfInfo();
Object.assign(selfInfo, _) Object.assign(selfInfo, _);
selfInfo.nick = selfInfo.uin selfInfo.nick = selfInfo.uin;
log("get self simple info", _) log("get self simple info", _);
} catch (e) { } catch (e) {
log("retry get self info") log("retry get self info");
} }
if (selfInfo.uin) { if (selfInfo.uin) {
try { try {
const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid)) const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid));
log("self info", userInfo); log("self info", userInfo);
if (userInfo) { if (userInfo) {
selfInfo.nick = userInfo.nick selfInfo.nick = userInfo.nick;
} else { } else {
return setTimeout(init, 1000) return setTimeout(init, 1000);
} }
} catch (e) { } catch (e) {
log("get self nickname failed", e.toString()) log("get self nickname failed", e.toString());
return setTimeout(init, 1000) return setTimeout(init, 1000);
} }
start(); start().then();
} } else {
else{
setTimeout(init, 1000) setTimeout(init, 1000)
} }
} }
setTimeout(init, 1000) setTimeout(init, 1000);
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
try { try {
hookNTQQApiCall(window);
hookNTQQApiReceive(window); hookNTQQApiReceive(window);
} catch (e) { } 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 {NTQQApi} from "./ntcall";
import {encodeSilk, log} from "../common/utils";
import fs from "fs";
export class SendMsgElementConstructor { 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 {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(picPath);
const imageSize = await NTQQApi.getImageSize(picPath); const imageSize = await NTQQApi.getImageSize(picPath);
const picElement = { const picElement = {
@@ -70,8 +80,13 @@ export class SendMsgElementConstructor {
}; };
} }
static async ptt(pttPath: string):Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(pttPath); 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 { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,
elementId: "", elementId: "",
@@ -80,7 +95,8 @@ export class SendMsgElementConstructor {
filePath: path, filePath: path,
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, 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, formatType: 1,
voiceType: 1, voiceType: 1,
voiceChangeType: 0, 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,10 +1,12 @@
import { BrowserWindow } from 'electron'; import {BrowserWindow} from 'electron';
import { getConfigUtil, log } from "../common/utils"; import {log, sleep} from "../common/utils";
import { NTQQApi, NTQQApiClass, sendMessagePool } from "./ntcall"; import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall";
import { Group, User } from "./types"; import {Group, RawMessage, User} from "./types";
import { RawMessage } from "./types"; import {addHistoryMsg, friends, groups, msgHistory} from "../common/data";
import { addHistoryMsg, friends, groups, msgHistory } from "../common/data"; import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import { v4 as uuidv4 } from 'uuid'; 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> = {}
@@ -34,7 +36,7 @@ interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
let receiveHooks: Array<{ let receiveHooks: Array<{
method: ReceiveCmd, method: ReceiveCmd,
hookFunc: (payload: any) => void, hookFunc: ((payload: any) => void | Promise<void>)
id: string id: string
}> = [] }> = []
@@ -50,7 +52,10 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
if (hook.method === ntQQApiMethodName) { if (hook.method === ntQQApiMethodName) {
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
try { try {
hook.hookFunc(receiveData.payload); let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then()
}
} catch (e) { } catch (e) {
log("hook error", e, receiveData.payload) log("hook error", e, receiveData.payload)
} }
@@ -75,6 +80,24 @@ export function hookNTQQApiReceive(window: BrowserWindow) {
window.webContents.send = patchSend; 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 { export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string {
const id = uuidv4() const id = uuidv4()
receiveHooks.push({ receiveHooks.push({
@@ -90,25 +113,103 @@ export function removeReceiveHook(id: string) {
receiveHooks.splice(index, 1); receiveHooks.splice(index, 1);
} }
async function updateGroups(_groups: Group[]) { async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) { for (let group of _groups) {
let existGroup = groups.find(g => g.groupCode == group.groupCode) let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (!existGroup) { if (existGroup) {
NTQQApi.getGroupMembers(group.groupCode).then(members => { Object.assign(existGroup, group);
if (members) { }
group.members = members else {
} groups.push(group);
}) existGroup = group;
groups.push(group) }
log("update group members", group.members)
} else { if (needUpdate) {
Object.assign(existGroup, group) const members = await NTQQApi.getGroupMembers(group.groupCode);
if (members) {
existGroup.members = members;
}
} }
} }
} }
registerReceiveHook<{ groupList: Group[] }>(ReceiveCmd.GROUPS, (payload) => updateGroups(payload.groupList).then()) async function processGroupEvent(payload) {
registerReceiveHook<{ groupList: Group[] }>(ReceiveCmd.GROUPS_UNIX, (payload) => updateGroups(payload.groupList).then()) 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<{ 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<{ registerReceiveHook<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
}>(ReceiveCmd.FRIENDS, payload => { }>(ReceiveCmd.FRIENDS, payload => {
@@ -125,16 +226,6 @@ registerReceiveHook<{
} }
}) })
// registerReceiveHook<any>(ReceiveCmd.USER_INFO, (payload)=>{
// log("user info", payload);
// })
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, (payload) => {
for (const message of payload.msgList) {
addHistoryMsg(message)
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message) // log("收到新消息push到历史记录", message)

View File

@@ -1,13 +1,10 @@
import { ipcMain } from "electron"; import {ipcMain} from "electron";
import { v4 as uuidv4 } from "uuid"; import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import { ReceiveCmd, hookApiCallbacks, registerReceiveHook, removeReceiveHook } from "./hook"; import {log} from "../common/utils";
import { log } from "../common/utils"; import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types";
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 * as fs from "fs"; import * as fs from "fs";
import {addHistoryMsg, msgHistory, selfInfo, uidMaps} from "../common/data";
import {v4 as uuidv4} from "uuid"
interface IPCReceiveEvent { interface IPCReceiveEvent {
eventName: string eventName: string
@@ -29,7 +26,6 @@ export enum NTQQApiClass {
export enum NTQQApiMethod { export enum NTQQApiMethod {
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate",
SELF_INFO = "fetchAuthData", SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelBuddyService/getBuddyList", FRIENDS = "nodeIKernelBuddyService/getBuddyList",
GROUPS = "nodeIKernelGroupService/getGroupList", GROUPS = "nodeIKernelGroupService/getGroupList",
@@ -44,7 +40,8 @@ export enum NTQQApiMethod {
MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild", MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = "nodeIKernelMsgService/recallMsg", RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg", SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia" DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment" // 合并转发
} }
enum NTQQApiChannel { enum NTQQApiChannel {
@@ -64,8 +61,24 @@ enum CallBackType {
METHOD METHOD
} }
interface NTQQApiParams {
methodName: NTQQApiMethod,
className?: NTQQApiClass,
channel?: NTQQApiChannel,
args?: unknown[],
cbCmd?: ReceiveCmd | null
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
} = params;
className = className ?? NTQQApiClass.NT_API;
channel = channel ?? NTQQApiChannel.IPC_UP_2;
args = args ?? [];
timeout = timeout ?? 5;
const uuid = uuidv4(); const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid) // log("callNTQQApi", channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
@@ -102,16 +115,18 @@ function callNTQQApi<ReturnType>(channel: NTQQApiChannel, className: NTQQApiClas
reject(`ntqq api timeout ${channel}, ${className}, ${methodName}`) reject(`ntqq api timeout ${channel}, ${className}, ${methodName}`)
} }
}, _timeout) }, _timeout)
const eventName = className + "-" + channel[channel.length - 1];
const apiArgs = [methodName, ...args]
ipcMain.emit( ipcMain.emit(
channel, channel,
{}, {},
{type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1]}, {type: 'request', callbackId: uuid, eventName},
[methodName, ...args], apiArgs
) )
}) })
} }
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
interface GeneralCallResult { interface GeneralCallResult {
@@ -123,34 +138,49 @@ interface GeneralCallResult {
export class NTQQApi { export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND) // static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static likeFriend(uid: string, count = 1) { static likeFriend(uid: string, count = 1) {
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND, [{ return callNTQQApi({
doLikeUserInfo: { methodName: NTQQApiMethod.LIKE_FRIEND,
friendUid: uid, args: [{
sourceId: 71, doLikeUserInfo: {
doLikeCount: count, friendUid: uid,
doLikeTollCount: 0 sourceId: 71,
} doLikeCount: count,
}, doLikeTollCount: 0
null]) }
}, null]
})
} }
static getSelfInfo() { 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) { static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ const result = await callNTQQApi<{ profiles: Map<string, User> }>({
profiles: Map<string, User> methodName: NTQQApiMethod.USER_INFO,
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.USER_INFO, args: [{force: true, uids: [uid]}, undefined],
[{force: true, uids: [uid]}, undefined], ReceiveCmd.USER_INFO) cbCmd: ReceiveCmd.USER_INFO
})
return result.profiles.get(uid) return result.profiles.get(uid)
} }
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: Friend[] }[] data: {
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.FRIENDS, [{force_update: forced}, undefined], ReceiveCmd.FRIENDS) categoryId: number,
categroyName: string,
categroyMbCount: number,
buddyList: Friend[]
}[]
}>(
{
methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmd.FRIENDS
})
let _friends: Friend[] = []; let _friends: Friend[] = [];
for (const fData of data.data) { for (const fData of data.data) {
_friends.push(...fData.buddyList) _friends.push(...fData.buddyList)
@@ -166,32 +196,41 @@ export class NTQQApi {
const result = await callNTQQApi<{ const result = await callNTQQApi<{
updateType: number, updateType: number,
groupList: Group[] groupList: Group[]
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUPS, [{force_update: forced}, undefined], cbCmd) }>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList return result.groupList
} }
static async getGroupMembers(groupQQ: string, num = 3000) { static async getGroupMembers(groupQQ: string, num = 3000) {
const sceneId = await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBER_SCENE, [{ const sceneId = await callNTQQApi({
groupCode: groupQQ, methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
scene: "groupMemberList_MainWindow" args: [{
}]) groupCode: groupQQ,
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId); // log("get group member sceneId", sceneId);
try { try {
const result = await callNTQQApi<{ const result = await callNTQQApi<{
result: { infos: any } result: { infos: any }
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.GROUP_MEMBERS, }>({
[{ methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId: sceneId, sceneId: sceneId,
num: num num: num
}, },
null null
]) ]
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos)) // log("members info", typeof result.result.infos, Object.keys(result.result.infos))
let values = result.result.infos.values() 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); // log("members info", values);
return values return members
} catch (e) { } catch (e) {
log(`get group ${groupQQ} members failed`, e) log(`get group ${groupQQ} members failed`, e)
return [] return []
@@ -200,31 +239,38 @@ export class NTQQApi {
static getFileType(filePath: string) { static getFileType(filePath: string) {
return callNTQQApi<{ return callNTQQApi<{ ext: string }>({
ext: string className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath]
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_TYPE, [filePath]) })
} }
static getFileMd5(filePath: string) { 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) { static copyFile(filePath: string, destPath: string) {
return callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.FILE_COPY, [{ return callNTQQApi<string>({
fromPath: filePath, className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_COPY, args: [{
toPath: destPath fromPath: filePath,
}]) toPath: destPath
}]
})
} }
static getImageSize(filePath: string) { static getImageSize(filePath: string) {
return callNTQQApi<{ return callNTQQApi<{ width: number, height: number }>({
width: number, className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath]
height: number })
}>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.FS_API, NTQQApiMethod.IMAGE_SIZE, [filePath])
} }
static getFileSize(filePath: string) { 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的文件夹 // 上传文件到QQ的文件夹
@@ -233,23 +279,25 @@ export class NTQQApi {
let ext = (await NTQQApi.getFileType(filePath))?.ext let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) { if (ext) {
ext = "." + ext ext = "." + ext
} } else {
else{
ext = "" ext = ""
} }
const fileName = `${md5}${ext}`; const fileName = `${md5}${ext}`;
const mediaPath = await callNTQQApi<string>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.MEDIA_FILE_PATH, [{ const mediaPath = await callNTQQApi<string>({
path_info: { methodName: NTQQApiMethod.MEDIA_FILE_PATH,
md5HexStr: md5, args: [{
fileName: fileName, path_info: {
elementType: 2, md5HexStr: md5,
elementSubType: 0, fileName: fileName,
thumbSize: 0, elementType: 2,
needCreate: true, elementSubType: 0,
downloadType: 1, thumbSize: 0,
file_uuid: "" needCreate: true,
} downloadType: 1,
}]) file_uuid: ""
}
}]
})
log("media path", mediaPath) log("media path", mediaPath)
await NTQQApi.copyFile(filePath, mediaPath); await NTQQApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQApi.getFileSize(filePath); const fileSize = await NTQQApi.getFileSize(filePath);
@@ -280,28 +328,32 @@ export class NTQQApi {
}, },
undefined, undefined,
] ]
await callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.DOWNLOAD_MEDIA, apiParams) await callNTQQApi({methodName: NTQQApiMethod.DOWNLOAD_MEDIA, args: apiParams})
return sourcePath return sourcePath
} }
static recallMsg(peer: Peer, msgIds: string[]) { static recallMsg(peer: Peer, msgIds: string[]) {
return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.RECALL_MSG, [{ return callNTQQApi({
peer, methodName: NTQQApiMethod.RECALL_MSG, args: [{
msgIds peer,
}, null]) msgIds
}, null]
})
} }
static sendMsg(peer: Peer, msgElements: SendMessageElement[]) { static sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false) {
const sendTimeout = 10 * 1000 const sendTimeout = 10 * 1000
return new Promise<RawMessage>((resolve, reject) => { return new Promise<RawMessage>((resolve, reject) => {
const peerUid = peer.peerUid; const peerUid = peer.peerUid;
let usingTime = 0; let usingTime = 0;
let success = false; let success = false;
let isTimeout = false;
const checkSuccess = () => { const checkSuccess = () => {
if (!success) { if (!success) {
sendMessagePool[peerUid] = null; sendMessagePool[peerUid] = null;
isTimeout = true;
reject("发送超时") reject("发送超时")
} }
} }
@@ -311,29 +363,99 @@ export class NTQQApi {
let lastSending = sendMessagePool[peerUid] let lastSending = sendMessagePool[peerUid]
if (sendTimeout < usingTime) { if (sendTimeout < usingTime) {
sendMessagePool[peerUid] = null; sendMessagePool[peerUid] = null;
isTimeout = true;
reject("发送超时") reject("发送超时")
} }
if (!!lastSending) { if (!!lastSending) {
// log("有正在发送的消息,等待中...") // log("有正在发送的消息,等待中...")
usingTime += 100; usingTime += 500;
setTimeout(checkLastSend, 100); setTimeout(checkLastSend, 500);
} else { } else {
log("可以进行发送消息,设置发送成功回调", sendMessagePool) log("可以进行发送消息,设置发送成功回调", sendMessagePool)
sendMessagePool[peerUid] = (rawMessage: RawMessage) => { sendMessagePool[peerUid] = (rawMessage: RawMessage) => {
success = true;
sendMessagePool[peerUid] = null; 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() checkLastSend()
callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.SEND_MSG, [{ callNTQQApi({
msgId: "0", methodName: NTQQApiMethod.SEND_MSG,
peer, msgElements, args: [{
msgAttributeInfos: new Map(), msgId: "0",
}, null]).then() 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 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, groupCode: string,
maxMember: number, maxMember: number,
memberCount: number, memberCount: number,
@@ -62,6 +63,7 @@ export enum ElementType {
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
PTT = 4, PTT = 4,
FACE = 6,
REPLY = 7, REPLY = 7,
} }
@@ -76,6 +78,7 @@ export interface SendTextElement {
atNtUid: string, atNtUid: string,
} }
} }
export interface SendPttElement { export interface SendPttElement {
elementType: ElementType.PTT, elementType: ElementType.PTT,
elementId: "", 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 { export enum AtType {
notAt = 0, notAt = 0,
@@ -140,6 +149,7 @@ export enum ChatType {
group = 2, group = 2,
temp = 100 temp = 100
} }
export interface PttElement { export interface PttElement {
canConvert2Text: boolean; canConvert2Text: boolean;
duration: number; // 秒数 duration: number; // 秒数
@@ -180,17 +190,36 @@ export interface PicElement {
fileUuid: string; 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 { export interface RawMessage {
msgId: string; msgId: string;
msgShortId?: number; // 自己维护的消息id msgShortId?: number; // 自己维护的消息id
msgTime: string; msgTime: string;
msgSeq: string; msgSeq: string;
senderUin: string; // 发送者QQ号 senderUid: string;
senderUin?: string; // 发送者QQ号
peerUid: string; // 群号 或者 QQ uid peerUid: string; // 群号 或者 QQ uid
peerUin: string; // 群号 或者 发送者QQ号 peerUin: string; // 群号 或者 发送者QQ号
sendNickName: string; sendNickName: string;
sendMemberName?: string; // 发送者群名片 sendMemberName?: string; // 发送者群名片
chatType: ChatType; chatType: ChatType;
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string; // 撤回时间, "0"是没有撤回
elements: { elements: {
elementId: string, elementId: string,
replyElement: { replyElement: {
@@ -208,17 +237,7 @@ export interface RawMessage {
picElement: PicElement; picElement: PicElement;
pttElement: PttElement; pttElement: PttElement;
arkElement: ArkElement; 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, OB11WebsocketResponse} from "./utils"
import {OB11Return, OB11WebsocketReturn} 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<OB11WebsocketReturn<ReturnDataType | null>> {
const result = await this.check(payload)
if (!result.valid) {
return OB11WebsocketResponse.error(result.message, 1400)
}
try {
const resData = await this._handle(payload)
return OB11WebsocketResponse.ok(resData, echo);
} catch (e) {
return OB11WebsocketResponse.error(e.toString(), 1200)
}
}
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,7 +1,7 @@
import { ActionName } from "./types"; import {ActionName} from "./types";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import { NTQQApi } from "../../ntqqapi/ntcall"; import {NTQQApi} from "../../ntqqapi/ntcall";
import { getHistoryMsgByShortId, msgHistory } from "../../common/data"; import {getHistoryMsgByShortId} from "../../common/data";
interface Payload { interface Payload {
message_id: number message_id: number

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 SendMsg from "./SendMsg";
import { ActionName } from "./types"; import {ActionName} from "./types";
class SendGroupMsg extends SendMsg{ class SendGroupMsg extends SendMsg{

View File

@@ -0,0 +1,266 @@
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"
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
}
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())
}
}
private convertMessage2List(message: OB11MessageMixType) {
if (typeof message === "string") {
message = [{
type: OB11MessageDataType.text,
data: {
text: message
}
}] as OB11MessageData[]
} 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) {
nodeIds.push(nodeId)
} else {
// 自定义的消息
// 提取消息段发给自己生成消息id
const {
sendElements,
deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group)
try {
log("开始生成转发节点", sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
nodeIds.push(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)
addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))
return returnMsg
}
}
export default SendMsg

View File

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

View File

@@ -9,6 +9,11 @@ import SendGroupMsg from './SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg' import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg' import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg"; import DeleteMsg from "./DeleteMsg";
import BaseAction from "./BaseAction";
import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage";
import GetStatus from "./GetStatus";
export const actionHandlers = [ export const actionHandlers = [
new GetMsg(), new GetMsg(),
@@ -16,5 +21,20 @@ export const actionHandlers = [
new GetFriendList(), new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(), new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg() new DeleteMsg(),
] new GetVersionInfo(),
new CanSendRecord(),
new CanSendImage(),
new GetStatus()
]
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,8 @@ export interface InvalidCheckResult {
[k: string | number]: any [k: string | number]: any
} }
export enum ActionName{ export enum ActionName {
TestForwardMsg = "test_forward_msg",
GetLoginInfo = "get_login_info", GetLoginInfo = "get_login_info",
GetFriendList = "get_friend_list", GetFriendList = "get_friend_list",
GetGroupInfo = "get_group_info", GetGroupInfo = "get_group_info",
@@ -22,5 +23,9 @@ export enum ActionName{
SendMsg = "send_msg", SendMsg = "send_msg",
SendGroupMsg = "send_group_msg", SendGroupMsg = "send_group_msg",
SendPrivateMsg = "send_private_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",
} }

View File

@@ -0,0 +1,36 @@
import {OB11Return, OB11WebsocketReturn} 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
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(data, "ok", 0)
}
static error(err: string, retcode: number) {
return OB11Response.res(null, "failed", retcode, err)
}
}
export class OB11WebsocketResponse {
static res<T>(data: T, status: string, retcode: number, echo: string, message: string = ""): OB11WebsocketReturn<T> {
return {
status: status,
retcode: retcode,
data: data,
echo: echo,
message: message
}
}
static ok<T>(data: T, echo: string = "") {
return OB11WebsocketResponse.res<T>(data, "ok", 0, echo)
}
static error(err: string, retcode: number, echo: string = "") {
return OB11WebsocketResponse.res(null, "failed", retcode, echo, err)
}
}

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,137 +0,0 @@
import { AtType, ChatType, Group } from "../../ntqqapi/types";
import {
addHistoryMsg,
friends,
getGroup,
getHistoryMsgByShortId,
getStrangerByUin,
} 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";
import * as fs from "fs";
export interface ReturnDataType {
message_id: number
}
class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
protected async _handle(payload: OB11PostSendMsg){
const peer: Peer = {
chatType: ChatType.friend,
peerUid: ""
}
let deleteAfterSentFiles: string[] = []
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 = getHistoryMsgByShortId(replyMsgId)
if (replyMsg) {
sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin, replyMsg.senderUin))
}
}
} break;
case OB11MessageDataType.image:
case OB11MessageDataType.voice: {
const file = sendMsg.data?.file
if (file) {
const {path, isLocal} = (await uri2local(uuid4(), 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))
}
}
}
}
}
}
// log("send msg:", peer, sendElements)
try {
const returnMsg = await NTQQApi.sendMsg(peer, sendElements)
addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f=>fs.unlink(f, ()=>{}))
return { message_id: returnMsg.msgShortId }
} catch (e) {
throw(e.toString())
}
}
}
export default SendMsg

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,21 +1,22 @@
import { import {
OB11MessageDataType,
OB11GroupMemberRole,
OB11Message,
OB11Group, OB11Group,
OB11GroupMember, OB11GroupMember,
OB11GroupMemberRole,
OB11Message,
OB11MessageData,
OB11MessageDataType,
OB11User OB11User
} from "./types"; } from "./types";
import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/types'; import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types';
import { getFriend, getGroupMember, getHistoryMsgBySeq, msgHistory, selfInfo } from '../common/data'; import {getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo} from '../common/data';
import { file2base64, getConfigUtil, log } from "../common/utils"; import {file2base64, getConfigUtil, log} from "../common/utils";
import { NTQQApi } from "../ntqqapi/ntcall"; import {NTQQApi} from "../ntqqapi/ntcall";
export class OB11Constructor { export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { 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 message_type = msg.chatType == ChatType.group ? "group" : "private";
const resMsg: OB11Message = { const resMsg: OB11Message = {
self_id: parseInt(selfInfo.uin), self_id: parseInt(selfInfo.uin),
@@ -41,6 +42,7 @@ export class OB11Constructor {
const member = await getGroupMember(msg.peerUin, msg.senderUin); const member = await getGroupMember(msg.peerUin, msg.senderUin);
if (member) { if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role); resMsg.sender.role = OB11Constructor.groupMemberRole(member.role);
resMsg.sender.nickname = member.nick
} }
} else if (msg.chatType == ChatType.friend) { } else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = "friend" resMsg.sub_type = "friend"
@@ -53,7 +55,7 @@ export class OB11Constructor {
} }
for (let element of msg.elements) { for (let element of msg.elements) {
let message_data: any = { let message_data: OB11MessageData | any = {
data: {}, data: {},
type: "unknown" type: "unknown"
} }
@@ -78,17 +80,17 @@ export class OB11Constructor {
} }
} else if (element.textElement) { } else if (element.textElement) {
message_data["type"] = "text" message_data["type"] = "text"
message_data["data"]["text"] = element.textElement.content resMsg.raw_message += message_data["data"]["text"] = element.textElement.content
} else if (element.picElement) { } else if (element.picElement) {
message_data["type"] = "image" message_data["type"] = "image"
message_data["data"]["file_id"] = element.picElement.fileUuid message_data["data"]["file_id"] = element.picElement.fileUuid
message_data["data"]["path"] = element.picElement.sourcePath message_data["data"]["path"] = element.picElement.sourcePath
message_data["data"]["file"] = element.picElement.sourcePath message_data["data"]["file"] = element.picElement.sourcePath
message_data["data"]["http_file"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl
try { try {
await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, element.picElement.thumbPath.get(0), element.picElement.sourcePath) element.elementId, element.picElement.thumbPath.get(0), element.picElement.sourcePath)
} catch (e) { } catch (e) {
message_data["data"]["http_file"] = IMAGE_HTTP_HOST + element.picElement.originImageUrl
} }
} else if (element.replyElement) { } else if (element.replyElement) {
message_data["type"] = "reply" message_data["type"] = "reply"
@@ -102,7 +104,8 @@ export class OB11Constructor {
message_data["type"] = OB11MessageDataType.voice; message_data["type"] = OB11MessageDataType.voice;
message_data["data"]["file"] = element.pttElement.filePath message_data["data"]["file"] = element.pttElement.filePath
message_data["data"]["file_id"] = element.pttElement.fileUuid 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 => { // window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => {
// console.log("语音转文字结果", text); // console.log("语音转文字结果", text);
// }).catch(err => { // }).catch(err => {
@@ -111,18 +114,24 @@ export class OB11Constructor {
} else if (element.arkElement) { } else if (element.arkElement) {
message_data["type"] = OB11MessageDataType.json; message_data["type"] = OB11MessageDataType.json;
message_data["data"]["data"] = element.arkElement.bytesData; 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) { if (message_data.data.file) {
message_data.data.file = message_data.data.http_file
} else if (message_data.data.file) {
let filePath: string = message_data.data.file; let filePath: string = message_data.data.file;
message_data.data.file = "file://" + filePath if (!enableLocalFile2Url) {
if (enableBase64) { message_data.data.file = "file://" + filePath
let {err, data} = await file2base64(filePath); } else { // 不使用本地路径
if (err) { if (message_data.data.http_file) {
console.log("文件转base64失败", err) message_data.data.file = message_data.data.http_file
} else { } 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
}
} }
} }
} }
@@ -178,7 +187,9 @@ export class OB11Constructor {
static group(group: Group): OB11Group { static group(group: Group): OB11Group {
return { return {
group_id: parseInt(group.groupCode), group_id: parseInt(group.groupCode),
group_name: group.groupName group_name: group.groupName,
member_count: group.memberCount,
max_member_count: group.maxMember
} }
} }

View File

@@ -0,0 +1,15 @@
import {selfInfo} from "../../common/data";
export enum EventType {
META = "meta_event",
REQUEST = "request",
NOTICE = "notice",
MESSAGE = "message"
}
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,168 +0,0 @@
import * as http from "http";
import * as websocket from "ws";
import express from "express";
import { Request } from 'express';
import { Response } from 'express';
import { getConfigUtil, log } from "../common/utils";
import { selfInfo } from "../common/data";
import { OB11Message, OB11Return, OB11MessageData } from './types';
import { actionHandlers } from "./actions";
import { OB11Response } from "./actions/utils";
import { ActionName } from "./actions/types";
import BaseAction from "./actions/BaseAction";
let wsServer: websocket.Server = null;
const JSONbig = require('json-bigint')({storeAsString: true});
const expressAPP = express();
let httpServer: http.Server = null;
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();
});
});
export function startHTTPServer(port: number) {
if (httpServer) {
httpServer.close();
}
expressAPP.get('/', (req: Request, res: Response) => {
res.send('LLOneBot已启动');
})
httpServer = expressAPP.listen(port, "0.0.0.0", () => {
console.log(`LLOneBot http server started 0.0.0.0:${port}`);
});
}
let wsEventClients: websocket.WebSocket[] = [];
type RouterHandler = (payload: any) => Promise<OB11Return<any>>
let routers: Record<string, RouterHandler> = {};
function wsReply(wsClient: websocket.WebSocket, data: OB11Return<any> | OB11Message) {
try {
wsClient.send(JSON.stringify(data))
} catch (e) {
log("websocket 回复失败", e)
}
}
export function startWSServer(port: number) {
if (wsServer) {
wsServer.close((err)=>{
log("ws server close failed!", err)
})
}
wsServer = new websocket.Server({port})
wsServer.on("connection", (ws, req) => {
const url = req.url;
// ws.send('Welcome to the LLOneBot WebSocket server! url:' + url);
if (url == "/api" || url == "/api/" || url == "/") {
ws.on("message", async (msg) => {
let receiveData: { action: ActionName, params: any } = {action: null, params: {}}
log("收到ws消息", msg.toString())
try {
receiveData = JSON.parse(msg.toString())
} catch (e) {
return wsReply(ws, OB11Response.error("json解析失败请检查数据格式"))
}
const handle: RouterHandler | undefined = routers[receiveData.action]
if (!handle) {
return wsReply(ws, OB11Response.error("不支持的api " + receiveData.action))
}
try {
const handleResult = await handle(receiveData.params)
wsReply(ws, handleResult)
} catch (e) {
wsReply(ws, OB11Response.error(`api处理出错:${e}`))
}
})
}
if (url == "/event" || url == "/event/" || url == "/") {
log("event上报ws客户端已连接")
wsEventClients.push(ws)
ws.on("close", () => {
log("event上报ws客户端已断开")
wsEventClients = wsEventClients.filter((c) => c != ws)
})
}
})
}
export function postMsg(msg: OB11Message) {
const {reportSelfMessage} = getConfigUtil().getConfig()
if (!reportSelfMessage) {
if (msg.user_id.toString() == 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(`新消息事件HTTP上报成功: ${host} ` + JSON.stringify(msg));
}, (err: any) => {
log(`新消息事件HTTP上报失败: ${host} ` + err + JSON.stringify(msg));
});
}
for (const wsClient of wsEventClients) {
log("新消息事件ws上报", msg)
new Promise((resolve, reject) => {
wsReply(wsClient, msg);
}).then();
}
}
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[action] = 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 {OB11WebsocketResponse} 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 = ""
log("收到反向Websocket消息", msg.toString())
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
} catch (e) {
return wsReply(this.websocket, OB11WebsocketResponse.error("json解析失败请检查数据格式", 1400, echo))
}
const action: BaseAction<any, any> = actionMap.get(receiveData.action);
if (!action) {
return wsReply(this.websocket, OB11WebsocketResponse.error("不支持的api " + receiveData.action, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(receiveData.params, echo);
wsReply(this.websocket, handleResult)
} catch (e) {
wsReply(this.websocket, OB11WebsocketResponse.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 {OB11WebsocketResponse} 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(OB11WebsocketResponse.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, OB11WebsocketResponse.error("不支持的api " + actionName, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(params, echo);
wsReply(wsClient, handleResult)
} catch (e) {
wsReply(wsClient, OB11WebsocketResponse.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 = ""
log("收到正向Websocket消息", msg.toString())
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
} catch (e) {
return wsReply(wsClient, OB11WebsocketResponse.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 {OB11WebsocketResponse} from "../../action/utils";
import {PostEventType} from "../postevent";
import {log} from "../../../common/utils";
export function wsReply(wsClient: websocket.WebSocket, data: OB11WebsocketResponse | 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,19 +1,18 @@
import { AtType } from "../ntqqapi/types"; import {AtType, RawMessage} from "../ntqqapi/types";
import { RawMessage } from "../ntqqapi/types";
export interface OB11User{ export interface OB11User {
user_id: number; user_id: number;
nickname: string; nickname: string;
remark?: string remark?: string
} }
export enum OB11UserSex{ export enum OB11UserSex {
male = "male", male = "male",
female = "female", female = "female",
unknown = "unknown" unknown = "unknown"
} }
export enum OB11GroupMemberRole{ export enum OB11GroupMemberRole {
owner = "owner", owner = "owner",
admin = "admin", admin = "admin",
member = "member", member = "member",
@@ -33,7 +32,7 @@ export interface OB11GroupMember {
title?: string title?: string
} }
export interface OB11Group{ export interface OB11Group {
group_id: number group_id: number
group_name: string group_name: string
member_count?: number member_count?: number
@@ -72,27 +71,16 @@ export interface OB11Message {
raw?: RawMessage 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> { export interface OB11Return<DataType> {
status: number status: string
retcode: number retcode: number
data: DataType data: DataType
message: string message: string,
} }
export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{} export interface OB11WebsocketReturn<DataType> extends OB11Return<DataType>{
echo: string
}
export enum OB11MessageDataType { export enum OB11MessageDataType {
text = "text", text = "text",
@@ -100,43 +88,89 @@ export enum OB11MessageDataType {
voice = "record", voice = "record",
at = "at", at = "at",
reply = "reply", reply = "reply",
json = "json" json = "json",
face = "face",
node = "node" // 合并转发消息
} }
export type OB11MessageData = { export interface OB11MessageText {
type: OB11MessageDataType.text, type: OB11MessageDataType.text,
content: string, data: {
data?: {
text: string, // 纯文本 text: string, // 纯文本
} }
} | { }
type: "image" | "voice" | "record",
file: string, // 本地路径 interface OB11MessageFileBase {
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,
data: { 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 { export interface OB11PostSendMsg {
message_type?: "private" | "group" message_type?: "private" | "group"
user_id: string, user_id: string,
group_id?: string, group_id?: string,
message: OB11MessageData[] | string | OB11MessageData; message: OB11MessageMixType;
} }
export interface OB11Version {
app_name: "LLOneBot"
app_version: string
protocol_version: "v11"
}
export interface OB11Status {
online: boolean | null,
good: boolean
}

View File

@@ -1,7 +1,7 @@
import { CONFIG_DIR, isGIF } from "../common/utils"; import {CONFIG_DIR, isGIF} from "../common/utils";
import * as path from 'path'; import * as path from 'path';
import { NTQQApi } from '../ntqqapi/ntcall'; import {OB11MessageData} from "./types";
import { OB11MessageData } from "./types";
const fs = require("fs").promises; const fs = require("fs").promises;
export async function uri2local(fileName: string, uri: string){ export async function uri2local(fileName: string, uri: string){

View File

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

View File

@@ -5,53 +5,113 @@
async function onSettingWindowCreated(view: Element) { async function onSettingWindowCreated(view: Element) {
window.llonebot.log("setting window created"); window.llonebot.log("setting window created");
let config = await window.llonebot.getConfig() 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 = ` let eleStr = `
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="hostItem vertical-list-item ${httpPostClass}">
<h2>事件上报地址(http)</h2> <h2>HTTP事件上报地址(http)</h2>
<input class="host input-text" type="text" value="${host}" <input class="httpHost input-text" type="text" value="${host}"
style="width:60%;padding: 5px" style="width:60%;padding: 5px"
placeholder="如果localhost上报失败试试局域网ip"/> placeholder="如:http://127.0.0.1:8080/onebot/v11/http"/>
</setting-item> </setting-item>
` `
return eleStr return eleStr
} }
let hostsEleStr = "" function createWsHostEleStr(host: string) {
for (const host of config.hosts) { let eleStr = `
hostsEleStr += creatHostEleStr(host); <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 = ` let html = `
<div class="config_view llonebot"> <div class="config_view llonebot">
<setting-section> <setting-section>
<setting-panel> <setting-panel>
<setting-list class="wrap"> <setting-list class="wrap">
<setting-item class="vertical-list-item" data-direction="row"> <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>
<setting-item class="vertical-list-item ${httpClass}" data-direction="row" style="display: ${config.ob11.enableHttp ? '' : 'none'}">
<setting-text>HTTP监听端口</setting-text> <setting-text>HTTP监听端口</setting-text>
<input id="port" type="number" value="${config.port}"/> <input id="httpPort" type="number" value="${config.ob11.httpPort}"/>
</setting-item> </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>
<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>
<setting-item class="vertical-list-item" data-direction="row"> <setting-item class="vertical-list-item" data-direction="row">
<setting-text>正向ws监听端口</setting-text> <setting-text>Access Token</setting-text>
<input id="wsPort" type="number" value="${config.wsPort}"/> <input id="token" type="text" placeholder="可为空" value="${config.token}"/>
</setting-item> </setting-item>
<div>
<button id="addHost" class="q-button">添加HTTP上报地址</button>
</div>
<div id="hostItems">
${hostsEleStr}
</div>
<button id="save" class="q-button">保存</button> <button id="save" class="q-button">保存</button>
</setting-list> </setting-list>
</setting-panel> </setting-panel>
<setting-panel> <setting-panel>
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="hostItem vertical-list-item">
<div> <div>
<div>上报文件进行base64编码</div> <div>上报文件不采用本地路径</div>
<div class="tips">开启,上报文件将以本地路径形式发送</div> <div class="tips">开启,上报图片为http连接语音为base64编码</div>
</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>
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="hostItem vertical-list-item">
<div> <div>
@@ -63,14 +123,14 @@ async function onSettingWindowCreated(view: Element) {
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="hostItem vertical-list-item">
<div> <div>
<div>上报自身消息</div> <div>上报自身消息</div>
<div class="tips">开启后上报自己发出的消息</div> <div class="tips">慎用,不然会自己和自己聊个不停</div>
</div> </div>
<setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? "is-active" : ""}></setting-switch> <setting-switch id="reportSelfMessage" ${config.reportSelfMessage ? "is-active" : ""}></setting-switch>
</setting-item> </setting-item>
<setting-item data-direction="row" class="hostItem vertical-list-item"> <setting-item data-direction="row" class="hostItem vertical-list-item">
<div> <div>
<div>日志</div> <div>日志</div>
<div class="tips">日志目录:${window.LiteLoader.plugins["LLOneBot"].path.data}</div> <div class="tips">目录:${window.LiteLoader.plugins["LLOneBot"].path.data}</div>
</div> </div>
<setting-switch id="log" ${config.log ? "is-active" : ""}></setting-switch> <setting-switch id="log" ${config.log ? "is-active" : ""}></setting-switch>
</setting-item> </setting-item>
@@ -96,54 +156,93 @@ async function onSettingWindowCreated(view: Element) {
const doc = parser.parseFromString(html, "text/html"); const doc = parser.parseFromString(html, "text/html");
function addHostEle(initValue: string = "") { function addHostEle(type: string, initValue: string = "") {
let addressDoc = parser.parseFromString(creatHostEleStr(initValue), "text/html"); let addressEle, hostItemsEle;
let addressEle = addressDoc.querySelector("setting-item") if (type === "ws") {
let hostItemsEle = document.getElementById("hostItems"); 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); 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) => { doc.getElementById(eleId)?.addEventListener("click", (e) => {
const switchEle = e.target as HTMLInputElement const switchEle = e.target as HTMLInputElement
if (config[configKey]) { if (_config[configKey]) {
config[configKey] = false _config[configKey] = false
switchEle.removeAttribute("is-active") switchEle.removeAttribute("is-active")
} else { } else {
config[configKey] = true _config[configKey] = true
switchEle.setAttribute("is-active", "") 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) 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("debug", "debug");
switchClick("switchBase64", "enableBase64"); switchClick("switchFileUrl", "enableLocalFile2Url");
switchClick("reportSelfMessage", "reportSelfMessage"); switchClick("reportSelfMessage", "reportSelfMessage");
switchClick("log", "log"); switchClick("log", "log");
doc.getElementById("save")?.addEventListener("click", doc.getElementById("save")?.addEventListener("click",
() => { () => {
const portEle: HTMLInputElement = document.getElementById("port") as HTMLInputElement const httpPortEle: HTMLInputElement = document.getElementById("httpPort") as HTMLInputElement;
const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement const httpHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("httpHost") as HTMLCollectionOf<HTMLInputElement>;
const hostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("host") as HTMLCollectionOf<HTMLInputElement>; const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement;
// const port = doc.querySelector("input[type=number]")?.value const wsHostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("wsHost") as HTMLCollectionOf<HTMLInputElement>;
// const host = doc.querySelector("input[type=text]")?.value const tokenEle = document.getElementById("token") as HTMLInputElement;
// 获取端口和host // 获取端口和host
const port = portEle.value const httpPort = httpPortEle.value
const wsPort = wsPortEle.value let httpHosts: string[] = [];
let hosts: string[] = [];
for (const hostEle of hostEles) { for (const hostEle of httpHostEles) {
if (hostEle.value) { const value = hostEle.value.trim();
hosts.push(hostEle.value); value && httpHosts.push(value);
}
} }
config.port = parseInt(port);
config.wsPort = parseInt(wsPort); const wsPort = wsPortEle.value;
config.hosts = hosts; 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); window.llonebot.setConfig(config);
alert("保存成功"); alert("保存成功");
}) })
@@ -153,7 +252,21 @@ async function onSettingWindowCreated(view: Element) {
view.appendChild(node); 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, "allowJs": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"moduleResolution": "node", "moduleResolution": "node",
// "sourceMap": true
}, },
"include": ["src/*"], "include": ["src/*"],
"exclude": [ "exclude": [

View File

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