Compare commits

..

128 Commits

Author SHA1 Message Date
linyuchen
112ef202d1 fix: ws max payload 2024-04-01 22:05:40 +08:00
linyuchen
267052afbb fix: egg 2024-03-31 22:01:20 +08:00
linyuchen
0c59371ed1 fix: report first temp msg 2024-03-31 21:45:17 +08:00
linyuchen
655225e027 feat: icon 2024-03-30 19:53:55 +08:00
linyuchen
bc49bf520c fix: reverse ws restart 2024-03-30 19:11:20 +08:00
linyuchen
dd03e384ce fix: group title
fix: http quick action handle friend request
2024-03-30 14:35:43 +08:00
linyuchen
ecd64529a4 chore: ver 3.20.5 2024-03-30 13:50:17 +08:00
linyuchen
016482c9e5 fix: friend request flag invalid 2024-03-30 13:48:06 +08:00
linyuchen
23be081d29 fix: some png can not send 2024-03-30 12:02:38 +08:00
linyuchen
33688e9e5c fix: image url can not access when appid=1406 2024-03-30 11:52:00 +08:00
linyuchen
de8c2e1168 chore: ver 3.20.3 2024-03-29 21:42:07 +08:00
linyuchen
2a1fc07b94 fix: image rkey expired 2024-03-29 21:26:29 +08:00
linyuchen
c1b6daaf32 refactor: emmm 2024-03-29 01:30:05 +08:00
linyuchen
02c973fe5e refactor: optimize save image rkey 2024-03-29 01:26:38 +08:00
linyuchen
d6b44053de chore: ver 3.20.2 2024-03-29 00:32:14 +08:00
linyuchen
1d69764952 refactor: optimize save config 2024-03-29 00:30:47 +08:00
linyuchen
d9377e4684 fix: kick group member event sub_type 2024-03-29 00:19:03 +08:00
linyuchen
f30dd81455 Merge branch 'main' into dev
# Conflicts:
#	src/onebot11/constructor.ts
2024-03-28 23:37:27 +08:00
linyuchen
0116f8d384 fix: user info level 2024-03-28 23:35:52 +08:00
linyuchen
88d68f4360 Merge pull request #166 from CHH2000day/dev
修复rkey缺失导致的某些图片无法获取
2024-03-28 23:03:00 +08:00
Ayatsuki Renge
ea0f5a9f80 fix:invalid image url due to missing rkey
ref:2c8094c8c8
2024-03-28 22:47:49 +08:00
linyuchen
4591c1b659 fix: some audio can't play 2024-03-27 23:17:56 +08:00
linyuchen
97a424f62e Merge pull request #163 from idanran/main
fix: audio
2024-03-27 22:44:07 +08:00
idanran
410ef5a050 fix: audio 2024-03-27 14:35:19 +00:00
linyuchen
128091dff9 chore: ver 3.20.0 2024-03-27 21:29:56 +08:00
linyuchen
c7b6fd89fd fix: bot join group event 2024-03-27 21:27:34 +08:00
linyuchen
b55f35549d feat: report forward msg,get_forward_msg 2024-03-27 20:07:56 +08:00
linyuchen
ca0a6cfb22 chore: ver 3.19.4 2024-03-25 19:04:30 +08:00
linyuchen
3303b30c4c Merge branch 'main' into dev 2024-03-25 19:01:34 +08:00
linyuchen
429d8deb5c feat: gocq api router add send_forward_msg 2024-03-25 19:01:28 +08:00
linyuchen
48f12fc30b fix: pic subType 2024-03-25 18:52:15 +08:00
linyuchen
41f0e8f574 Merge pull request #159 from MisakaTAT/main
feat: added an gocqhttp extended api send_forward_msg
2024-03-25 18:11:05 +08:00
MisakaTAT
cd50df3a56 feat: added an gocqhttp extended api send_forward_msg 2024-03-25 17:51:04 +08:00
linyuchen
4461c7ed47 fix: group card event old_card 2024-03-25 15:07:35 +08:00
linyuchen
e5f4992eb3 feat: market face 2024-03-24 21:32:25 +08:00
linyuchen
468f1710b9 fix: group member role not sync 2024-03-24 20:21:18 +08:00
linyuchen
626d445dc3 chore: ver 3.19.2 2024-03-24 12:10:23 +08:00
linyuchen
b413a224be fix: send forward 2024-03-24 12:02:56 +08:00
linyuchen
6542f2e63b fix: get group list
fix: 兼容 cc
2024-03-24 11:57:02 +08:00
linyuchen
94c928905e fix: get self uin on old QQ 2024-03-24 00:48:30 +08:00
linyuchen
c14f8b21c2 fix: send private msg 2024-03-24 00:10:13 +08:00
linyuchen
6d5ccc6664 fix: add field busid of upload group file event
fix: operator_id typo of group_increase event
2024-03-23 23:24:17 +08:00
linyuchen
79090d764f Merge pull request #156 from idanran/main
fix: audio
2024-03-23 22:43:18 +08:00
idanran
6ab0cd7f4b fix: audio 2024-03-23 14:41:08 +00:00
linyuchen
bb3bce203d fix: audio sample rate 2024-03-23 22:25:48 +08:00
linyuchen
36f7f1b026 refactor: audio.ts 2024-03-23 21:14:24 +08:00
linyuchen
5a0dbdb5ce refactor: remove guess silk duration 2024-03-23 21:12:43 +08:00
linyuchen
48d62be2d6 Merge branch 'main' into dev 2024-03-23 21:10:09 +08:00
linyuchen
b314e2f3a0 refactor: log dir 2024-03-23 21:08:34 +08:00
linyuchen
63b9204a4b Merge pull request #154 from idanran/main
fix: audio encoding exception in some cases
2024-03-23 20:51:16 +08:00
idanran
bf701c2110 fix: audio encoding exception in some cases 2024-03-23 11:57:13 +00:00
linyuchen
95b4b11f02 chore: ver 3.19.0 2024-03-23 19:27:30 +08:00
linyuchen
1735babb7d feat: http post quick operation 2024-03-23 19:16:07 +08:00
linyuchen
89c3f07cba refactor: parse video|file element 2024-03-23 12:03:22 +08:00
linyuchen
5cf45a452b merge main 2024-03-23 00:08:46 +08:00
linyuchen
23d5fa7218 Merge branch 'main' into dev 2024-03-23 00:00:49 +08:00
linyuchen
983d2462d4 refactor: action folder
feat: group card event
feat: group title event
2024-03-23 00:00:43 +08:00
Misa Liu
3c68bc77ce chore: Refactoring GitHub issue template 2024-03-22 17:43:40 +08:00
Misa Liu
501211fb57 fix(renderer): Fix typo & format error 2024-03-22 16:56:17 +08:00
linyuchen
0cd41a8a52 feat: ask save config dialog 2024-03-21 21:53:17 +08:00
linyuchen
d339a778df fix: get_group_msg_history return type 2024-03-21 19:54:59 +08:00
linyuchen
dc843f77a3 chore: ver 3.18.1 2024-03-21 18:15:08 +08:00
linyuchen
b103f2015c chore: ver 3.18.1 2024-03-21 18:14:57 +08:00
linyuchen
baf35d5496 fix: get_group_msg_history on qq version < 22106 2024-03-21 18:10:01 +08:00
linyuchen
5c34afc228 fix: audio duration 2024-03-21 13:34:49 +08:00
linyuchen
a8a6290b70 chore: ver 3.18.0 2024-03-21 13:21:08 +08:00
linyuchen
9d50c6d4fd fix: audio duration 2024-03-21 13:18:59 +08:00
linyuchen
175a8ceb3d Merge branch 'main' into dev
# Conflicts:
#	src/common/utils/file.ts
2024-03-21 13:05:15 +08:00
linyuchen
31601981f2 Merge remote-tracking branch 'origin/main' 2024-03-21 13:03:40 +08:00
linyuchen
6a8c5ec24a fix: auto create temp dir 2024-03-21 13:03:20 +08:00
linyuchen
ebca6a07c5 fix: auto create temp dir 2024-03-21 13:02:15 +08:00
linyuchen
4f9345e4e5 fix: send forward msg message param 2024-03-21 12:23:16 +08:00
linyuchen
ac17dbefe0 feat: http post secret 2024-03-21 12:21:52 +08:00
linyuchen
c9486b4f55 Merge pull request #145 from idanran/main
fix: unable to send voice in some cases
2024-03-21 10:16:27 +08:00
idanran
35951fd61a fix: unable to send voice in some cases 2024-03-20 17:32:21 +00:00
linyuchen
fdc23d7721 fix: silk duration 2024-03-20 22:47:24 +08:00
linyuchen
560428a5f9 fix: url boolean param 2024-03-20 21:00:24 +08:00
linyuchen
e276d0e4f8 Merge branch 'dev' 2024-03-20 18:53:08 +08:00
linyuchen
965aa48729 fix: check new version 2024-03-20 18:52:44 +08:00
linyuchen
51e332ec38 Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-03-20 18:25:50 +08:00
linyuchen
1307679dae fix: stranger info 2024-03-20 18:25:09 +08:00
linyuchen
7966bf75c3 Merge pull request #143 from LLOneBot/fix-update
Fix: update
2024-03-20 12:24:17 +08:00
手瓜一十雪
d5a3687f2b fix: checkVersion 2024-03-20 12:03:25 +08:00
linyuchen
7cafbdfae5 fix: set p_skey cookie 2024-03-20 11:36:15 +08:00
linyuchen
103bf94170 test pskey 2024-03-20 11:06:00 +08:00
linyuchen
235328e4fe feat: get pskey & skey 2024-03-19 23:45:56 +08:00
linyuchen
c371f1c5a3 Merge branch 'dev'
# Conflicts:
#	manifest.json
2024-03-19 20:47:25 +08:00
linyuchen
d0377bd2d3 chore: ver 3.17.0 2024-03-19 20:38:59 +08:00
linyuchen
aae10181b5 chore: ver 3.17.0 2024-03-19 20:35:57 +08:00
linyuchen
a298377717 feat: send video not need ffmpeg 2024-03-19 20:35:30 +08:00
linyuchen
8afe0af940 Merge branch 'no-ffprobe' into dev 2024-03-19 20:12:38 +08:00
linyuchen
352793d05f feat: send json 2024-03-19 20:10:10 +08:00
linyuchen
3a443f4ebf feat: default video thumb 2024-03-19 18:58:44 +08:00
linyuchen
917b55c1c3 Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-03-19 18:40:43 +08:00
linyuchen
01d77827a8 feat: api /get_file 2024-03-19 18:39:22 +08:00
linyuchen
ead79a39f7 Merge pull request #141 from MisaLiu/fix_select
使用自制的 `<setting-select>` 组件来避免下拉框滚动不跟随问题
2024-03-19 17:38:43 +08:00
HIMlaoS_Misa
ebc245b9f3 fix: Uncomment code 2024-03-19 17:36:11 +08:00
HIMlaoS_Misa
47d6dc09db fix: Missing brace 2024-03-19 17:32:14 +08:00
HIMlaoS_Misa
165fcb13cb fix: Remove unused config code 2024-03-19 17:31:18 +08:00
HIMlaoS_Misa
c2405abdd3 Merge branch 'dev' into fix_select 2024-03-19 17:29:01 +08:00
Misa Liu
56492b21dd fix: Use custom setting-select component 2024-03-19 17:25:17 +08:00
linyuchen
37c4f02118 refactor: upgrade 2024-03-19 16:32:12 +08:00
linyuchen
92a2d8b5e2 test no ffmpeg 2024-03-19 14:57:38 +08:00
linyuchen
3a964af0b0 refactor: http download function 2024-03-19 14:36:58 +08:00
linyuchen
fa5540da5c Merge pull request #137 from LLOneBot/feat-update
feat: upgrade
2024-03-19 12:35:55 +08:00
linyuchen
eccf588569 feat: api /get_group_msg_history 2024-03-19 12:33:08 +08:00
手瓜一十雪
aad165ce5e chore: remove test version 2024-03-19 12:03:17 +08:00
手瓜一十雪
10c48a5b86 feat:check update 2024-03-19 12:01:57 +08:00
手瓜一十雪
63c2b95cbb feat:download update 2024-03-19 11:28:37 +08:00
手瓜一十雪
1d130d4580 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into feat-update 2024-03-19 11:23:41 +08:00
手瓜一十雪
2dd5d81ffe feat: update text 2024-03-19 11:18:41 +08:00
手瓜一十雪
affefca19f fix:checkVersion error 2024-03-19 11:02:41 +08:00
手瓜一十雪
7381fb3e11 feat:update renderer 2024-03-19 10:47:58 +08:00
linyuchen
9679f29f48 Merge branch 'main' into dev 2024-03-19 00:56:33 +08:00
linyuchen
dda5ea3972 feat: stranger info add sex & qq_level 2024-03-19 00:45:59 +08:00
linyuchen
b12d205059 feat: stranger info add sex & qq_level 2024-03-19 00:37:20 +08:00
linyuchen
6ea6b33e9a refactor: file utils 2024-03-19 00:33:51 +08:00
手瓜一十雪
b5655a1a5f fix: updater real download url 2024-03-18 19:37:57 +08:00
linyuchen
dc559ce36c Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-03-18 11:46:38 +08:00
linyuchen
9ed67628bc Merge pull request #133 from HollisMeynell/main
add: support for file download
2024-03-18 11:44:21 +08:00
spring
5aecf45959 add: support for file download 2024-03-18 11:32:56 +08:00
linyuchen
911841401a refactor: utils structure 2024-03-18 11:10:53 +08:00
linyuchen
4c6bd3df0b refactor: utils structure 2024-03-18 11:07:08 +08:00
linyuchen
c5932bcd98 refactor: utils update 2024-03-18 10:56:50 +08:00
linyuchen
3abc9f2ae0 chore: ver 3.16.1 2024-03-18 09:53:41 +08:00
linyuchen
e716c28e9a Merge branch 'dev'
# Conflicts:
#	src/main/main.ts
#	src/version.ts
2024-03-18 09:53:13 +08:00
linyuchen
9209ae766c feat: 接收戳一戳开关
feat: message_sent 事件添加 target_id 字段
feat: 回复临时消息
2024-03-18 09:49:47 +08:00
linyuchen
af8cf1882c refactor: member qq_level 2024-03-18 06:59:11 +08:00
107 changed files with 4044 additions and 1709 deletions

View File

@@ -1,20 +0,0 @@
---
name: Bug反馈
about: 报个Bug
title: ''
labels: bug
assignees: ''
---
QQ版本
LLOneBot版本
调用LLOneBot的方式或者应用端(如postman直接调用或NoneBot2、Koishi)
BUG描述
复现步骤:
LLOneBot日志:

81
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Bug 反馈
description: 报告可能的 LLOneBot 异常行为
title: '[BUG] '
labels: bug
body:
- type: markdown
attributes:
value: |
欢迎来到 LLOneBot 的 Issue Tracker请填写以下表格来提交 Bug。
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
- type: input
id: system-version
attributes:
label: 系统版本
description: 运行 QQNT 的系统版本
placeholder: Windows 10 Pro Workstation 22H2
validations:
required: true
- type: input
id: qqnt-version
attributes:
label: QQNT 版本
description: 可在 QQNT 的「关于」或是在 LiteLoaderQQNT 的设置页中找到
placeholder: 9.9.7-21804
validations:
required: true
- type: input
id: llonebot-version
attributes:
label: LLOneBot 版本
description: 可在 LiteLoaderQQNT 的设置页或是 QQNT 的设置页侧栏中找到
placeholder: 3.18.0
validations:
required: true
- type: input
id: onebot-client-version
attributes:
label: OneBot 客户端
description: 连接至 LLOneBot 的客户端版本信息
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: 发生了什么?
description: 填写你认为的 LLOneBot 的不正常行为
validations:
required: true
- type: textarea
id: how-reproduce
attributes:
label: 如何复现
description: 填写应当如何操作才能触发这个不正常行为
placeholder: |
1. xxx
2. xxx
3. xxx
validations:
required: true
- type: textarea
id: what-expected
attributes:
label: 期望的结果?
description: 填写你认为 LLOneBot 应当执行的正常行为
validations:
required: true
- type: textarea
id: llonebot-log
attributes:
label: LLOneBot 运行日志
description: 在 LLOneBot 的设置页中打开「写入日志」然后粘贴相关日志内容到此处
render: shell
- type: textarea
id: onebot-client-log
attributes:
label: OneBot 客户端运行日志
description: 粘贴 OneBot 客户端的相关日志内容到此处
render: shell

View File

@@ -1,10 +1,10 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.16.0", "name": "LLOneBot v3.20.7",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新", "description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新",
"version": "3.16.0", "version": "3.20.7",
"icon": "./icon.jpg", "icon": "./icon.jpg",
"authors": [ "authors": [
{ {

254
package-lock.json generated
View File

@@ -9,12 +9,12 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"compressing": "^1.10.0",
"express": "^4.18.2", "express": "^4.18.2",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1", "level": "^8.0.1",
"node-stream-zip": "^1.15.0", "silk-wasm": "^3.3.4",
"silk-wasm": "^3.2.3",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"
@@ -551,6 +551,15 @@
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
} }
}, },
"node_modules/@eggjs/yauzl": {
"version": "2.11.0",
"resolved": "https://registry.npmmirror.com/@eggjs/yauzl/-/yauzl-2.11.0.tgz",
"integrity": "sha512-Jq+k2fCZJ3i3HShb0nxLUiAgq5pwo8JTT1TrH22JoehZQ0Nm2dvByGIja1NYfNyuE4Tx5/Dns5nVsBN/mlC8yg==",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer2": "^1.2.0"
}
},
"node_modules/@electron/get": { "node_modules/@electron/get": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
@@ -2151,6 +2160,47 @@
} }
] ]
}, },
"node_modules/bl": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/bl/-/bl-1.2.3.tgz",
"integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==",
"dependencies": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
}
},
"node_modules/bl/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/bl/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/bl/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/bl/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@@ -2281,15 +2331,33 @@
"ieee754": "^1.2.1" "ieee754": "^1.2.1"
} }
}, },
"node_modules/buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"dependencies": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"node_modules/buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
},
"node_modules/buffer-crc32": { "node_modules/buffer-crc32": {
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"engines": { "engines": {
"node": "*" "node": "*"
} }
}, },
"node_modules/buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2515,6 +2583,36 @@
"optional": true, "optional": true,
"peer": true "peer": true
}, },
"node_modules/compressing": {
"version": "1.10.0",
"resolved": "https://registry.npmmirror.com/compressing/-/compressing-1.10.0.tgz",
"integrity": "sha512-k2vpbZLaJoHe9euyUZjYYE8vOrbR19aU3HcWIYw5EBXiUs34ygfDVnXU+ubI41JXMriHutnoiu0ZFdwCkH6jPA==",
"dependencies": {
"@eggjs/yauzl": "^2.11.0",
"flushwritable": "^1.0.0",
"get-ready": "^1.0.0",
"iconv-lite": "^0.5.0",
"mkdirp": "^0.5.1",
"pump": "^3.0.0",
"streamifier": "^0.1.1",
"tar-stream": "^1.5.2",
"yazl": "^2.4.2"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/compressing/node_modules/iconv-lite": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.5.2.tgz",
"integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/concat-map/-/concat-map-0.0.1.tgz",
@@ -2559,6 +2657,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/create-require": { "node_modules/create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/create-require/-/create-require-1.1.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/create-require/-/create-require-1.1.1.tgz",
@@ -2813,7 +2916,6 @@
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"dependencies": { "dependencies": {
"once": "^1.4.0" "once": "^1.4.0"
} }
@@ -3636,6 +3738,14 @@
"pend": "~1.2.0" "pend": "~1.2.0"
} }
}, },
"node_modules/fd-slicer2": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/fd-slicer2/-/fd-slicer2-1.2.0.tgz",
"integrity": "sha512-3lBUNUckhMZduCc4g+Pw4Ve16LD9vpX9b8qUkkKq2mgDRLYWzblszZH2luADnJqjJe+cypngjCuKRm/IW12rRw==",
"dependencies": {
"pend": "^1.2.0"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3738,6 +3848,11 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/flushwritable": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/flushwritable/-/flushwritable-1.0.0.tgz",
"integrity": "sha512-3VELfuWCLVzt5d2Gblk8qcqFro6nuwvxwMzHaENVDHI7rxcBRtMCwTk/E9FXcgh+82DSpavPNDueA9+RxXJoFg=="
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/for-each/-/for-each-0.3.3.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/for-each/-/for-each-0.3.3.tgz",
@@ -3763,6 +3878,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs-extra": { "node_modules/fs-extra": {
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -3859,6 +3979,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-ready": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/get-ready/-/get-ready-1.0.0.tgz",
"integrity": "sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw=="
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -4859,11 +4984,21 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://mirrors.cloud.tencent.com/npm/minimist/-/minimist-1.2.8.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/module-error": { "node_modules/module-error": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/module-error/-/module-error-1.0.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/module-error/-/module-error-1.0.2.tgz",
@@ -4931,18 +5066,6 @@
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true "dev": true
}, },
"node_modules/node-stream-zip": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
"integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==",
"engines": {
"node": ">=0.12.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/antelle"
}
},
"node_modules/normalize-url": { "node_modules/normalize-url": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@@ -5052,7 +5175,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
} }
@@ -5165,8 +5287,7 @@
"node_modules/pend": { "node_modules/pend": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
"dev": true
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
@@ -5232,6 +5353,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -5257,7 +5383,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"dependencies": { "dependencies": {
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
"once": "^1.3.1" "once": "^1.3.1"
@@ -5770,9 +5895,9 @@
} }
}, },
"node_modules/silk-wasm": { "node_modules/silk-wasm": {
"version": "3.2.3", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/silk-wasm/-/silk-wasm-3.2.3.tgz", "resolved": "https://registry.npmjs.org/silk-wasm/-/silk-wasm-3.3.4.tgz",
"integrity": "sha512-zZ3hgMpiPR6cFnKvCPgPpCwx6n5RoJCbEGIFlge2kAxAmgzBTf0b2F2xIPG5W4obUhQPQXXTTH074eGZJK01xw==" "integrity": "sha512-cvjp9Zw50uPB46EfifHlO8gIh6buZOUKQaL+9BbPoLgH4bAp8wEEzVmPI34gIiltOUyeuEknm4DDGnE3kEEQ/A=="
}, },
"node_modules/slash": { "node_modules/slash": {
"version": "4.0.0", "version": "4.0.0",
@@ -5833,6 +5958,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/streamifier": {
"version": "0.1.1",
"resolved": "https://registry.npmmirror.com/streamifier/-/streamifier-0.1.1.tgz",
"integrity": "sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -5959,6 +6092,55 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/tar-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-1.6.2.tgz",
"integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==",
"dependencies": {
"bl": "^1.0.0",
"buffer-alloc": "^1.2.0",
"end-of-stream": "^1.0.0",
"fs-constants": "^1.0.0",
"readable-stream": "^2.3.0",
"to-buffer": "^1.1.1",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tar-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/tar-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/tar-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/tar-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.28.1", "version": "5.28.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/terser/-/terser-5.28.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/terser/-/terser-5.28.1.tgz",
@@ -5985,6 +6167,11 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true "dev": true
}, },
"node_modules/to-buffer": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.1.1.tgz",
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
},
"node_modules/to-fast-properties": { "node_modules/to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -6528,8 +6715,7 @@
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"dev": true
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.16.0", "version": "8.16.0",
@@ -6551,6 +6737,14 @@
} }
} }
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -6567,6 +6761,14 @@
"fd-slicer": "~1.1.0" "fd-slicer": "~1.1.0"
} }
}, },
"node_modules/yazl": {
"version": "2.5.1",
"resolved": "https://registry.npmmirror.com/yazl/-/yazl-2.5.1.tgz",
"integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==",
"dependencies": {
"buffer-crc32": "~0.2.3"
}
},
"node_modules/yn": { "node_modules/yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/yn/-/yn-3.1.1.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/yn/-/yn-3.1.1.tgz",

View File

@@ -14,12 +14,12 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"compressing": "^1.10.0",
"express": "^4.18.2", "express": "^4.18.2",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"level": "^8.0.1", "level": "^8.0.1",
"node-stream-zip": "^1.15.0", "silk-wasm": "^3.3.4",
"silk-wasm": "^3.2.3",
"utf-8-validate": "^6.0.3", "utf-8-validate": "^6.0.3",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"

View File

@@ -3,5 +3,5 @@ export const CHANNEL_SET_CONFIG = 'llonebot_set_config'
export const CHANNEL_LOG = 'llonebot_log' export const CHANNEL_LOG = 'llonebot_log'
export const CHANNEL_ERROR = 'llonebot_error' export const CHANNEL_ERROR = 'llonebot_error'
export const CHANNEL_UPDATE = 'llonebot_update' export const CHANNEL_UPDATE = 'llonebot_update'
export const CHANNEL_CHECKVERSION = 'llonebot_checkversion' export const CHANNEL_CHECK_VERSION = 'llonebot_check_version'
export const CHANNEL_SELECT_FILE = 'llonebot_select_ffmpeg' export const CHANNEL_SELECT_FILE = 'llonebot_select_ffmpeg'

View File

@@ -1,6 +1,11 @@
import fs from "fs"; import fs from "fs";
import fsPromise from "fs/promises";
import {Config, OB11Config} from './types'; import {Config, OB11Config} from './types';
import {mergeNewProperties} from "./utils";
import {mergeNewProperties} from "./utils/helper";
import path from "node:path";
import {selfInfo} from "./data";
import {DATA_DIR} from "./utils";
export const HOOK_LOG = false; export const HOOK_LOG = false;
@@ -26,6 +31,7 @@ export class ConfigUtil {
let ob11Default: OB11Config = { let ob11Default: OB11Config = {
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: "",
wsPort: 3001, wsPort: 3001,
wsHosts: [], wsHosts: [],
enableHttp: true, enableHttp: true,
@@ -44,6 +50,7 @@ export class ConfigUtil {
reportSelfMessage: false, reportSelfMessage: false,
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
enablePoke: false
}; };
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
@@ -84,3 +91,8 @@ export class ConfigUtil {
} }
} }
} }
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}

View File

@@ -6,8 +6,9 @@ import {
type SelfInfo type SelfInfo
} from '../ntqqapi/types' } from '../ntqqapi/types'
import {type FileCache, type LLOneBotError} from './types' import {type FileCache, type LLOneBotError} from './types'
import {isNumeric, log} from "./utils";
import {NTQQGroupApi} from "../ntqqapi/api/group"; import {NTQQGroupApi} from "../ntqqapi/api/group";
import {log} from "./utils/log";
import {isNumeric} from "./utils/helper";
export const selfInfo: SelfInfo = { export const selfInfo: SelfInfo = {
uid: '', uid: '',
@@ -20,7 +21,9 @@ export let friends: Friend[] = []
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>() export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
otherError: '' httpServerError: '',
wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误'
} }
@@ -91,9 +94,9 @@ export async function refreshGroupMembers(groupQQ: string) {
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号 export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) { export function getUidByUin(uin: string) {
for (const key in uidMaps) { for (const uid in uidMaps) {
if (uidMaps[key] === uin) { if (uidMaps[uid] === uin) {
return key return uid
} }
} }
} }

View File

@@ -1,9 +1,11 @@
import {Level} from "level"; import {Level} from "level";
import {type GroupNotify, RawMessage} from "../ntqqapi/types"; import {type GroupNotify, RawMessage} from "../ntqqapi/types";
import {DATA_DIR, log} from "./utils"; import {DATA_DIR} from "./utils";
import {selfInfo} from "./data"; import {selfInfo} from "./data";
import {FileCache} from "./types"; import {FileCache} from "./types";
import {log} from "./utils/log";
type ReceiveTempUinMap = Record<string, string>;
class DBUtil { class DBUtil {
public readonly DB_KEY_PREFIX_MSG_ID = "msg_id_"; public readonly DB_KEY_PREFIX_MSG_ID = "msg_id_";
@@ -11,8 +13,9 @@ class DBUtil {
public readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_"; public readonly DB_KEY_PREFIX_MSG_SEQ_ID = "msg_seq_id_";
public readonly DB_KEY_PREFIX_FILE = "file_"; public readonly DB_KEY_PREFIX_FILE = "file_";
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = "group_notify_"; public readonly DB_KEY_PREFIX_GROUP_NOTIFY = "group_notify_";
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = "received_temp_uin_map";
public db: Level; public db: Level;
public cache: Record<string, RawMessage | string | FileCache | GroupNotify> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number; private currentShortId: number;
/* /*
@@ -67,6 +70,17 @@ class DBUtil {
}, expiredMilliSecond) }, expiredMilliSecond)
} }
public async getReceivedTempUinMap(): Promise<ReceiveTempUinMap> {
try{
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = JSON.parse(await this.db.get(this.DB_KEY_RECEIVED_TEMP_UIN_MAP));
}catch (e) {
}
return (this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] || {}) as ReceiveTempUinMap;
}
public setReceivedTempUinMap(data: ReceiveTempUinMap) {
this.cache[this.DB_KEY_RECEIVED_TEMP_UIN_MAP] = data;
this.db.put(this.DB_KEY_RECEIVED_TEMP_UIN_MAP, JSON.stringify(data)).then();
}
private addCache(msg: RawMessage) { private addCache(msg: RawMessage) {
const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId const longIdKey = this.DB_KEY_PREFIX_MSG_ID + msg.msgId
const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId const shortIdKey = this.DB_KEY_PREFIX_MSG_SHORT_ID + msg.msgShortId
@@ -208,14 +222,14 @@ class DBUtil {
return this.currentShortId; return this.currentShortId;
} }
async addFileCache(fileName: string, data: FileCache) { async addFileCache(fileNameOrUuid: string, data: FileCache) {
const key = this.DB_KEY_PREFIX_FILE + fileName; const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid;
if (this.cache[key]) { if (this.cache[key]) {
return return
} }
let cacheDBData = {...data} let cacheDBData = {...data}
delete cacheDBData['downloadFunc'] delete cacheDBData['downloadFunc']
this.cache[fileName] = data; this.cache[fileNameOrUuid] = data;
try { try {
await this.db.put(key, JSON.stringify(cacheDBData)); await this.db.put(key, JSON.stringify(cacheDBData));
} catch (e) { } catch (e) {
@@ -223,8 +237,8 @@ class DBUtil {
} }
} }
async getFileCache(fileName: string): Promise<FileCache | undefined> { async getFileCache(fileNameOrUuid: string): Promise<FileCache | undefined> {
const key = this.DB_KEY_PREFIX_FILE + fileName; const key = this.DB_KEY_PREFIX_FILE + (fileNameOrUuid);
if (this.cache[key]) { if (this.cache[key]) {
return this.cache[key] as FileCache return this.cache[key] as FileCache
} }

View File

@@ -1,6 +1,8 @@
import express, {Express, json, Request, Response} from "express"; import express, {Express, Request, Response} from "express";
import {getConfigUtil, log} from "../utils";
import http from "http"; import http from "http";
import {log} from "../utils/log";
import {getConfigUtil} from "../config";
import {llonebotError} from "../data";
type RegisterHandler = (res: Response, payload: any) => Promise<any> type RegisterHandler = (res: Response, payload: any) => Promise<any>
@@ -51,13 +53,20 @@ export abstract class HttpServerBase {
}; };
start(port: number) { start(port: number) {
this.expressAPP.get('/', (req: Request, res: Response) => { try {
res.send(`${this.name}已启动`); this.expressAPP.get('/', (req: Request, res: Response) => {
}) res.send(`${this.name}已启动`);
this.listen(port); })
this.listen(port);
llonebotError.httpServerError = ""
} catch (e) {
log("HTTP服务启动失败", e.toString())
llonebotError.httpServerError = "HTTP服务启动失败, " + e.toString()
}
} }
stop() { stop() {
llonebotError.httpServerError = ""
if (this.server) { if (this.server) {
this.server.close() this.server.close()
this.server = null; this.server = null;

View File

@@ -1,7 +1,9 @@
import {WebSocket, WebSocketServer} from "ws"; import {WebSocket, WebSocketServer} from "ws";
import {getConfigUtil, log} from "../utils";
import urlParse from "url"; import urlParse from "url";
import {IncomingMessage} from "node:http"; import {IncomingMessage} from "node:http";
import {log} from "../utils/log";
import {getConfigUtil} from "../config";
import {llonebotError} from "../data";
class WebsocketClientBase { class WebsocketClientBase {
private wsClient: WebSocket private wsClient: WebSocket
@@ -28,7 +30,14 @@ export class WebsocketServerBase {
} }
start(port: number) { start(port: number) {
this.ws = new WebSocketServer({port}); try {
this.ws = new WebSocketServer({port,
maxPayload: 1024 * 1024 * 1024
});
llonebotError.wsServerError = ''
}catch (e) {
llonebotError.wsServerError = "正向ws服务启动失败, " + e.toString()
}
this.ws.on("connection", (wsClient, req) => { this.ws.on("connection", (wsClient, req) => {
const url = req.url.split("?").shift() const url = req.url.split("?").shift()
this.authorize(wsClient, req); this.authorize(wsClient, req);
@@ -40,6 +49,7 @@ export class WebsocketServerBase {
} }
stop() { stop() {
llonebotError.wsServerError = ''
this.ws.close((err) => { this.ws.close((err) => {
log("ws server close failed!", err) log("ws server close failed!", err)
}); });

View File

@@ -1,6 +1,7 @@
export interface OB11Config { export interface OB11Config {
httpPort: number httpPort: number
httpHosts: string[] httpHosts: string[]
httpSecret?: string
wsPort: number wsPort: number
wsHosts: string[] wsHosts: string[]
enableHttp?: boolean enableHttp?: boolean
@@ -14,6 +15,7 @@ export interface CheckVersion {
version: string version: string
} }
export interface Config { export interface Config {
imageRKey?: string;
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval?: number // ms
@@ -24,9 +26,12 @@ export interface Config {
autoDeleteFile?: boolean autoDeleteFile?: boolean
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
enablePoke?: boolean
} }
export interface LLOneBotError { export interface LLOneBotError {
httpServerError?: string
wsServerError?: string
ffmpegError?: string ffmpegError?: string
otherError?: string otherError?: string
} }
@@ -35,6 +40,8 @@ export interface FileCache {
fileName: string fileName: string
filePath: string filePath: string
fileSize: string fileSize: string
fileUuid?: string
url?: string url?: string
msgId?: string
downloadFunc?: () => Promise<void> downloadFunc?: () => Promise<void>
} }

View File

@@ -1,402 +0,0 @@
import * as path from "node:path";
import { selfInfo } from "./data";
import { ConfigUtil } from "./config";
import util from "util";
import { encode, getDuration, isWav } from "silk-wasm";
import fs from 'fs';
import * as crypto from 'crypto';
import { v4 as uuidv4 } from "uuid";
import ffmpeg from "fluent-ffmpeg"
import * as https from "node:https";
import { version } from "../version";
export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath)
}
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
// 判断是否为最新版本
export async function checkVersion() {
const latestVersionText = await getRemoteVersion();
const latestVersion = latestVersionText.split(".");
const currentVersion = version.split(".");
for (let k in [0, 1, 2]) {
if (latestVersion[k] > currentVersion[k]) {
return { result: false, version: latestVersionText };
}
}
return { result: true, version: version };
}
export async function updateLLOneBot() {
let mirrorGithubList = ["https://mirror.ghproxy.com"];
const latestVersion = await getRemoteVersion();
if (latestVersion && latestVersion != "") {
const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip";
const realUrl = mirrorGithubList[0] + downloadUrl;
}
return false;
}
export async function getRemoteVersion() {
let mirrorGithubList = ["https://521github.com"];
let Version = "";
for (let i = 0; i < mirrorGithubList.length; i++) {
let mirrorGithub = mirrorGithubList[i];
let tVersion = await getRemoteVersionByMirror(mirrorGithub);
if (tVersion && tVersion != "") {
Version = tVersion;
break;
}
}
return Version;
}
export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = "error";
let reqPromise = async function (): Promise<string> {
return new Promise((resolve, reject) => {
https.get(mirrorGithub + "/LLOneBot/LLOneBot/releases", res => {
let list = [];
res.on('data', chunk => {
list.push(chunk);
});
res.on('end', () => {
resolve(Buffer.concat(list).toString());
});
}).on('error', err => {
reject();
});
});
}
try {
releasePage = await reqPromise();
if (releasePage === "error") return "";
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0];
}
catch { }
return "";
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
let currentDateTime = new Date().toLocaleString();
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(DATA_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
})
}
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
const fd = fs.openSync(path, 'r');
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
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> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
let result = {
err: "",
data: ""
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err) {
result.err = err.toString();
}
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 function checkFfmpeg(newPath: string = null): Promise<boolean> {
return new Promise((resolve, reject) => {
if (newPath) {
ffmpeg.setFfmpegPath(newPath);
ffmpeg.getAvailableFormats((err, formats) => {
if (err) {
log('ffmpeg is not installed or not found in PATH:', err);
resolve(false)
} else {
log('ffmpeg is installed.');
resolve(true);
}
})
}
});
}
export async function encodeSilk(filePath: string) {
const fsp = require("fs").promises
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 isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath));
}
// 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 pttPath = path.join(DATA_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath);
const wavPath = pttPath + ".wav"
if (!_isWav) {
log(`语音文件${filePath}正在转换成wav`)
// let voiceData = await fsp.readFile(filePath)
await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav").audioChannels(2).on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", () => {
filePath = wavPath
resolve(wavPath);
});
})
}
// const sampleRate = await getAudioSampleRate(filePath) || 0;
// log("音频采样率", sampleRate)
const pcm = fs.readFileSync(filePath);
const silk = await encode(pcm, 0);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => { });
log(`语音文件${filePath}转换成功!`, pttPath)
return {
converted: true,
path: pttPath,
duration: silk.duration,
};
} else {
const pcm = fs.readFileSync(filePath);
let duration = 0;
try {
duration = getDuration(pcm);
} catch (e) {
log("获取语音文件时长失败", filePath, e.stack)
duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log("使用文件大小估算时长", duration)
}
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}
export async function getVideoInfo(filePath: string) {
const size = fs.statSync(filePath).size;
return new Promise<{ width: number, height: number, time: number, format: string, size: number, filePath: string }>((resolve, reject) => {
ffmpeg(filePath).ffprobe((err, metadata) => {
if (err) {
reject(err);
} else {
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
if (videoStream) {
console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`);
} else {
console.log('未找到视频流信息。');
}
resolve({
width: videoStream.width, height: videoStream.height,
time: parseInt(videoStream.duration),
format: metadata.format.format_name,
size,
filePath
});
}
});
})
}
export async function encodeMp4(filePath: string) {
let videoInfo = await getVideoInfo(filePath);
log("视频信息", videoInfo)
if (videoInfo.format.indexOf("mp4") === -1) {
log("视频需要转换为MP4格式", filePath)
// 转成mp4
const newPath: string = await new Promise<string>((resolve, reject) => {
const newPath = filePath + ".mp4"
ffmpeg(filePath)
.toFormat('mp4')
.on('error', (err) => {
reject(`转换视频格式失败: ${err.message}`);
})
.on('end', () => {
log('视频转换为MP4格式完成');
resolve(newPath); // 返回转换后的文件路径
})
.save(newPath);
});
return await getVideoInfo(newPath)
}
return videoInfo
}
export function isNull(value: any) {
return value === undefined || value === null;
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}

137
src/common/utils/audio.ts Normal file
View File

@@ -0,0 +1,137 @@
import fs from "fs";
import {encode, getDuration, getWavFileInfo, isWav} from "silk-wasm";
import fsPromise from "fs/promises";
import {log} from "./log";
import path from "node:path";
import {DATA_DIR, TEMP_DIR} from "./index";
import {v4 as uuidv4} from "uuid";
import {getConfigUtil} from "../config";
import ffmpeg from "fluent-ffmpeg";
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 isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath));
}
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath)
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log(`通过文件大小估算语音的时长:`, duration)
return duration
}
// function verifyDuration(oriDuration: number, guessDuration: number) {
// // 单位都是秒
// if (oriDuration - guessDuration > 10) {
// return guessDuration
// }
// oriDuration = Math.max(1, oriDuration)
// return oriDuration
// }
// 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 pttPath = path.join(TEMP_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath);
const wavPath = pttPath + ".wav"
const convert = async () => {
return await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav")
.audioChannels(1)
.audioFrequency(24000)
.on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", () => {
filePath = wavPath
resolve(wavPath);
});
})
}
let wav: Buffer
if (!_isWav) {
log(`语音文件${filePath}正在转换成wav`)
await convert()
} else {
wav = fs.readFileSync(filePath)
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const {fmt} = getWavFileInfo(wav)
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
wav = undefined
await convert()
}
}
wav ||= fs.readFileSync(filePath);
const silk = await encode(wav, 0);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => {
});
// const gDuration = await guessDuration(pttPath)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000
};
} else {
const silk = fs.readFileSync(filePath);
let duration = 0;
try {
duration = getDuration(silk) / 1000
} catch (e) {
log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
duration = await guessDuration(filePath);
}
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}

258
src/common/utils/file.ts Normal file
View File

@@ -0,0 +1,258 @@
import fs from "fs";
import fsPromise from "fs/promises";
import crypto from "crypto";
import util from "util";
import path from "node:path";
import {v4 as uuidv4} from "uuid";
import {log, TEMP_DIR} from "./index";
import {dbUtil} from "../db";
import * as fileType from "file-type";
import {net} from "electron";
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
const fd = fs.openSync(path, 'r');
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
return buffer.toString() === 'GIF8'
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
let result = {
err: "",
data: ""
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err) {
result.err = err.toString();
}
return result;
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
}
export interface HttpDownloadOptions {
url: string;
headers?: Record<string, string> | string;
}
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
let chunks: Buffer[] = [];
let url: string;
let headers: Record<string, string> = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36"
};
if (typeof options === "string") {
url = options;
} else {
url = options.url;
if (options.headers) {
if (typeof options.headers === "string") {
headers = JSON.parse(options.headers);
} else {
headers = options.headers;
}
}
}
const fetchRes = await net.fetch(url, headers);
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
const blob = await fetchRes.blob();
let buffer = await blob.arrayBuffer();
return Buffer.from(buffer);
}
type Uri2LocalRes = {
success: boolean,
errMsg: string,
fileName: string,
ext: string,
path: string,
isLocal: boolean
}
export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> {
let res = {
success: false,
errMsg: "",
fileName: "",
ext: "",
path: "",
isLocal: false
}
if (!fileName) {
fileName = uuidv4();
}
let filePath = path.join(TEMP_DIR, fileName)
let url = null;
try {
url = new URL(uri);
} catch (e) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
}
// log("uri protocol", url.protocol, uri);
if (url.protocol == "base64:") {
// base64转成文件
let base64Data = uri.split("base64://")[1]
try {
const buffer = Buffer.from(base64Data, 'base64');
fs.writeFileSync(filePath, buffer);
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件
let buffer: Buffer = null;
try {
buffer = await httpDownload(uri);
} catch (e) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name) {
fileName = pathInfo.name
if (pathInfo.ext) {
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
res.fileName = fileName
filePath = path.join(TEMP_DIR, uuidv4() + fileName)
fs.writeFileSync(filePath, buffer);
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string;
if (url.protocol === "file:") {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === "win32") {
filePath = pathname.slice(1)
} else {
filePath = pathname
}
} else {
const cache = await dbUtil.getFileCache(uri);
if (cache) {
filePath = cache.filePath
} else {
filePath = uri;
}
}
res.isLocal = true
}
// else{
// res.errMsg = `不支持的file协议,` + url.protocol
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
if (ext) {
log("获取文件类型", ext, filePath)
fs.renameSync(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
}
}
res.success = true
res.path = filePath
return res
}
export async function copyFolder(sourcePath: string, destPath: string) {
try {
const entries = await fsPromise.readdir(sourcePath, {withFileTypes: true});
await fsPromise.mkdir(destPath, {recursive: true});
for (let entry of entries) {
const srcPath = path.join(sourcePath, entry.name);
const dstPath = path.join(destPath, entry.name);
if (entry.isDirectory()) {
await copyFolder(srcPath, dstPath);
} else {
try {
await fsPromise.copyFile(srcPath, dstPath);
} catch (error) {
console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`);
// 这里可以决定是否要继续复制其他文件
}
}
}
} catch (error) {
console.error('复制文件夹时出错:', error);
}
}

View File

@@ -0,0 +1,68 @@
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
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 function isNull(value: any) {
return value === undefined || value === null;
}
/**
* 将字符串按最大长度分割并添加换行符
* @param str 原始字符串
* @param maxLength 每行的最大字符数
* @returns 处理后的字符串,超过长度的地方将会换行
*/
export function wrapText(str: string, maxLength: number): string {
// 初始化一个空字符串用于存放结果
let result: string = '';
// 循环遍历字符串每次步进maxLength个字符
for (let i = 0; i < str.length; i += maxLength) {
// 从i开始截取长度为maxLength的字符串段并添加到结果字符串
// 如果不是第一段,先添加一个换行符
if (i > 0) result += '\n';
result += str.substring(i, i + maxLength);
}
return result;
}

View File

@@ -1,342 +1,18 @@
import * as path from "node:path"; import path from "node:path";
import {selfInfo} from "../data"; import fs from "fs";
import {ConfigUtil} from "../config";
import util from "util";
import {encode, getDuration, isWav} from "silk-wasm";
import fs from 'fs';
import * as crypto from 'crypto';
import {v4 as uuidv4} from "uuid";
import ffmpeg from "fluent-ffmpeg"
export * from './file'
export * from './helper'
export * from './log'
export * from './qqlevel'
export * from './qqpkg'
export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
export const TEMP_DIR = path.join(DATA_DIR, "temp");
export function getConfigUtil() { export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin;
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) if (!fs.existsSync(TEMP_DIR)) {
return new ConfigUtil(configFilePath) fs.mkdirSync(TEMP_DIR, {recursive: true});
}
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
let currentDateTime = new Date().toLocaleString();
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const currentDate = `${year}-${month}-${day}`;
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(DATA_DIR, `llonebot-${currentDate}.log`), logMsg, (err: any) => {
})
}
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
const fd = fs.openSync(path, 'r');
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
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> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
function check() {
if (fs.existsSync(path)) {
resolve();
} else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`));
} else {
setTimeout(check, 100);
}
}
check();
});
}
export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile);
let result = {
err: "",
data: ""
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err) {
result.err = err.toString();
}
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 function checkFfmpeg(newPath: string = null): Promise<boolean> {
return new Promise((resolve, reject) => {
if (newPath) {
ffmpeg.setFfmpegPath(newPath);
ffmpeg.getAvailableFormats((err, formats) => {
if (err) {
log('ffmpeg is not installed or not found in PATH:', err);
resolve(false)
} else {
log('ffmpeg is installed.');
resolve(true);
}
})
}
});
}
export async function encodeSilk(filePath: string) {
const fsp = require("fs").promises
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 isWavFile(filePath: string) {
return isWav(fs.readFileSync(filePath));
}
// 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 pttPath = path.join(DATA_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath);
const wavPath = pttPath + ".wav"
if (!_isWav) {
log(`语音文件${filePath}正在转换成wav`)
// let voiceData = await fsp.readFile(filePath)
await new Promise((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
if (ffmpegPath) {
ffmpeg.setFfmpegPath(ffmpegPath);
}
ffmpeg(filePath).toFormat("wav").audioChannels(2).on('end', function () {
log('wav转换完成');
})
.on('error', function (err) {
log(`wav转换出错: `, err.message,);
reject(err);
})
.save(wavPath)
.on("end", () => {
filePath = wavPath
resolve(wavPath);
});
})
}
// const sampleRate = await getAudioSampleRate(filePath) || 0;
// log("音频采样率", sampleRate)
const pcm = fs.readFileSync(filePath);
const silk = await encode(pcm, 0);
fs.writeFileSync(pttPath, silk.data);
fs.unlink(wavPath, (err) => { });
log(`语音文件${filePath}转换成功!`, pttPath)
return {
converted: true,
path: pttPath,
duration: silk.duration,
};
} else {
const pcm = fs.readFileSync(filePath);
let duration = 0;
try {
duration = getDuration(pcm);
} catch (e) {
log("获取语音文件时长失败", filePath, e.stack)
duration = fs.statSync(filePath).size / 1024 / 3 // 每3kb大约1s
duration = Math.floor(duration)
duration = Math.max(1, duration)
log("使用文件大小估算时长", duration)
}
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
}
}
export async function getVideoInfo(filePath: string) {
const size = fs.statSync(filePath).size;
return new Promise<{ width: number, height: number, time: number, format: string, size: number, filePath: string }>((resolve, reject) => {
ffmpeg(filePath).ffprobe( (err, metadata) => {
if (err) {
reject(err);
} else {
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
if (videoStream) {
console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`);
} else {
console.log('未找到视频流信息。');
}
resolve({
width: videoStream.width, height: videoStream.height,
time: parseInt(videoStream.duration),
format: metadata.format.format_name,
size,
filePath
});
}
});
})
}
export async function encodeMp4(filePath: string) {
let videoInfo = await getVideoInfo(filePath);
log("视频信息", videoInfo)
if (videoInfo.format.indexOf("mp4") === -1) {
log("视频需要转换为MP4格式", filePath)
// 转成mp4
const newPath: string = await new Promise<string>((resolve, reject) => {
const newPath = filePath + ".mp4"
ffmpeg(filePath)
.toFormat('mp4')
.on('error', (err) => {
reject(`转换视频格式失败: ${err.message}`);
})
.on('end', () => {
log('视频转换为MP4格式完成');
resolve(newPath); // 返回转换后的文件路径
})
.save(newPath);
});
return await getVideoInfo(newPath)
}
return videoInfo
}
export function isNull(value: any) {
return value === undefined || value === null;
}
export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
// 创建一个流式读取器
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
stream.on('end', () => {
// 文件读取完成,计算哈希
const md5 = hash.digest('hex');
resolve(md5);
});
stream.on('error', (err: Error) => {
// 处理可能的读取错误
reject(err);
});
});
} }
export {getVideoInfo} from "./video";
export {checkFfmpeg} from "./video";
export {encodeSilk} from "./audio";

37
src/common/utils/log.ts Normal file
View File

@@ -0,0 +1,37 @@
import {selfInfo} from "../data";
import fs from "fs";
import path from "node:path";
import {DATA_DIR, truncateString} from "./index";
import {getConfigUtil} from "../config";
const date = new Date();
const logFileName = `llonebot-${date.toLocaleString("zh-CN")}.log`.replace(/\//g, "-").replace(/:/g, "-");
const logDir = path.join(DATA_DIR, "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, {recursive: true});
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : ""
let logMsg = "";
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
}
let currentDateTime = new Date().toLocaleString();
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(logDir, logFileName), logMsg, (err: any) => {
})
}

View File

@@ -8,3 +8,5 @@ type QQPkgInfo = {
} }
export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, "app/package.json")) export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, "app/package.json"))
export const isQQ998: boolean = qqPkgInfo.buildVersion >= "22106"

View File

@@ -0,0 +1,98 @@
import { version } from "../../version";
import * as path from "node:path";
import * as fs from "node:fs";
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from ".";
import compressing from "compressing";
const downloadMirrorHosts = ["https://mirror.ghproxy.com/"];
const checkVersionMirrorHosts = ["https://521github.com"];
export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion();
const latestVersion = latestVersionText.split(".");
log("llonebot last version", latestVersion);
const currentVersion: string[] = version.split(".");
log("llonebot current version", currentVersion);
for (let k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) {
log("")
return { result: true, version: latestVersionText };
}
else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) {
break;
}
}
return { result: false, version: version };
}
export async function upgradeLLOneBot() {
const latestVersion = await getRemoteVersion();
if (latestVersion && latestVersion != "") {
const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip";
const filePath = path.join(TEMP_DIR, "./update-" + latestVersion + ".zip");
let downloadSuccess = false;
// 多镜像下载
for (const mirrorGithub of downloadMirrorHosts) {
try {
const buffer = await httpDownload(mirrorGithub + downloadUrl);
fs.writeFileSync(filePath, buffer)
downloadSuccess = true;
break;
} catch (e) {
log("llonebot upgrade error", e);
}
}
if (!downloadSuccess) {
log("llonebot upgrade error", "download failed");
return false;
}
const temp_ver_dir = path.join(TEMP_DIR, "LLOneBot" + latestVersion);
let uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => {
compressing.zip.uncompress(filePath, temp_ver_dir).then(() => {
resolve(true);
}).catch((reason: any) => {
log("llonebot upgrade failed, ", reason);
if (reason?.errno == -4082) {
resolve(true);
}
resolve(false);
});
});
}
const uncompressedResult = await uncompressedPromise();
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR);
return uncompressedResult;
}
return false;
}
export async function getRemoteVersion() {
let Version = "";
for (let i = 0; i < checkVersionMirrorHosts.length; i++) {
let mirrorGithub = checkVersionMirrorHosts[i];
let tVersion = await getRemoteVersionByMirror(mirrorGithub);
if (tVersion && tVersion != "") {
Version = tVersion;
break;
}
}
return Version;
}
export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = "error";
try {
releasePage = (await httpDownload(mirrorGithub + "/LLOneBot/LLOneBot/releases")).toString();
// log("releasePage", releasePage);
if (releasePage === "error") return "";
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0];
} catch {
}
return "";
}

88
src/common/utils/video.ts Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,28 +4,36 @@ import {BrowserWindow, dialog, ipcMain} from 'electron';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import {Config} from "../common/types"; import {Config} from "../common/types";
import { import {
CHANNEL_CHECK_VERSION,
CHANNEL_ERROR, CHANNEL_ERROR,
CHANNEL_GET_CONFIG, CHANNEL_GET_CONFIG,
CHANNEL_LOG, CHANNEL_LOG,
CHANNEL_CHECKVERSION,
CHANNEL_SELECT_FILE, CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
CHANNEL_UPDATE, CHANNEL_UPDATE,
} from "../common/channels"; } from "../common/channels";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {checkFfmpeg, checkVersion, DATA_DIR, getConfigUtil, log, updateLLOneBot} from "../common/utils"; import {DATA_DIR} from "../common/utils";
import { import {
friendRequests, friendRequests,
getFriend, getFriend,
getGroup, getGroup,
getGroupMember, getGroupMember, groups,
llonebotError, llonebotError,
refreshGroupMembers, refreshGroupMembers,
selfInfo selfInfo,
uidMaps
} from "../common/data"; } from "../common/data";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook"; import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor"; import {OB11Constructor} from "../onebot11/constructor";
import {ChatType, FriendRequestNotify, GroupNotifies, GroupNotifyTypes, RawMessage} from "../ntqqapi/types"; import {
ChatType,
FriendRequestNotify,
GroupMemberRole,
GroupNotifies,
GroupNotifyTypes,
RawMessage
} from "../ntqqapi/types";
import {ob11HTTPServer} from "../onebot11/server/http"; import {ob11HTTPServer} from "../onebot11/server/http";
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent";
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent";
@@ -41,19 +49,24 @@ import {NTQQUserApi} from "../ntqqapi/api/user";
import {NTQQGroupApi} from "../ntqqapi/api/group"; import {NTQQGroupApi} from "../ntqqapi/api/group";
import {registerPokeHandler} from "../ntqqapi/external/ccpoke"; import {registerPokeHandler} from "../ntqqapi/external/ccpoke";
import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent"; import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent";
import {checkNewVersion, upgradeLLOneBot} from "../common/utils/upgrade";
import {log} from "../common/utils/log";
import {getConfigUtil} from "../common/config";
import {checkFfmpeg} from "../common/utils/video";
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
let running = false; let running = false;
let mainWindow: BrowserWindow | null = null;
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log("llonebot main onLoad"); log("llonebot main onLoad");
ipcMain.handle(CHANNEL_CHECKVERSION, async (event, arg) => { ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkVersion(); return checkNewVersion();
}); });
ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => { ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => {
return updateLLOneBot(); return upgradeLLOneBot();
}); });
ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => { ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => {
const selectPath = new Promise<string>((resolve, reject) => { const selectPath = new Promise<string>((resolve, reject) => {
@@ -88,15 +101,44 @@ function onLoad() {
if (!fs.existsSync(DATA_DIR)) { if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, {recursive: true}); fs.mkdirSync(DATA_DIR, {recursive: true});
} }
ipcMain.handle(CHANNEL_ERROR, (event, arg) => { ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
return llonebotError; const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? "" : "没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常"
let {httpServerError, wsServerError, otherError, ffmpegError} = llonebotError;
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace("\n\n", "\n")
error = error.trim();
log("查询llonebot错误信息", error);
return error;
}) })
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
return config; return config;
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event, config: Config) => { ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => {
setConfig(config).then(); if (!ask) {
setConfig(config).then().catch(e => {
log("保存设置失败", e.stack)
});
return
}
dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: ['确认', '取消'],
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
title: '确认保存',
message: '是否保存?',
detail: 'LLOneBot配置已更改是否保存'
}).then(result => {
if (result.response === 0) {
setConfig(config).then().catch(e => {
log("保存设置失败", e.stack)
});
} else {
}
}).catch(err => {
log("保存设置询问弹窗错误", err);
});
}) })
ipcMain.on(CHANNEL_LOG, (event, arg) => { ipcMain.on(CHANNEL_LOG, (event, arg) => {
@@ -115,11 +157,18 @@ function onLoad() {
OB11Constructor.message(message).then((msg) => { OB11Constructor.message(message).then((msg) => {
if (debug) { if (debug) {
msg.raw = message; msg.raw = message;
} else {
if (msg.message.length === 0) {
return
}
} }
const isSelfMsg = msg.user_id.toString() == selfInfo.uin const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) { if (isSelfMsg && !reportSelfMessage) {
return return
} }
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin);
}
postOB11Event(msg); postOB11Event(msg);
// log("post msg", msg) // log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.stack.toString())); }).catch(e => log("constructMessage error: ", e.stack.toString()));
@@ -133,17 +182,21 @@ function onLoad() {
} }
async function startReceiveHook() { async function startReceiveHook() {
registerPokeHandler((id, isGroup) => { if (getConfigUtil().getConfig().enablePoke) {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`) registerPokeHandler((id, isGroup) => {
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent; log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
if (isGroup) { let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
pokeEvent = new OB11GroupPokeEvent(parseInt(id)); if (isGroup) {
}else{ pokeEvent = new OB11GroupPokeEvent(parseInt(id));
pokeEvent = new OB11FriendPokeEvent(parseInt(id)); } else {
} pokeEvent = new OB11FriendPokeEvent(parseInt(id));
postOB11Event(pokeEvent); }
}) postOB11Event(pokeEvent);
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => { })
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try { try {
await postReceiveMsg(payload.msgList); await postReceiveMsg(payload.msgList);
} catch (e) { } catch (e) {
@@ -177,7 +230,6 @@ function onLoad() {
parseInt(operatorId), parseInt(operatorId),
oriMessage.msgShortId oriMessage.msgShortId
) )
postOB11Event(groupRecallEvent); postOB11Event(groupRecallEvent);
} }
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了 // 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
@@ -245,15 +297,28 @@ function onLoad() {
log("变动管理员获取成功") log("变动管理员获取成功")
groupAdminNoticeEvent.user_id = parseInt(member1.uin); groupAdminNoticeEvent.user_id = parseInt(member1.uin);
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "unset" : "set"; groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "unset" : "set";
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
postOB11Event(groupAdminNoticeEvent, true); postOB11Event(groupAdminNoticeEvent, true);
} else { } else {
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode)); log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
} }
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT) { } else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
// log("有成员退出通知"); log("有成员退出通知", notify);
// const member1 = await getGroupMember(notify.group.groupCode, null, notify.user1.uid); try {
// let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin)) const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid);
// postEvent(groupDecreaseEvent, true); let operatorId = member1.uin;
let subType: GroupDecreaseSubType = "leave";
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid);
operatorId = member2.uin;
subType = "kick";
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin), parseInt(operatorId), subType)
postOB11Event(groupDecreaseEvent, true);
} catch (e) {
log("获取群通知的成员信息失败", notify, e.stack.toString())
}
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) { } else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log("有加群请求"); log("有加群请求");
let groupRequestEvent = new OB11GroupRequestEvent(); let groupRequestEvent = new OB11GroupRequestEvent();
@@ -294,8 +359,9 @@ function onLoad() {
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) { for (const req of payload.data.buddyReqs) {
if (req.isUnread && !friendRequests[req.sourceId] && (parseInt(req.reqTime) > startTime / 1000)) { let flag = req.friendUid + req.reqTime;
friendRequests[req.sourceId] = req; if (req.isUnread && (parseInt(req.reqTime) > startTime / 1000)) {
friendRequests[flag] = req;
log("有新的好友请求", req); log("有新的好友请求", req);
let friendRequestEvent = new OB11FriendRequestEvent(); let friendRequestEvent = new OB11FriendRequestEvent();
try { try {
@@ -304,7 +370,7 @@ function onLoad() {
} catch (e) { } catch (e) {
log("获取加好友者QQ号失败", e); log("获取加好友者QQ号失败", e);
} }
friendRequestEvent.flag = req.sourceId.toString(); friendRequestEvent.flag = flag;
friendRequestEvent.comment = req.extWords; friendRequestEvent.comment = req.extWords;
postOB11Event(friendRequestEvent); postOB11Event(friendRequestEvent);
} }
@@ -316,22 +382,18 @@ function onLoad() {
async function start() { async function start() {
log("llonebot pid", process.pid) log("llonebot pid", process.pid)
llonebotError.otherError = "";
startTime = Date.now(); startTime = Date.now();
dbUtil.getReceivedTempUinMap().then(m => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key;
}
})
startReceiveHook().then(); startReceiveHook().then();
NTQQGroupApi.getGroups(true).then() NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
// 检查ffmpeg
checkFfmpeg(config.ffmpeg).then(exist => {
if (!exist) {
llonebotError.ffmpegError = `没有找到ffmpeg,音频只能发送wav和silk`
}
})
if (config.ob11.enableHttp) { if (config.ob11.enableHttp) {
try { ob11HTTPServer.start(config.ob11.httpPort)
ob11HTTPServer.start(config.ob11.httpPort)
} catch (e) {
log("http server start failed", e);
}
} }
if (config.ob11.enableWs) { if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort); ob11WebsocketServer.start(config.ob11.wsPort);
@@ -354,23 +416,31 @@ function onLoad() {
} catch (e) { } catch (e) {
log("retry get self info", e); log("retry get self info", e);
} }
log("self info", selfInfo); if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin;
selfInfo.uid = globalThis.authData?.uid;
selfInfo.nick = selfInfo.uin;
}
log("self info", selfInfo, globalThis.authData);
if (selfInfo.uin) { if (selfInfo.uin) {
try { async function getUserNick() {
const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid)); try {
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
} else {
getSelfNickCount++; getSelfNickCount++;
if (getSelfNickCount < 10) { const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid));
return setTimeout(init, 1000); log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
return
} }
} catch (e) {
log("get self nickname failed", e.stack);
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000);
} }
} catch (e) {
log("get self nickname failed", e.toString());
return setTimeout(init, 1000);
} }
getUserNick().then()
start().then(); start().then();
} else { } else {
setTimeout(init, 1000) setTimeout(init, 1000)
@@ -385,6 +455,7 @@ function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) { if (selfInfo.uid) {
return return
} }
mainWindow = window;
log("window create", window.webContents.getURL().toString()) log("window create", window.webContents.getURL().toString())
try { try {
hookNTQQApiCall(window); hookNTQQApiCall(window);
@@ -400,6 +471,7 @@ try {
console.log(e.toString()) console.log(e.toString())
} }
// 这两个函数都是可选的 // 这两个函数都是可选的
export { export {
onBrowserWindowCreated onBrowserWindowCreated

View File

@@ -1,12 +1,13 @@
import {Config} from "../common/types"; import {Config} from "../common/types";
import {checkFfmpeg, getConfigUtil} from "../common/utils";
import {ob11HTTPServer} from "../onebot11/server/http"; import {ob11HTTPServer} from "../onebot11/server/http";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {llonebotError} from "../common/data"; import {llonebotError} from "../common/data";
import {getConfigUtil} from "../common/config";
import {checkFfmpeg, log} from "../common/utils";
export async function setConfig(config: Config) { export async function setConfig(config: Config) {
let oldConfig = getConfigUtil().getConfig(); let oldConfig = {...(getConfigUtil().getConfig())};
getConfigUtil().setConfig(config) getConfigUtil().setConfig(config)
if (config.ob11.httpPort != oldConfig.ob11.httpPort && config.ob11.enableHttp) { if (config.ob11.httpPort != oldConfig.ob11.httpPort && config.ob11.enableHttp) {
ob11HTTPServer.restart(config.ob11.httpPort); ob11HTTPServer.restart(config.ob11.httpPort);
@@ -20,6 +21,7 @@ export async function setConfig(config: Config) {
// 正向ws端口变化重启服务 // 正向ws端口变化重启服务
if (config.ob11.wsPort != oldConfig.ob11.wsPort) { if (config.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(config.ob11.wsPort); ob11WebsocketServer.restart(config.ob11.wsPort);
llonebotError.wsServerError = ''
} }
// 判断是否启用或关闭正向ws // 判断是否启用或关闭正向ws
if (config.ob11.enableWs != oldConfig.ob11.enableWs) { if (config.ob11.enableWs != oldConfig.ob11.enableWs) {
@@ -40,24 +42,19 @@ export async function setConfig(config: Config) {
if (config.ob11.enableWsReverse) { if (config.ob11.enableWsReverse) {
// 判断反向ws地址有变化 // 判断反向ws地址有变化
if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) { if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
log("反向ws地址有变化, 重启反向ws服务")
ob11ReverseWebsockets.restart(); ob11ReverseWebsockets.restart();
} else { } else {
for (const newHost of config.ob11.wsHosts) { for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) { if (!oldConfig.ob11.wsHosts.includes(newHost)) {
log("反向ws地址有变化, 重启反向ws服务")
ob11ReverseWebsockets.restart(); ob11ReverseWebsockets.restart();
break; break;
} }
} }
} }
} }
log("old config", oldConfig)
// 检查ffmpeg log("配置已更新", config)
if (config.ffmpeg) { checkFfmpeg(config.ffmpeg).then()
checkFfmpeg(config.ffmpeg).then(success => {
if (success) {
llonebotError.ffmpegError = ''
}
})
}
} }

View File

@@ -4,21 +4,23 @@ import {
CacheFileListItem, CacheFileListItem,
CacheFileType, CacheFileType,
CacheScanResult, CacheScanResult,
ChatCacheList, ChatCacheListItemBasic, ChatCacheList,
ChatCacheListItemBasic,
ChatType, ChatType,
ElementType ElementType
} from "../types"; } from "../types";
import path from "path"; import path from "path";
import {log} from "../../common/utils";
import fs from "fs"; import fs from "fs";
import {ReceiveCmdS} from "../hook"; import {ReceiveCmdS} from "../hook";
import {log} from "../../common/utils/log";
export class NTQQFileApi{ export class NTQQFileApi {
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({ return await callNTQQApi<{ ext: string }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath] className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath]
}) })
} }
static async getFileMd5(filePath: string) { static async getFileMd5(filePath: string) {
return await callNTQQApi<string>({ return await callNTQQApi<string>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,
@@ -26,6 +28,7 @@ export class NTQQFileApi{
args: [filePath] args: [filePath]
}) })
} }
static async copyFile(filePath: string, destPath: string) { static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({ return await callNTQQApi<string>({
className: NTQQApiClass.FS_API, className: NTQQApiClass.FS_API,
@@ -36,13 +39,15 @@ export class NTQQFileApi{
}] }]
}) })
} }
static async getFileSize(filePath: string) { static async getFileSize(filePath: string) {
return await callNTQQApi<number>({ return await callNTQQApi<number>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath] className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath]
}) })
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath); const md5 = await NTQQFileApi.getFileMd5(filePath);
let ext = (await NTQQFileApi.getFileType(filePath))?.ext let ext = (await NTQQFileApi.getFileType(filePath))?.ext
if (ext) { if (ext) {
@@ -61,7 +66,7 @@ export class NTQQFileApi{
md5HexStr: md5, md5HexStr: md5,
fileName: fileName, fileName: fileName,
elementType: elementType, elementType: elementType,
elementSubType: 0, elementSubType,
thumbSize: 0, thumbSize: 0,
needCreate: true, needCreate: true,
downloadType: 1, downloadType: 1,
@@ -76,17 +81,22 @@ export class NTQQFileApi{
md5, md5,
fileName, fileName,
path: mediaPath, path: mediaPath,
fileSize fileSize,
ext
} }
} }
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string) {
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, isFile: boolean = false) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
return sourcePath return sourcePath
} }
const apiParams = [ const apiParams = [
{ {
getReq: { getReq: {
fileModelId: "0",
downloadSourceType: 0,
triggerType: 1,
msgId: msgId, msgId: msgId,
chatType: chatType, chatType: chatType,
peerUid: peerUid, peerUid: peerUid,
@@ -96,20 +106,21 @@ export class NTQQFileApi{
filePath: thumbPath, filePath: thumbPath,
}, },
}, },
undefined, null,
] ]
// log("需要下载media", sourcePath); // log("需要下载media", sourcePath);
await callNTQQApi({ await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA, methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams, args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string } }) => { cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => {
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath); log("media 下载完成判断", payload.notifyInfo.msgId, msgId);
return payload.notifyInfo.filePath == sourcePath; return payload.notifyInfo.msgId == msgId;
} }
}) })
return sourcePath return sourcePath
} }
static async getImageSize(filePath: string) { static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number, height: number }>({ return await callNTQQApi<{ width: number, height: number }>({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath] className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath]
@@ -118,7 +129,7 @@ export class NTQQFileApi{
} }
export class NTQQFileCacheApi{ export class NTQQFileCacheApi {
static async setCacheSilentScan(isSilent: boolean = true) { static async setCacheSilentScan(isSilent: boolean = true) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_SET_SILENCE, methodName: NTQQApiMethod.CACHE_SET_SILENCE,
@@ -127,6 +138,7 @@ export class NTQQFileCacheApi{
}, null] }, null]
}); });
} }
static getCacheSessionPathList() { static getCacheSessionPathList() {
return callNTQQApi<{ return callNTQQApi<{
key: string, key: string,
@@ -136,6 +148,7 @@ export class NTQQFileCacheApi{
methodName: NTQQApiMethod.CACHE_PATH_SESSION, methodName: NTQQApiMethod.CACHE_PATH_SESSION,
}); });
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么 return callNTQQApi<any>({ // TODO: 目前还不知道真正的返回值是什么
methodName: NTQQApiMethod.CACHE_CLEAR, methodName: NTQQApiMethod.CACHE_CLEAR,
@@ -144,6 +157,7 @@ export class NTQQFileCacheApi{
}, null] }, null]
}); });
} }
static addCacheScannedPaths(pathMap: object = {}) { static addCacheScannedPaths(pathMap: object = {}) {
return callNTQQApi<GeneralCallResult>({ return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
@@ -152,6 +166,7 @@ export class NTQQFileCacheApi{
}, null] }, null]
}); });
} }
static scanCache() { static scanCache() {
callNTQQApi<GeneralCallResult>({ callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH, methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
@@ -163,6 +178,7 @@ export class NTQQFileCacheApi{
timeoutSecond: 300, timeoutSecond: 300,
}); });
} }
static getHotUpdateCachePath() { static getHotUpdateCachePath() {
return callNTQQApi<string>({ return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API, className: NTQQApiClass.HOTUPDATE_API,
@@ -176,6 +192,7 @@ export class NTQQFileCacheApi{
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP
}); });
} }
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => { return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({ callNTQQApi<ChatCacheList>({
@@ -190,6 +207,7 @@ export class NTQQFileCacheApi{
.catch(e => rej(e)); .catch(e => rej(e));
}); });
} }
static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
const _lastRecord = lastRecord ? lastRecord : {fileType: fileType}; const _lastRecord = lastRecord ? lastRecord : {fileType: fileType};
@@ -204,6 +222,7 @@ export class NTQQFileCacheApi{
}, null] }, null]
}) })
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,

View File

@@ -37,10 +37,10 @@ export class NTQQFriendApi{
}, null] }, null]
}) })
} }
static async handleFriendRequest(sourceId: number, accept: boolean,) { static async handleFriendRequest(flag: string, accept: boolean,) {
const request: FriendRequest = friendRequests[sourceId] const request: FriendRequest = friendRequests[flag]
if (!request) { if (!request) {
throw `sourceId ${sourceId}, 对应的好友请求不存在` throw `flat: ${flag}, 对应的好友请求不存在`
} }
const result = await callNTQQApi<GeneralCallResult>({ const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
@@ -54,7 +54,7 @@ export class NTQQFriendApi{
} }
] ]
}) })
delete friendRequests[sourceId]; delete friendRequests[flag];
return result; return result;
} }

View File

@@ -2,15 +2,15 @@ import {ReceiveCmdS} from "../hook";
import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types"; import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types";
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall";
import {uidMaps} from "../../common/data"; import {uidMaps} from "../../common/data";
import {log} from "../../common/utils";
import {BrowserWindow} from "electron";
import {dbUtil} from "../../common/db"; import {dbUtil} from "../../common/db";
import {log} from "../../common/utils/log";
import {NTQQWindowApi, NTQQWindows} from "./window";
export class NTQQGroupApi{ export class NTQQGroupApi{
static async getGroups(forced = false) { static async getGroups(forced = false) {
let cbCmd = ReceiveCmdS.GROUPS let cbCmd = ReceiveCmdS.GROUPS
if (process.platform != "win32") { if (process.platform != "win32") {
cbCmd = ReceiveCmdS.GROUPS_UNIX cbCmd = ReceiveCmdS.GROUPS_STORE
} }
const result = await callNTQQApi<{ const result = await callNTQQApi<{
updateType: number, updateType: number,
@@ -74,25 +74,7 @@ export class NTQQGroupApi{
} }
static async getGroupIgnoreNotifies() { static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies(); await NTQQGroupApi.getGroupNotifies();
const result = callNTQQApi<GroupNotifies>({ return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow,[], ReceiveCmdS.GROUP_NOTIFY);
className: NTQQApiClass.WINDOW_API,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
"GroupNotifyFilterWindow"
]
})
// 关闭窗口
setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())
if (w.webContents.getURL().indexOf("#/notify-filter/") != -1) {
w.close();
}
}
}, 2000);
return result;
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq) const notify: GroupNotify = await dbUtil.getGroupNotify(seq)

7
src/ntqqapi/api/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export * from "./file";
export * from "./friend";
export * from "./group";
export * from "./msg";
export * from "./user";
export * from "./webapi";
export * from "./window";

View File

@@ -1,9 +1,11 @@
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall";
import {ChatType, RawMessage, SendMessageElement} from "../types"; import {ChatType, RawMessage, SendMessageElement} from "../types";
import {log, sleep} from "../../common/utils";
import {dbUtil} from "../../common/db"; import {dbUtil} from "../../common/db";
import {selfInfo} from "../../common/data"; import {selfInfo} from "../../common/data";
import {ReceiveCmdS, registerReceiveHook} from "../hook"; import {ReceiveCmdS, registerReceiveHook} from "../hook";
import {log} from "../../common/utils/log";
import {sleep} from "../../common/utils/helper";
import {isQQ998} from "../../common/utils";
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
@@ -14,12 +16,44 @@ export interface Peer {
} }
export class NTQQMsgApi { export class NTQQMsgApi {
static async activateGroupChat(groupCode: string) { static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: NTQQApiMethod.GET_MULTI_MSG,
args: [{
peer,
rootMsgId,
parentMsgId
}, null]
})
}
static async activateChat(peer: Peer) {
// await this.fetchRecentContact(); // await this.fetchRecentContact();
// await sleep(500); // await sleep(500);
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ADD_ACTIVE_CHAT, methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
args: [{peer:{peerUid: groupCode, chatType: ChatType.group}, cnt: 20}] args: [{peer, cnt: 20}, null]
})
}
static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样
args: [{peer, cnt: 20}, null]
})
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG,
args: [{
peer,
msgId,
cnt: count,
queryOrder: true,
}, null]
}) })
} }
static async fetchRecentContact(){ static async fetchRecentContact(){

View File

@@ -2,7 +2,10 @@ import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../nt
import {SelfInfo, User} from "../types"; import {SelfInfo, User} from "../types";
import {ReceiveCmdS} from "../hook"; import {ReceiveCmdS} from "../hook";
import {uidMaps} from "../../common/data"; import {uidMaps} from "../../common/data";
import {NTQQWindowApi, NTQQWindows} from "./window";
import {isQQ998, sleep} from "../../common/utils";
let userInfoCache: Record<string, User> = {}; // uid: User
export class NTQQUserApi{ export class NTQQUserApi{
static async setQQAvatar(filePath: string) { static async setQQAvatar(filePath: string) {
@@ -29,29 +32,86 @@ export class NTQQUserApi{
}) })
return result.profiles.get(uid) return result.profiles.get(uid)
} }
static async getUserDetailInfo(uid: string) { static async getUserDetailInfo(uid: string, getLevel=false) {
const result = await callNTQQApi<{ info: User }>({ // this.getUserInfo(uid);
methodName: NTQQApiMethod.USER_DETAIL_INFO, let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
cbCmd: ReceiveCmdS.USER_DETAIL_INFO, const fetchInfo = async ()=>{
afterFirstCmd: false, const result = await callNTQQApi<{ info: User }>({
cmdCB: (payload) => { methodName,
const success = payload.info.uid == uid cbCmd: ReceiveCmdS.USER_DETAIL_INFO,
// log("get user detail info", success, uid, payload) afterFirstCmd: false,
return success cmdCB: (payload) => {
}, const success = payload.info.uid == uid
args: [ // log("get user detail info", success, uid, payload)
{ return success
uid
}, },
null args: [
] {
}) uid
const info = result.info },
if (info?.uin) { null
uidMaps[info.uid] = info.uin ]
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
} }
return info // 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000);
}
let userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
} }
static async getPSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: "qun.qq.com"
}
]
})
}
static async getSkey(groupName: string, groupCode: string): Promise<{data: string}> {
return await NTQQWindowApi.openWindow<{data: string}>(NTQQWindows.GroupHomeWorkWindow, [{
groupName,
groupCode,
"source": "funcbar"
}], ReceiveCmdS.SKEY_UPDATE, 1);
// return await callNTQQApi<string>({
// className: NTQQApiClass.GROUP_HOME_WORK,
// methodName: NTQQApiMethod.UPDATE_SKEY,
// args: [
// {
// domain: "qun.qq.com"
// }
// ]
// })
// return await callNTQQApi<GeneralCallResult>({
// methodName: NTQQApiMethod.GET_SKEY,
// args: [
// {
// "domains": [
// "qzone.qq.com",
// "qlive.qq.com",
// "qun.qq.com",
// "gamecenter.qq.com",
// "vip.qq.com",
// "qianbao.qq.com",
// "qidian.qq.com"
// ],
// "isForNewPCQQ": false
// },
// null
// ]
// })
}
} }

86
src/ntqqapi/api/webapi.ts Normal file
View File

@@ -0,0 +1,86 @@
import {groups} from "../../common/data";
import {log} from "../../common/utils";
import {NTQQUserApi} from "./user";
export class WebApi{
private static bkn: string;
private static skey: string;
private static pskey: string;
private static cookie: string
private defaultHeaders: Record<string,string> = {
"User-Agent": "QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0"
}
constructor() {
}
public async addGroupDigest(groupCode: string, msgSeq: string){
const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`
const res = await this.request(url)
return await res.json()
}
public async getGroupDigest(groupCode: string){
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`
const res = await this.request(url)
log(res.headers)
return await res.json()
}
private genBkn(sKey: string){
sKey = sKey || "";
let hash = 5381;
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
}
return (hash & 0x7FFFFFFF).toString();
}
private async init(){
if (!WebApi.bkn) {
const group = groups[0];
WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data;
WebApi.bkn = this.genBkn(WebApi.skey);
let cookie = await NTQQUserApi.getPSkey();
const pskeyRegex = /p_skey=([^;]+)/;
const match = cookie.match(pskeyRegex);
const pskeyValue = match ? match[1] : null;
WebApi.pskey = pskeyValue;
if (cookie.indexOf("skey=;") !== -1) {
cookie = cookie.replace("skey=;", `skey=${WebApi.skey};`);
}
WebApi.cookie = cookie;
// for(const kv of WebApi.cookie.split(";")){
// const [key, value] = kv.split("=");
// }
// log("set cookie", key, value)
// await session.defaultSession.cookies.set({
// url: 'https://qun.qq.com', // 你要请求的域名
// name: key.trim(),
// value: value.trim(),
// expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间例如设置为当前时间之后的300秒
// });
// }
}
}
private async request(url: string, method: "GET" | "POST" = "GET", headers: Record<string, string> = {}){
await this.init();
url += "&bkn=" + WebApi.bkn;
let _headers: Record<string, string> = {
...this.defaultHeaders, ...headers,
"Cookie": WebApi.cookie,
credentials: 'include'
}
log("request", url, _headers)
const options = {
method: method,
headers: _headers
}
return fetch(url, options)
}
}

49
src/ntqqapi/api/window.ts Normal file
View File

@@ -0,0 +1,49 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall";
import {ReceiveCmd} from "../hook";
import {BrowserWindow} from "electron";
export interface NTQQWindow{
windowName: string,
windowUrlHash: string,
}
export class NTQQWindows{
static GroupHomeWorkWindow: NTQQWindow = {
windowName: "GroupHomeWorkWindow",
windowUrlHash: "#/group-home-work"
}
static GroupNotifyFilterWindow: NTQQWindow = {
windowName: "GroupNotifyFilterWindow",
windowUrlHash: "#/group-notify-filter"
}
static GroupEssenceWindow: NTQQWindow = {
windowName: "GroupEssenceWindow",
windowUrlHash: "#/group-essence"
}
}
export class NTQQWindowApi{
// 打开窗口并获取对应的下发事件
static async openWindow<R=GeneralCallResult>(ntQQWindow: NTQQWindow, args: any[], cbCmd: ReceiveCmd=null, autoCloseSeconds: number=2){
const result = await callNTQQApi<R>({
className: NTQQApiClass.WINDOW_API,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW,
cbCmd,
afterFirstCmd: false,
args: [
ntQQWindow.windowName,
...args
]
})
setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())
if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) {
w.close();
}
}
}, autoCloseSeconds * 1000);
return result;
}
}

View File

@@ -11,10 +11,13 @@ import {
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement
} from "./types"; } from "./types";
import {calculateFileMD5, encodeSilk, getVideoInfo, isGIF, log, sleep} from "../common/utils";
import {promises as fs} from "node:fs"; import {promises as fs} from "node:fs";
import ffmpeg from "fluent-ffmpeg" import ffmpeg from "fluent-ffmpeg"
import {NTQQFileApi} from "./api/file"; import {NTQQFileApi} from "./api/file";
import {calculateFileMD5, isGIF} from "../common/utils/file";
import {log} from "../common/utils/log";
import {defaultVideoThumb, getVideoInfo} from "../common/utils/video";
import {encodeSilk} from "../common/utils/audio";
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
@@ -59,32 +62,32 @@ export class SendMsgElementConstructor {
} }
} }
static async pic(picPath: string, summary: string = ""): Promise<SendPicElement> { static async pic(picPath: string, summary: string = "", subType: 0|1=0): Promise<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC); const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType);
if (fileSize === 0) { if (fileSize === 0) {
throw "文件异常大小为0"; throw "文件异常大小为0";
} }
const imageSize = await NTQQFileApi.getImageSize(picPath); const imageSize = await NTQQFileApi.getImageSize(picPath);
const picElement = { const picElement = {
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, fileSize: fileSize.toString(),
picWidth: imageSize.width, picWidth: imageSize.width,
picHeight: imageSize.height, picHeight: imageSize.height,
fileName: fileName, fileName: fileName,
sourcePath: path, sourcePath: path,
original: true, original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg, picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: 0, picSubType: subType,
fileUuid: "", fileUuid: "",
fileSubId: "", fileSubId: "",
thumbFileSize: 0, thumbFileSize: 0,
summary, summary
}; };
log("图片信息", picElement)
return { return {
elementType: ElementType.PIC, elementType: ElementType.PIC,
elementId: "", elementId: "",
picElement picElement,
}; };
} }
@@ -106,29 +109,45 @@ export class SendMsgElementConstructor {
return element; return element;
} }
static async video(filePath: string, fileName: string = ""): Promise<SendVideoElement> { static async video(filePath: string, fileName: string = "", diyThumbPath: string = ""): Promise<SendVideoElement> {
let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) { if (fileSize === 0) {
throw "文件异常大小为0"; throw "文件异常大小为0";
} }
// const videoInfo = await encodeMp4(path);
// path = videoInfo.filePath
// md5 = videoInfo.md5;
// fileSize = videoInfo.size;
// log("上传视频", md5, path, fileSize, fileName || _fileName)
const pathLib = require("path"); const pathLib = require("path");
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumb = pathLib.dirname(thumb) thumb = pathLib.dirname(thumb)
// log("thumb 目录", thumb) // log("thumb 目录", thumb)
const videoInfo = await getVideoInfo(path); let videoInfo = {
log("视频信息", videoInfo) width: 1920, height: 1080,
time: 15,
format: "mp4",
size: fileSize,
filePath
};
try {
videoInfo = await getVideoInfo(path);
log("视频信息", videoInfo)
} catch (e) {
log("获取视频信息失败", e)
}
const createThumb = new Promise<string>((resolve, reject) => { const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png` const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumb, thumbFileName)
ffmpeg(filePath) ffmpeg(filePath)
.on("end", () => { .on("end", () => {
}) })
.on("error", (err) => { .on("error", (err) => {
reject(err); log("获取视频封面失败,使用默认封面", err)
if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
}).catch(reject)
} else {
fs.writeFile(thumbPath, defaultVideoThumb).then(() => {
resolve(thumbPath);
}).catch(reject)
}
}) })
.screenshots({ .screenshots({
timestamps: [0], timestamps: [0],
@@ -136,7 +155,7 @@ export class SendMsgElementConstructor {
folder: thumb, folder: thumb,
size: videoInfo.width + "x" + videoInfo.height size: videoInfo.width + "x" + videoInfo.height
}).on("end", () => { }).on("end", () => {
resolve(pathLib.join(thumb, thumbFileName)); resolve(thumbPath);
}); });
}) })
let thumbPath = new Map() let thumbPath = new Map()
@@ -193,7 +212,7 @@ export class SendMsgElementConstructor {
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, duration: duration,
formatType: 1, formatType: 1,
voiceType: 1, voiceType: 1,
voiceChangeType: 0, voiceChangeType: 0,
@@ -223,7 +242,11 @@ export class SendMsgElementConstructor {
return { return {
elementType: ElementType.ARK, elementType: ElementType.ARK,
elementId: "", elementId: "",
arkElement: data arkElement: {
bytesData: data,
linkInfo: null,
subElementType: null
}
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import {log} from "../../../common/utils"; import {log} from "../../../common/utils/log";
let pokeEngine: any = null let pokeEngine: any = null

View File

@@ -1,20 +1,23 @@
import {BrowserWindow} from 'electron'; import {BrowserWindow} from 'electron';
import {getConfigUtil, log, sleep} from "../common/utils";
import {NTQQApiClass} from "./ntcall"; import {NTQQApiClass} from "./ntcall";
import {NTQQMsgApi, sendMessagePool} from "./api/msg" import {NTQQMsgApi, sendMessagePool} from "./api/msg"
import {ChatType, Group, RawMessage, User} from "./types"; import {ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User} from "./types";
import {friends, groups, selfInfo, tempGroupCodeMap} from "../common/data"; import {friends, getGroupMember, groups, selfInfo, tempGroupCodeMap, uidMaps} from "../common/data";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {v4 as uuidv4} from "uuid" import {v4 as uuidv4} from "uuid"
import {postOB11Event} from "../onebot11/server/postOB11Event"; import {postOB11Event} from "../onebot11/server/postOB11Event";
import {HOOK_LOG} from "../common/config"; import {getConfigUtil, HOOK_LOG} from "../common/config";
import fs from "fs"; import fs from "fs";
import {dbUtil} from "../common/db"; import {dbUtil} from "../common/db";
import {NTQQGroupApi} from "./api/group"; import {NTQQGroupApi} from "./api/group";
import {log} from "../common/utils/log";
import {sleep} from "../common/utils/helper";
import {OB11Constructor} from "../onebot11/constructor";
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export let ReceiveCmdS = { export let ReceiveCmdS = {
RECENT_CONTACT: "nodeIKernelRecentContactListener/onRecentContactListChangedVer2",
UPDATE_MSG: "nodeIKernelMsgListener/onMsgInfoListUpdate", UPDATE_MSG: "nodeIKernelMsgListener/onMsgInfoListUpdate",
UPDATE_ACTIVE_MSG: "nodeIKernelMsgListener/onActiveMsgInfoUpdate", UPDATE_ACTIVE_MSG: "nodeIKernelMsgListener/onActiveMsgInfoUpdate",
NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`, NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`,
@@ -23,7 +26,8 @@ export let ReceiveCmdS = {
USER_INFO: "nodeIKernelProfileListener/onProfileSimpleChanged", USER_INFO: "nodeIKernelProfileListener/onProfileSimpleChanged",
USER_DETAIL_INFO: "nodeIKernelProfileListener/onProfileDetailInfoChanged", USER_DETAIL_INFO: "nodeIKernelProfileListener/onProfileDetailInfoChanged",
GROUPS: "nodeIKernelGroupListener/onGroupListUpdate", GROUPS: "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX: "onGroupListUpdate", GROUPS_STORE: "onGroupListUpdate",
GROUP_MEMBER_INFO_UPDATE: "nodeIKernelGroupListener/onMemberInfoChange",
FRIENDS: "onBuddyListChange", FRIENDS: "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaDownloadComplete", MEDIA_DOWNLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated", UNREAD_GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
@@ -32,6 +36,7 @@ export let ReceiveCmdS = {
SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH: "nodeIKernelStorageCleanListener/onFinishScan", CACHE_SCAN_FINISH: "nodeIKernelStorageCleanListener/onFinishScan",
MEDIA_UPLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaUploadComplete", MEDIA_UPLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaUploadComplete",
SKEY_UPDATE: "onSkeyUpdate"
} }
export type ReceiveCmd = typeof ReceiveCmdS[keyof typeof ReceiveCmdS] export type ReceiveCmd = typeof ReceiveCmdS[keyof typeof ReceiveCmdS]
@@ -59,45 +64,56 @@ let receiveHooks: Array<{
export function hookNTQQApiReceive(window: BrowserWindow) { export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send; const originalSend = window.webContents.send;
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
// console.log("hookNTQQApiReceive", channel, args)
let isLogger = false
try { try {
if (!args[0]?.eventName?.startsWith("ns-LoggerApi")) { isLogger = args[0]?.eventName?.startsWith("ns-LoggerApi")
HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args))
}
} catch (e) { } catch (e) {
} }
if (args?.[1] instanceof Array) { if (!isLogger) {
for (let receiveData of args?.[1]) { try {
const ntQQApiMethodName = receiveData.cmdName; HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) } catch (e) {
for (let hook of receiveHooks) { log("hook log error", e, args)
if (hook.method.includes(ntQQApiMethodName)) { }
new Promise((resolve, reject) => { }
try { try {
let _ = hook.hookFunc(receiveData.payload) if (args?.[1] instanceof Array) {
if (hook.hookFunc.constructor.name === "AsyncFunction") { for (let receiveData of args?.[1]) {
(_ as Promise<void>).then() const ntQQApiMethodName = receiveData.cmdName;
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData))
for (let hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(receiveData.payload)
if (hook.hookFunc.constructor.name === "AsyncFunction") {
(_ as Promise<void>).then()
}
} catch (e) {
log("hook error", e, receiveData.payload)
} }
} catch (e) { }).then()
log("hook error", e, receiveData.payload) }
}
}).then()
} }
} }
} }
} if (args[0]?.callbackId) {
if (args[0]?.callbackId) { // log("hookApiCallback", hookApiCallbacks, args)
// log("hookApiCallback", hookApiCallbacks, args) const callbackId = args[0].callbackId;
const callbackId = args[0].callbackId; if (hookApiCallbacks[callbackId]) {
if (hookApiCallbacks[callbackId]) { // log("callback found")
// log("callback found") new Promise((resolve, reject) => {
new Promise((resolve, reject) => { hookApiCallbacks[callbackId](args[1]);
hookApiCallbacks[callbackId](args[1]); }).then()
}).then() delete hookApiCallbacks[callbackId];
delete hookApiCallbacks[callbackId]; }
} }
} catch (e) {
log("hookNTQQApiReceive error", e.stack.toString(), args)
} }
return originalSend.call(window.webContents, channel, ...args); originalSend.call(window.webContents, channel, ...args);
} }
window.webContents.send = patchSend; window.webContents.send = patchSend;
} }
@@ -109,12 +125,19 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcMsg = new Proxy(ipc_message_proxy, { const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
// console.log(thisArg, args);
let isLogger = false
try { try {
if (args[3][1][0] !== "info") { isLogger = args[3][0].eventName.startsWith("ns-LoggerApi")
HOOK_LOG && log("call NTQQ api", thisArg, args);
}
} catch (e) { } catch (e) {
}
if (!isLogger) {
try {
HOOK_LOG && log("call NTQQ api", thisArg, args);
} catch (e) {
}
} }
return target.apply(thisArg, args); return target.apply(thisArg, args);
}, },
@@ -124,6 +147,31 @@ export function hookNTQQApiCall(window: BrowserWindow) {
} else { } else {
webContents._events["-ipc-message"] = proxyIpcMsg; webContents._events["-ipc-message"] = proxyIpcMsg;
} }
const ipc_invoke_proxy = webContents._events["-ipc-invoke"]?.[0] || webContents._events["-ipc-invoke"];
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
// console.log(args);
HOOK_LOG && log("call NTQQ invoke api", thisArg, args)
args[0]["_replyChannel"]["sendReply"] = new Proxy(args[0]["_replyChannel"]["sendReply"], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs);
}
});
let ret = target.apply(thisArg, args);
try {
HOOK_LOG && log("call NTQQ invoke api return", ret)
} catch (e) {
}
return ret;
}
});
if (webContents._events["-ipc-invoke"]?.[0]) {
webContents._events["-ipc-invoke"][0] = proxyIpcInvoke;
} else {
webContents._events["-ipc-invoke"] = proxyIpcInvoke;
}
} }
export function registerReceiveHook<PayloadType>(method: ReceiveCmd | ReceiveCmd[], hookFunc: (payload: PayloadType) => void): string { export function registerReceiveHook<PayloadType>(method: ReceiveCmd | ReceiveCmd[], hookFunc: (payload: PayloadType) => void): string {
@@ -145,19 +193,20 @@ export function removeReceiveHook(id: string) {
} }
let activatedGroups: string[] = []; let activatedGroups: string[] = [];
async function updateGroups(_groups: Group[], needUpdate: boolean = true) { async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) { for (let group of _groups) {
// log("update group", group) log("update group", group)
if (!activatedGroups.includes(group.groupCode)) { // if (!activatedGroups.includes(group.groupCode)) {
NTQQMsgApi.activateGroupChat(group.groupCode).then((r) => { NTQQMsgApi.activateChat({peerUid: group.groupCode, chatType: ChatType.group}).then((r) => {
activatedGroups.push(group.groupCode); // activatedGroups.push(group.groupCode);
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r) // log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) { // if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500); // setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else { // }else {
// } // }
}).catch(log) }).catch(log)
} // }
let existGroup = groups.find(g => g.groupCode == group.groupCode); let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) { if (existGroup) {
Object.assign(existGroup, group); Object.assign(existGroup, group);
@@ -176,13 +225,14 @@ async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
} }
} }
async function processGroupEvent(payload) { async function processGroupEvent(payload: { groupList: Group[] }) {
try { try {
const newGroupList = payload.groupList; const newGroupList = payload.groupList;
for (const group of newGroupList) { for (const group of newGroupList) {
let existGroup = groups.find(g => g.groupCode == group.groupCode); let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) { if (existGroup) {
if (existGroup.memberCount > group.memberCount) { if (existGroup.memberCount > group.memberCount) {
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`);
const oldMembers = existGroup.members; const oldMembers = existGroup.members;
await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时 await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
@@ -195,25 +245,33 @@ async function processGroupEvent(payload) {
newMembersSet.add(member.uin); newMembersSet.add(member.uin);
} }
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) { for (const member of oldMembers) {
if (!newMembersSet.has(member.uin)) { if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))); postOB11Event(new OB11GroupDecreaseEvent(parseInt(group.groupCode), parseInt(member.uin), parseInt(member.uin), "leave"));
break; break;
} }
} }
} }
} }
} }
updateGroups(newGroupList, false).then(); updateGroups(newGroupList, false).then();
} catch (e) { } catch (e) {
updateGroups(payload.groupList).then(); updateGroups(payload.groupList).then();
console.log(e); log("更新群信息错误", e.stack.toString());
} }
} }
// 群列表变动 // 群列表变动
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS, (payload) => { registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then(); updateGroups(payload.groupList).then();
} else { } else {
@@ -222,7 +280,9 @@ registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROU
} }
} }
}) })
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS_UNIX, (payload) => { registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) { if (payload.updateType != 2) {
updateGroups(payload.groupList).then(); updateGroups(payload.groupList).then();
} else { } else {
@@ -232,6 +292,36 @@ registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROU
} }
}) })
registerReceiveHook<{
groupCode: string,
dataSource: number,
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode;
const members = Array.from(payload.members.values());
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin);
if (existMember) {
Object.assign(existMember, member);
}
}
// const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
// 好友列表变动 // 好友列表变动
registerReceiveHook<{ registerReceiveHook<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[]
@@ -239,6 +329,7 @@ registerReceiveHook<{
for (const fData of payload.data) { for (const fData of payload.data) {
const _friends = fData.buddyList; const _friends = fData.buddyList;
for (let friend of _friends) { for (let friend of _friends) {
NTQQMsgApi.activateChat({peerUid: friend.uid, chatType: ChatType.friend}).then()
let existFriend = friends.find(f => f.uin == friend.uin) let existFriend = friends.find(f => f.uin == friend.uin)
if (!existFriend) { if (!existFriend) {
friends.push(friend) friends.push(friend)
@@ -249,8 +340,26 @@ registerReceiveHook<{
} }
}) })
// 新消息
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 保存一下uid
for (const message of payload.msgList) {
const uid = message.senderUid;
const uin = message.senderUin;
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then(receivedTempUinMap => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid;
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin;
}
}
// 自动清理新消息文件
const {autoDeleteFile} = getConfigUtil().getConfig(); const {autoDeleteFile} = getConfigUtil().getConfig();
if (!autoDeleteFile) { if (!autoDeleteFile) {
return return
@@ -309,3 +418,39 @@ registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({msgR
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => { registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
selfInfo.online = info.info.status !== 20 selfInfo.online = info.info.status !== 20
}) })
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number, sortedContactList: string[],
changedList: {
id: string, // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue;
activatedPeerUids.push(changedContact.id)
const peer = {peerUid: changedContact.id, chatType: changedContact.chatType}
if (changedContact.chatType === ChatType.temp) {
log("收到临时会话消息", peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(
() => {
NTQQMsgApi.getMsgHistory(peer, "", 20).then(({msgList}) => {
let lastTempMsg = msgList.pop()
log("激活窗口之前的第一条临时会话消息:", lastTempMsg)
if ((Date.now() / 1000) - parseInt(lastTempMsg.msgTime) < 5) {
OB11Constructor.message(lastTempMsg).then(r => postOB11Event(r))
}
})
}
)
} else {
NTQQMsgApi.activateChat(peer).then()
}
}
}
})

View File

@@ -1,8 +1,11 @@
import {ipcMain} from "electron"; import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook"; import {hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook} from "./hook";
import {log} from "../common/utils";
import {v4 as uuidv4} from "uuid" import {v4 as uuidv4} from "uuid"
import {log} from "../common/utils/log";
import {NTQQWindow, NTQQWindowApi, NTQQWindows} from "./api/window";
import {WebApi} from "./api/webapi";
import {HOOK_LOG} from "../common/config";
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = "ns-ntApi", NT_API = "ns-ntApi",
@@ -11,13 +14,19 @@ export enum NTQQApiClass {
WINDOW_API = "ns-WindowApi", WINDOW_API = "ns-WindowApi",
HOTUPDATE_API = "ns-HotUpdateApi", HOTUPDATE_API = "ns-HotUpdateApi",
BUSINESS_API = "ns-BusinessApi", BUSINESS_API = "ns-BusinessApi",
GLOBAL_DATA = "ns-GlobalDataApi" GLOBAL_DATA = "ns-GlobalDataApi",
SKEY_API = "ns-SkeyApi",
GROUP_HOME_WORK = "ns-GroupHomeWork",
GROUP_ESSENCE = "ns-GroupEssence",
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact", RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact",
ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息 ACTIVE_CHAT_PREVIEW = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ADD_ACTIVE_CHAT_2 = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", ACTIVE_CHAT_HISTORY = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelf",
GET_MULTI_MSG = "nodeIKernelMsgService/getMultiMsg",
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike",
SELF_INFO = "fetchAuthData", SELF_INFO = "fetchAuthData",
FRIENDS = "nodeIKernelBuddyService/getBuddyList", FRIENDS = "nodeIKernelBuddyService/getBuddyList",
@@ -26,6 +35,7 @@ export enum NTQQApiMethod {
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList", GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo", USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo", USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
USER_DETAIL_INFO_WITH_BIZ_INFO = "nodeIKernelProfileService/getUserDetailInfoWithBizInfo",
FILE_TYPE = "getFileType", FILE_TYPE = "getFileType",
FILE_MD5 = "getFileMd5", FILE_MD5 = "getFileMd5",
FILE_COPY = "copyFile", FILE_COPY = "copyFile",
@@ -65,7 +75,9 @@ export enum NTQQApiMethod {
OPEN_EXTRA_WINDOW = 'openExternalWindow', OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader' SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_SKEY = "nodeIKernelTipOffService/getPskey",
UPDATE_SKEY = "updatePskey"
} }
enum NTQQApiChannel { enum NTQQApiChannel {
@@ -98,7 +110,7 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
timeout = timeout ?? 5; timeout = timeout ?? 5;
afterFirstCmd = afterFirstCmd ?? true; afterFirstCmd = afterFirstCmd ?? true;
const uuid = uuidv4(); const uuid = uuidv4();
// log("callNTQQApi", channel, className, methodName, args, uuid) HOOK_LOG && log("callNTQQApi", channel, className, methodName, args, uuid)
return new Promise((resolve: (data: ReturnType) => void, reject) => { return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) // log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000 const _timeout = timeout * 1000
@@ -109,7 +121,7 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
} }
const apiArgs = [methodName, ...args] const apiArgs = [methodName, ...args]
if (!cbCmd) { if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别 // QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true success = true
resolve(r) resolve(r)
@@ -153,7 +165,12 @@ export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
ipcMain.emit( ipcMain.emit(
channel, channel,
{}, {
sender: {
send: (..._args: unknown[]) => {
},
},
},
{type: 'request', callbackId: uuid, eventName}, {type: 'request', callbackId: uuid, eventName},
apiArgs apiArgs
) )
@@ -177,5 +194,4 @@ export class NTQQApi {
] ]
}) })
} }
} }

View File

@@ -38,6 +38,7 @@ export enum GroupMemberRole {
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle: string;
avatarPath: string; avatarPath: string;
cardName: string; cardName: string;
cardType: number; cardType: number;

View File

@@ -1,4 +1,5 @@
import {GroupMemberRole} from "./group"; import {GroupMemberRole} from "./group";
import exp from "constants";
export enum ElementType { export enum ElementType {
TEXT = 1, TEXT = 1,
@@ -48,24 +49,29 @@ export enum PicType {
jpg = 1000 jpg = 1000
} }
export enum PicSubType {
normal = 0, // 普通图片,大图
face = 1 // 表情包小图
}
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC, elementType: ElementType.PIC,
elementId: "", elementId: "",
picElement: { picElement: {
md5HexStr: string, md5HexStr: string,
fileSize: number, fileSize: number | string,
picWidth: number, picWidth: number,
picHeight: number, picHeight: number,
fileName: string, fileName: string,
sourcePath: string, sourcePath: string,
original: boolean, original: boolean,
picType: PicType, picType: PicType,
picSubType: number, picSubType: PicSubType,
fileUuid: string, fileUuid: string,
fileSubId: string, fileSubId: string,
thumbFileSize: number, thumbFileSize: number,
summary: string, summary: string,
} },
} }
export interface SendReplyElement { export interface SendReplyElement {
@@ -98,7 +104,8 @@ export interface FileElement {
"fileSha3"?: "", "fileSha3"?: "",
"fileUuid"?: "", "fileUuid"?: "",
"fileSubId"?: "", "fileSubId"?: "",
"thumbFileSize"?: number "thumbFileSize"?: number,
fileBizId?: number
} }
export interface SendFileElement { export interface SendFileElement {
@@ -165,9 +172,11 @@ export interface ArkElement {
} }
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn" export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn"
export const IMAGE_HTTP_HOST_NT = "https://multimedia.nt.qq.com.cn"
export interface PicElement { export interface PicElement {
originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/ originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
originImageMd5?: string;
sourcePath: string; // 图片本地路径 sourcePath: string; // 图片本地路径
thumbPath: Map<number, string>; thumbPath: Map<number, string>;
picWidth: number; picWidth: number;
@@ -180,6 +189,7 @@ export interface PicElement {
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
INVITE_NEW_MEMBER = 12, INVITE_NEW_MEMBER = 12,
MEMBER_NEW_TITLE = 17
} }
export interface GrayTipElement { export interface GrayTipElement {
@@ -196,6 +206,9 @@ export interface GrayTipElement {
groupElement: TipGroupElement, groupElement: TipGroupElement,
xmlElement: { xmlElement: {
content: string; content: string;
},
jsonGrayTipElement: {
jsonStr: string;
} }
} }
@@ -204,6 +217,45 @@ export interface FaceElement {
faceType: 1 faceType: 1
} }
export interface MarketFaceElement {
"itemType": 6,
"faceInfo": 1,
"emojiPackageId": 203875,
"subType": 3,
"mediaType": 0,
"imageWidth": 200,
"imageHeight": 200,
"faceName": string,
"emojiId": "094d53bd1c9ac5d35d04b08e8a6c992c",
"key": "a8b1dd0aebc8d910",
"param": null,
"mobileParam": null,
"sourceType": null,
"startTime": null,
"endTime": null,
"emojiType": 1,
"hasIpProduct": null,
"voiceItemHeightArr": null,
"sourceName": null,
"sourceJumpUrl": null,
"sourceTypeName": null,
"backColor": null,
"volumeColor": null,
"staticFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c_aio.png",
"dynamicFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c",
"supportSize": [
{
"width": 300,
"height": 300
},
{
"width": 200,
"height": 200
}
],
"apngSupportSize": null
}
export interface VideoElement { export interface VideoElement {
"filePath": string, "filePath": string,
"fileName": string, "fileName": string,
@@ -229,6 +281,34 @@ export interface VideoElement {
"sourceVideoCodecFormat"?: number "sourceVideoCodecFormat"?: number
} }
export interface MarkdownElement {
content: string,
}
export interface InlineKeyboardElementRowButton{
"id": "",
"label": string,
"visitedLabel": string,
"style": 1, // 未知
"type": 2, // 未知
"clickLimit": 0, // 未知
"unsupportTips": "请升级新版手机QQ",
"data": string,
"atBotShowChannelList": false,
"permissionType": 2,
"specifyRoleIds": [],
"specifyTinyids": [],
"isReply": false,
"anchor": 0,
"enter": false,
"subscribeDataTemplateIds": []
}
export interface InlineKeyboardElement {
rows: [{
buttons: InlineKeyboardElementRowButton[]
}]
}
export interface TipAioOpGrayTipElement { // 这是什么提示来着? export interface TipAioOpGrayTipElement { // 这是什么提示来着?
operateType: number, operateType: number,
peerUid: string, peerUid: string,
@@ -237,6 +317,7 @@ export interface TipAioOpGrayTipElement { // 这是什么提示来着?
export enum TipGroupElementType { export enum TipGroupElementType {
memberIncrease = 1, memberIncrease = 1,
kicked = 3, // 被移出群
ban = 8 ban = 8
} }
@@ -247,7 +328,7 @@ export interface TipGroupElement {
"memberUid": string, "memberUid": string,
"memberNick": string, "memberNick": string,
"memberRemark": string, "memberRemark": string,
"adminUid": string, // 同意加群的管理员uid "adminUid": string,
"adminNick": string, "adminNick": string,
"adminRemark": string, "adminRemark": string,
"createGroup": null, "createGroup": null,
@@ -279,6 +360,11 @@ export interface TipGroupElement {
} }
} }
export interface MultiForwardMsgElement{
xmlContent: string, // xml格式的消息内容
resId: string,
fileName: string,
}
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string;
@@ -316,5 +402,9 @@ export interface RawMessage {
faceElement: FaceElement; faceElement: FaceElement;
videoElement: VideoElement; videoElement: VideoElement;
fileElement: FileElement; fileElement: FileElement;
marketFaceElement: MarketFaceElement;
inlineKeyboardElement: InlineKeyboardElement;
markdownElement: MarkdownElement;
multiForwardMsgElement: MultiForwardMsgElement;
}[]; }[];
} }

View File

@@ -4,8 +4,9 @@ export enum GroupNotifyTypes {
INVITED_JOIN = 4, // 有人接受了邀请入群 INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST = 7, JOIN_REQUEST = 7,
ADMIN_SET = 8, ADMIN_SET = 8,
KICK_MEMBER = 9,
MEMBER_EXIT = 11, // 主动退出
ADMIN_UNSET = 12, ADMIN_UNSET = 12,
MEMBER_EXIT = 11, // 主动退出?
} }

View File

@@ -18,7 +18,54 @@ export interface User {
longNick?: string; // 签名 longNick?: string; // 签名
remark?: string; remark?: string;
sex?: Sex; sex?: Sex;
"qqLevel"?: QQLevel qqLevel?: QQLevel,
qid?: string
"birthday_year"?: number,
"birthday_month"?: number,
"birthday_day"?: number,
"topTime"?: string,
"constellation"?: number,
"shengXiao"?: number,
"kBloodType"?: number,
"homeTown"?: string, //"0-0-0",
"makeFriendCareer"?: number,
"pos"?: string,
"eMail"?: string
"phoneNum"?: string,
"college"?: string,
"country"?: string,
"province"?: string,
"city"?: string,
"postCode"?: string,
"address"?: string,
"isBlock"?: boolean,
"isSpecialCareOpen"?: boolean,
"isSpecialCareZone"?: boolean,
"ringId"?: string,
"regTime"?: number,
interest?: string,
"labels"?: string[],
"isHideQQLevel"?: number,
"privilegeIcon"?: {
"jumpUrl": string,
"openIconList": unknown[],
"closeIconList": unknown[]
},
"photoWall"?: {
"picList": unknown[]
},
"vipFlag"?: boolean,
"yearVipFlag"?: boolean,
"svipFlag"?: boolean,
"vipLevel"?: number,
"status"?: number,
"qidianMasterFlag"?: number,
"qidianCrewFlag"?: number,
"qidianCrewFlag2"?: number,
"extStatus"?: number,
"recommendImgFlag"?: number,
"disableEmojiShortCuts"?: number,
"pendantId"?: string,
} }
export interface SelfInfo extends User { export interface SelfInfo extends User {

View File

@@ -1,7 +1,8 @@
import {ActionName, BaseCheckResult} from "./types" import {ActionName, BaseCheckResult} from "./types"
import {OB11Response} from "./utils" import {OB11Response} from "./OB11Response"
import {OB11Return} from "../types"; import {OB11Return} from "../types";
import {log} from "../../common/utils";
import {log} from "../../common/utils/log";
class BaseAction<PayloadType, ReturnDataType> { class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName actionName: ActionName

View File

@@ -1,28 +0,0 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
import {log} from "../../common/utils";
interface Payload {
method: string,
args: any[],
}
export default class Debug extends BaseAction<Payload, any> {
actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> {
log("debug call ntqq api", payload);
const method = NTQQApi[payload.method]
if (!method) {
throw `${method} 不存在`
}
const result = method(...payload.args);
if (method.constructor.name === "AsyncFunction") {
return await result
}
return result
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

View File

@@ -1,47 +0,0 @@
import BaseAction from "./BaseAction";
import {getConfigUtil} from "../../common/utils";
import fs from "fs/promises";
import {dbUtil} from "../../common/db";
export interface GetFilePayload {
file: string // 文件名
}
export interface GetFileResponse {
file?: string // path
url?: string
file_size?: string
file_name?: string
base64?: string
}
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file)
const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig()
if (!cache) {
throw new Error('file not found')
}
if (cache.downloadFunc) {
await cache.downloadFunc()
}
let res: GetFileResponse = {
file: cache.filePath,
url: cache.url,
file_size: cache.fileSize,
file_name: cache.fileName
}
if (enableLocalFile2Url) {
if (!cache.url) {
res.base64 = await fs.readFile(cache.filePath, 'base64')
}
}
if (autoDeleteFile) {
setTimeout(() => {
fs.unlink(cache.filePath)
}, autoDeleteFileSecond * 1000)
}
return res
}
}

View File

@@ -1,16 +0,0 @@
import {OB11Group} from '../types';
import {OB11Constructor} from "../constructor";
import {groups} from "../../common/data";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
class GetGroupList extends BaseAction<null, OB11Group[]> {
actionName = ActionName.GetGroupList
protected async _handle(payload: null) {
return OB11Constructor.groups(groups);
}
}
export default GetGroupList

View File

@@ -1,5 +1,6 @@
import {OB11Return} from '../types'; import {OB11Return} from '../types';
import {isNull} from '../../common/utils';
import {isNull} from "../../common/utils/helper";
export class OB11Response { export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> { static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {

View File

@@ -0,0 +1,110 @@
import BaseAction from "../BaseAction";
import fs from "fs/promises";
import {dbUtil} from "../../../common/db";
import {getConfigUtil} from "../../../common/config";
import {log, sleep, uri2local} from "../../../common/utils";
import {NTQQFileApi} from "../../../ntqqapi/api/file";
import {ActionName} from "../types";
import {FileElement, RawMessage, VideoElement} from "../../../ntqqapi/types";
export interface GetFilePayload {
file: string // 文件名或者fileUuid
}
export interface GetFileResponse {
file?: string // path
url?: string
file_size?: string
file_name?: string
base64?: string
}
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage): {id: string, element: VideoElement | FileElement}{
let element = msg.elements.find(e=>e.fileElement)
if (!element){
element = msg.elements.find(e=>e.videoElement)
return {id: element.elementId, element: element.videoElement}
}
return {id: element.elementId, element: element.fileElement}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const cache = await dbUtil.getFileCache(payload.file)
const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig()
if (!cache) {
throw new Error('file not found')
}
if (cache.downloadFunc) {
await cache.downloadFunc()
}
try {
await fs.access(cache.filePath, fs.constants.F_OK)
} catch (e) {
log("file not found", e)
if (cache.url){
const downloadResult = await uri2local(cache.url)
if (downloadResult.success) {
cache.filePath = downloadResult.path
dbUtil.addFileCache(payload.file, cache).then()
} else {
throw new Error("file download failed. " + downloadResult.errMsg)
}
}
else{
// 没有url的可能是私聊文件或者群文件需要自己下载
log("需要调用 NTQQ 下载文件api")
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg){
log("找到了文件 msg", msg)
let element = this.getElement(msg);
log("找到了文件 element", element);
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.id, "", "", true)
await sleep(1000);
msg = await dbUtil.getMsgByLongId(cache.msgId)
log("下载完成后的msg", msg)
cache.filePath = this.getElement(msg).element.filePath
dbUtil.addFileCache(payload.file, cache).then()
}
}
}
}
let res: GetFileResponse = {
file: cache.filePath,
url: cache.url,
file_size: cache.fileSize,
file_name: cache.fileName
}
if (enableLocalFile2Url) {
if (!cache.url) {
try{
res.base64 = await fs.readFile(cache.filePath, 'base64')
}catch (e) {
throw new Error("文件下载失败. " + e)
}
}
}
// if (autoDeleteFile) {
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res
}
}
export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile
protected async _handle(payload: {file_id: string, file: string}): Promise<GetFileResponse> {
if (!payload.file_id) {
throw new Error('file_id 不能为空')
}
payload.file = payload.file_id
return super._handle(payload);
}
}

View File

@@ -1,5 +1,5 @@
import {GetFileBase} from "./GetFile"; import {GetFileBase} from "./GetFile";
import {ActionName} from "./types"; import {ActionName} from "../types";
export default class GetImage extends GetFileBase { export default class GetImage extends GetFileBase {

View File

@@ -1,5 +1,5 @@
import {GetFileBase, GetFilePayload, GetFileResponse} from "./GetFile"; import {GetFileBase, GetFilePayload, GetFileResponse} from "./GetFile";
import {ActionName} from "./types"; import {ActionName} from "../types";
interface Payload extends GetFilePayload { interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'

View File

@@ -0,0 +1,73 @@
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import fs from "fs";
import {join as joinPath} from "node:path";
import {calculateFileMD5, httpDownload, TEMP_DIR} from "../../../common/utils";
import {v4 as uuid4} from "uuid";
interface Payload {
thread_count?: number
url?: string
base64?: string
name?: string
headers?: string | string[]
}
interface FileResponse {
file: string
}
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile
protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name
let name = payload.name || uuid4();
const filePath = joinPath(TEMP_DIR, name);
if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64')
} else if (payload.url) {
const headers = this.getHeaders(payload.headers);
let buffer = await httpDownload({url: payload.url, headers: headers})
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary');
} else {
throw new Error("不存在任何文件, 无法下载")
}
if (fs.existsSync(filePath)) {
if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath);
const newPath = joinPath(TEMP_DIR, md5);
fs.renameSync(filePath, newPath);
return { file: newPath }
}
return { file: filePath }
} else {
throw new Error("文件写入失败, 检查权限")
}
}
getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers = {};
if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]');
}
if (Array.isArray(headersIn)) {
for (const headerItem of headersIn) {
const spilt = headerItem.indexOf('=');
if (spilt < 0) {
headers[headerItem] = "";
} else {
const key = headerItem.substring(0, spilt);
headers[key] = headerItem.substring(0, spilt + 1);
}
}
}
if (!headers['Content-Type']) {
headers['Content-Type'] = 'application/octet-stream';
}
return headers;
}
}

View File

@@ -0,0 +1,39 @@
import BaseAction from "../BaseAction";
import {OB11ForwardMessage, OB11Message, OB11MessageData} from "../../types";
import {NTQQMsgApi, Peer} from "../../../ntqqapi/api";
import {dbUtil} from "../../../common/db";
import {OB11Constructor} from "../../constructor";
import {ActionName} from "../types";
interface Payload {
message_id: string; // long msg id
}
interface Response{
messages: (OB11Message & {content: OB11MessageData})[]
}
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any>{
actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> {
const rootMsg = await dbUtil.getMsgByLongId(payload.message_id)
if (!rootMsg){
throw Error("msg not found")
}
let data = await NTQQMsgApi.getMultiMsg({chatType: rootMsg.chatType, peerUid: rootMsg.peerUid}, rootMsg.msgId, rootMsg.msgId)
if (data.result !== 0){
throw Error("找不到相关的聊天记录" + data.errMsg)
}
let msgList = data.msgList
let messages = await Promise.all(msgList.map(async msg => {
let resMsg = await OB11Constructor.message(msg)
resMsg.message_id = await dbUtil.addMsg(msg);
return resMsg
}))
messages.map(msg => {
(<OB11ForwardMessage>msg).content = msg.message;
delete msg.message;
})
return {messages}
}
}

View File

@@ -0,0 +1,39 @@
import BaseAction from "../BaseAction";
import {OB11Message, OB11User} from "../../types";
import {groups} from "../../../common/data";
import {ActionName} from "../types";
import {ChatType} from "../../../ntqqapi/types";
import {dbUtil} from "../../../common/db";
import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
import {OB11Constructor} from "../../constructor";
import {log} from "../../../common/utils";
interface Payload {
group_id: number
message_seq: number,
count: number
}
interface Response{
messages: OB11Message[]
}
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> {
const group = groups.find(group => group.groupCode === payload.group_id.toString())
if (!group) {
throw `${payload.group_id}不存在`
}
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || "0"
// log("startMsgId", startMsgId)
let msgList = (await NTQQMsgApi.getMsgHistory({chatType: ChatType.group, peerUid: group.groupCode}, startMsgId, parseInt(payload.count?.toString()) || 20)).msgList
await Promise.all(msgList.map(async msg => {
msg.msgShortId = await dbUtil.addMsg(msg)
}))
const ob11MsgList = await Promise.all(msgList.map(msg=>OB11Constructor.message(msg)))
return {"messages": ob11MsgList}
}
}

View File

@@ -1,8 +1,9 @@
import BaseAction from "../BaseAction"; import BaseAction from "../BaseAction";
import {OB11User} from "../../types"; import {OB11User} from "../../types";
import {getFriend, getGroupMember, groups} from "../../../common/data"; import {getUidByUin, uidMaps} from "../../../common/data";
import {OB11Constructor} from "../../constructor"; import {OB11Constructor} from "../../constructor";
import {ActionName} from "../types"; import {ActionName} from "../types";
import {NTQQUserApi} from "../../../ntqqapi/api/user";
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> {
@@ -10,16 +11,10 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: numbe
protected async _handle(payload: { user_id: number }): Promise<OB11User> { protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString() const user_id = payload.user_id.toString()
const friend = await getFriend(user_id) const uid = getUidByUin(user_id)
if (friend) { if (!uid) {
return OB11Constructor.friend(friend); throw new Error("查无此人")
} }
for (const group of groups) { return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true))
const member = await getGroupMember(group.groupCode, user_id)
if (member) {
return OB11Constructor.groupMember(group.groupCode, member) as OB11User
}
}
throw ("查无此人")
} }
} }

View File

@@ -1,16 +1,20 @@
import SendMsg from "../SendMsg"; import SendMsg, {convertMessage2List} from "../msg/SendMsg";
import {OB11PostSendMsg} from "../../types"; import {OB11PostSendMsg} from "../../types";
import {ActionName} from "../types"; import {ActionName} from "../types";
export class GoCQHTTPSendGroupForwardMsg extends SendMsg { export class GoCQHTTPSendForwardMsg extends SendMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg; actionName = ActionName.GoCQHTTP_SendForwardMsg;
protected async check(payload: OB11PostSendMsg) { protected async check(payload: OB11PostSendMsg) {
payload.message = this.convertMessage2List(payload.messages); if (payload.messages) payload.message = convertMessage2List(payload.messages);
return super.check(payload); return super.check(payload);
} }
} }
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendGroupForwardMsg { export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg; actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
}
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
} }

View File

@@ -3,9 +3,9 @@ import {getGroup} from "../../../common/data";
import {ActionName} from "../types"; import {ActionName} from "../types";
import {SendMsgElementConstructor} from "../../../ntqqapi/constructor"; import {SendMsgElementConstructor} from "../../../ntqqapi/constructor";
import {ChatType, SendFileElement} from "../../../ntqqapi/types"; import {ChatType, SendFileElement} from "../../../ntqqapi/types";
import {uri2local} from "../../utils";
import fs from "fs"; import fs from "fs";
import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
import {uri2local} from "../../../common/utils";
interface Payload{ interface Payload{
group_id: number group_id: number

View File

@@ -1,8 +1,8 @@
import {OB11Group} from '../types'; import {OB11Group} from '../../types';
import {getGroup} 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

View File

@@ -0,0 +1,22 @@
import {OB11Group} from '../../types';
import {OB11Constructor} from "../../constructor";
import {groups} from "../../../common/data";
import BaseAction from "../BaseAction";
import {ActionName} from "../types";
import {NTQQGroupApi} from "../../../ntqqapi/api";
import {log} from "../../../common/utils";
class GetGroupList extends BaseAction<null, OB11Group[]> {
actionName = ActionName.GetGroupList
protected async _handle(payload: null) {
// if (groups.length === 0) {
// const groups = await NTQQGroupApi.getGroups(true)
// log("get groups", groups)
// }
return OB11Constructor.groups(groups);
}
}
export default GetGroupList

View File

@@ -1,10 +1,11 @@
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";
import {NTQQUserApi} from "../../ntqqapi/api/user"; import {NTQQUserApi} from "../../../ntqqapi/api/user";
import {isNull, log} from "../../common/utils"; import {log} from "../../../common/utils/log";
import {isNull} from "../../../common/utils/helper";
export interface PayloadType { export interface PayloadType {
@@ -20,7 +21,7 @@ class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
if (member) { if (member) {
if (isNull(member.sex)){ if (isNull(member.sex)){
log("获取群成员详细信息") log("获取群成员详细信息")
let info = (await NTQQUserApi.getUserDetailInfo(member.uid)) let info = (await NTQQUserApi.getUserDetailInfo(member.uid, true))
log("群成员详细信息结果", info) log("群成员详细信息结果", info)
Object.assign(member, info); Object.assign(member, info);
} }

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 {OB11Constructor} from "../constructor"; import {OB11Constructor} from "../../constructor";
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
export interface PayloadType { export interface PayloadType {
group_id: number group_id: number

View File

@@ -1,5 +1,5 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
export default class GetGuildList extends BaseAction<null, null> { export default class GetGuildList extends BaseAction<null, null> {
actionName = ActionName.GetGuildList actionName = ActionName.GetGuildList

View File

@@ -1,7 +1,8 @@
import SendMsg from "./SendMsg"; import SendMsg from "../msg/SendMsg";
import {ActionName, BaseCheckResult} from "./types"; import {ActionName, BaseCheckResult} from "../types";
import {OB11PostSendMsg} from "../types"; import {OB11PostSendMsg} from "../../types";
import {log} from "../../common/utils";
import {log} from "../../../common/utils/log";
class SendGroupMsg extends SendMsg { class SendGroupMsg extends SendMsg {

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {GroupRequestOperateTypes} from "../../ntqqapi/types"; import {GroupRequestOperateTypes} from "../../../ntqqapi/types";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload { interface Payload {
flag: string, flag: string,
@@ -16,8 +16,9 @@ export default class SetGroupAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString(); const seq = payload.flag.toString();
const approve = payload.approve.toString() === "true";
await NTQQGroupApi.handleGroupRequest(seq, await NTQQGroupApi.handleGroupRequest(seq,
payload.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason payload.reason
) )
return null return null

View File

@@ -1,8 +1,8 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {getGroupMember} from "../../common/data"; import {getGroupMember} from "../../../common/data";
import {GroupMemberRole} from "../../ntqqapi/types"; import {GroupMemberRole} from "../../../ntqqapi/types";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload { interface Payload {
group_id: number, group_id: number,
@@ -15,10 +15,11 @@ export default class SetGroupAdmin extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id) const member = await getGroupMember(payload.group_id, payload.user_id)
const enable = payload.enable.toString() === "true"
if (!member) { if (!member) {
throw `群成员${payload.user_id}不存在` throw `群成员${payload.user_id}不存在`
} }
await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal) await NTQQGroupApi.setMemberRole(payload.group_id.toString(), member.uid, enable ? GroupMemberRole.admin : GroupMemberRole.normal)
return null return null
} }
} }

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {getGroupMember} from "../../common/data"; import {getGroupMember} from "../../../common/data";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload { interface Payload {
group_id: number, group_id: number,

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {getGroupMember} from "../../common/data"; import {getGroupMember} from "../../../common/data";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload { interface Payload {
group_id: number, group_id: number,

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {getGroupMember} from "../../common/data"; import {getGroupMember} from "../../../common/data";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload { interface Payload {
group_id: number, group_id: number,

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {log} from "../../common/utils"; import {ActionName} from "../types";
import {ActionName} from "./types"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {log} from "../../../common/utils/log";
interface Payload { interface Payload {
group_id: number, group_id: number,

View File

@@ -1,6 +1,6 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload { interface Payload {
group_id: number, group_id: number,

View File

@@ -1,6 +1,6 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQGroupApi} from "../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
interface Payload { interface Payload {
group_id: number, group_id: number,
@@ -11,8 +11,8 @@ export default class SetGroupWholeBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupWholeBan actionName = ActionName.SetGroupWholeBan
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const enable = payload.enable.toString() === "true"
await NTQQGroupApi.banGroup(payload.group_id.toString(), !!payload.enable) await NTQQGroupApi.banGroup(payload.group_id.toString(), enable)
return null return null
} }
} }

View File

@@ -1,43 +1,48 @@
import GetMsg from './GetMsg' import GetMsg from './msg/GetMsg'
import GetLoginInfo from './GetLoginInfo' import GetLoginInfo from './system/GetLoginInfo'
import GetFriendList from './GetFriendList' import GetFriendList from './user/GetFriendList'
import GetGroupList from './GetGroupList' import GetGroupList from './group/GetGroupList'
import GetGroupInfo from './GetGroupInfo' import GetGroupInfo from './group/GetGroupInfo'
import GetGroupMemberList from './GetGroupMemberList' import GetGroupMemberList from './group/GetGroupMemberList'
import GetGroupMemberInfo from './GetGroupMemberInfo' import GetGroupMemberInfo from './group/GetGroupMemberInfo'
import SendGroupMsg from './SendGroupMsg' import SendGroupMsg from './group/SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg' import SendPrivateMsg from './msg/SendPrivateMsg'
import SendMsg from './SendMsg' import SendMsg from './msg/SendMsg'
import DeleteMsg from "./DeleteMsg"; import DeleteMsg from "./msg/DeleteMsg";
import BaseAction from "./BaseAction"; import BaseAction from "./BaseAction";
import GetVersionInfo from "./GetVersionInfo"; import GetVersionInfo from "./system/GetVersionInfo";
import CanSendRecord from "./CanSendRecord"; import CanSendRecord from "./system/CanSendRecord";
import CanSendImage from "./CanSendImage"; import CanSendImage from "./system/CanSendImage";
import GetStatus from "./GetStatus"; import GetStatus from "./system/GetStatus";
import {GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg"; import {GoCQHTTPSendForwardMsg, GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg";
import GoCQHTTPGetStrangerInfo from "./go-cqhttp/GetStrangerInfo"; import GoCQHTTPGetStrangerInfo from "./go-cqhttp/GetStrangerInfo";
import SendLike from "./SendLike"; import SendLike from "./user/SendLike";
import SetGroupAddRequest from "./SetGroupAddRequest"; import SetGroupAddRequest from "./group/SetGroupAddRequest";
import SetGroupLeave from "./SetGroupLeave"; import SetGroupLeave from "./group/SetGroupLeave";
import GetGuildList from "./GetGuildList"; import GetGuildList from "./group/GetGuildList";
import Debug from "./Debug"; import Debug from "./llonebot/Debug";
import SetFriendAddRequest from "./SetFriendAddRequest"; import SetFriendAddRequest from "./user/SetFriendAddRequest";
import SetGroupWholeBan from "./SetGroupWholeBan"; import SetGroupWholeBan from "./group/SetGroupWholeBan";
import SetGroupName from "./SetGroupName"; import SetGroupName from "./group/SetGroupName";
import SetGroupBan from "./SetGroupBan"; import SetGroupBan from "./group/SetGroupBan";
import SetGroupKick from "./SetGroupKick"; import SetGroupKick from "./group/SetGroupKick";
import SetGroupAdmin from "./SetGroupAdmin"; import SetGroupAdmin from "./group/SetGroupAdmin";
import SetGroupCard from "./SetGroupCard"; import SetGroupCard from "./group/SetGroupCard";
import GetImage from "./GetImage"; import GetImage from "./file/GetImage";
import GetRecord from "./GetRecord"; import GetRecord from "./file/GetRecord";
import GoCQHTTPMarkMsgAsRead from "./MarkMsgAsRead"; import GoCQHTTPMarkMsgAsRead from "./msg/MarkMsgAsRead";
import CleanCache from "./CleanCache"; import CleanCache from "./system/CleanCache";
import GoCQHTTPUploadGroupFile from "./go-cqhttp/UploadGroupFile"; import GoCQHTTPUploadGroupFile from "./go-cqhttp/UploadGroupFile";
import {GetConfigAction, SetConfigAction} from "./llonebot/Config"; import {GetConfigAction, SetConfigAction} from "./llonebot/Config";
import GetGroupAddRequest from "./llonebot/GetGroupAddRequest"; import GetGroupAddRequest from "./llonebot/GetGroupAddRequest";
import SetQQAvatar from './llonebot/SetQQAvatar' import SetQQAvatar from './llonebot/SetQQAvatar'
import GoCQHTTPDownloadFile from "./go-cqhttp/DownloadFile";
import GoCQHTTPGetGroupMsgHistory from "./go-cqhttp/GetGroupMsgHistory";
import GetFile from "./file/GetFile";
import {GoCQHTTGetForwardMsgAction} from "./go-cqhttp/GetForwardMsg";
export const actionHandlers = [ export const actionHandlers = [
new GetFile(),
new Debug(), new Debug(),
new GetConfigAction(), new GetConfigAction(),
new SetConfigAction(), new SetConfigAction(),
@@ -69,12 +74,16 @@ export const actionHandlers = [
new CleanCache(), new CleanCache(),
//以下为go-cqhttp api //以下为go-cqhttp api
new GoCQHTTPSendForwardMsg(),
new GoCQHTTPSendGroupForwardMsg(), new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(), new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(), new GoCQHTTPGetStrangerInfo(),
new GoCQHTTPDownloadFile(),
new GetGuildList(), new GetGuildList(),
new GoCQHTTPMarkMsgAsRead(), new GoCQHTTPMarkMsgAsRead(),
new GoCQHTTPUploadGroupFile(), new GoCQHTTPUploadGroupFile(),
new GoCQHTTPGetGroupMsgHistory(),
new GoCQHTTGetForwardMsgAction(),
] ]

View File

@@ -1,8 +1,8 @@
import BaseAction from "../BaseAction"; import BaseAction from "../BaseAction";
import {Config} from "../../../common/types"; import {Config} from "../../../common/types";
import {getConfigUtil} from "../../../common/utils";
import {ActionName} from "../types"; import {ActionName} from "../types";
import {setConfig} from "../../../main/setConfig"; import {setConfig} from "../../../main/setConfig";
import {getConfigUtil} from "../../../common/config";
export class GetConfigAction extends BaseAction<null, Config> { export class GetConfigAction extends BaseAction<null, Config> {

View File

@@ -0,0 +1,42 @@
import BaseAction from "../BaseAction";
// import * as ntqqApi from "../../../ntqqapi/api";
import {
NTQQMsgApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQUserApi,
NTQQFileApi,
NTQQFileCacheApi,
NTQQWindowApi,
} from "../../../ntqqapi/api";
import {ActionName} from "../types";
import {log} from "../../../common/utils/log";
interface Payload {
method: string,
args: any[],
}
export default class Debug extends BaseAction<Payload, any> {
actionName = ActionName.Debug
protected async _handle(payload: Payload): Promise<any> {
log("debug call ntqq api", payload);
const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi]
for (const ntqqApiClass of ntqqApi) {
log("ntqqApiClass", ntqqApiClass)
const method = ntqqApiClass[payload.method]
if (method) {
const result = method(...payload.args);
if (method.constructor.name === "AsyncFunction") {
return await result
}
return result
}
}
throw `${payload.method}方法 不存在`
// const info = await NTQQApi.getUserDetailInfo(friends[0].uid);
// return info
}
}

View File

@@ -2,9 +2,9 @@ import {GroupNotify, GroupNotifyStatus} from "../../../ntqqapi/types";
import BaseAction from "../BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "../types"; import {ActionName} from "../types";
import {uidMaps} from "../../../common/data"; import {uidMaps} from "../../../common/data";
import {log} from "../../../common/utils";
import {NTQQUserApi} from "../../../ntqqapi/api/user"; import {NTQQUserApi} from "../../../ntqqapi/api/user";
import {NTQQGroupApi} from "../../../ntqqapi/api/group"; import {NTQQGroupApi} from "../../../ntqqapi/api/group";
import {log} from "../../../common/utils/log";
interface OB11GroupRequestNotify { interface OB11GroupRequestNotify {
group_id: number, group_id: number,

View File

@@ -1,9 +1,8 @@
import BaseAction from "../BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "../types"; import {ActionName} from "../types";
import { uri2local } from "../../utils";
import * as fs from "node:fs"; import * as fs from "node:fs";
import { checkFileReceived } from "../../../common/utils";
import {NTQQUserApi} from "../../../ntqqapi/api/user"; import {NTQQUserApi} from "../../../ntqqapi/api/user";
import {checkFileReceived, uri2local} from "../../../common/utils/file";
// import { log } from "../../../common/utils"; // import { log } from "../../../common/utils";
interface Payload { interface Payload {

View File

@@ -1,7 +1,7 @@
import {ActionName} from "./types"; import {ActionName} from "../types";
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {dbUtil} from "../../common/db"; import {dbUtil} from "../../../common/db";
import {NTQQMsgApi} from "../../ntqqapi/api/msg"; import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
interface Payload { interface Payload {
message_id: number message_id: number

View File

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

View File

@@ -1,5 +1,5 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
interface Payload{ interface Payload{
message_id: number message_id: number

View File

@@ -2,12 +2,19 @@ import {
AtType, AtType,
ChatType, ChatType,
ElementType, ElementType,
Group, Group, PicSubType,
RawMessage, RawMessage,
SendArkElement, SendArkElement,
SendMessageElement SendMessageElement
} from "../../ntqqapi/types"; } from "../../../ntqqapi/types";
import {friends, getFriend, getGroup, getGroupMember, getUidByUin, selfInfo,} from "../../common/data"; import {
friends,
getFriend,
getGroup,
getGroupMember,
getUidByUin,
selfInfo,
} from "../../../common/data";
import { import {
OB11MessageCustomMusic, OB11MessageCustomMusic,
OB11MessageData, OB11MessageData,
@@ -15,18 +22,19 @@ import {
OB11MessageMixType, OB11MessageMixType,
OB11MessageNode, OB11MessageNode,
OB11PostSendMsg OB11PostSendMsg
} from '../types'; } from '../../types';
import {Peer} from "../../ntqqapi/api/msg"; import {Peer} from "../../../ntqqapi/api/msg";
import {SendMsgElementConstructor} from "../../ntqqapi/constructor"; import {SendMsgElementConstructor} from "../../../ntqqapi/constructor";
import {uri2local} from "../utils"; import BaseAction from "../BaseAction";
import BaseAction from "./BaseAction"; import {ActionName, BaseCheckResult} from "../types";
import {ActionName, BaseCheckResult} from "./types";
import * as fs from "node:fs"; import * as fs from "node:fs";
import {log, sleep} from "../../common/utils"; import {decodeCQCode} from "../../cqcode";
import {decodeCQCode} from "../cqcode"; import {dbUtil} from "../../../common/db";
import {dbUtil} from "../../common/db"; import {ALLOW_SEND_TEMP_MSG} from "../../../common/config";
import {ALLOW_SEND_TEMP_MSG} from "../../common/config"; import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
import {NTQQMsgApi} from "../../ntqqapi/api/msg"; import {log} from "../../../common/utils/log";
import {sleep} from "../../../common/utils/helper";
import {uri2local} from "../../../common/utils";
function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean { function checkUri(uri: string): boolean {
@@ -67,11 +75,156 @@ export interface ReturnDataType {
message_id: number message_id: number
} }
export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) {
if (typeof message === "string") {
if (!autoEscape) {
message = decodeCQCode(message.toString())
} else {
message = [{
type: OB11MessageDataType.text,
data: {
text: message
}
}]
}
} else if (!Array.isArray(message)) {
message = [message]
}
return message;
}
export async function 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: {
if (!group) {
continue
}
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)
const atMember = await getGroupMember(group?.groupCode, 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) {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(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.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice: {
let file = sendMsg.data?.file
const payloadFileName = sendMsg.data?.name
if (file) {
const cache = await dbUtil.getFileCache(file)
if (cache) {
if (fs.existsSync(cache.filePath)) {
file = "file://" + cache.filePath
} else if (cache.downloadFunc) {
await cache.downloadFunc()
file = cache.filePath;
} else if (cache.url) {
file = cache.url
}
log("找到文件缓存", file);
}
const {path, isLocal, fileName, errMsg} = (await uri2local(file))
if (errMsg) {
throw errMsg
}
if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.file) {
log("发送文件", path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName));
} else if (sendMsg.type === OB11MessageDataType.video) {
log("发送视频", path, payloadFileName || fileName)
let thumb = sendMsg.data?.thumb;
if (thumb) {
let uri2LocalRes = await uri2local(thumb)
if (uri2LocalRes.success) {
thumb = uri2LocalRes.path;
}
}
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb));
} else if (sendMsg.type === OB11MessageDataType.voice) {
sendElements.push(await SendMsgElementConstructor.ptt(path));
} else if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(await SendMsgElementConstructor.pic(path, sendMsg.data.summary || "", <PicSubType>parseInt(sendMsg.data?.subType?.toString()) || 0));
}
}
}
}
break;
case OB11MessageDataType.json: {
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data))
}
break
}
}
return {
sendElements,
deleteAfterSentFiles
}
}
export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
if (!sendElements.length) {
throw ("消息体无法解析")
}
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000);
log("消息发送结果", returnMsg)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))
return returnMsg
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> { export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> { protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = this.convertMessage2List(payload.message); const messages = convertMessage2List(payload.message);
const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node) const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node)
if (fmNum && fmNum != messages.length) { if (fmNum && fmNum != messages.length) {
return { return {
@@ -79,7 +232,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素" message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素"
} }
} }
if (payload.group_id && !(await getGroup(payload.group_id))) { if (payload.message_type !== "private" && payload.group_id &&!(await getGroup(payload.group_id))) {
return { return {
valid: false, valid: false,
message: `${payload.group_id}不存在` message: `${payload.group_id}不存在`
@@ -87,7 +240,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
if (payload.user_id && payload.message_type !== "group") { if (payload.user_id && payload.message_type !== "group") {
if (!(await getFriend(payload.user_id))) { if (!(await getFriend(payload.user_id))) {
if (!ALLOW_SEND_TEMP_MSG) { if (!ALLOW_SEND_TEMP_MSG && !(await dbUtil.getReceivedTempUinMap())[payload.user_id.toString()]) {
return { return {
valid: false, valid: false,
message: `不能发送临时消息` message: `不能发送临时消息`
@@ -141,7 +294,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} else { } else {
throw ("发送消息参数错误, 请指定group_id或user_id") throw ("发送消息参数错误, 请指定group_id或user_id")
} }
const messages = this.convertMessage2List(payload.message); const messages = convertMessage2List(payload.message);
if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) { if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) {
try { try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group) const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
@@ -165,27 +318,13 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
// log("send msg:", peer, sendElements) // log("send msg:", peer, sendElements)
const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group) const {sendElements, deleteAfterSentFiles} = await createSendElements(messages, group)
const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles) const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map(f => fs.unlink(f, () => { deleteAfterSentFiles.map(f => fs.unlink(f, () => {
})); }));
return {message_id: returnMsg.msgShortId} return {message_id: returnMsg.msgShortId}
} }
protected convertMessage2List(message: OB11MessageMixType) {
if (typeof message === "string") {
// message = [{
// type: OB11MessageDataType.text,
// data: {
// text: message
// }
// }] as OB11MessageData[]
message = decodeCQCode(message.toString())
} else if (!Array.isArray(message)) {
message = [message]
}
return message;
}
private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number { private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
if (Array.isArray(payload.message)) { if (Array.isArray(payload.message)) {
@@ -254,7 +393,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const { const {
sendElements, sendElements,
deleteAfterSentFiles deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group); } = await createSendElements(convertMessage2List(messageNode.data.content), group);
log("开始生成转发节点", sendElements); log("开始生成转发节点", sendElements);
let sendElementsSplit: SendMessageElement[][] = [] let sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0; let splitIndex = 0;
@@ -276,7 +415,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
// log("分割后的转发节点", sendElementsSplit) // log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) { for (const eles of sendElementsSplit) {
const nodeMsg = await this.send(selfPeer, eles, [], true); const nodeMsg = await sendMsg(selfPeer, eles, [], true);
nodeMsgIds.push(nodeMsg.msgId) nodeMsgIds.push(nodeMsg.msgId)
await sleep(500); await sleep(500);
log("转发节点生成成功", nodeMsg.msgId); log("转发节点生成成功", nodeMsg.msgId);
@@ -338,121 +477,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
} }
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: {
if (!group) {
continue
}
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)
const atMember = await getGroupMember(group?.groupCode, 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) {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(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.file:
case OB11MessageDataType.video:
case OB11MessageDataType.voice: {
let file = sendMsg.data?.file
const payloadFileName = sendMsg.data?.name
if (file) {
const cache = await dbUtil.getFileCache(file)
if (cache) {
if (fs.existsSync(cache.filePath)) {
file = "file://" + cache.filePath
} else if (cache.downloadFunc) {
await cache.downloadFunc()
file = cache.filePath;
} else if (cache.url) {
file = cache.url
}
log("找到文件缓存", file);
}
const {path, isLocal, fileName, errMsg} = (await uri2local(file))
if (errMsg) {
throw errMsg
}
if (path) {
if (!isLocal) { // 只删除http和base64转过来的文件
deleteAfterSentFiles.push(path)
}
if (sendMsg.type === OB11MessageDataType.file) {
log("发送文件", path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName));
} else if (sendMsg.type === OB11MessageDataType.video) {
log("发送视频", path, payloadFileName || fileName)
sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName));
} else if (sendMsg.type === OB11MessageDataType.voice) {
sendElements.push(await SendMsgElementConstructor.ptt(path));
}else if (sendMsg.type === OB11MessageDataType.image) {
sendElements.push(await SendMsgElementConstructor.pic(path, sendMsg.data.summary || ""));
}
}
}
}
break;
}
}
return {
sendElements,
deleteAfterSentFiles
}
}
private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) {
if (!sendElements.length) {
throw ("消息体无法解析")
}
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000);
log("消息发送结果", returnMsg)
returnMsg.msgShortId = await dbUtil.addMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))
return returnMsg
}
private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement { private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement {
const musicJson = { const musicJson = {

View File

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

View File

@@ -1,4 +1,4 @@
import {ActionName} from "./types"; import {ActionName} from "../types";
import CanSendRecord from "./CanSendRecord"; import CanSendRecord from "./CanSendRecord";
interface ReturnType { interface ReturnType {

View File

@@ -1,5 +1,5 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
interface ReturnType { interface ReturnType {
yes: boolean yes: boolean

View File

@@ -1,14 +1,14 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
import fs from "fs"; import fs from "fs";
import Path from "path"; import Path from "path";
import { import {
ChatType, ChatType,
ChatCacheListItemBasic, ChatCacheListItemBasic,
CacheFileType CacheFileType
} from '../../ntqqapi/types'; } from '../../../ntqqapi/types';
import {dbUtil} from "../../common/db"; import {dbUtil} from "../../../common/db";
import {NTQQFileApi, NTQQFileCacheApi} from "../../ntqqapi/api/file"; import {NTQQFileApi, NTQQFileCacheApi} from "../../../ntqqapi/api/file";
export default class CleanCache extends BaseAction<void, void> { export default class CleanCache extends BaseAction<void, void> {
actionName = ActionName.CleanCache actionName = ActionName.CleanCache

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,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {OB11Status} from "../types"; import {OB11Status} from "../../types";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {selfInfo} from "../../common/data"; import {selfInfo} from "../../../common/data";
export default class GetStatus extends BaseAction<any, OB11Status> { export default class GetStatus extends BaseAction<any, OB11Status> {

View File

@@ -1,7 +1,7 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {OB11Version} from "../types"; import {OB11Version} from "../../types";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {version} from "../../version"; import {version} from "../../../version";
export default class GetVersionInfo extends BaseAction<any, OB11Version> { export default class GetVersionInfo extends BaseAction<any, OB11Version> {
actionName = ActionName.GetVersionInfo actionName = ActionName.GetVersionInfo

View File

@@ -14,11 +14,14 @@ export interface InvalidCheckResult {
} }
export enum ActionName { export enum ActionName {
// llonebot
GetGroupIgnoreAddRequest = "get_group_ignore_add_request", GetGroupIgnoreAddRequest = "get_group_ignore_add_request",
SetQQAvatar = "set_qq_avatar", SetQQAvatar = "set_qq_avatar",
GetConfig = "get_config", GetConfig = "get_config",
SetConfig = "set_config", SetConfig = "set_config",
Debug = "llonebot_debug", Debug = "llonebot_debug",
GetFile = "get_file",
// onebot 11
SendLike = "send_like", SendLike = "send_like",
GetLoginInfo = "get_login_info", GetLoginInfo = "get_login_info",
GetFriendList = "get_friend_list", GetFriendList = "get_friend_list",
@@ -48,10 +51,14 @@ export enum ActionName {
GetRecord = "get_record", GetRecord = "get_record",
CleanCache = "clean_cache", CleanCache = "clean_cache",
// 以下为go-cqhttp api // 以下为go-cqhttp api
GoCQHTTP_SendForwardMsg = "send_forward_msg",
GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg", GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg",
GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg", GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg",
GoCQHTTP_GetStrangerInfo = "get_stranger_info", GoCQHTTP_GetStrangerInfo = "get_stranger_info",
GetGuildList = "get_guild_list", GetGuildList = "get_guild_list",
GoCQHTTP_MarkMsgAsRead = "mark_msg_as_read", GoCQHTTP_MarkMsgAsRead = "mark_msg_as_read",
GoCQHTTP_UploadGroupFile = "upload_group_file", GoCQHTTP_UploadGroupFile = "upload_group_file",
GoCQHTTP_DownloadFile = "download_file",
GoCQHTTP_GetGroupMsgHistory = "get_group_msg_history",
GoCQHTTP_GetForwardMsg = "get_forward_msg",
} }

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,8 +1,8 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {getFriend, getUidByUin, uidMaps} from "../../common/data"; import {getFriend, getUidByUin, uidMaps} from "../../../common/data";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {log} from "../../common/utils"; import {NTQQFriendApi} from "../../../ntqqapi/api/friend";
import {NTQQFriendApi} from "../../ntqqapi/api/friend"; import {log} from "../../../common/utils/log";
interface Payload { interface Payload {
user_id: number, user_id: number,

View File

@@ -1,6 +1,6 @@
import BaseAction from "./BaseAction"; import BaseAction from "../BaseAction";
import {ActionName} from "./types"; import {ActionName} from "../types";
import {NTQQFriendApi} from "../../ntqqapi/api/friend"; import {NTQQFriendApi} from "../../../ntqqapi/api/friend";
interface Payload { interface Payload {
flag: string, flag: string,
@@ -12,7 +12,8 @@ export default class SetFriendAddRequest extends BaseAction<Payload, null> {
actionName = ActionName.SetFriendAddRequest; actionName = ActionName.SetFriendAddRequest;
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
await NTQQFriendApi.handleFriendRequest(parseInt(payload.flag), payload.approve) const approve = payload.approve.toString() === "true";
await NTQQFriendApi.handleFriendRequest(payload.flag, approve)
return null; return null;
} }
} }

View File

@@ -14,14 +14,15 @@ import {
GrayTipElementSubType, GrayTipElementSubType,
Group, Group,
GroupMember, GroupMember,
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT,
RawMessage, RawMessage,
SelfInfo, Sex, SelfInfo,
Sex,
TipGroupElementType, TipGroupElementType,
User User,
VideoElement
} from '../ntqqapi/types'; } from '../ntqqapi/types';
import {getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data'; import {getFriend, getGroupMember, selfInfo, tempGroupCodeMap} from '../common/data';
import {getConfigUtil, log, sleep} from "../common/utils";
import {EventType} from "./event/OB11BaseEvent"; import {EventType} from "./event/OB11BaseEvent";
import {encodeCQCode} from "./cqcode"; import {encodeCQCode} from "./cqcode";
import {dbUtil} from "../common/db"; import {dbUtil} from "../common/db";
@@ -32,12 +33,19 @@ import {OB11GroupNoticeEvent} from "./event/notice/OB11GroupNoticeEvent";
import {NTQQUserApi} from "../ntqqapi/api/user"; import {NTQQUserApi} from "../ntqqapi/api/user";
import {NTQQFileApi} from "../ntqqapi/api/file"; import {NTQQFileApi} from "../ntqqapi/api/file";
import {calcQQLevel} from "../common/utils/qqlevel"; import {calcQQLevel} from "../common/utils/qqlevel";
import {log} from "../common/utils/log";
import {sleep} from "../common/utils/helper";
import {getConfigUtil} from "../common/config";
import {OB11GroupTitleEvent} from "./event/notice/OB11GroupTitleEvent";
import {OB11GroupCardEvent} from "./event/notice/OB11GroupCardEvent";
import {OB11GroupDecreaseEvent} from "./event/notice/OB11GroupDecreaseEvent";
let lastRKeyUpdateTime = 0;
export class OB11Constructor { export class OB11Constructor {
static async message(msg: RawMessage): Promise<OB11Message> { static async message(msg: RawMessage): Promise<OB11Message> {
let config = getConfigUtil().getConfig();
const {enableLocalFile2Url, ob11: {messagePostFormat}} = getConfigUtil().getConfig() const {enableLocalFile2Url, ob11: {messagePostFormat}} = config;
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),
@@ -133,9 +141,32 @@ export class OB11Constructor {
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
const url = element.picElement.originImageUrl const url = element.picElement.originImageUrl
const fileMd5 = element.picElement.md5HexStr const fileMd5 = element.picElement.md5HexStr
const fileUuid = element.picElement.fileUuid
// let currentRKey = config.imageRKey || "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
if (url) { if (url) {
message_data["data"]["url"] = IMAGE_HTTP_HOST + url if (url.startsWith("/download")) {
} else if (fileMd5 && element.picElement.fileUuid.indexOf("_") === -1) { // fileuuid有下划线的是Linux发送的这个url是另外的格式目前尚未得知如何组装 if (url.includes("&rkey=")) {
// 正则提取rkey
// const rkey = url.match(/&rkey=([^&]+)/)[1]
// // log("图片url已有rkey", rkey)
// if (rkey != currentRKey){
// config.imageRKey = rkey
// if (Date.now() - lastRKeyUpdateTime > 1000 * 60) {
// lastRKeyUpdateTime = Date.now()
// getConfigUtil().setConfig(config)
// }
// }
message_data["data"]["url"] = IMAGE_HTTP_HOST + url
}
else{
// 有可能会碰到appid为1406的这个不能使用新的NT域名并且需要把appid改为1407才可访问
message_data["data"]["url"] = `${IMAGE_HTTP_HOST}/download?appid=1407&fileid=${fileUuid}&rkey=${currentRKey}&spec=0`
}
} else {
message_data["data"]["url"] = IMAGE_HTTP_HOST + url
}
} else if (fileMd5) {
message_data["data"]["url"] = `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${fileMd5.toUpperCase()}/0` message_data["data"]["url"] = `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${fileMd5.toUpperCase()}/0`
} }
// message_data["data"]["file_id"] = element.picElement.fileUuid // message_data["data"]["file_id"] = element.picElement.fileUuid
@@ -152,35 +183,25 @@ export class OB11Constructor {
}).then() }).then()
// 不在自动下载图片 // 不在自动下载图片
} else if (element.videoElement) { } else if (element.videoElement || element.fileElement) {
message_data["type"] = OB11MessageDataType.video; const videoOrFileElement = element.videoElement || element.fileElement
message_data["data"]["file"] = element.videoElement.fileName const ob11MessageDataType = element.videoElement ? OB11MessageDataType.video : OB11MessageDataType.file
message_data["data"]["path"] = element.videoElement.filePath message_data["type"] = ob11MessageDataType;
// message_data["data"]["file_id"] = element.videoElement.fileUuid message_data["data"]["file"] = videoOrFileElement.fileName
message_data["data"]["file_size"] = element.videoElement.fileSize message_data["data"]["path"] = videoOrFileElement.filePath
dbUtil.addFileCache(element.videoElement.fileName, { message_data["data"]["file_id"] = videoOrFileElement.fileUuid
fileName: element.videoElement.fileName, message_data["data"]["file_size"] = videoOrFileElement.fileSize
filePath: element.videoElement.filePath, dbUtil.addFileCache(videoOrFileElement.fileUuid, {
fileSize: element.videoElement.fileSize, msgId: msg.msgId,
fileName: videoOrFileElement.fileName,
filePath: videoOrFileElement.filePath,
fileSize: videoOrFileElement.fileSize,
downloadFunc: async () => { downloadFunc: async () => {
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, await NTQQFileApi.downloadMedia(
element.elementId, element.videoElement.thumbPath.get(0), element.videoElement.filePath) msg.msgId, msg.chatType, msg.peerUid,
} element.elementId,
}).then() ob11MessageDataType == OB11MessageDataType.video ? (videoOrFileElement as VideoElement).thumbPath.get(0) : null,
// 怎么拿到url呢 videoOrFileElement.filePath)
} else if (element.fileElement) {
message_data["type"] = OB11MessageDataType.file;
message_data["data"]["file"] = element.fileElement.fileName
// message_data["data"]["path"] = element.fileElement.filePath
// message_data["data"]["file_id"] = element.fileElement.fileUuid
message_data["data"]["file_size"] = element.fileElement.fileSize
dbUtil.addFileCache(element.fileElement.fileName, {
fileName: element.fileElement.fileName,
filePath: element.fileElement.filePath,
fileSize: element.fileElement.fileSize,
downloadFunc: async () => {
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, null, element.fileElement.filePath)
} }
}).then() }).then()
// 怎么拿到url呢 // 怎么拿到url呢
@@ -208,6 +229,15 @@ export class OB11Constructor {
} else if (element.faceElement) { } else if (element.faceElement) {
message_data["type"] = OB11MessageDataType.face; message_data["type"] = OB11MessageDataType.face;
message_data["data"]["id"] = element.faceElement.faceIndex.toString(); message_data["data"]["id"] = element.faceElement.faceIndex.toString();
} else if (element.marketFaceElement) {
message_data["type"] = OB11MessageDataType.mface;
message_data["data"]["text"] = element.marketFaceElement.faceName;
} else if (element.markdownElement){
message_data["type"] = OB11MessageDataType.markdown;
message_data["data"]["data"] = element.markdownElement.content;
} else if (element.multiForwardMsgElement){
message_data["type"] = OB11MessageDataType.forward;
message_data["data"]["id"] = msg.msgId
} }
if (message_data.type !== "unknown" && message_data.data) { if (message_data.type !== "unknown" && message_data.data) {
const cqCode = encodeCQCode(message_data); const cqCode = encodeCQCode(message_data);
@@ -226,6 +256,14 @@ export class OB11Constructor {
if (msg.chatType !== ChatType.group) { if (msg.chatType !== ChatType.group) {
return; return;
} }
if (msg.senderUin){
let member = await getGroupMember(msg.peerUid, msg.senderUin);
if (member && member.cardName !== msg.sendMemberName) {
const event = new OB11GroupCardEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), msg.sendMemberName, member.cardName)
member.cardName = msg.sendMemberName;
return event
}
}
// log("group msg", msg); // log("group msg", msg);
for (let element of msg.elements) { for (let element of msg.elements) {
const grayTipElement = element.grayTipElement const grayTipElement = element.grayTipElement
@@ -249,18 +287,16 @@ export class OB11Constructor {
// log("构造群增加事件", event) // log("构造群增加事件", event)
return event; return event;
} }
} } else if (groupElement.type === TipGroupElementType.ban) {
else if (groupElement.type === TipGroupElementType.ban) {
log("收到群群员禁言提示", groupElement) log("收到群群员禁言提示", groupElement)
const memberUid = groupElement.shutUp.member.uid const memberUid = groupElement.shutUp.member.uid
const adminUid = groupElement.shutUp.admin.uid const adminUid = groupElement.shutUp.admin.uid
let memberUin: string = "" let memberUin: string = ""
let duration = parseInt(groupElement.shutUp.duration) let duration = parseInt(groupElement.shutUp.duration)
let sub_type: "ban" | "lift_ban" = duration > 0 ? "ban" : "lift_ban" let sub_type: "ban" | "lift_ban" = duration > 0 ? "ban" : "lift_ban"
if (memberUid){ if (memberUid) {
memberUin = (await getGroupMember(msg.peerUid, memberUid))?.uin || (await NTQQUserApi.getUserDetailInfo(memberUid))?.uin memberUin = (await getGroupMember(msg.peerUid, memberUid))?.uin || (await NTQQUserApi.getUserDetailInfo(memberUid))?.uin
} } else {
else {
memberUin = "0"; // 0表示全员禁言 memberUin = "0"; // 0表示全员禁言
if (duration > 0) { if (duration > 0) {
duration = -1 duration = -1
@@ -271,16 +307,27 @@ export class OB11Constructor {
return new OB11GroupBanEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, sub_type); return new OB11GroupBanEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, sub_type);
} }
} }
} else if (groupElement.type == TipGroupElementType.kicked){
else if (element.fileElement){ log("收到我被踢出提示", groupElement)
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {id: element.fileElement.fileUuid, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize)}) const adminUin = (await getGroupMember(msg.peerUid, groupElement.adminUid))?.uin || (await NTQQUserApi.getUserDetailInfo(groupElement.adminUid))?.uin
if (adminUin) {
return new OB11GroupDecreaseEvent(parseInt(msg.peerUid), parseInt(selfInfo.uin), parseInt(adminUin), "kick_me");
}
}
} else if (element.fileElement) {
return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {
id: element.fileElement.fileUuid,
name: element.fileElement.fileName,
size: parseInt(element.fileElement.fileSize),
busid: element.fileElement.fileBizId || 0
})
} }
if (grayTipElement) { if (grayTipElement) {
if (grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER){ if (grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER) {
log("收到新人被邀请进群消息", grayTipElement) log("收到新人被邀请进群消息", grayTipElement)
const xmlElement = grayTipElement.xmlElement const xmlElement = grayTipElement.xmlElement
if (xmlElement?.content){ if (xmlElement?.content) {
const regex = /jp="(\d+)"/g; const regex = /jp="(\d+)"/g;
let matches = []; let matches = [];
@@ -289,11 +336,45 @@ export class OB11Constructor {
while ((match = regex.exec(xmlElement.content)) !== null) { while ((match = regex.exec(xmlElement.content)) !== null) {
matches.push(match[1]); matches.push(match[1]);
} }
if (matches.length === 2){ // log("新人进群匹配到的QQ号", matches)
if (matches.length === 2) {
const [inviter, invitee] = matches; const [inviter, invitee] = matches;
return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), "invite"); return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), "invite");
} }
} }
} else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr)
/*
{
align: 'center',
items: [
{ txt: '恭喜', type: 'nor' },
{
col: '3',
jp: '5',
param: ["QQ号"],
txt: '林雨辰',
type: 'url'
},
{ txt: '获得群主授予的', type: 'nor' },
{
col: '3',
jp: '',
txt: '好好好',
type: 'url'
},
{ txt: '头衔', type: 'nor' }
]
}
* */
const memberUin = json.items[1].param[0]
const title = json.items[3].txt
log("收到群成员新头衔消息", json)
getGroupMember(msg.peerUid, memberUin).then(member => {
member.memberSpecialTitle = title
})
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title)
} }
} }
} }
@@ -303,15 +384,16 @@ export class OB11Constructor {
return { return {
user_id: parseInt(friend.uin), user_id: parseInt(friend.uin),
nickname: friend.nick, nickname: friend.nick,
remark: friend.remark remark: friend.remark,
sex: OB11Constructor.sex(friend.sex),
level: friend.qqLevel && calcQQLevel(friend.qqLevel) || 0
} }
} }
static selfInfo(selfInfo: SelfInfo): OB11User { static selfInfo(selfInfo: SelfInfo): OB11User {
return { return {
user_id: parseInt(selfInfo.uin), user_id: parseInt(selfInfo.uin),
nickname: selfInfo.nick nickname: selfInfo.nick,
} }
} }
@@ -327,7 +409,7 @@ export class OB11Constructor {
}[role] }[role]
} }
static sex(sex: Sex): OB11UserSex{ static sex(sex: Sex): OB11UserSex {
const sexMap = { const sexMap = {
[Sex.male]: OB11UserSex.male, [Sex.male]: OB11UserSex.male,
[Sex.female]: OB11UserSex.female, [Sex.female]: OB11UserSex.female,
@@ -335,6 +417,7 @@ export class OB11Constructor {
} }
return sexMap[sex] || OB11UserSex.unknown return sexMap[sex] || OB11UserSex.unknown
} }
static groupMember(group_id: string, member: GroupMember): OB11GroupMember { static groupMember(group_id: string, member: GroupMember): OB11GroupMember {
return { return {
group_id: parseInt(group_id), group_id: parseInt(group_id),
@@ -344,7 +427,8 @@ export class OB11Constructor {
sex: OB11Constructor.sex(member.sex), sex: OB11Constructor.sex(member.sex),
age: 0, age: 0,
area: "", area: "",
level: member.qqLevel && calcQQLevel(member.qqLevel) || 0, level: 0,
qq_level: member.qqLevel && calcQQLevel(member.qqLevel) || 0,
join_time: 0, // 暂时没法获取 join_time: 0, // 暂时没法获取
last_sent_time: 0, // 暂时没法获取 last_sent_time: 0, // 暂时没法获取
title_expire_time: 0, title_expire_time: 0,
@@ -353,6 +437,20 @@ export class OB11Constructor {
is_robot: member.isRobot, is_robot: member.isRobot,
shut_up_timestamp: member.shutUpTime, shut_up_timestamp: member.shutUpTime,
role: OB11Constructor.groupMemberRole(member.role), role: OB11Constructor.groupMemberRole(member.role),
title: member.memberSpecialTitle || "",
}
}
static stranger(user: User): OB11User {
return {
...user,
user_id: parseInt(user.uin),
nickname: user.nick,
sex: OB11Constructor.sex(user.sex),
age: 0,
qid: user.qid,
login_days: 0,
level: user.qqLevel && calcQQLevel(user.qqLevel) || 0,
} }
} }

View File

@@ -0,0 +1,16 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupCardEvent extends OB11GroupNoticeEvent {
notice_type = "group_card";
card_new: string;
card_old: string;
constructor(groupId: number, userId: number, cardNew: string, cardOld: string) {
super();
this.group_id = groupId;
this.user_id = userId;
this.card_new = cardNew;
this.card_old = cardOld;
}
}

View File

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

View File

@@ -0,0 +1,15 @@
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupTitleEvent extends OB11GroupNoticeEvent {
notice_type = "notify";
sub_type = "title";
title: string
constructor(groupId: number, userId: number, title: string) {
super();
this.group_id = groupId;
this.user_id = userId;
this.title = title;
}
}

View File

@@ -3,7 +3,8 @@ import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export interface GroupUploadFile{ export interface GroupUploadFile{
id: string, id: string,
name: string, name: string,
size: number size: number,
busid: number,
} }
export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent { export class OB11GroupUploadNoticeEvent extends OB11GroupNoticeEvent {

View File

@@ -7,7 +7,6 @@ class OB11PokeEvent extends OB11BaseNoticeEvent{
sub_type = "poke" sub_type = "poke"
target_id = parseInt(selfInfo.uin) target_id = parseInt(selfInfo.uin)
user_id: number user_id: number
} }
export class OB11FriendPokeEvent extends OB11PokeEvent{ export class OB11FriendPokeEvent extends OB11PokeEvent{

View File

@@ -1,8 +1,8 @@
import {Response} from "express"; import {Response} from "express";
import {getConfigUtil} from "../../common/utils"; import {OB11Response} from "../action/OB11Response";
import {OB11Response} from "../action/utils";
import {HttpServerBase} from "../../common/server/http"; import {HttpServerBase} from "../../common/server/http";
import {actionHandlers} from "../action"; import {actionHandlers} from "../action";
import {getConfigUtil} from "../../common/config";
class OB11HTTPServer extends HttpServerBase { class OB11HTTPServer extends HttpServerBase {
name = "OneBot V11 server" name = "OneBot V11 server"

View File

@@ -1,13 +1,53 @@
import {getConfigUtil, log} from "../../common/utils"; import {OB11Message, OB11MessageAt, OB11MessageData} from "../types";
import {OB11Message} from "../types"; import {getGroup, selfInfo} from "../../common/data";
import {selfInfo} from "../../common/data";
import {OB11BaseMetaEvent} from "../event/meta/OB11BaseMetaEvent"; import {OB11BaseMetaEvent} from "../event/meta/OB11BaseMetaEvent";
import {OB11BaseNoticeEvent} from "../event/notice/OB11BaseNoticeEvent"; import {OB11BaseNoticeEvent} from "../event/notice/OB11BaseNoticeEvent";
import {WebSocket as WebSocketClass} from "ws"; import {WebSocket as WebSocketClass} from "ws";
import {wsReply} from "./ws/reply"; import {wsReply} from "./ws/reply";
import {log} from "../../common/utils/log";
import {getConfigUtil} from "../../common/config";
import crypto from 'crypto';
import {NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, Peer} from "../../ntqqapi/api";
import {ChatType, Group, GroupRequestOperateTypes} from "../../ntqqapi/types";
import {convertMessage2List, createSendElements, sendMsg} from "../action/msg/SendMsg";
import {dbUtil} from "../../common/db";
import {OB11FriendRequestEvent} from "../event/request/OB11FriendRequest";
import {OB11GroupRequestEvent} from "../event/request/OB11GroupRequest";
import {isNull} from "../../common/utils";
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
interface QuickActionPrivateMessage {
reply?: string;
auto_escape?: boolean;
}
interface QuickActionGroupMessage extends QuickActionPrivateMessage {
// 回复群消息
at_sender?: boolean
delete?: boolean
kick?: boolean
ban?: boolean
ban_duration?: number
//
}
interface QuickActionFriendRequest {
approve?: boolean
remark?: string
}
interface QuickActionGroupRequest {
approve?: boolean
reason?: string
}
type QuickAction =
QuickActionPrivateMessage
& QuickActionGroupMessage
& QuickActionFriendRequest
& QuickActionGroupRequest
const eventWSList: WebSocketClass[] = []; const eventWSList: WebSocketClass[] = [];
export function registerWsEventSender(ws: WebSocketClass) { export function registerWsEventSender(ws: WebSocketClass) {
@@ -23,6 +63,7 @@ export function unregisterWsEventSender(ws: WebSocketClass) {
export function postWsEvent(event: PostEventType) { export function postWsEvent(event: PostEventType) {
for (const ws of eventWSList) { for (const ws of eventWSList) {
log(ws)
new Promise(() => { new Promise(() => {
wsReply(ws, event); wsReply(ws, event);
}).then() }).then()
@@ -33,23 +74,100 @@ export function postOB11Event(msg: PostEventType, reportSelf = false) {
const config = getConfigUtil().getConfig(); const config = getConfigUtil().getConfig();
// 判断msg是否是event // 判断msg是否是event
if (!config.reportSelfMessage && !reportSelf) { if (!config.reportSelfMessage && !reportSelf) {
if ((msg as OB11Message).user_id.toString() == selfInfo.uin) { if (msg.post_type === "message" && (msg as OB11Message).user_id.toString() == selfInfo.uin) {
return return
} }
} }
if (config.ob11.enableHttpPost) { if (config.ob11.enableHttpPost) {
const msgStr = JSON.stringify(msg);
const hmac = crypto.createHmac('sha1', config.ob11.httpSecret);
hmac.update(msgStr);
const sig = hmac.digest('hex');
let headers = {
"Content-Type": "application/json",
"x-self-id": selfInfo.uin
}
if (config.ob11.httpSecret) {
headers["x-signature"] = "sha1=" + sig;
}
for (const host of config.ob11.httpHosts) { for (const host of config.ob11.httpHosts) {
fetch(host, { fetch(host, {
method: "POST", method: "POST",
headers: { headers,
"Content-Type": "application/json", body: msgStr
"x-self-id": selfInfo.uin }).then(async (res) => {
}, log(`新消息事件HTTP上报成功: ${host} `, msgStr);
body: JSON.stringify(msg) // todo: 处理不够优雅应该使用高级泛型进行QuickAction类型识别
}).then((res: any) => { let resJson: QuickAction;
log(`新消息事件HTTP上报成功: ${host} ` + JSON.stringify(msg)); try {
resJson = await res.json();
log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson))
} catch (e) {
log(`新消息事件HTTP上报没有返回快速操作不需要处理`)
return
}
if (msg.post_type === "message") {
msg = msg as OB11Message;
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id)
resJson = resJson as QuickActionPrivateMessage | QuickActionGroupMessage
const reply = resJson.reply
let peer: Peer = {
chatType: ChatType.friend,
peerUid: msg.user_id.toString()
}
if (msg.message_type == "private") {
if (msg.sub_type === "group") {
peer.chatType = ChatType.temp
}
} else {
peer.chatType = ChatType.group
peer.peerUid = msg.group_id.toString()
}
if (reply) {
let group: Group = null
let replyMessage: OB11MessageData[] = []
if (msg.message_type == "group") {
group = await getGroup(msg.group_id.toString())
if ((resJson as QuickActionGroupMessage).at_sender) {
replyMessage.push({
type: "at",
data: {
qq: msg.user_id.toString()
}
} as OB11MessageAt)
}
}
replyMessage = replyMessage.concat(convertMessage2List(reply, resJson.auto_escape))
const {sendElements, deleteAfterSentFiles} = await createSendElements(replyMessage, group)
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then()
} else if (resJson.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage.msgId]).then()
} else if (resJson.kick) {
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage.senderUid]).then()
} else if (resJson.ban) {
NTQQGroupApi.banMember(peer.peerUid, [{
uid: rawMessage.senderUid,
timeStamp: resJson.ban_duration || 60 * 30
}],).then()
}
} else if (msg.post_type === "request") {
if ((msg as OB11FriendRequestEvent).request_type === "friend") {
resJson = resJson as QuickActionFriendRequest
if (!isNull(resJson.approve)) {
// todo: set remark
NTQQFriendApi.handleFriendRequest(((msg as OB11FriendRequestEvent).flag), resJson.approve).then()
}
} else if ((msg as OB11GroupRequestEvent).request_type === "group") {
resJson = resJson as QuickActionGroupRequest
if (!isNull(resJson.approve)) {
NTQQGroupApi.handleGroupRequest((msg as OB11FriendRequestEvent).flag, resJson.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, resJson.reason).then()
}
}
}
}, (err: any) => { }, (err: any) => {
log(`新消息事件HTTP上报失败: ${host} ` + err + JSON.stringify(msg)); log(`新消息事件HTTP上报失败: ${host} `, err, msg);
}); });
} }
} }

View File

@@ -1,16 +1,15 @@
import {getConfigUtil, log} from "../../../common/utils";
import {selfInfo} from "../../../common/data"; import {selfInfo} from "../../../common/data";
import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent"; import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent";
import {ActionName} from "../../action/types"; import {ActionName} from "../../action/types";
import {OB11Response} from "../../action/utils"; import {OB11Response} from "../../action/OB11Response";
import BaseAction from "../../action/BaseAction"; import BaseAction from "../../action/BaseAction";
import {actionMap} from "../../action"; import {actionMap} from "../../action";
import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postOB11Event"; import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postOB11Event";
import {wsReply} from "./reply"; import {wsReply} from "./reply";
import {WebSocket as WebSocketClass} from "ws"; import {WebSocket as WebSocketClass} from "ws";
import {OB11HeartbeatEvent} from "../../event/meta/OB11HeartbeatEvent"; import {OB11HeartbeatEvent} from "../../event/meta/OB11HeartbeatEvent";
import {log} from "../../../common/utils/log";
import {getConfigUtil} from "../../../common/config";
export let rwsList: ReverseWebsocket[] = []; export let rwsList: ReverseWebsocket[] = [];
@@ -79,6 +78,7 @@ export class ReverseWebsocket {
private connect() { private connect() {
const {token, heartInterval} = getConfigUtil().getConfig() const {token, heartInterval} = getConfigUtil().getConfig()
this.websocket = new WebSocketClass(this.url, { this.websocket = new WebSocketClass(this.url, {
maxPayload: 1024 * 1024 * 1024,
handshakeTimeout: 2000, handshakeTimeout: 2000,
perMessageDeflate: false, perMessageDeflate: false,
headers: { headers: {
@@ -129,8 +129,13 @@ class OB11ReverseWebsockets {
stop() { stop() {
for (let rws of rwsList) { for (let rws of rwsList) {
rws.stop(); try {
rws.stop();
}catch (e) {
log("反向ws关闭:", e.stack)
}
} }
rwsList.length = 0;
} }
restart() { restart() {

View File

@@ -1,7 +1,6 @@
import {WebSocket} from "ws"; import {WebSocket} from "ws";
import {getConfigUtil, log} from "../../../common/utils";
import {actionMap} from "../../action"; import {actionMap} from "../../action";
import {OB11Response} from "../../action/utils"; import {OB11Response} from "../../action/OB11Response";
import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postOB11Event"; import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postOB11Event";
import {ActionName} from "../../action/types"; import {ActionName} from "../../action/types";
import BaseAction from "../../action/BaseAction"; import BaseAction from "../../action/BaseAction";
@@ -11,6 +10,8 @@ import {WebsocketServerBase} from "../../../common/server/websocket";
import {IncomingMessage} from "node:http"; import {IncomingMessage} from "node:http";
import {wsReply} from "./reply"; import {wsReply} from "./reply";
import {selfInfo} from "../../../common/data"; import {selfInfo} from "../../../common/data";
import {log} from "../../../common/utils/log";
import {getConfigUtil} from "../../../common/config";
let heartbeatRunning = false; let heartbeatRunning = false;

View File

@@ -1,7 +1,8 @@
import {WebSocket as WebSocketClass} from "ws"; import {WebSocket as WebSocketClass} from "ws";
import {OB11Response} from "../../action/utils"; import {OB11Response} from "../../action/OB11Response";
import {PostEventType} from "../postOB11Event"; import {PostEventType} from "../postOB11Event";
import {isNull, log} from "../../../common/utils"; import {log} from "../../../common/utils/log";
import {isNull} from "../../../common/utils/helper";
export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) { export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) {
try { try {

View File

@@ -1,10 +1,15 @@
import {RawMessage} from "../ntqqapi/types"; import {PicSubType, RawMessage} from "../ntqqapi/types";
import {EventType} from "./event/OB11BaseEvent"; import {EventType} from "./event/OB11BaseEvent";
export interface OB11User { export interface OB11User {
user_id: number; user_id: number;
nickname: string; nickname: string;
remark?: string remark?: string;
sex?: OB11UserSex;
level?: number;
age?: number;
qid?: string;
login_days?: number;
} }
export enum OB11UserSex { export enum OB11UserSex {
@@ -29,6 +34,7 @@ export interface OB11GroupMember {
join_time?: number join_time?: number
last_sent_time?: number last_sent_time?: number
level?: number level?: number
qq_level?: number
role?: OB11GroupMemberRole role?: OB11GroupMemberRole
title?: string title?: string
area?: string area?: string
@@ -64,6 +70,7 @@ export enum OB11MessageType {
} }
export interface OB11Message { export interface OB11Message {
target_id?: number; // 自己发送的消息才有此字段
self_id?: number, self_id?: number,
time: number, time: number,
message_id: number, message_id: number,
@@ -81,6 +88,10 @@ export interface OB11Message {
raw?: RawMessage raw?: RawMessage
} }
export interface OB11ForwardMessage extends OB11Message {
content: OB11MessageData[] | string;
}
export interface OB11Return<DataType> { export interface OB11Return<DataType> {
status: string status: string
retcode: number retcode: number
@@ -101,9 +112,19 @@ export enum OB11MessageDataType {
reply = "reply", reply = "reply",
json = "json", json = "json",
face = "face", face = "face",
node = "node", // 合并转发消息 mface = "mface", // 商城表情
markdown = "markdown",
node = "node", // 合并转发消息节点
forward = "forward", // 合并转发消息,用于上报
xml = "xml"
} }
export interface OB11MessageMFace{
type: OB11MessageDataType.mface,
data: {
text: string
}
}
export interface OB11MessageText { export interface OB11MessageText {
type: OB11MessageDataType.text, type: OB11MessageDataType.text,
data: { data: {
@@ -113,17 +134,20 @@ export interface OB11MessageText {
interface OB11MessageFileBase { interface OB11MessageFileBase {
data: { data: {
thumb?: string;
name?: string; name?: string;
file: string, file: string,
url?: string; url?: string;
} }
} }
export interface OB11MessageImage extends OB11MessageFileBase { export interface OB11MessageImage extends OB11MessageFileBase {
type: OB11MessageDataType.image type: OB11MessageDataType.image
data: OB11MessageFileBase['data'] & { data: OB11MessageFileBase['data'] & {
summary ? : string; // 图片摘要 summary ? : string; // 图片摘要
} subType?: PicSubType
},
} }
export interface OB11MessageRecord extends OB11MessageFileBase { export interface OB11MessageRecord extends OB11MessageFileBase {
@@ -183,12 +207,17 @@ export interface OB11MessageCustomMusic{
} }
} }
export interface OB11MessageJson {
type: OB11MessageDataType.json
data: {config: {token: string}} & any
}
export type OB11MessageData = export type OB11MessageData =
OB11MessageText | OB11MessageText |
OB11MessageFace | OB11MessageFace | OB11MessageMFace |
OB11MessageAt | OB11MessageReply | OB11MessageAt | OB11MessageReply |
OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo | OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo |
OB11MessageNode | OB11MessageCustomMusic OB11MessageNode | OB11MessageCustomMusic | OB11MessageJson
export interface OB11PostSendMsg { export interface OB11PostSendMsg {
message_type?: "private" | "group" message_type?: "private" | "group"

View File

@@ -1,128 +0,0 @@
import {DATA_DIR, isGIF, log} from "../common/utils";
import {v4 as uuidv4} from "uuid";
import * as path from 'node:path';
import * as fileType from 'file-type';
import {dbUtil} from "../common/db";
const fs = require("fs").promises;
type Uri2LocalRes = {
success: boolean,
errMsg: string,
fileName: string,
ext: string,
path: string,
isLocal: boolean
}
export async function uri2local(uri: string, fileName: string = null) : Promise<Uri2LocalRes>{
let res = {
success: false,
errMsg: "",
fileName: "",
ext: "",
path: "",
isLocal: false
}
if (!fileName) {
fileName = uuidv4();
}
let filePath = path.join(DATA_DIR, fileName)
let url = null;
try{
url = new URL(uri);
}catch (e) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
}
// log("uri protocol", url.protocol, uri);
if (url.protocol == "base64:") {
// base64转成文件
let base64Data = uri.split("base64://")[1]
try {
const buffer = Buffer.from(base64Data, 'base64');
await fs.writeFile(filePath, buffer);
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == "http:" || url.protocol == "https:") {
// 下载文件
let fetchRes: Response;
try{
fetchRes = await fetch(url)
}catch (e) {
res.errMsg = `${url}下载失败`
return res
}
if (!fetchRes.ok) {
res.errMsg = `${url}下载失败,` + fetchRes.statusText
return res
}
let blob = await fetchRes.blob();
let buffer = await blob.arrayBuffer();
try {
const pathInfo = path.parse(decodeURIComponent(url.pathname))
if (pathInfo.name){
fileName = pathInfo.name
if (pathInfo.ext){
fileName += pathInfo.ext
// res.ext = pathInfo.ext
}
}
res.fileName = fileName
filePath = path.join(DATA_DIR, uuidv4() + fileName)
await fs.writeFile(filePath, Buffer.from(buffer));
} catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString()
return res
}
} else {
let pathname: string;
if (url.protocol === "file:") {
// await fs.copyFile(url.pathname, filePath);
pathname = decodeURIComponent(url.pathname)
if (process.platform === "win32") {
filePath = pathname.slice(1)
} else {
filePath = pathname
}
} else {
const cache = await dbUtil.getFileCache(uri);
if (cache) {
filePath = cache.filePath
} else {
filePath = uri;
}
}
res.isLocal = true
}
// else{
// res.errMsg = `不支持的file协议,` + url.protocol
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
try {
let ext: string = (await fileType.fileTypeFromFile(filePath)).ext
if (ext) {
log("获取文件类型", ext, filePath)
await fs.rename(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
}
}
res.success = true
res.path = filePath
return res
}

Some files were not shown because too many files have changed in this diff Show More