Compare commits

...

216 Commits

Author SHA1 Message Date
linyuchen
2a1aa8c649 feat: image subType 2024-07-13 14:26:23 +08:00
linyuchen
1633734e08 Merge branch 'dev' 2024-07-13 14:09:45 +08:00
linyuchen
dff92e6f27 chore: version 3.27.0
feat: support poke
feat: LLOneBot global switch
2024-07-13 14:09:03 +08:00
linyuchen
dba5e30d5d doc: plugin description 2024-07-10 13:48:05 +08:00
linyuchen
2d04ab2e72 fix: crychic crash 2024-07-10 13:47:44 +08:00
linyuchen
1a015ac8d3 Merge pull request #262 from LLOneBot/dev
get_record 支持 out_format 进行转码,和其他小修复
2024-06-21 17:39:53 +08:00
linyuchen
6390620ddd chore: version 3.26.7 2024-06-21 17:33:48 +08:00
linyuchen
0d19005dc3 refactor: remove duplicate import 2024-06-21 17:28:17 +08:00
linyuchen
c6479dd2c4 Merge remote-tracking branch 'origin/dev' into dev 2024-06-21 16:21:15 +08:00
linyuchen
8871331b7c 🐛 fix: ws echo #261 2024-06-21 16:20:59 +08:00
linyuchen
e01148b86a 🐛 fix: ws echo 2024-06-21 16:20:26 +08:00
linyuchen
2f87e3818e Merge pull request #260 from idranme/main
perf: audio
2024-06-21 10:36:29 +08:00
linyuchen
2c8a594c38 Merge branch 'dev' into main 2024-06-21 10:36:14 +08:00
idranme
1508dab7fe perf: audio 2024-06-18 19:15:56 +00:00
linyuchen
958b21e47e fix: wait get_file download complete 2024-06-17 17:41:23 +08:00
linyuchen
781c3311ae fix: get_file cache not found 2024-06-17 16:20:37 +08:00
linyuchen
52850d172e feat: decode silk 2024-06-17 16:05:38 +08:00
linyuchen
52a065542e chore: v3.26.6 2024-06-10 14:38:20 +08:00
linyuchen
fd10469685 feat: video url 2024-06-10 14:35:00 +08:00
linyuchen
a2ee75b113 refactor: sent msg status waiter 2024-06-09 15:27:33 +08:00
linyuchen
0f7f243b98 Merge pull request #250 from Bluefissure/reverse-ws-ua
feat: add ua to reverse websocket headers
2024-06-06 17:35:21 +08:00
Bluefissure
97d7996a50 fix: add version to ua 2024-06-06 08:53:37 +00:00
Bluefissure
b658d164f9 feat: add ua to reverse websocket headers 2024-06-06 08:48:18 +00:00
linyuchen
f150ae478b chore: v3.26.5 2024-06-01 20:19:05 +08:00
linyuchen
d1f68553f1 fix: 加载卡顿,群成员名片变动 2024-06-01 20:18:38 +08:00
linyuchen
f47f0800de Merge remote-tracking branch 'origin/main' 2024-05-29 16:56:08 +08:00
linyuchen
b7ddefc950 fix: QZone cookies 2024-05-29 16:38:22 +08:00
linyuchen
25b3325a44 fix: comment 2024-05-29 16:28:46 +08:00
linyuchen
c281b87bab merge main 2024-05-29 16:27:06 +08:00
linyuchen
c0946ddda2 chore: version 3.26.4 2024-05-29 16:26:04 +08:00
linyuchen
1128cf679c refactor: send file timeout 2024-05-29 16:25:42 +08:00
linyuchen
ff65a42350 Merge pull request #242 from LLOneBot/dev
feat: support qzone cookies
2024-05-29 16:24:32 +08:00
手瓜一十雪
c459587dcd refactor: get cookies 2024-05-29 12:03:35 +08:00
手瓜一十雪
6f8ea9677f feat: support qzone cookies 2024-05-28 17:14:24 +08:00
手瓜一十雪
38197527fa Merge branch 'main' into dev 2024-05-28 17:11:13 +08:00
手瓜一十雪
21b2bd2c8e feat: cookies 2024-05-28 17:11:07 +08:00
linyuchen
25158eee55 chore: version 3.26.3 2024-05-28 16:41:28 +08:00
linyuchen
1aa804f255 chore: version 3.26.3 2024-05-28 16:41:22 +08:00
linyuchen
fbe101339d fix: #237 2024-05-28 16:40:51 +08:00
linyuchen
a4aeb8171d fix: QQ package.json on macOS 2024-05-28 15:42:22 +08:00
linyuchen
27f98a459c fix: member info change on version 24108 2024-05-28 15:31:59 +08:00
linyuchen
e6b0eaa46d Merge pull request #235 from LLOneBot/dev
快速操作回复自动引用原消息开关
2024-05-24 17:14:54 +08:00
linyuchen
f336317a33 chore: version 3.26.2 2024-05-24 17:12:35 +08:00
linyuchen
17b44cc0fa refactor: #226 Quick operation reply automatically quotes the original message switch 2024-05-24 17:10:41 +08:00
linyuchen
debe3a8597 chore: version 3.26.1 2024-05-24 08:54:23 +08:00
linyuchen
f36c5e849f Merge pull request #234 from LLOneBot/dev
fix: #215 get_forward_msg params missing id(onebot11)
2024-05-24 08:52:34 +08:00
linyuchen
abbd6797c4 fix: #215 get_forward_msg params missing id(onebot11) 2024-05-24 08:50:22 +08:00
linyuchen
fdb7784a7d Merge pull request #233 from LLOneBot/dev
[Feature] OneBot11消息构造添加raw字段,单条转发消息接口返回message_id
2024-05-24 08:40:44 +08:00
linyuchen
92b49015b0 feat: Forward single msg return message_id 2024-05-24 08:36:42 +08:00
linyuchen
1765ffff7b style: format 2024-05-24 08:15:08 +08:00
linyuchen
3024316b5b feat: #232 /get_msg, /get_group_msg_history add raw message 2024-05-24 08:11:38 +08:00
linyuchen
9a0d89bfbf Update README.md 2024-05-19 07:52:12 +08:00
linyuchen
807ef3b700 Merge pull request #228 from LLOneBot/dev
feat: Quick operation reply auto quote original message
2024-05-18 16:53:37 +08:00
linyuchen
948f10d4e3 feat: Quick operation reply auto quote original message 2024-05-18 16:51:34 +08:00
linyuchen
0f99b5cb87 Merge pull request #227 from LLOneBot/dev
fix: Send msg timeout minimum
2024-05-18 16:36:30 +08:00
linyuchen
6413b0ff82 fix: Send msg timeout minimum 2024-05-18 16:34:12 +08:00
linyuchen
39713d8e11 Merge branch 'main' into dev 2024-05-18 16:31:22 +08:00
linyuchen
739a497af6 chore: v3.26.0 2024-05-18 13:16:45 +08:00
linyuchen
de2fe9b0aa Merge pull request #225 from LLOneBot/dev
Feature: #209,New API get_friends_with_category
2024-05-18 13:11:30 +08:00
linyuchen
44448895a0 feat: 209 2024-05-18 13:09:45 +08:00
linyuchen
cfd9097769 feat: 209 2024-05-18 13:08:44 +08:00
linyuchen
627042fd25 Merge pull request #224 from LLOneBot/dev
Fix: #219,发送视频图片进行文件大小判断,超时时间根据文件大小(512kb/s)动态调整
2024-05-18 12:53:42 +08:00
linyuchen
b51ce24d0c fix: #219 2024-05-18 12:50:11 +08:00
linyuchen
fc0881eccc Merge pull request #223 from LLOneBot/dev
fix: #218
2024-05-18 12:13:23 +08:00
linyuchen
6b8509d2b2 fix: #218 2024-05-18 12:12:16 +08:00
linyuchen
cf1d67a5cf Merge pull request #222 from LLOneBot/dev
Feature: websocket .handle_quick_operation
2024-05-18 11:47:56 +08:00
linyuchen
473ebd25b8 fix: promise catch 2024-05-18 11:46:51 +08:00
linyuchen
d4427cfff4 feat: .handle_quick_operation of websocket 2024-05-18 11:45:42 +08:00
linyuchen
9d2e9786cc chore: v3.25.0 2024-05-15 23:03:19 +08:00
linyuchen
9968f714c7 chore: v3.25.0 2024-05-15 23:03:04 +08:00
linyuchen
bd212c4bf3 remove debug 2024-05-15 22:45:13 +08:00
linyuchen
32c7f904db fix: Http download headers 2024-05-15 22:44:15 +08:00
linyuchen
2ef017282f feat: get_group_honor_info 2024-05-15 22:33:55 +08:00
手瓜一十雪
9672f67a23 feat: new Api GetGroupEssence&GetGroupHonorInfo 2024-05-15 21:12:10 +08:00
手瓜一十雪
6e5cfd827c feat: webapi 2024-05-15 21:05:33 +08:00
手瓜一十雪
5402bef4a9 Merge branch 'main' into dev 2024-05-15 20:56:52 +08:00
linyuchen
4194512cce fix: Get cookies miss uin 2024-05-15 19:47:11 +08:00
linyuchen
b3aad8b0d9 fix: Check pic fil name ext 2024-05-15 19:19:10 +08:00
linyuchen
1489c6df25 feat: New face 2024-05-15 18:47:38 +08:00
linyuchen
2e225045e6 feat: Get cookies support domain 2024-05-15 17:57:15 +08:00
手瓜一十雪
11ed06148c fix: checkVersion Mirror 2024-05-14 14:43:52 +08:00
linyuchen
a3fc018186 fix: Compatible with win7 2024-05-12 20:36:27 +08:00
linyuchen
9692bf6ec6 refactor: Rename native node module dirname 2024-05-11 14:56:01 +08:00
linyuchen
9b3916307a fix: All images are the first image in single msg
fix: remote rkey
2024-05-11 14:52:59 +08:00
linyuchen
fdf96b479c Merge branch 'main' into dev
# Conflicts:
#	src/ntqqapi/external/cpmodule.ts
#	src/ntqqapi/external/crychic/index.ts
#	src/ntqqapi/external/moehook/hook.ts
#	src/onebot11/action/msg/SendMsg.ts
#	tsconfig.json
2024-05-10 20:28:44 +08:00
linyuchen
25c7a6096d refactor: path alias
fix: moehook
2024-05-10 20:23:30 +08:00
student_2333
627955e7fd chore: format 2024-05-10 13:34:49 +08:00
student_2333
43e9b070a9 fix: try 2 fix cannot parse msg err 2024-05-10 13:33:48 +08:00
linyuchen
78bb36a2bb fix: Music sign return null then throw exception 2024-05-07 17:46:47 +08:00
linyuchen
58e6e3cbda fix: Music sign return null then throw exception 2024-05-07 17:39:44 +08:00
linyuchen
1da086ce0a chore: v3.24.2 2024-05-05 20:20:30 +08:00
linyuchen
e9d43a9449 fix: http download filename special character 2024-05-05 20:06:07 +08:00
linyuchen
ce31052661 refactor: OB11Message add message_seq filed 2024-05-05 19:42:48 +08:00
linyuchen
3fd9b0a183 fix: 表情回应兼容int类型的emoji_id 2024-05-05 13:07:07 +08:00
linyuchen
7e1dee8e07 fix: msg db cache missing shortId 2024-05-04 23:35:19 +08:00
linyuchen
f2854fdf00 fix: report self recall twice 2024-05-04 20:30:39 +08:00
linyuchen
1fad95a55b chore: Version 3.24.1 2024-05-04 11:34:41 +08:00
linyuchen
5342e1521c Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/ntqqapi/external/moehook/MoeHoo-linux-x64.node
2024-05-03 21:26:31 +08:00
linyuchen
c0bb7def20 fix: Get image rkey on Linux x64 2024-05-03 21:25:47 +08:00
student_2333
3c532526df chore: sync external files 2024-05-01 15:25:49 +08:00
student_2333
05c6cae86f fix: reference before define 2024-05-01 11:10:42 +08:00
linyuchen
24a49f035e fix: music params check 2024-05-01 02:14:20 +08:00
linyuchen
ec27d73605 fix: copy .node 2024-04-30 23:12:36 +08:00
linyuchen
59cd28a2fd feat: FriendAddNotice 2024-04-30 23:06:50 +08:00
linyuchen
bcb6b51241 feat: send mface with summary param 2024-04-30 19:45:59 +08:00
linyuchen
b00ca24fe3 feat: send mface 2024-04-30 19:42:34 +08:00
linyuchen
3a4cdc1e34 Merge branch 'main' of https://github.com/markyfsun/LLOneBot into mface 2024-04-30 19:35:43 +08:00
linyuchen
de4d901412 refactor: 获取rkey后进行检查rkey是否正确 2024-04-30 19:26:51 +08:00
linyuchen
297c495df9 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/ntqqapi/external/crychic/index.ts
#	src/ntqqapi/external/moehook/hook.ts
2024-04-30 18:58:25 +08:00
student_2333
b78bd235f9 fix 2024-04-30 14:13:40 +08:00
student_2333
23d32a1464 Merge branch 'main' into markyfsun/main 2024-04-30 14:07:18 +08:00
手瓜一十雪
25c3d51d69 Merge pull request #206 from LLOneBot/feat/music-card
feat: music card sign
2024-04-30 13:45:15 +08:00
student_2333
05091798f4 fix 2024-04-30 13:40:15 +08:00
student_2333
78c6050d61 refactor 2024-04-30 13:08:33 +08:00
student_2333
2abdcd23db fix 2024-04-30 13:07:51 +08:00
student_2333
1d7100a053 fix 2024-04-30 13:05:08 +08:00
student_2333
6ff49722d8 feat: music card sign 2024-04-30 12:50:38 +08:00
student_2333
9c6abd5167 Merge branch 'main' into markyfsun/main 2024-04-30 11:35:38 +08:00
student_2333
dc1e1ea21b style: reformat 2024-04-30 11:28:24 +08:00
student_2333
f38e544815 style: reformat 2024-04-30 11:24:33 +08:00
student_2333
bb0fcd8614 chore(dep) 2024-04-30 11:22:26 +08:00
linyuchen
710fa3f686 Update README.md thanks list 2024-04-29 19:30:00 +08:00
linyuchen
91089cdb9e refactor: import native .node 2024-04-29 16:25:27 +08:00
linyuchen
58f544862b refactor: private/group image rkey 2024-04-29 11:57:58 +08:00
linyuchen
09ab8cbe93 fix: private/group image rkey 2024-04-28 10:30:41 +08:00
linyuchen
4ce4f3d3a5 fix: image rkey 2024-04-28 09:26:55 +08:00
linyuchen
b5ab717634 优化发送语音或者不支持的消息类型错误提示 2024-04-28 09:19:14 +08:00
markyfsun
2e55924a19 feat: market face 2024-04-27 23:01:47 +08:00
linyuchen
fe3ac3060a Merge remote-tracking branch 'origin/main' 2024-04-26 01:27:10 +08:00
linyuchen
e7e06d655f optimize get file 2024-04-25 23:28:35 +08:00
linyuchen
dec531c567 fix: get image rkey 2024-04-25 23:27:39 +08:00
linyuchen
05f0985f7f feat: upload private file 2024-04-25 23:27:14 +08:00
linyuchen
ac852cc382 feat: msg emoji like 2024-04-25 23:26:46 +08:00
linyuchen
b7855e91f6 feat: msg emoji like 2024-04-25 23:25:38 +08:00
linyuchen
3ae2d2a1e6 feat: forward single msg 2024-04-25 23:24:58 +08:00
linyuchen
857625469f Merge pull request #199 from disymayufei/patch-2
向README.md中添加了一个警告信息
2024-04-20 17:18:52 +08:00
Disy
ca3f68a42a chore: Update caution message
添加了一个警告信息,希望可以起到警示作用,防止一些小白私自将仓库和插件信息广泛传播出去引发tx的警觉
2024-04-20 14:18:26 +08:00
手瓜一十雪
1d47f89011 Merge pull request #197 from jinyu2022/main
添加CORS允许跨源访问
2024-04-17 15:22:38 +08:00
堇羽
2c24e234c8 添加CORS允许跨源访问 2024-04-17 07:14:23 +00:00
linyuchen
5562a3251d feat: get cookies 2024-04-16 23:55:21 +08:00
linyuchen
019b590f36 refactor: auto escape cq code for send msg 2024-04-16 23:23:19 +08:00
linyuchen
c2b3316603 fix: send empty forward msg
fix: ignore post history msg before login
fix: quit group not sync to groups of data
feat: support post url params
feat: support port http heart
2024-04-16 23:16:25 +08:00
linyuchen
f8890b309b fix: face msg faceType 2024-04-11 18:57:58 +08:00
linyuchen
b5e578733f fix: quick reply friend msg 2024-04-11 18:17:02 +08:00
linyuchen
51602b987e fix: ws 没有上报群文件上传事件 2024-04-08 00:21:24 +08:00
linyuchen
b501af6e0e feat: 骰子魔法表情 & 猜拳魔法表情 2024-04-07 18:51:26 +08:00
linyuchen
81821e74d8 fix: 手动频繁切换聊天窗口时导致旧的窗口接收不到消息 2024-04-07 17:37:52 +08:00
linyuchen
959eab441e Merge branch 'dev' of github.com:linyuchen/LiteLoaderQQNT-OneBotApi into dev 2024-04-06 23:58:27 +08:00
linyuchen
441c0c6946 feat: @全体的时候判断剩余次数 2024-04-06 23:57:07 +08:00
linyuchen
240cdade07 fix: getFriend 2024-04-06 23:30:40 +08:00
linyuchen
0132d97bd9 Merge pull request #177 from idanran/main
fix: audio may fail to convert
2024-04-04 12:37:37 +08:00
linyuchen
b34c7f045c fix: at all when member isn't admin 2024-04-04 12:26:28 +08:00
idanran
ab91313e69 fix 2024-04-04 04:22:56 +00:00
idanran
1f8966aaf4 fix: audio may fail to convert 2024-04-04 02:08:08 +00:00
linyuchen
ec073da3f6 feat: 发送戳一戳 2024-04-03 00:03:50 +08:00
linyuchen
80131e0472 fix: send msg auto_escape 2024-04-02 12:51:08 +08:00
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
191 changed files with 14393 additions and 13831 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,39 +1,39 @@
name: "publish" name: 'publish'
on: on:
push: push:
tags: tags:
- "v*" - 'v*'
jobs: jobs:
build-and-publish: build-and-publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: setup node - name: setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 18
- name: install dependenies - name: install dependenies
run: | run: |
export ELECTRON_SKIP_BINARY_DOWNLOAD=1 export ELECTRON_SKIP_BINARY_DOWNLOAD=1
npm install npm install
- name: build - name: build
run: npm run build run: npm run build
- name: zip - name: zip
run: | run: |
sudo apt install zip -y sudo apt install zip -y
cp manifest.json ./dist/manifest.json cp manifest.json ./dist/manifest.json
cd ./dist/ cd ./dist/
zip -r ../LLOneBot.zip ./* zip -r ../LLOneBot.zip ./*
- name: publish - name: publish
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: "LLOneBot.zip" artifacts: 'LLOneBot.zip'
draft: true draft: true
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}

2
.gitignore vendored
View File

@@ -3,4 +3,4 @@ package-lock.json
dist/ dist/
out/ out/
.idea/ .idea/
.DS_Store .DS_Store

4
.prettierrc.yml Normal file
View File

@@ -0,0 +1,4 @@
semi: false
singleQuote: true
trailingComma: all
printWidth: 120

14
CHANGELOG Normal file
View File

@@ -0,0 +1,14 @@
# 3.24.0
## 修复
* 修复图片rkey导致链接失效的问题
* 修复/get_image, /get_file 无法获取图片的问题
* 修复上报他人管理员被取消通知
## 新增
* 新增表情回应发送和上报
* 新增商城表情发送,和上报 url
* 新增转发单条消息接口 `forward_friend_single_msg`, `forward_group_single_msg`
* 新增新增好友事件

View File

@@ -1,7 +1,10 @@
# LLOneBot
# LLOneBot API
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发 LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
> [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于:B站微博知乎抖音等发布和讨论*任何*与本插件存在相关性的信息**
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 安装方法 ## 安装方法
@@ -20,11 +23,11 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
## TODO ## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用 - [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR - [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录 - [x] 转发消息记录
- [x] 好友点赞api - [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等 - [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息 - [x] 视频消息
@@ -36,17 +39,21 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
- [ ] 框架对接文档 - [ ] 框架对接文档
## onebot11文档 ## onebot11文档
<https://11.onebot.dev/> <https://11.onebot.dev/>
## Stargazers over time ## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot) [![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
## 鸣谢 ## 鸣谢
* [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) - [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [chronocat](https://github.com/chrononeko/chronocat/) - [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI)
* [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) - [chronocat](https://github.com/chrononeko/chronocat/)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm)
## 友链 ## 友链
* [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架

View File

@@ -1,73 +1,89 @@
import cp from 'vite-plugin-cp'; import cp from 'vite-plugin-cp'
import "./scripts/gen-version" import './scripts/gen-version'
import path from 'node:path'
const external = ["silk-wasm", "ws", const external = [
"level", "classic-level", "abstract-level", "level-supports", "level-transcoder", 'silk-wasm',
"module-error", "catering", "node-gyp-build"]; 'ws',
'level',
'classic-level',
'abstract-level',
'level-supports',
'level-transcoder',
'module-error',
'catering',
'node-gyp-build',
]
function genCpModule(module: string) { function genCpModule(module: string) {
return {src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false} return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }
} }
let config = { let config = {
main: { main: {
build: { build: {
outDir: "dist/main", outDir: 'dist/main',
emptyOutDir: true, emptyOutDir: true,
lib: { lib: {
formats: ["cjs"], formats: ['cjs'],
entry: {"main": "src/main/main.ts"}, entry: { main: 'src/main/main.ts' },
}, },
rollupOptions: { rollupOptions: {
external, external,
input: "src/main/main.ts", input: 'src/main/main.ts',
} },
},
resolve: {
alias: {
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg'
},
},
plugins: [cp({
targets: [
...external.map(genCpModule),
{src: './manifest.json', dest: 'dist'}, {src: './icon.jpg', dest: 'dist'},
{src: './src/ntqqapi/external/ccpoke/poke-win32-x64.node', dest: 'dist/main/ccpoke/'},
]
})]
}, },
preload: { resolve: {
// vite config options alias: {
build: { '@': path.resolve(__dirname, './src'),
outDir: "dist/preload", './lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
emptyOutDir: true, },
lib: {
formats: ["cjs"],
entry: {"preload": "src/preload.ts"},
},
rollupOptions: {
// external: externalAll,
input: "src/preload.ts",
}
},
resolve: {}
}, },
renderer: { plugins: [
// vite config options cp({
build: { targets: [
outDir: "dist/renderer", ...external.map(genCpModule),
emptyOutDir: true, { src: './manifest.json', dest: 'dist' },
lib: { { src: './icon.jpg', dest: 'dist' },
formats: ["es"], { src: './src/ntqqapi/native/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
entry: {"renderer": "src/renderer/index.ts"}, // { src: './src/ntqqapi/native/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
}, // { src: './src/ntqqapi/native/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
rollupOptions: { ],
// external: externalAll, }),
input: "src/renderer/index.ts", ],
} },
}, preload: {
resolve: {} // vite config options
} build: {
outDir: 'dist/preload',
emptyOutDir: true,
lib: {
formats: ['cjs'],
entry: { preload: 'src/preload.ts' },
},
rollupOptions: {
// external: externalAll,
input: 'src/preload.ts',
},
},
resolve: {},
},
renderer: {
// vite config options
build: {
outDir: 'dist/renderer',
emptyOutDir: true,
lib: {
formats: ['es'],
entry: { renderer: 'src/renderer/index.ts' },
},
rollupOptions: {
// external: externalAll,
input: 'src/renderer/index.ts',
},
},
resolve: {},
},
} }
export default config; export default config

View File

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

6782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,17 +9,21 @@
"build-mac": "npm run build && npm run deploy-mac", "build-mac": "npm run build && npm run deploy-mac",
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/", "deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/",
"build-win": "npm run build && npm run deploy-win", "build-win": "npm run build && npm run deploy-win",
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"" "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"",
"format": "prettier -cw .",
"check": "tsc"
}, },
"author": "", "author": "",
"license": "ISC", "license": "MIT",
"dependencies": { "dependencies": {
"compressing": "^1.10.0", "compressing": "^1.10.0",
"cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"fast-xml-parser": "^4.3.6",
"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",
"silk-wasm": "^3.2.4", "silk-wasm": "^3.6.0",
"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"
@@ -35,7 +39,7 @@
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-plugin-import": "^2.25.2", "eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ", "eslint-plugin-n": "^15.0.0 || ^16.0.0",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "*", "typescript": "*",

View File

@@ -4,19 +4,19 @@ import { version } from '../src/version'
const manifestPath = path.join(__dirname, '../manifest.json') const manifestPath = path.join(__dirname, '../manifest.json')
function readManifest (): any { function readManifest(): any {
if (fs.existsSync(manifestPath)) { if (fs.existsSync(manifestPath)) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
} }
} }
function writeManifest (manifest: any) { function writeManifest(manifest: any) {
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)) fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
} }
const manifest = readManifest() const manifest = readManifest()
if (version !== manifest.version) { if (version !== manifest.version) {
manifest.version = version manifest.version = version
manifest.name = `LLOneBot v${version}` manifest.name = `LLOneBot v${version}`
writeManifest(manifest) writeManifest(manifest)
} }

View File

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

View File

@@ -1,97 +1,105 @@
import fs from "fs"; import fs from 'fs'
import {Config, OB11Config} from './types'; import fsPromise from 'fs/promises'
import { Config, OB11Config } from './types'
import {mergeNewProperties} from "./utils/helper"; import { mergeNewProperties } from './utils/helper'
import path from "node:path"; import path from 'node:path'
import {selfInfo} from "./data"; import { selfInfo } from './data'
import {DATA_DIR} from "./utils"; import { DATA_DIR } from './utils'
export const HOOK_LOG = false; export const HOOK_LOG = false
export const ALLOW_SEND_TEMP_MSG = false; export const ALLOW_SEND_TEMP_MSG = false
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string; private readonly configPath: string
private config: Config | null = null; private config: Config | null = null
constructor(configPath: string) { constructor(configPath: string) {
this.configPath = configPath; this.configPath = configPath
}
getConfig(cache = true) {
if (this.config && cache) {
return this.config
} }
getConfig(cache = true) { return this.reloadConfig()
if (this.config && cache) { }
return this.config;
}
return this.reloadConfig(); reloadConfig(): Config {
let ob11Default: OB11Config = {
httpPort: 3000,
httpHosts: [],
httpSecret: '',
wsPort: 3001,
wsHosts: [],
enableHttp: true,
enableHttpPost: true,
enableWs: true,
enableWsReverse: false,
messagePostFormat: 'array',
enableHttpHeart: false,
enableQOAutoQuote: false
}
let defaultConfig: Config = {
enableLLOB: true,
ob11: ob11Default,
heartInterval: 60000,
token: '',
enableLocalFile2Url: false,
debug: false,
log: false,
reportSelfMessage: false,
autoDeleteFile: false,
autoDeleteFileSecond: 60,
enablePoke: false,
musicSignUrl: '',
} }
reloadConfig(): Config { if (!fs.existsSync(this.configPath)) {
let ob11Default: OB11Config = { this.config = defaultConfig
httpPort: 3000, return this.config
httpHosts: [], } else {
httpSecret: "", const data = fs.readFileSync(this.configPath, 'utf-8')
wsPort: 3001, let jsonData: Config = defaultConfig
wsHosts: [], try {
enableHttp: true, jsonData = JSON.parse(data)
enableHttpPost: true, } catch (e) {
enableWs: true, this.config = defaultConfig
enableWsReverse: false, return this.config
messagePostFormat: "array", }
} mergeNewProperties(defaultConfig, jsonData)
let defaultConfig: Config = { this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
ob11: ob11Default, this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
heartInterval: 60000, this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
token: "", // console.log("get config", jsonData);
enableLocalFile2Url: false, this.config = jsonData
debug: false, return this.config
log: false,
reportSelfMessage: false,
autoDeleteFile: false,
autoDeleteFileSecond: 60,
enablePoke: false
};
if (!fs.existsSync(this.configPath)) {
this.config = defaultConfig;
return this.config;
} else {
const data = fs.readFileSync(this.configPath, "utf-8");
let jsonData: Config = defaultConfig;
try {
jsonData = JSON.parse(data)
} catch (e) {
this.config = defaultConfig;
return this.config;
}
mergeNewProperties(defaultConfig, jsonData);
this.checkOldConfig(jsonData.ob11, jsonData, "httpPort", "http");
this.checkOldConfig(jsonData.ob11, jsonData, "httpHosts", "hosts");
this.checkOldConfig(jsonData.ob11, jsonData, "wsPort", "wsPort");
// console.log("get config", jsonData);
this.config = jsonData;
return this.config;
}
} }
}
setConfig(config: Config) { setConfig(config: Config) {
this.config = config; this.config = config
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8") fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8')
} }
private checkOldConfig(currentConfig: Config | OB11Config, private checkOldConfig(
oldConfig: Config | OB11Config, currentConfig: Config | OB11Config,
currentKey: string, oldKey: string) { oldConfig: Config | OB11Config,
// 迁移旧的配置到新配置,避免用户重新填写配置 currentKey: string,
const oldValue = oldConfig[oldKey]; oldKey: string,
if (oldValue) { ) {
currentConfig[currentKey] = oldValue; // 迁移旧的配置到新配置,避免用户重新填写配置
delete oldConfig[oldKey]; const oldValue = oldConfig[oldKey]
} if (oldValue) {
currentConfig[currentKey] = oldValue
delete oldConfig[oldKey]
} }
}
} }
export function getConfigUtil() { export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`) const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }

View File

@@ -1,104 +1,124 @@
import { import {
type Friend, CategoryFriend,
type FriendRequest, type Friend,
type Group, type FriendRequest,
type GroupMember, type Group,
type SelfInfo type GroupMember,
type SelfInfo,
User,
} from '../ntqqapi/types' } from '../ntqqapi/types'
import {type FileCache, type LLOneBotError} from './types' import { type FileCache, type LLOneBotError } from './types'
import {NTQQGroupApi} from "../ntqqapi/api/group"; import { NTQQGroupApi } from '../ntqqapi/api/group'
import {log} from "./utils/log"; import { log } from './utils/log'
import {isNumeric} from "./utils/helper"; import { isNumeric } from './utils/helper'
import { NTQQFriendApi } from '../ntqqapi/api'
import { WebApiGroupMember } from '@/ntqqapi/api/webapi'
export const selfInfo: SelfInfo = { export const selfInfo: SelfInfo = {
uid: '', uid: '',
uin: '', uin: '',
nick: '', nick: '',
online: true online: true,
}
export const WebGroupData = {
GroupData: new Map<string, Array<WebApiGroupMember>>(),
GroupTime: new Map<string, number>(),
} }
export let groups: Group[] = [] export let groups: Group[] = []
export let friends: Friend[] = [] 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: '',
httpServerError: '', httpServerError: '',
wsServerError: '', wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误' otherError: 'LLOnebot未能正常启动请检查日志查看错误',
} }
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> { export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid) ? "uin" : "uid" let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
let filterValue = uinOrUid let filterValue = uinOrUid
let friend = friends.find(friend => friend[filterKey] === filterValue.toString()) let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
// if (!friend) { if (!friend) {
// try { try {
// friends = (await NTQQApi.getFriends(true)) const _friends = await NTQQFriendApi.getFriends(true)
// friend = friends.find(friend => friend[filterKey] === filterValue.toString()) friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
// } catch (e) { if (friend) {
// // log("刷新好友列表失败", e.stack.toString()) friends.push(friend)
// } }
// } } catch (e) {
return friend log('刷新好友列表失败', e.stack.toString())
}
}
return friend
} }
export async function getGroup(qq: string): Promise<Group | undefined> { export async function getGroup(qq: string): Promise<Group | undefined> {
let group = groups.find(group => group.groupCode === qq.toString()) let group = groups.find((group) => group.groupCode === qq.toString())
if (!group) { if (!group) {
try { try {
const _groups = await NTQQGroupApi.getGroups(true); const _groups = await NTQQGroupApi.getGroups(true)
group = _groups.find(group => group.groupCode === qq.toString()) group = _groups.find((group) => group.groupCode === qq.toString())
if (group) { if (group) {
groups.push(group) groups.push(group)
} }
} catch (e) { } catch (e) {
}
} }
return group }
return group
}
export function deleteGroup(groupCode: string) {
const groupIndex = groups.findIndex((group) => group.groupCode === groupCode.toString())
// log(groups, groupCode, groupIndex);
if (groupIndex !== -1) {
log('删除群', groupCode)
groups.splice(groupIndex, 1)
}
} }
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) { export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString() groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString() memberUinOrUid = memberUinOrUid.toString()
const group = await getGroup(groupQQ) const group = await getGroup(groupQQ)
if (group) { if (group) {
const filterKey = isNumeric(memberUinOrUid) ? "uin" : "uid" const filterKey = isNumeric(memberUinOrUid) ? 'uin' : 'uid'
const filterValue = memberUinOrUid const filterValue = memberUinOrUid
let filterFunc: (member: GroupMember) => boolean = member => member[filterKey] === filterValue let filterFunc: (member: GroupMember) => boolean = (member) => member[filterKey] === filterValue
let member = group.members?.find(filterFunc) let member = group.members?.find(filterFunc)
if (!member) { if (!member) {
try { try {
const _members = await NTQQGroupApi.getGroupMembers(groupQQ) const _members = await NTQQGroupApi.getGroupMembers(groupQQ)
if (_members.length > 0) { if (_members.length > 0) {
group.members = _members group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
} }
return member } catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
} }
return null return member
}
return null
} }
export async function refreshGroupMembers(groupQQ: string) { export async function refreshGroupMembers(groupQQ: string) {
const group = groups.find(group => group.groupCode === groupQQ) const group = groups.find((group) => group.groupCode === groupQQ)
if (group) { if (group) {
group.members = await NTQQGroupApi.getGroupMembers(groupQQ) group.members = await NTQQGroupApi.getGroupMembers(groupQQ)
} }
} }
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
}
} }
}
} }
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号 export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号
export let rawFriends: CategoryFriend[] = []

View File

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

View File

@@ -1,114 +1,119 @@
import express, {Express, Request, Response} from "express"; import express, { Express, Request, Response } from 'express'
import http from "http"; import http from 'http'
import {log} from "../utils/log"; import cors from 'cors'
import {getConfigUtil} from "../config"; import { log } from '../utils/log'
import {llonebotError} from "../data"; import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
type RegisterHandler = (res: Response, payload: any) => Promise<any> type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase { export abstract class HttpServerBase {
name: string = "LLOneBot"; name: string = 'LLOneBot'
private readonly expressAPP: Express; private readonly expressAPP: Express
private server: http.Server = null; private server: http.Server = null
constructor() { constructor() {
this.expressAPP = express(); this.expressAPP = express()
this.expressAPP.use(express.urlencoded({extended: true, limit: "5000mb"})); // 添加 CORS 中间件
this.expressAPP.use((req, res, next) => { this.expressAPP.use(cors())
// 兼容处理没有带content-type的请求 this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' }))
// log("req.headers['content-type']", req.headers['content-type']) this.expressAPP.use((req, res, next) => {
req.headers['content-type'] = 'application/json'; // 兼容处理没有带content-type的请求
const originalJson = express.json({limit: "5000mb"}); // log("req.headers['content-type']", req.headers['content-type'])
// 调用原始的express.json()处理器 req.headers['content-type'] = 'application/json'
originalJson(req, res, (err) => { const originalJson = express.json({ limit: '5000mb' })
if (err) { // 调用原始的express.json()处理器
log("Error parsing JSON:", err); originalJson(req, res, (err) => {
return res.status(400).send("Invalid JSON"); if (err) {
} log('Error parsing JSON:', err)
next(); return res.status(400).send('Invalid JSON')
}); }
}); next()
})
})
}
authorize(req: Request, res: Response, next: () => void) {
let serverToken = getConfigUtil().getConfig().token
let clientToken = ''
const authHeader = req.get('authorization')
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()
log('receive http header token', clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
clientToken = req.query.access_token[0].toString()
} else {
clientToken = req.query.access_token.toString()
}
log('receive http url token', clientToken)
} }
authorize(req: Request, res: Response, next: () => void) { if (serverToken && clientToken != serverToken) {
let serverToken = getConfigUtil().getConfig().token; return res.status(403).send(JSON.stringify({ message: 'token verify failed!' }))
let clientToken = "" }
const authHeader = req.get("authorization") next()
if (authHeader) { }
clientToken = authHeader.split("Bearer ").pop()
log("receive http header token", clientToken)
} else if (req.query.access_token) {
if (Array.isArray(req.query.access_token)) {
clientToken = req.query.access_token[0].toString();
} else {
clientToken = req.query.access_token.toString();
}
log("receive http url token", clientToken)
}
if (serverToken && clientToken != serverToken) { start(port: number) {
return res.status(403).send(JSON.stringify({message: 'token verify failed!'})); try {
} this.expressAPP.get('/', (req: Request, res: Response) => {
next(); res.send(`${this.name}已启动`)
}; })
this.listen(port)
llonebotError.httpServerError = ''
} catch (e) {
log('HTTP服务启动失败', e.toString())
llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString()
}
}
start(port: number) { stop() {
try { llonebotError.httpServerError = ''
this.expressAPP.get('/', (req: Request, res: Response) => { if (this.server) {
res.send(`${this.name}已启动`); this.server.close()
}) this.server = null
this.listen(port); }
llonebotError.httpServerError = "" }
} catch (e) {
log("HTTP服务启动失败", e.toString()) restart(port: number) {
llonebotError.httpServerError = "HTTP服务启动失败, " + e.toString() this.stop()
} this.start(port)
}
abstract handleFailed(res: Response, payload: any, err: any): void
registerRouter(method: 'post' | 'get' | string, url: string, handler: RegisterHandler) {
if (!url.startsWith('/')) {
url = '/' + url
} }
stop() { if (!this.expressAPP[method]) {
llonebotError.httpServerError = "" const err = `${this.name} register router failed${method} not exist`
if (this.server) { log(err)
this.server.close() throw err
this.server = null;
}
} }
this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => {
let payload = req.body
if (method == 'get') {
payload = req.query
} else if (req.query) {
payload = { ...req.query, ...req.body }
}
log('收到http请求', url, payload)
try {
res.send(await handler(res, payload))
} catch (e) {
this.handleFailed(res, payload, e.stack.toString())
}
})
}
restart(port: number) { protected listen(port: number) {
this.stop() this.server = this.expressAPP.listen(port, '0.0.0.0', () => {
this.start(port) const info = `${this.name} started 0.0.0.0:${port}`
} console.log(info)
log(info)
abstract handleFailed(res: Response, payload: any, err: any): void })
}
registerRouter(method: "post" | "get" | string, url: string, handler: RegisterHandler) { }
if (!url.startsWith("/")) {
url = "/" + url
}
if (!this.expressAPP[method]) {
const err = `${this.name} register router failed${method} not exist`;
log(err);
throw err;
}
this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => {
let payload = req.body;
if (method == "get") {
payload = req.query
}
log("收到http请求", url, payload);
try {
res.send(await handler(res, payload))
} catch (e) {
this.handleFailed(res, payload, e.stack.toString())
}
});
}
protected listen(port: number) {
this.server = this.expressAPP.listen(port, "0.0.0.0", () => {
const info = `${this.name} started 0.0.0.0:${port}`
console.log(info);
log(info);
});
}
}

View File

@@ -1,104 +1,93 @@
import {WebSocket, WebSocketServer} from "ws"; import { WebSocket, WebSocketServer } from 'ws'
import urlParse from "url"; import urlParse from 'url'
import {IncomingMessage} from "node:http"; import { IncomingMessage } from 'node:http'
import {log} from "../utils/log"; import { log } from '../utils/log'
import {getConfigUtil} from "../config"; import { getConfigUtil } from '../config'
import {llonebotError} from "../data"; import { llonebotError } from '../data'
class WebsocketClientBase { class WebsocketClientBase {
private wsClient: WebSocket private wsClient: WebSocket
constructor() { constructor() {}
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg)
} }
}
send(msg: string) { onMessage(msg: string) {}
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg);
}
}
onMessage(msg: string) {
}
} }
export class WebsocketServerBase { export class WebsocketServerBase {
private ws: WebSocketServer = null; private ws: WebSocketServer = null
constructor() { constructor() {
console.log(`llonebot websocket service started`) console.log(`llonebot websocket service started`)
}
start(port: number) {
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) => {
const url = req.url.split('?').shift()
this.authorize(wsClient, req)
this.onConnect(wsClient, url, req)
wsClient.on('message', async (msg) => {
this.onMessage(wsClient, url, msg.toString())
})
})
}
start(port: number) { stop() {
try { llonebotError.wsServerError = ''
this.ws = new WebSocketServer({port}); this.ws.close((err) => {
llonebotError.wsServerError = '' log('ws server close failed!', err)
}catch (e) { })
llonebotError.wsServerError = "正向ws服务启动失败, " + e.toString() this.ws = null
} }
this.ws.on("connection", (wsClient, req) => {
const url = req.url.split("?").shift()
this.authorize(wsClient, req);
this.onConnect(wsClient, url, req);
wsClient.on("message", async (msg) => {
this.onMessage(wsClient, url, msg.toString())
})
})
}
stop() { restart(port: number) {
llonebotError.wsServerError = '' this.stop()
this.ws.close((err) => { this.start(port)
log("ws server close failed!", err) }
});
this.ws = null;
}
restart(port: number) { authorize(wsClient: WebSocket, req) {
this.stop(); let token = getConfigUtil().getConfig().token
this.start(port); const url = req.url.split('?').shift()
} log('ws connect', url)
let clientToken: string = ''
authorize(wsClient: WebSocket, req) { const authHeader = req.headers['authorization']
let token = getConfigUtil().getConfig().token; if (authHeader) {
const url = req.url.split("?").shift(); clientToken = authHeader.split('Bearer ').pop()
log("ws connect", url) log('receive ws header token', clientToken)
let clientToken: string = "" } else {
const authHeader = req.headers['authorization']; const parsedUrl = urlParse.parse(req.url, true)
if (authHeader) { const urlToken = parsedUrl.query.access_token
clientToken = authHeader.split("Bearer ").pop() if (urlToken) {
log("receive ws header token", clientToken); if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else { } else {
const parsedUrl = urlParse.parse(req.url, true); clientToken = urlToken
const urlToken = parsedUrl.query.access_token;
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log("receive ws url token", clientToken);
}
}
if (token && clientToken != token) {
this.authorizeFailed(wsClient)
return wsClient.close()
} }
log('receive ws url token', clientToken)
}
} }
if (token && clientToken != token) {
authorizeFailed(wsClient: WebSocket) { this.authorizeFailed(wsClient)
return wsClient.close()
} }
}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { authorizeFailed(wsClient: WebSocket) {}
} onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {}
onMessage(wsClient: WebSocket, url: string, msg: string) { onMessage(wsClient: WebSocket, url: string, msg: string) {}
} sendHeart() {}
}
sendHeart() {
}
}

View File

@@ -9,12 +9,15 @@ export interface OB11Config {
enableWs?: boolean enableWs?: boolean
enableWsReverse?: boolean enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string' messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息
} }
export interface CheckVersion { export interface CheckVersion {
result: boolean, result: boolean
version: string version: string
} }
export interface Config { export interface Config {
enableLLOB: boolean
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval?: number // ms
@@ -26,6 +29,8 @@ export interface Config {
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
enablePoke?: boolean enablePoke?: boolean
musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean
} }
export interface LLOneBotError { export interface LLOneBotError {
@@ -42,5 +47,6 @@ export interface FileCache {
fileUuid?: string fileUuid?: string
url?: string url?: string
msgId?: string msgId?: string
elementId: string
downloadFunc?: () => Promise<void> downloadFunc?: () => Promise<void>
} }

View File

@@ -0,0 +1,83 @@
import path from 'node:path'
import fs from 'node:fs'
import os from 'node:os'
import { systemPlatform } from './system'
export const exePath = process.execPath
function getPKGPath() {
let p = path.join(path.dirname(exePath), 'resources', 'app', 'package.json')
if (systemPlatform === 'darwin') {
p = path.join(path.dirname(path.dirname(exePath)), 'Resources', 'app', 'package.json')
}
return p
}
export const pkgInfoPath = getPKGPath()
let configVersionInfoPath: string
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json')
}
else {
const userPath = os.homedir()
const appDataPath = path.resolve(userPath, './.config/QQ')
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json')
}
if (typeof configVersionInfoPath !== 'string') {
throw new Error('Something went wrong when load QQ info path')
}
export { configVersionInfoPath }
type QQPkgInfo = {
version: string;
buildVersion: string;
platform: string;
eleArch: string;
}
type QQVersionConfigInfo = {
baseVersion: string;
curVersion: string;
prevVersion: string;
onErrorVersions: Array<any>;
buildId: string;
}
let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.9-23361',
'curVersion': '9.9.9-23361',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '23361',
}
if (fs.existsSync(configVersionInfoPath)) {
try {
const _ = JSON.parse(fs.readFileSync(configVersionInfoPath).toString())
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _)
} catch (e) {
console.error('Load QQ version config info failed, Use default version', e)
}
}
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo
export const qqPkgInfo: QQPkgInfo = require(pkgInfoPath)
// platform_type: 3,
// app_type: 4,
// app_version: '9.9.9-23159',
// qua: 'V1_WIN_NQ_9.9.9_23159_GW_B',
// appid: '537213764',
// platVer: '10.0.26100',
// clientVer: '9.9.9-23159',
let _appid: string = '537213803' // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') {
_appid = '537213827'
}
// todo: mac 平台的 appid
export const appid = _appid
export const isQQ998: boolean = qqPkgInfo.buildVersion >= '22106'

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

@@ -0,0 +1,170 @@
import fs from 'fs'
import fsPromise from 'fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm'
import { log } from './log'
import path from 'node:path'
import { TEMP_DIR } from './index'
import { v4 as uuidv4 } from 'uuid'
import { getConfigUtil } from '../config'
import { spawn } from 'node:child_process'
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 file = await fsPromise.readFile(filePath)
const pttPath = path.join(TEMP_DIR, uuidv4())
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = isWav(file)
const pcmPath = pttPath + '.pcm'
let sampleRate = 0
const convert = () => {
return new Promise<Buffer>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000
const data = fs.readFileSync(pcmPath)
fs.unlink(pcmPath, (err) => {
})
return resolve(data)
}
log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`)
reject(Error(`FFmpeg处理转换失败`))
})
})
}
let input: Buffer
if (!_isWav) {
input = await convert()
} else {
input = file
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
const { fmt } = getWavFileInfo(input)
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
input = await convert()
}
}
const silk = await encode(input, sampleRate)
fs.writeFileSync(pttPath, silk.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
}
} else {
const silk = file
let duration = 0
try {
duration = getDuration(silk) / 1000
} catch (e) {
log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack)
duration = await guessDuration(filePath)
}
return {
converted: false,
path: filePath,
duration,
}
}
} catch (error) {
log('convert silk failed', error.stack)
return {}
}
}
export async function decodeSilk(inputFilePath: string, outFormat: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' = 'mp3') {
const silkArrayBuffer = await fsPromise.readFile(inputFilePath)
const data = (await decode(silkArrayBuffer, 24000)).data
const fileName = path.join(TEMP_DIR, path.basename(inputFilePath))
const outPCMPath = fileName + '.pcm'
const outFilePath = fileName + '.' + outFormat
await fsPromise.writeFile(outPCMPath, data)
const convert = () => {
return new Promise<string>((resolve, reject) => {
const ffmpegPath = getConfigUtil().getConfig().ffmpeg || process.env.FFMPEG_PATH || 'ffmpeg'
const cp = spawn(ffmpegPath, [
'-y',
'-f', 's16le', // PCM format
'-ar', '24000', // Sample rate
'-ac', '1', // Number of audio channels
'-i', outPCMPath,
outFilePath,
])
cp.on('error', (err) => {
log(`FFmpeg处理转换出错: `, err.message)
return reject(err)
})
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255]
if (code == null || EXIT_CODES.includes(code)) {
fs.unlink(outPCMPath, (err) => {
})
return resolve(outFilePath)
}
const exitErr = `FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`
log(exitErr)
reject(Error(`FFmpeg处理转换失败,${exitErr}`))
})
})
}
return convert()
}

View File

@@ -1,380 +1,256 @@
import fs from "fs"; import fs from 'fs'
import fsPromise from "fs/promises"; import fsPromise from 'fs/promises'
import crypto from "crypto"; import crypto from 'crypto'
import ffmpeg from "fluent-ffmpeg"; import util from 'util'
import util from "util"; import path from 'node:path'
import {encode, getDuration, isWav} from "silk-wasm"; import { v4 as uuidv4 } from 'uuid'
import path from "node:path"; import { log, TEMP_DIR } from './index'
import {v4 as uuidv4} from "uuid"; import { dbUtil } from '../db'
import {checkFfmpeg, DATA_DIR, log, TEMP_DIR} from "./index"; import * as fileType from 'file-type'
import {getConfigUtil} from "../config"; import { net } from 'electron'
import {dbUtil} from "../db";
import * as fileType from "file-type";
import {net} from "electron";
import config from "../../../electron.vite.config";
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4); const buffer = Buffer.alloc(4)
const fd = fs.openSync(path, 'r'); const fd = fs.openSync(path, 'r')
fs.readSync(fd, buffer, 0, 4, 0); fs.readSync(fd, buffer, 0, 4, 0)
fs.closeSync(fd); fs.closeSync(fd)
return buffer.toString() === 'GIF8' return buffer.toString() === 'GIF8'
} }
// 定义一个异步函数来检查文件是否存在 // 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> { export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const startTime = Date.now(); const startTime = Date.now()
function check() { function check() {
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
resolve(); resolve()
} else if (Date.now() - startTime > timeout) { } else if (Date.now() - startTime > timeout) {
reject(new Error(`文件不存在: ${path}`)); reject(new Error(`文件不存在: ${path}`))
} else { } else {
setTimeout(check, 100); setTimeout(check, 100)
} }
} }
check(); check()
}); })
} }
export async function file2base64(path: string) { export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile)
let result = { let result = {
err: "", err: '',
data: "" data: '',
} }
try { try {
// 读取文件内容 // 读取文件内容
// if (!fs.existsSync(path)){ // if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\"); // 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 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 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 { try {
const pttPath = path.join(DATA_DIR, uuidv4()); await checkFileReceived(path, 5000)
if (getFileHeader(filePath) !== "02232153494c4b") { } catch (e: any) {
log(`语音文件${filePath}需要转换成silk`) result.err = e.toString()
const _isWav = await isWavFile(filePath); return result
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(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);
});
})
}
// 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) => {
});
const gDuration = await guessDuration(pttPath)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
return {
converted: true,
path: pttPath,
duration: verifyDuration(silk.duration / 1000, gDuration),
};
} else {
const silk = fs.readFileSync(filePath);
let duration = 0;
const gDuration = await guessDuration(filePath)
try {
duration = verifyDuration(getDuration(silk) / 1000, gDuration);
} catch (e) {
log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
duration = gDuration;
}
return {
converted: false,
path: filePath,
duration: duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
} }
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> { export function calculateFileMD5(filePath: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 创建一个流式读取器 // 创建一个流式读取器
const stream = fs.createReadStream(filePath); const stream = fs.createReadStream(filePath)
const hash = crypto.createHash('md5'); const hash = crypto.createHash('md5')
stream.on('data', (data: Buffer) => { stream.on('data', (data: Buffer) => {
// 当读取到数据时,更新哈希对象的状态 // 当读取到数据时,更新哈希对象的状态
hash.update(data); hash.update(data)
}); })
stream.on('end', () => { stream.on('end', () => {
// 文件读取完成,计算哈希 // 文件读取完成,计算哈希
const md5 = hash.digest('hex'); const md5 = hash.digest('hex')
resolve(md5); resolve(md5)
}); })
stream.on('error', (err: Error) => { stream.on('error', (err: Error) => {
// 处理可能的读取错误 // 处理可能的读取错误
reject(err); reject(err)
}); })
}); })
} }
export interface HttpDownloadOptions { export interface HttpDownloadOptions {
url: string; url: string
headers?: Record<string, string> | string; headers?: Record<string, string> | string
} }
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> { export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
let chunks: Buffer[] = []; let chunks: Buffer[] = []
let url: string; let url: string
let headers: Record<string, 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" '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; if (typeof options === 'string') {
} else { url = options
url = options.url; } else {
if (options.headers) { url = options.url
if (typeof options.headers === "string") { if (options.headers) {
headers = JSON.parse(options.headers); if (typeof options.headers === 'string') {
} else { headers = JSON.parse(options.headers)
headers = options.headers; } else {
} headers = options.headers
} }
} }
const fetchRes = await net.fetch(url, headers); }
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`) const fetchRes = await net.fetch(url, {headers})
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
const blob = await fetchRes.blob(); const blob = await fetchRes.blob()
let buffer = await blob.arrayBuffer(); let buffer = await blob.arrayBuffer()
return Buffer.from(buffer); return Buffer.from(buffer)
} }
type Uri2LocalRes = { type Uri2LocalRes = {
success: boolean, success: boolean
errMsg: string, errMsg: string
fileName: string, fileName: string
ext: string, ext: string
path: string, path: string
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> { export async function uri2local(uri: string, fileName: string = null): Promise<Uri2LocalRes> {
let res = { let res = {
success: false, success: false,
errMsg: "", errMsg: '',
fileName: "", fileName: '',
ext: "", ext: '',
path: "", path: '',
isLocal: false isLocal: false,
} }
if (!fileName) { if (!fileName) {
fileName = uuidv4(); fileName = uuidv4()
} }
let filePath = path.join(TEMP_DIR, fileName) let filePath = path.join(TEMP_DIR, fileName)
let url = null; let url = null
try { try {
url = new URL(uri); url = new URL(uri)
} catch (e) { } catch (e) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` 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 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
}
}
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_')
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) { export async function copyFolder(sourcePath: string, destPath: string) {
try { try {
const entries = await fsPromise.readdir(sourcePath, {withFileTypes: true}); const entries = await fsPromise.readdir(sourcePath, { withFileTypes: true })
await fsPromise.mkdir(destPath, {recursive: true}); await fsPromise.mkdir(destPath, { recursive: true })
for (let entry of entries) { for (let entry of entries) {
const srcPath = path.join(sourcePath, entry.name); const srcPath = path.join(sourcePath, entry.name)
const dstPath = path.join(destPath, entry.name); const dstPath = path.join(destPath, entry.name)
if (entry.isDirectory()) { if (entry.isDirectory()) {
await copyFolder(srcPath, dstPath); await copyFolder(srcPath, dstPath)
} else { } else {
try { try {
await fsPromise.copyFile(srcPath, dstPath); await fsPromise.copyFile(srcPath, dstPath)
} catch (error) { } catch (error) {
console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`); console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`)
// 这里可以决定是否要继续复制其他文件 // 这里可以决定是否要继续复制其他文件
}
}
} }
} catch (error) { }
console.error('复制文件夹时出错:', error);
} }
} } catch (error) {
console.error('复制文件夹时出错:', error)
}
}

View File

@@ -1,48 +1,48 @@
export function truncateString(obj: any, maxLength = 500) { export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') { if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => { Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') { if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断 // 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) { if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'; obj[key] = obj[key].substring(0, maxLength) + '...'
} }
} else if (typeof obj[key] === 'object') { } else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用 // 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength); truncateString(obj[key], maxLength)
} }
}); })
} }
return obj; return obj
} }
export function isNumeric(str: string) { export function isNumeric(str: string) {
return /^\d+$/.test(str); return /^\d+$/.test(str)
} }
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms))
} }
// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 // 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象
export function mergeNewProperties(newObj: any, oldObj: any) { export function mergeNewProperties(newObj: any, oldObj: any) {
Object.keys(newObj).forEach(key => { Object.keys(newObj).forEach((key) => {
// 如果老对象不存在当前属性,则直接复制 // 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) { if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key]; oldObj[key] = newObj[key]
} else { } else {
// 如果老对象和新对象的当前属性都是对象,则递归合并 // 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') { if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key]); mergeNewProperties(newObj[key], oldObj[key])
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') { } else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖 // 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]; oldObj[key] = newObj[key]
} }
} }
}); })
} }
export function isNull(value: any) { export function isNull(value: any) {
return value === undefined || value === null; return value === undefined || value === null
} }
/** /**
@@ -52,17 +52,46 @@ export function isNull(value: any) {
* @returns 处理后的字符串,超过长度的地方将会换行 * @returns 处理后的字符串,超过长度的地方将会换行
*/ */
export function wrapText(str: string, maxLength: number): string { export function wrapText(str: string, maxLength: number): string {
// 初始化一个空字符串用于存放结果 // 初始化一个空字符串用于存放结果
let result: string = ''; let result: string = ''
// 循环遍历字符串每次步进maxLength个字符 // 循环遍历字符串每次步进maxLength个字符
for (let i = 0; i < str.length; i += maxLength) { for (let i = 0; i < str.length; i += maxLength) {
// 从i开始截取长度为maxLength的字符串段并添加到结果字符串 // 从i开始截取长度为maxLength的字符串段并添加到结果字符串
// 如果不是第一段,先添加一个换行符 // 如果不是第一段,先添加一个换行符
if (i > 0) result += '\n'; if (i > 0) result += '\n'
result += str.substring(i, i + maxLength); result += str.substring(i, i + maxLength)
} }
return result; return result
} }
/**
* 函数缓存装饰器根据方法名、参数、自定义key生成缓存键在一定时间内返回缓存结果
* @param ttl 超时时间,单位毫秒
* @param customKey 自定义缓存键前缀,可为空,防止方法名参数名一致时导致缓存键冲突
* @returns 处理后缓存或调用原方法的结果
*/
export function cacheFunc(ttl: number, customKey: string='') {
const cache = new Map<string, { expiry: number; value: any }>();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalMethod = descriptor.value;
const className = target.constructor.name; // 获取类名
const methodName = propertyKey; // 获取方法名
descriptor.value = async function (...args: any[]){
const cacheKey = `${customKey}${className}.${methodName}:${JSON.stringify(args)}`;
const cached = cache.get(cacheKey);
if (cached && cached.expiry > Date.now()) {
return cached.value;
} else {
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, { value: result, expiry: Date.now() + ttl });
return result;
}
};
return descriptor;
};
}

View File

@@ -1,17 +1,19 @@
import path from "node:path"; import path from 'node:path'
import fs from "fs"; import fs from 'fs'
export * from './file' export * from './file'
export * from './helper' export * from './helper'
export * from './log' export * from './log'
export * from './qqlevel' export * from './qqlevel'
export * from './qqpkg' export * from './QQBasicInfo'
export * from './upgrade' 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 const TEMP_DIR = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin; export const PLUGIN_DIR = global.LiteLoader.plugins['LLOneBot'].path.plugin
if (!fs.existsSync(TEMP_DIR)) { if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, {recursive: true}); fs.mkdirSync(TEMP_DIR, { recursive: true })
} }
export {getVideoInfo} from "./video"; export { getVideoInfo } from './video'
export {checkFfmpeg} from "./video"; export { checkFfmpeg } from './video'
export { encodeSilk } from './audio'
export { isQQ998 } from './QQBasicInfo'

View File

@@ -1,34 +1,35 @@
import {selfInfo} from "../data"; import { selfInfo } from '../data'
import fs from "fs"; import fs from 'fs'
import path from "node:path"; import path from 'node:path'
import {DATA_DIR, truncateString} from "./index"; import { DATA_DIR, truncateString } from './index'
import {getConfigUtil} from "../config"; 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[]) { export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) { if (!getConfigUtil().getConfig().log) {
return //console.log(...msg); 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) => {
}) 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

@@ -1,7 +1,7 @@
// QQ等级换算 // QQ等级换算
import {QQLevel} from "../../ntqqapi/types"; import { QQLevel } from '../../ntqqapi/types'
export function calcQQLevel(level: QQLevel) { export function calcQQLevel(level: QQLevel) {
const {crownNum, sunNum, moonNum, starNum} = level const { crownNum, sunNum, moonNum, starNum } = level
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum
} }

View File

@@ -1,12 +0,0 @@
import path from "path";
type QQPkgInfo = {
version: string;
buildVersion: string;
platform: string;
eleArch: string;
}
export const qqPkgInfo: QQPkgInfo = require(path.join(process.resourcesPath, "app/package.json"))
export const isQQ998: boolean = qqPkgInfo.buildVersion >= "22106"

107
src/common/utils/request.ts Normal file
View File

@@ -0,0 +1,107 @@
import https from 'node:https';
import http from 'node:http';
import { log } from '@/common/utils/log'
export class RequestUtil {
// 适用于获取服务器下发cookies时获取仅GET
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
client.get(url, (res) => {
let cookies: { [key: string]: string } = {};
const handleRedirect = (res: http.IncomingMessage) => {
//console.log(res.headers.location);
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies
log('redirectCookies', redirectCookies)
cookies = { ...cookies, ...redirectCookies };
resolve(cookies);
});
} else {
resolve(cookies);
}
} else {
resolve(cookies);
}
};
res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
handleRedirect(res);
});
if (res.headers['set-cookie']) {
// console.log(res.headers['set-cookie']);
log('set-cookie', url, res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('=');
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
}
});
}
}).on('error', (err) => {
reject(err);
});
});
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
let option = new URL(url);
const protocol = url.startsWith('https://') ? https : http;
const options = {
hostname: option.hostname,
port: option.port,
path: option.href,
method: method,
headers: headers
};
return new Promise((resolve, reject) => {
const req = protocol.request(options, (res: any) => {
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
});
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
if (isJsonRet) {
const responseJson = JSON.parse(responseBody);
resolve(responseJson as T);
} else {
resolve(responseBody as T);
}
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
} catch (parseError) {
reject(parseError);
}
});
});
req.on('error', (error: any) => {
reject(error);
});
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
if (isArgJson) {
req.write(JSON.stringify(data));
} else {
req.write(data);
}
}
req.end();
});
}
// 请求返回都是原始内容
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: Record<string, string> = {}) {
return this.HttpGetJson<string>(url, method, data, headers, false, false);
}
}

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

@@ -0,0 +1,37 @@
import { log } from './log'
export interface IdMusicSignPostData {
type: 'qq' | '163'
id: string | number
}
export interface CustomMusicSignPostData {
type: 'custom'
url: string
audio: string
title: string
image?: string
singer?: string
}
export type MusicSignPostData = IdMusicSignPostData | CustomMusicSignPostData
export class MusicSign {
private readonly url: string
constructor(url: string) {
this.url = url
}
async sign(postData: MusicSignPostData): Promise<string> {
const resp = await fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
})
if (!resp.ok) throw new Error(resp.statusText)
const data = await resp.text()
log('音乐消息生成成功', data)
return data
}
}

View File

@@ -0,0 +1,10 @@
import os from 'node:os';
import path from 'node:path';
export const systemPlatform = os.platform();
export const cpuArch = os.arch();
export const systemVersion = os.release();
// export const hostname = os.hostname(); // win7不支持
const homeDir = os.homedir();
export const downloadsPath = path.join(homeDir, 'Downloads');
export const systemName = os.type();

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,12 @@
import {webContents} from 'electron'; import { webContents } from 'electron'
function sendIPCMsg(channel: string, ...data: any) { function sendIPCMsg(channel: string, ...data: any) {
let contents = webContents.getAllWebContents(); let contents = webContents.getAllWebContents()
for (const content of contents) { for (const content of contents) {
try { try {
content.send(channel, ...data) content.send(channel, ...data)
} catch (e) { } catch (e) {
console.log("llonebot send ipc msg to render error:", e) console.log('llonebot send ipc msg to render error:', e)
}
} }
}
} }

View File

@@ -1,423 +1,564 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import {BrowserWindow, dialog, ipcMain} from 'electron'; 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_ERROR, CHANNEL_CHECK_VERSION,
CHANNEL_GET_CONFIG, CHANNEL_ERROR,
CHANNEL_LOG, CHANNEL_GET_CONFIG,
CHANNEL_CHECK_VERSION, CHANNEL_LOG,
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 {DATA_DIR, wrapText} from "../common/utils"; import { DATA_DIR, qqPkgInfo } from '../common/utils'
import { import {
friendRequests, friendRequests,
getFriend, getFriend,
getGroup, getGroup,
getGroupMember, getGroupMember,
llonebotError, groups,
refreshGroupMembers, llonebotError,
selfInfo, uidMaps refreshGroupMembers,
} from "../common/data"; selfInfo,
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook"; uidMaps,
import {OB11Constructor} from "../onebot11/constructor"; } from '../common/data'
import {ChatType, FriendRequestNotify, GroupNotifies, GroupNotifyTypes, RawMessage} from "../ntqqapi/types"; import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import {ob11HTTPServer} from "../onebot11/server/http"; import { OB11Constructor } from '../onebot11/constructor'
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; import {
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; ChatType,
import {postOB11Event} from "../onebot11/server/postOB11Event"; FriendRequestNotify,
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; GroupMemberRole,
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent"; GroupNotifies,
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest"; GroupNotifyTypes,
import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest"; RawMessage,
import * as path from "node:path"; } from '../ntqqapi/types'
import {dbUtil} from "../common/db"; import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import {setConfig} from "./setConfig"; import { OB11FriendRecallNoticeEvent } from '../onebot11/event/notice/OB11FriendRecallNoticeEvent'
import {NTQQUserApi} from "../ntqqapi/api/user"; import { OB11GroupRecallNoticeEvent } from '../onebot11/event/notice/OB11GroupRecallNoticeEvent'
import {NTQQGroupApi} from "../ntqqapi/api/group"; import { postOb11Event } from '../onebot11/server/post-ob11-event'
import {registerPokeHandler} from "../ntqqapi/external/ccpoke"; import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent"; import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
import {checkNewVersion, upgradeLLOneBot} from "../common/utils/upgrade"; import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import {log} from "../common/utils/log"; import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import {getConfigUtil} from "../common/config"; import * as path from 'node:path'
import {checkFfmpeg} from "../common/utils/video"; import { dbUtil } from '../common/db'
import { setConfig } from './setConfig'
import { NTQQUserApi } from '../ntqqapi/api/user'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { crychic } from '../ntqqapi/native/crychic'
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'
import '../ntqqapi/native/wrapper'
import { sentMessages } from '@/ntqqapi/api'
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_CHECK_VERSION, async (event, arg) => { ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => {
return checkNewVersion(); return checkNewVersion()
}); })
ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => { ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => {
return upgradeLLOneBot(); 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) => {
dialog dialog
.showOpenDialog({ .showOpenDialog({
title: "请选择ffmpeg", title: '请选择ffmpeg',
properties: ["openFile"], properties: ['openFile'],
buttonLabel: "确定", buttonLabel: '确定',
}) })
.then((result) => { .then((result) => {
log("选择文件", result); log('选择文件', result)
if (!result.canceled) { if (!result.canceled) {
const _selectPath = path.join(result.filePaths[0]); const _selectPath = path.join(result.filePaths[0])
resolve(_selectPath); resolve(_selectPath)
// let config = getConfigUtil().getConfig() // let config = getConfigUtil().getConfig()
// config.ffmpeg = path.join(result.filePaths[0]); // config.ffmpeg = path.join(result.filePaths[0]);
// getConfigUtil().setConfig(config); // getConfigUtil().setConfig(config);
} }
resolve("") resolve('')
}) })
.catch((err) => { .catch((err) => {
reject(err); reject(err)
});
}) })
try {
return await selectPath;
} catch (e) {
log("选择文件出错", e)
return ""
}
}) })
if (!fs.existsSync(DATA_DIR)) { try {
fs.mkdirSync(DATA_DIR, {recursive: true}); return await selectPath
} catch (e) {
log('选择文件出错', e)
return ''
} }
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { })
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) if (!fs.existsSync(DATA_DIR)) {
llonebotError.ffmpegError = ffmpegOk ? "" : "没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常" fs.mkdirSync(DATA_DIR, { recursive: true })
let {httpServerError, wsServerError, otherError, ffmpegError} = llonebotError; }
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
error = error.replace("\n\n", "\n") const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
error = error.trim(); llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常'
return error; let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
}) let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { error = error.replace('\n\n', '\n')
const config = getConfigUtil().getConfig() error = error.trim()
return config; log('查询llonebot错误信息', error)
}) return error
ipcMain.on(CHANNEL_SET_CONFIG, (event, config: Config) => { })
setConfig(config).then(); ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
}) const config = getConfigUtil().getConfig()
return config
})
ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => {
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) => {
log(arg); log(arg)
})
async function postReceiveMsg(msgList: RawMessage[]) {
const { debug, reportSelfMessage } = getConfigUtil().getConfig()
for (let message of msgList) {
// 过滤启动之前的消息
// log('收到新消息', message);
if (parseInt(message.msgTime) < startTime / 1000) {
continue
}
// log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message)
// }
OB11Constructor.message(message)
.then((msg) => {
if (!debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin)
}
postOb11Event(msg)
// log("post msg", msg)
})
.catch((e) => log('constructMessage error: ', e.stack.toString()))
OB11Constructor.GroupEvent(message).then((groupEvent) => {
if (groupEvent) {
// log("post group event", groupEvent);
postOb11Event(groupEvent)
}
})
OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
if (friendAddEvent) {
// log("post friend add event", friendAddEvent);
postOb11Event(friendAddEvent)
}
})
}
}
async function startReceiveHook() {
startHook().then()
if (getConfigUtil().getConfig().enablePoke) {
if ( qqPkgInfo.buildVersion > '23873'){
log(`当前版本${qqPkgInfo.buildVersion}不支持发送戳一戳模块`)
return
}
crychic.loadNode()
crychic.registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id))
}
else {
pokeEvent = new OB11FriendPokeEvent(parseInt(id))
}
postOb11Event(pokeEvent)
})
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList)
} catch (e) {
log('report message error: ', e.stack.toString())
}
}) })
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
const sentMessage = sentMessages[message.msgId]
if (sentMessage){
Object.assign(sentMessage, message)
}
log('message update', message.msgId, message)
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
continue
}
recallMsgIds.push(message.msgId)
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then()
message.msgShortId = oriMessage.msgShortId
OB11Constructor.RecallEvent(message).then((recallEvent) => {
if (recallEvent) {
log('post recall event', recallEvent)
postOb11Event(recallEvent)
}
})
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
dbUtil.updateMsg(message).then()
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
const { reportSelfMessage } = getConfigUtil().getConfig()
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord])
} catch (e) {
log('report self message error: ', e.stack.toString())
}
})
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notify: GroupNotifies
try {
notify = await NTQQGroupApi.getGroupNotifies()
} catch (e) {
// log("获取群通知详情失败", e);
return
}
async function postReceiveMsg(msgList: RawMessage[]) { const notifies = notify.notifies.slice(0, payload.unreadCount)
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); // log("获取群通知详情完成", notifies, payload);
for (let message of msgList) {
// log("收到新消息", message.msgId, message.msgSeq) for (const notify of notifies) {
// if (message.senderUin !== selfInfo.uin){ try {
message.msgShortId = await dbUtil.addMsg(message); notify.time = Date.now()
// const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
// if (notifyTime < startTime) {
// continue;
// } // }
let existNotify = await dbUtil.getGroupNotify(notify.seq)
OB11Constructor.message(message).then((msg) => { if (existNotify) {
if (debug) { continue
msg.raw = message;
} else {
if (msg.message.length === 0) {
return
}
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin);
}
postOB11Event(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.stack.toString()));
OB11Constructor.GroupEvent(message).then(groupEvent => {
if (groupEvent) {
// log("post group event", groupEvent);
postOB11Event(groupEvent);
}
})
}
}
async function startReceiveHook() {
if (getConfigUtil().getConfig().enablePoke) {
registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id));
} else {
pokeEvent = new OB11FriendPokeEvent(parseInt(id));
}
postOB11Event(pokeEvent);
})
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList);
} catch (e) {
log("report message error: ", e.stack.toString());
} }
}) log('收到群通知', notify)
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => { await dbUtil.addGroupNotify(notify)
for (const message of payload.msgList) { // let member2: GroupMember;
// log("message update", message.sendStatus, message.msgId, message.msgSeq) // if (notify.user2.uid) {
if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断 // member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// 撤回消息上报 // }
const oriMessage = await dbUtil.getMsgByLongId(message.msgId) if (
if (!oriMessage) { [GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET, GroupNotifyTypes.ADMIN_UNSET_OTHER].includes(
continue notify.type,
} )
oriMessage.recallTime = message.recallTime ) {
dbUtil.updateMsg(oriMessage).then(); const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid)
if (message.chatType == ChatType.friend) { log('有管理员变动通知')
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId); refreshGroupMembers(notify.group.groupCode).then()
postOB11Event(friendRecallEvent); let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
} else if (message.chatType == ChatType.group) { groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode)
let operatorId = message.senderUin log('开始获取变动的管理员')
for (const element of message.elements) { if (member1) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid log('变动管理员获取成功')
const operator = await getGroupMember(message.peerUin, operatorUid) groupAdminNoticeEvent.user_id = parseInt(member1.uin)
operatorId = operator.uin groupAdminNoticeEvent.sub_type = [
} GroupNotifyTypes.ADMIN_UNSET,
const groupRecallEvent = new OB11GroupRecallNoticeEvent( GroupNotifyTypes.ADMIN_UNSET_OTHER,
parseInt(message.peerUin), ].includes(notify.type)
parseInt(message.senderUin), ? 'unset'
parseInt(operatorId), : 'set'
oriMessage.msgShortId // member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
) postOb11Event(groupAdminNoticeEvent, true)
}
postOB11Event(groupRecallEvent); else {
} log('获取群通知的成员信息失败', notify, getGroup(notify.group.groupCode))
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了 }
continue }
else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify)
try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid)
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'
} }
dbUtil.updateMsg(message).then(); 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, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => { log('有加群请求')
const {reportSelfMessage} = getConfigUtil().getConfig(); let groupRequestEvent = new OB11GroupRequestEvent()
if (!reportSelfMessage) { groupRequestEvent.group_id = parseInt(notify.group.groupCode)
return let requestQQ = uidMaps[notify.user1.uid]
} if (!requestQQ) {
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord]);
} catch (e) {
log("report self message error: ", e.stack.toString());
}
})
registerReceiveHook<{
"doubt": boolean,
"oldestUnreadSeq": string,
"unreadCount": number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notify: GroupNotifies;
try { try {
notify = await NTQQGroupApi.getGroupNotifies(); requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
} catch (e) { } catch (e) {
// log("获取群通知详情失败", e); log('获取加群人QQ号失败', e)
return
} }
}
const notifies = notify.notifies.slice(0, payload.unreadCount) groupRequestEvent.user_id = parseInt(requestQQ) || 0
// log("获取群通知详情完成", notifies, payload); groupRequestEvent.sub_type = 'add'
groupRequestEvent.comment = notify.postscript
for (const notify of notifies) { groupRequestEvent.flag = notify.seq
try { if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
notify.time = Date.now(); // groupRequestEvent.sub_type = 'invite'
// const notifyTime = parseInt(notify.seq) / 1000 let invitorQQ = uidMaps[notify.user2.uid]
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`); if (!invitorQQ) {
// if (notifyTime < startTime) { try {
// continue; let invitor = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))
// } groupRequestEvent.invitor_id = parseInt(invitor.uin)
let existNotify = await dbUtil.getGroupNotify(notify.seq); } catch (e) {
if (existNotify) { groupRequestEvent.invitor_id = 0
continue log('获取邀请人QQ号失败', e)
} }
log("收到群通知", notify);
await dbUtil.addGroupNotify(notify);
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) {
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid);
log("有管理员变动通知");
refreshGroupMembers(notify.group.groupCode).then()
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode);
log("开始获取变动的管理员")
if (member1) {
log("变动管理员获取成功")
groupAdminNoticeEvent.user_id = parseInt(member1.uin);
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "unset" : "set";
postOB11Event(groupAdminNoticeEvent, true);
} else {
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
}
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT) {
// log("有成员退出通知");
// const member1 = await getGroupMember(notify.group.groupCode, null, notify.user1.uid);
// let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin))
// postEvent(groupDecreaseEvent, true);
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log("有加群请求");
let groupRequestEvent = new OB11GroupRequestEvent();
groupRequestEvent.group_id = parseInt(notify.group.groupCode);
let requestQQ = ""
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin;
} catch (e) {
log("获取加群人QQ号失败", e)
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0;
groupRequestEvent.sub_type = "add"
groupRequestEvent.comment = notify.postscript;
groupRequestEvent.flag = notify.seq;
postOB11Event(groupRequestEvent);
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log("收到邀请我加群通知")
let groupInviteEvent = new OB11GroupRequestEvent();
groupInviteEvent.group_id = parseInt(notify.group.groupCode);
let user_id = (await getFriend(notify.user2.uid))?.uin
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id);
groupInviteEvent.sub_type = "invite";
groupInviteEvent.flag = notify.seq;
postOB11Event(groupInviteEvent);
}
} catch (e) {
log("解析群通知失败", e.stack.toString());
}
} }
}
} else if (payload.doubt) { postOb11Event(groupRequestEvent)
// 可能有群管理员变动
} }
}) else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => { let groupInviteEvent = new OB11GroupRequestEvent()
for (const req of payload.data.buddyReqs) { groupInviteEvent.group_id = parseInt(notify.group.groupCode)
if (req.isUnread && !friendRequests[req.sourceId] && (parseInt(req.reqTime) > startTime / 1000)) { let user_id = uidMaps[notify.user2.uid]
friendRequests[req.sourceId] = req; if (!user_id) {
log("有新的好友请求", req); user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
let friendRequestEvent = new OB11FriendRequestEvent(); }
try { groupInviteEvent.user_id = parseInt(user_id)
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid) groupInviteEvent.sub_type = 'invite'
friendRequestEvent.user_id = parseInt(requester.uin); // groupInviteEvent.invitor_id = parseInt(user_id)
} catch (e) { groupInviteEvent.flag = notify.seq
log("获取加好友者QQ号失败", e); postOb11Event(groupInviteEvent)
}
friendRequestEvent.flag = req.sourceId.toString();
friendRequestEvent.comment = req.extWords;
postOB11Event(friendRequestEvent);
}
} }
} catch (e) {
log('解析群通知失败', e.stack.toString())
}
}
}
else if (payload.doubt) {
// 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
let flag = req.friendUid + req.reqTime
if (req.isUnread && parseInt(req.reqTime) > startTime / 1000) {
friendRequests[flag] = req
log('有新的好友请求', req)
let friendRequestEvent = new OB11FriendRequestEvent()
try {
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
friendRequestEvent.flag = flag
friendRequestEvent.comment = req.extWords
postOb11Event(friendRequestEvent)
}
}
})
}
let startTime = 0 // 毫秒
async function start() {
log('llonebot pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB){
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
llonebotError.otherError = ''
startTime = Date.now()
dbUtil.getReceivedTempUinMap().then((m) => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key
}
})
try{
log('start get groups')
const _groups = await NTQQGroupApi.getGroups()
log('_groups', _groups)
await Promise.all(
_groups.map(async (group) => {
try {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = members
groups.push(group)
} catch (e) {
log('获取群成员失败', e)
}
}) })
)
}
catch (e) {
log('获取群列表失败', e)
}
finally {
log('start activate group member info')
NTQQGroupApi.activateMemberInfoChange().then().catch(log)
NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
} }
let startTime = 0;
async function start() { if (config.ob11.enableHttp) {
log("llonebot pid", process.pid) ob11HTTPServer.start(config.ob11.httpPort)
llonebotError.otherError = ""; }
startTime = Date.now(); if (config.ob11.enableWs) {
dbUtil.getReceivedTempUinMap().then(m=>{ ob11WebsocketServer.start(config.ob11.wsPort)
for (const [key, value] of Object.entries(m)) { }
uidMaps[value] = key; if (config.ob11.enableWsReverse) {
} ob11ReverseWebsockets.start()
}) }
startReceiveHook().then(); if (config.ob11.enableHttpHeart) {
NTQQGroupApi.getGroups(true).then() httpHeart.start()
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
}
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
}
log("LLOneBot start")
} }
let getSelfNickCount = 0; log('LLOneBot start')
const init = async () => { }
let getSelfNickCount = 0
const init = async () => {
try {
log('start get self info')
const _ = await NTQQUserApi.getSelfInfo()
log('get self info api result:', _)
Object.assign(selfInfo, _)
selfInfo.nick = selfInfo.uin
} catch (e) {
log('retry get self info', e)
}
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) {
async function getUserNick() {
try { try {
log("start get self info") getSelfNickCount++
const _ = await NTQQUserApi.getSelfInfo(); const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
log("get self info api result:", _); log('self info', userInfo)
Object.assign(selfInfo, _); if (userInfo) {
selfInfo.nick = selfInfo.uin; selfInfo.nick = userInfo.nick
return
}
} catch (e) { } catch (e) {
log("retry get self info", e); log('get self nickname failed', e.stack)
} }
log("self info", selfInfo); if (getSelfNickCount < 10) {
if (selfInfo.uin) { return setTimeout(getUserNick, 1000)
try {
const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid));
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
} else {
getSelfNickCount++;
if (getSelfNickCount < 10) {
return setTimeout(init, 1000);
}
}
} catch (e) {
log("get self nickname failed", e.toString());
return setTimeout(init, 1000);
}
start().then();
} else {
setTimeout(init, 1000)
} }
} }
setTimeout(init, 1000);
}
getUserNick().then()
start().then()
}
else {
setTimeout(init, 1000)
}
}
setTimeout(init, 1000)
}
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) { if (selfInfo.uid) {
return return
} }
log("window create", window.webContents.getURL().toString()) mainWindow = window
try { log('window create', window.webContents.getURL().toString())
hookNTQQApiCall(window); try {
hookNTQQApiReceive(window); hookNTQQApiCall(window)
} catch (e) { hookNTQQApiReceive(window)
log("LLOneBot hook error: ", e.toString()) } catch (e) {
} log('LLOneBot hook error: ', e.toString())
}
} }
try { try {
onLoad(); onLoad()
} catch (e: any) { } catch (e: any) {
console.log(e.toString()) console.log(e.toString())
} }
// 这两个函数都是可选的 // 这两个函数都是可选的
export { export { onBrowserWindowCreated }
onBrowserWindowCreated
}

View File

@@ -1,56 +1,67 @@
import {Config} from "../common/types"; import { Config } from '../common/types'
import {ob11HTTPServer} from "../onebot11/server/http"; import { httpHeart, 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 { getConfigUtil } from '../common/config'
import {checkFfmpeg} from "../common/utils"; 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)
} }
// 判断是否启用或关闭HTTP服务 // 判断是否启用或关闭HTTP服务
if (!config.ob11.enableHttp) { if (!config.ob11.enableHttp) {
ob11HTTPServer.stop(); ob11HTTPServer.stop()
} else {
ob11HTTPServer.start(config.ob11.httpPort)
}
// 正向ws端口变化重启服务
if (config.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(config.ob11.wsPort)
llonebotError.wsServerError = ''
}
// 判断是否启用或关闭正向ws
if (config.ob11.enableWs != oldConfig.ob11.enableWs) {
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort)
} else { } else {
ob11HTTPServer.start(config.ob11.httpPort); ob11WebsocketServer.stop()
}
// 正向ws端口变化重启服务
if (config.ob11.wsPort != oldConfig.ob11.wsPort) {
ob11WebsocketServer.restart(config.ob11.wsPort);
llonebotError.wsServerError = ''
}
// 判断是否启用或关闭正向ws
if (config.ob11.enableWs != oldConfig.ob11.enableWs) {
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
} else {
ob11WebsocketServer.stop();
}
}
// 判断是否启用或关闭反向ws
if (config.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
} else {
ob11ReverseWebsockets.stop();
}
} }
}
// 判断是否启用或关闭反向ws
if (config.ob11.enableWsReverse != oldConfig.ob11.enableWsReverse) {
if (config.ob11.enableWsReverse) { if (config.ob11.enableWsReverse) {
// 判断反向ws地址有变化 ob11ReverseWebsockets.start()
if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) { } else {
ob11ReverseWebsockets.restart(); ob11ReverseWebsockets.stop()
} else {
for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
ob11ReverseWebsockets.restart();
break;
}
}
}
} }
checkFfmpeg(config.ffmpeg).then() }
} if (config.ob11.enableWsReverse) {
// 判断反向ws地址有变化
if (config.ob11.wsHosts.length != oldConfig.ob11.wsHosts.length) {
log('反向ws地址有变化, 重启反向ws服务')
ob11ReverseWebsockets.restart()
} else {
for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
log('反向ws地址有变化, 重启反向ws服务')
ob11ReverseWebsockets.restart()
break
}
}
}
}
if (config.ob11.enableHttpHeart) {
// 启动http心跳
httpHeart.start()
} else {
// 关闭http心跳
httpHeart.stop()
}
log('old config', oldConfig)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
}

View File

@@ -1,235 +1,318 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import { import {
CacheFileList, CacheFileList,
CacheFileListItem, CacheFileListItem,
CacheFileType, CacheFileType,
CacheScanResult, CacheScanResult,
ChatCacheList, ChatCacheList,
ChatCacheListItemBasic, ChatCacheListItemBasic,
ChatType, ChatType,
ElementType ElementType,
} from "../types"; IMAGE_HTTP_HOST,
import path from "path"; IMAGE_HTTP_HOST_NT, PicElement,
import fs from "fs"; } from '../types'
import {ReceiveCmdS} from "../hook"; import path from 'path'
import {log} from "../../common/utils/log"; import fs from 'fs'
import { ReceiveCmdS } from '../hook'
import { log } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import { Peer } from '@/ntqqapi/api/msg'
export class NTQQFileApi { export class NTQQFileApi {
static async getFileType(filePath: string) { static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> {
return await callNTQQApi<{ ext: string }>({ return (await wrapperApi.NodeIQQNTWrapperSession.getRichMediaService().getVideoPlayUrlV2(peer,
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath] msgId,
}) elementId,
} 0,
{ downSourceType: 1, triggerType: 1 })).urlResult?.domainUrl[0]?.url;
}
static async getFileType(filePath: string) {
return await callNTQQApi<{ ext: string }>({
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,
methodName: NTQQApiMethod.FILE_MD5, methodName: NTQQApiMethod.FILE_MD5,
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,
methodName: NTQQApiMethod.FILE_COPY, methodName: NTQQApiMethod.FILE_COPY,
args: [{ args: [
fromPath: filePath, {
toPath: destPath fromPath: filePath,
}] toPath: destPath,
}) },
} ],
})
}
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) {
ext = "." + ext ext = '.' + ext
} else { } else {
ext = "" ext = ''
}
let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf(".") === -1) {
fileName += ext;
}
const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: elementType,
elementSubType: 0,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ""
}
}]
})
log("media path", mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath);
const fileSize = await NTQQFileApi.getFileSize(filePath);
return {
md5,
fileName,
path: mediaPath,
fileSize
}
} }
let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) {
fileName += ext
}
const mediaPath = await callNTQQApi<string>({
methodName: NTQQApiMethod.MEDIA_FILE_PATH,
args: [
{
path_info: {
md5HexStr: md5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: '',
},
},
],
})
log('media path', mediaPath)
await NTQQFileApi.copyFile(filePath, mediaPath)
const fileSize = await NTQQFileApi.getFileSize(filePath)
return {
md5,
fileName,
path: mediaPath,
fileSize,
ext,
}
}
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, isFile: boolean = false) { static async downloadMedia(
// 用于下载收到的消息中的图片等 msgId: string,
if (sourcePath && fs.existsSync(sourcePath)) { chatType: ChatType,
return sourcePath peerUid: string,
} elementId: string,
const apiParams = [ thumbPath: string,
{ sourcePath: string,
getReq: { force: boolean = false,
fileModelId: "0", ) {
downloadSourceType: 0, // 用于下载收到的消息中的图片等
triggerType: 1, if (sourcePath && fs.existsSync(sourcePath)) {
msgId: msgId, if (force) {
chatType: chatType, fs.unlinkSync(sourcePath)
peerUid: peerUid, } else {
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => {
log("media 下载完成判断", payload.notifyInfo.msgId, msgId);
return payload.notifyInfo.msgId == msgId;
}
})
return sourcePath return sourcePath
}
} }
const apiParams = [
{
getReq: {
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath,
},
},
null,
]
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string; msgId: string } }) => {
log('media 下载完成判断', payload.notifyInfo.msgId, msgId)
return payload.notifyInfo.msgId == msgId
},
})
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],
})
}
static async getImageUrl(picElement: PicElement, chatType: ChatType) {
const isPrivateImage = chatType !== ChatType.group
const url = picElement.originImageUrl // 没有域名
const md5HexStr = picElement.md5HexStr
const fileMd5 = picElement.md5HexStr
const fileUuid = picElement.fileUuid
if (url) {
if (url.startsWith('/download')) {
// console.log('rkey', rkey);
if (url.includes('&rkey=')) {
return IMAGE_HTTP_HOST_NT + url
}
const rkeyData = await rkeyManager.getRkey();
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url
}
} else if (fileMd5 || md5HexStr) {
// 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
} }
log('图片url获取失败', picElement)
return ''
}
} }
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,
args: [{ args: [
isSilent {
}, null] isSilent,
}); },
} null,
],
})
}
static getCacheSessionPathList() { static getCacheSessionPathList() {
return callNTQQApi<{ return callNTQQApi<
key: string, {
value: string key: string
}[]>({ value: string
className: NTQQApiClass.OS_API, }[]
methodName: NTQQApiMethod.CACHE_PATH_SESSION, >({
}); className: NTQQApiClass.OS_API,
} 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>({
methodName: NTQQApiMethod.CACHE_CLEAR, // TODO: 目前还不知道真正的返回值是什么
args: [{ methodName: NTQQApiMethod.CACHE_CLEAR,
keys: cacheKeys args: [
}, null] {
}); keys: cacheKeys,
} },
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,
args: [{ args: [
pathMap: {...pathMap}, {
}, null] pathMap: { ...pathMap },
}); },
} null,
],
})
}
static scanCache() { static scanCache() {
callNTQQApi<GeneralCallResult>({ callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.CACHE_SCAN_FINISH, methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true, classNameIsRegister: true,
}).then(); }).then()
return callNTQQApi<CacheScanResult>({ return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN, methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null], args: [null, null],
timeoutSecond: 300, timeoutSecond: 300,
}); })
} }
static getHotUpdateCachePath() { static getHotUpdateCachePath() {
return callNTQQApi<string>({ return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API, className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE,
}); })
} }
static getDesktopTmpPath() { static getDesktopTmpPath() {
return callNTQQApi<string>({ return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API, className: NTQQApiClass.BUSINESS_API,
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>({
methodName: NTQQApiMethod.CACHE_CHAT_GET, methodName: NTQQApiMethod.CACHE_CHAT_GET,
args: [{ args: [
chatType: type, {
pageSize, chatType: type,
order: 1, pageSize,
pageIndex order: 1,
}, null] pageIndex,
}).then(list => res(list)) },
.catch(e => rej(e)); null,
}); ],
} })
.then((list) => res(list))
.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 }
return callNTQQApi<CacheFileList>({ return callNTQQApi<CacheFileList>({
methodName: NTQQApiMethod.CACHE_FILE_GET, methodName: NTQQApiMethod.CACHE_FILE_GET,
args: [{ args: [
fileType: fileType, {
restart: true, fileType: fileType,
pageSize: pageSize, restart: true,
order: 1, pageSize: pageSize,
lastRecord: _lastRecord, order: 1,
}, null] lastRecord: _lastRecord,
}) },
} 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,
args: [{ args: [
chats, {
fileKeys chats,
}, null] fileKeys,
}); },
} null,
],
} })
}
}

View File

@@ -1,61 +1,65 @@
import {Friend, FriendRequest} from "../types"; import { Friend, FriendRequest } from '../types'
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import {friendRequests} from "../../common/data"; import { friendRequests } from '../../common/data'
import { log } from '../../common/utils'
export class NTQQFriendApi{ export class NTQQFriendApi {
static async getFriends(forced = false) { static async getFriends(forced = false) {
const data = await callNTQQApi<{ const data = await callNTQQApi<{
data: { data: {
categoryId: number, categoryId: number
categroyName: string, categroyName: string
categroyMbCount: number, categroyMbCount: number
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>( }>({
{ methodName: NTQQApiMethod.FRIENDS,
methodName: NTQQApiMethod.FRIENDS, args: [{ force_update: forced }, undefined],
args: [{force_update: forced}, undefined], cbCmd: ReceiveCmdS.FRIENDS,
cbCmd: ReceiveCmdS.FRIENDS afterFirstCmd: false,
}) })
let _friends: Friend[] = []; // log('获取好友列表', data)
for (const fData of data.data) { let _friends: Friend[] = []
_friends.push(...fData.buddyList) for (const fData of data.data) {
} _friends.push(...fData.buddyList)
return _friends
} }
static async likeFriend(uid: string, count = 1) { return _friends
return await callNTQQApi<GeneralCallResult>({ }
methodName: NTQQApiMethod.LIKE_FRIEND, static async likeFriend(uid: string, count = 1) {
args: [{ return await callNTQQApi<GeneralCallResult>({
doLikeUserInfo: { methodName: NTQQApiMethod.LIKE_FRIEND,
friendUid: uid, args: [
sourceId: 71, {
doLikeCount: count, doLikeUserInfo: {
doLikeTollCount: 0 friendUid: uid,
} sourceId: 71,
}, null] doLikeCount: count,
}) doLikeTollCount: 0,
},
},
null,
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
throw `flat: ${flag}, 对应的好友请求不存在`
} }
static async handleFriendRequest(sourceId: number, accept: boolean,) { const result = await callNTQQApi<GeneralCallResult>({
const request: FriendRequest = friendRequests[sourceId] methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
if (!request) { args: [
throw `sourceId ${sourceId}, 对应的好友请求不存在` {
} approvalInfo: {
const result = await callNTQQApi<GeneralCallResult>({ friendUid: request.friendUid,
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST, reqTime: request.reqTime,
args: [ accept,
{ },
"approvalInfo": { },
"friendUid": request.friendUid, ],
"reqTime": request.reqTime, })
accept delete friendRequests[flag]
} return result
} }
] }
})
delete friendRequests[sourceId];
return result;
}
}

View File

@@ -1,206 +1,285 @@
import {ReceiveCmdS} from "../hook"; 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 { deleteGroup, uidMaps } from '../../common/data'
import {BrowserWindow} from "electron"; import { dbUtil } from '../../common/db'
import {dbUtil} from "../../common/db"; import { log } from '../../common/utils/log'
import {log} from "../../common/utils/log"; import { NTQQWindowApi, NTQQWindows } from './window'
import {NTQQWindowApi, NTQQWindows} from "./window";
export class NTQQGroupApi{ export class NTQQGroupApi {
static async getGroups(forced = false) {
let cbCmd = ReceiveCmdS.GROUPS static async activateMemberListChange(){
if (process.platform != "win32") { return await callNTQQApi<GeneralCallResult>({
cbCmd = ReceiveCmdS.GROUPS_UNIX methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async activateMemberInfoChange(){
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE,
classNameIsRegister: true,
args: [],
})
}
static async getGroupAllInfo(groupCode: string, source: number=4){
return await callNTQQApi<GeneralCallResult & Group>({
methodName: NTQQApiMethod.GET_GROUP_ALL_INFO,
args: [
{
groupCode,
source
},
null,
],
})
}
static async getGroups(forced = false) {
// let cbCmd = ReceiveCmdS.GROUPS
// if (process.platform != 'win32') {
// cbCmd = ReceiveCmdS.GROUPS_STORE
// }
const result = await callNTQQApi<{
updateType: number
groupList: Group[]
}>({
methodName: NTQQApiMethod.GROUPS,
args: [{ force_update: forced }, undefined],
cbCmd: [ReceiveCmdS.GROUPS, ReceiveCmdS.GROUPS_STORE],
afterFirstCmd: false,
})
log('get groups result', result)
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> {
const sceneId = await callNTQQApi({
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [
{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow',
},
],
})
// log("get group member sceneId", sceneId);
try {
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [
{
sceneId: sceneId,
num: num,
},
null,
],
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
const values = result.result.infos.values()
const members: GroupMember[] = Array.from(values)
for (const member of members) {
uidMaps[member.uid] = member.uin
}
// log(uidMaps);
// log("members info", values);
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
return []
}
}
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean=false) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GROUP_MEMBERS_INFO,
args: [
{
forceUpdate,
groupCode,
uids
},
null,
],
})
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: 14 }, null],
})
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies()
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow,
[],
ReceiveCmdS.GROUP_NOTIFY,
)
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
doubt: false,
operateMsg: {
operateType: operateType, // 2 拒绝
targetMsg: {
seq: seq, // 通知序列号
type: notify.type,
groupCode: notify.group.groupCode,
postscript: reason,
},
},
},
null,
],
})
}
static async quitGroup(groupQQ: string) {
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
if (result.result === 0) {
deleteGroup(groupQQ)
}
return result
}
static async kickMember(
groupQQ: string,
kickUids: string[],
refuseForever: boolean = false,
kickReason: string = '',
) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
},
],
})
}
static async banMember(groupQQ: string, memList: Array<{ uid: string; timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
},
],
})
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
NTQQGroupApi.activateMemberListChange().then().catch(log)
const res = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
NTQQGroupApi.getGroupMembersInfo(groupQQ, [memberUid], true).then().catch(log)
return res;
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
}
static async getGroupAtAllRemainCount(groupCode: string) {
return await callNTQQApi<
GeneralCallResult & {
atInfo: {
canAtAll: boolean
RemainAtAllCountForUin: number
RemainAtAllCountForGroup: number
atTimesMsg: string
canNotAtAllMsg: ''
} }
const result = await callNTQQApi<{ }
updateType: number, >({
groupList: Group[] methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT,
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) args: [
return result.groupList {
} groupCode,
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> { },
const sceneId = await callNTQQApi({ null,
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE, ],
args: [{ })
groupCode: groupQQ, }
scene: "groupMemberList_MainWindow"
}]
})
// log("get group member sceneId", sceneId);
try {
const result = await callNTQQApi<{
result: { infos: any }
}>({
methodName: NTQQApiMethod.GROUP_MEMBERS,
args: [{
sceneId: sceneId,
num: num
},
null
]
})
// log("members info", typeof result.result.infos, Object.keys(result.result.infos))
const values = result.result.infos.values()
const members: GroupMember[] = Array.from(values) // 头衔不可用
for (const member of members) { static async setGroupTitle(groupQQ: string, uid: string, title: string) {
uidMaps[member.uid] = member.uin; return await callNTQQApi<GeneralCallResult>({
} methodName: NTQQApiMethod.SET_GROUP_TITLE,
// log(uidMaps); args: [
// log("members info", values); {
log(`get group ${groupQQ} members success`) groupCode: groupQQ,
return members uid,
} catch (e) { title,
log(`get group ${groupQQ} members failed`, e) },
return [] null,
} ],
} })
static async getGroupNotifies() { }
// 获取管理员变更 static publishGroupBulletin(groupQQ: string, title: string, content: string) {}
// 加群通知,退出通知,需要管理员权限 }
callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
}).then()
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
args: [
{"doubt": false, "startSeq": "", "number": 14},
null
]
});
}
static async getGroupIgnoreNotifies() {
await NTQQGroupApi.getGroupNotifies();
return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow,[], ReceiveCmdS.GROUP_NOTIFY);
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = await dbUtil.getGroupNotify(seq)
if (!notify) {
throw `${seq}对应的加群通知不存在`
}
// delete groupNotifies[seq];
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
"doubt": false,
"operateMsg": {
"operateType": operateType, // 2 拒绝
"targetMsg": {
"seq": seq, // 通知序列号
"type": notify.type,
"groupCode": notify.group.groupCode,
"postscript": reason
}
}
},
null
]
});
}
static async quitGroup(groupQQ: string) {
await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args: [
{"groupCode": groupQQ},
null
]
})
}
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
return await callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.KICK_MEMBER,
args: [
{
groupCode: groupQQ,
kickUids,
refuseForever,
kickReason,
}
]
}
)
}
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>(
{
methodName: NTQQApiMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
}
]
}
)
}
static async banGroup(groupQQ: string, shutUp: boolean) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp
}, null
]
})
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName
}, null
]
})
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role
}, null
]
})
}
static async setGroupName(groupQQ: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName
}, null
]
})
}
// 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_GROUP_TITLE,
args: [
{
groupCode: groupQQ,
uid,
title
}, null
]
})
}
static publishGroupBulletin(groupQQ: string, title: string, content: string) {
}
}

View File

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

View File

@@ -1,200 +1,296 @@
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 {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 { log } from '../../common/utils/log'
import {sleep} from "../../common/utils/helper"; import { sleep } from '../../common/utils/helper'
import {isQQ998} from "../../common/utils"; 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: callbackFunc
export let sentMessages: Record<string, RawMessage> = {} // msgId: RawMessage
export interface Peer { export interface Peer {
chatType: ChatType chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串 peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: "" guildId?: ''
}
async function sendWaiter(peer: Peer, waitComplete = true, timeout: number = 10000) {
// 等待上一个相同的peer发送完
const peerUid = peer.peerUid
let checkLastSendUsingTime = 0
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw '发送超时'
}
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500)
checkLastSendUsingTime += 500
return await waitLastSend()
}
else {
return
}
}
await waitLastSend()
let sentMessage: RawMessage = null
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid]
sentMessage = rawMessage
sentMessages[rawMessage.msgId] = rawMessage
}
let checkSendCompleteUsingTime = 0
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if (sentMessage.sendStatus == 2) {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
}
else {
delete sentMessages[sentMessage.msgId]
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw '发送超时'
}
await sleep(500)
return await checkSendComplete()
}
return checkSendComplete()
} }
export class NTQQMsgApi { export class NTQQMsgApi {
static async activateGroupChat(groupCode: string) { static enterOrExitAIO(peer: Peer, enter: boolean) {
// await this.fetchRecentContact(); return callNTQQApi<GeneralCallResult>({
// await sleep(500); methodName: NTQQApiMethod.ENTER_OR_EXIT_AIO,
return await callNTQQApi<GeneralCallResult>({ args: [
methodName: NTQQApiMethod.ADD_ACTIVE_CHAT, {
args: [{peer:{peerUid: groupCode, chatType: ChatType.group}, cnt: 20}, null] "info_list": [
})
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: isQQ998 ? NTQQApiMethod.HISTORY_MSG_998 : NTQQApiMethod.HISTORY_MSG,
args: [{
peer,
msgId,
cnt: count,
queryOrder: true,
}, null]
})
}
static async fetchRecentContact(){
await callNTQQApi({
methodName: NTQQApiMethod.RECENT_CONTACT,
args: [
{
fetchParam: {
anchorPointContact: {
contactId: '',
sortField: '',
pos: 0,
},
relativeMoveCount: 0,
listType: 2, // 1普通消息2群助手内的消息
count: 200,
fetchOld: true,
},
}
]
})
}
static async recallMsg(peer: Peer, msgIds: string[]) {
return await callNTQQApi({
methodName: NTQQApiMethod.RECALL_MSG,
args: [{
peer,
msgIds
}, null]
})
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[],
waitComplete = true, timeout = 10000) {
const peerUid = peer.peerUid
// 等待上一个相同的peer发送完
let checkLastSendUsingTime = 0;
const waitLastSend = async () => {
if (checkLastSendUsingTime > timeout) {
throw ("发送超时")
}
let lastSending = sendMessagePool[peer.peerUid]
if (lastSending) {
// log("有正在发送的消息,等待中...")
await sleep(500);
checkLastSendUsingTime += 500;
return await waitLastSend();
} else {
return;
}
}
await waitLastSend();
let sentMessage: RawMessage = null;
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => {
delete sendMessagePool[peerUid];
sentMessage = rawMessage;
}
let checkSendCompleteUsingTime = 0;
const checkSendComplete = async (): Promise<RawMessage> => {
if (sentMessage) {
if (waitComplete) {
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) {
return sentMessage
}
} else {
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw ('发送超时')
}
await sleep(500)
return await checkSendComplete()
}
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
return await checkSendComplete()
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [
destPeer
],
commentElements: [],
msgAttributeInfos: new Map()
},
null,
]
})
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const msgInfos = msgIds.map(id => {
return {msgId: id, senderShowName: selfInfo.nick}
})
const apiArgs = [
{ {
msgInfos, peer,
srcContact: srcPeer, "option": enter ? 1 : 2
dstContact: destPeer, }
commentElements: [], ]
msgAttributeInfos: new Map() },
{
"send": true
},
],
})
}
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString()
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.EMOJI_LIKE,
args: [
{
peer,
msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
}
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 getMsgBoxInfo(peer: Peer) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.GET_MSG_BOX_INFO,
args: [
{
contacts: [
peer
],
},
null,
],
})
}
static async activateChat(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
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() {
await callNTQQApi({
methodName: NTQQApiMethod.RECENT_CONTACT,
args: [
{
fetchParam: {
anchorPointContact: {
contactId: '',
sortField: '',
pos: 0,
}, },
null, relativeMoveCount: 0,
] listType: 2, // 1普通消息2群助手内的消息
return await new Promise<RawMessage>((resolve, reject) => { count: 200,
let complete = false fetchOld: true,
setTimeout(() => { },
if (!complete) { },
reject("转发消息超时"); ],
} })
}, 5000) }
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
// log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true
await dbUtil.addMsg(msg)
resolve(msg)
log('转发消息成功:', payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
}
static async recallMsg(peer: Peer, msgIds: string[]) {
return await callNTQQApi({
methodName: NTQQApiMethod.RECALL_MSG,
args: [
{
peer,
msgIds,
},
null,
],
})
}
} static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
const waiter = sendWaiter(peer, waitComplete, timeout)
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [
{
msgId: '0',
peer,
msgElements,
msgAttributeInfos: new Map(),
},
null,
],
}).then()
return await waiter
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const waiter = sendWaiter(destPeer, true, 10000)
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FORWARD_MSG,
args: [
{
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
}).then().catch(log)
return await waiter
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const msgInfos = msgIds.map((id) => {
return { msgId: id, senderShowName: selfInfo.nick }
})
const apiArgs = [
{
msgInfos,
srcContact: srcPeer,
dstContact: destPeer,
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
]
return await new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject('转发消息超时')
}
}, 5000)
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find((ele) => ele.arkElement)
if (!arkElement) {
// log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true
await dbUtil.addMsg(msg)
resolve(msg)
log('转发消息成功:', payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs,
}).then((result) => {
log('转发消息结果:', result, apiArgs)
if (result.result !== 0) {
complete = true
reject('转发消息失败,' + JSON.stringify(result))
}
})
})
}
}

59
src/ntqqapi/api/rkey.ts Normal file
View File

@@ -0,0 +1,59 @@
//远端rkey获取
import { log } from '@/common/utils'
interface ServerRkeyData{
group_rkey: string;
private_rkey: string;
expired_time: number;
}
class RkeyManager {
serverUrl: string = '';
private rkeyData: ServerRkeyData = {
group_rkey: '',
private_rkey: '',
expired_time: 0
};
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
}
async getRkey(){
if (this.isExpired()) {
try {
await this.refreshRkey();
} catch (e) {
log('获取rkey失败', e);
}
}
return this.rkeyData;
}
isExpired(): boolean {
const now = new Date().getTime() / 1000;
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`);
return now > this.rkeyData.expired_time;
}
async refreshRkey(): Promise<any> {
//刷新rkey
this.rkeyData = await this.fetchServerRkey();
}
async fetchServerRkey(){
return new Promise<ServerRkeyData>((resolve, reject) => {
fetch(this.serverUrl)
.then(response => {
if (!response.ok) {
return reject(response.statusText); // 请求失败,返回错误信息
}
return response.json(); // 解析 JSON 格式的响应体
})
.then(data => {
resolve(data);
})
.catch(error => {
reject(error);
});
});
}
}
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey');

View File

@@ -1,56 +1,165 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import {SelfInfo, User} from "../types"; import { Group, SelfInfo, User } from '../types'
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {uidMaps} from "../../common/data"; import { selfInfo, uidMaps } from '../../common/data'
import { NTQQWindowApi, NTQQWindows } from './window'
import { cacheFunc, isQQ998, log, sleep } from '../../common/utils'
import { wrapperApi } from '@/ntqqapi/native/wrapper'
import * as https from 'https'
import { RequestUtil } from '@/common/utils/request'
let userInfoCache: Record<string, User> = {} // uid: User
export class NTQQUserApi{ export interface ClientKeyData extends GeneralCallResult {
static async setQQAvatar(filePath: string) { url: string;
return await callNTQQApi<GeneralCallResult>({ keyIndex: string;
methodName: NTQQApiMethod.SET_QQ_AVATAR, clientKey: string;
args: [{ expireTime: string;
path:filePath }
}, null],
timeoutSecond: 10 // 10秒不一定够 export class NTQQUserApi {
}); static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [
{
path: filePath,
},
null,
],
timeoutSecond: 10, // 10秒不一定够
})
}
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO,
timeoutSecond: 2,
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{ force: true, uids: [uid] }, undefined],
cbCmd: ReceiveCmdS.USER_INFO,
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
// this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
if (!withBizInfo) {
methodName = NTQQApiMethod.USER_DETAIL_INFO
}
const fetchInfo = async () => {
const result = await callNTQQApi<{ info: User }>({
methodName,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid,
},
null,
],
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
}
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000)
}
let userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
}
// return 'p_uin=o0xxx; p_skey=orXDssiGF8axxxxxxxxxxxxxx_; skey='
static async getCookieWithoutSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: 'qun.qq.com',
},
],
})
}
static async getQzoneCookies() {
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + selfInfo.uin + '%2Finfocenter&keyindex=19%27'
let cookies: { [key: string]: string; } = {};
try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl);
} catch (e: any) {
log('获取QZone Cookies失败', e)
cookies = {}
}
return cookies;
}
static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey()
if (clientKeyData.result !== 0) {
throw new Error('获取clientKey失败')
}
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex;
return (await RequestUtil.HttpsGetCookies(url))?.skey;
}
@cacheFunc(60 * 30 * 1000)
static async getCookies(domain: string) {
if (domain.endsWith("qzone.qq.com")) {
let data = (await NTQQUserApi.getQzoneCookies());
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + selfInfo.uin + '; uin=o' + selfInfo.uin;
return { bkn: NTQQUserApi.genBkn(data.p_skey), cookies: CookieValue };
}
const skey = await this.getSkey();
const pskey = (await this.getPSkey([domain])).get(domain);
if (!pskey || !skey) {
throw new Error('获取Cookies失败')
}
const bkn = NTQQUserApi.genBkn(skey)
const cookies = `p_skey=${pskey}; skey=${skey}; p_uin=o${selfInfo.uin}; uin=o${selfInfo.uin}`;
return { cookies, bkn }
}
static 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
} }
static async getSelfInfo() { return (hash & 0x7fffffff).toString()
return await callNTQQApi<SelfInfo>({ }
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{force: true, uids: [uid]}, undefined],
cbCmd: ReceiveCmdS.USER_INFO
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string) {
const result = await callNTQQApi<{ info: User }>({
methodName: NTQQApiMethod.USER_DETAIL_INFO,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid
},
null
]
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
}
} static async getPSkey(domains: string[]): Promise<Map<string, string>> {
const res = await wrapperApi.NodeIQQNTWrapperSession.getTipOffService().getPskey(domains, true)
if (res.result !== 0) {
throw new Error(`获取Pskey失败: ${res.errMsg}`)
}
return res.domainPskeyMap
}
static async getClientKey(): Promise<ClientKeyData> {
return await wrapperApi.NodeIQQNTWrapperSession.getTicketService().forceFetchClientKey('')
}
}

View File

@@ -1,87 +1,366 @@
import {net, session} from "electron"; import { WebGroupData, groups, selfInfo } from '@/common/data';
import {NTQQApi} from "../ntcall"; import { log } from '@/common/utils/log';
import {groups} from "../../common/data"; import { NTQQUserApi } from './user';
import {log} from "../../common/utils"; import { RequestUtil } from '@/common/utils/request';
export enum WebHonorType {
export class WebApi{ ALL = 'all',
private static bkn: string; TALKACTIVE = 'talkative',
private static skey: string; PERFROMER = 'performer',
private static pskey: string; LEGEND = 'legend',
private static cookie: string STORONGE_NEWBI = 'strong_newbie',
private defaultHeaders: Record<string,string> = { EMOTION = 'emotion'
"User-Agent": "QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0" }
export interface WebApiGroupMember {
uin: number
role: number
g: number
join_time: number
last_speak_time: number
lv: {
point: number
level: number
}
card: string
tags: string
flag: number
nick: string
qage: number
rm: number
}
interface WebApiGroupMemberRet {
ec: number
errcode: number
em: string
cache: number
adm_num: number
levelname: any
mems: WebApiGroupMember[]
count: number
svr_time: number
max_count: number
search_count: number
extmode: number
}
export interface WebApiGroupNoticeFeed {
u: number//发送者
fid: string//fid
pubt: number//时间
msg: {
text: string
text_face: string
title: string,
pics?: {
id: string,
w: string,
h: string
}[]
}
type: number
fn: number
cn: number
vn: number
settings: {
is_show_edit_card: number
remind_ts: number
tip_window_type: number
confirm_required: number
}
read_num: number
is_read: number
is_all_confirm: number
}
export interface WebApiGroupNoticeRet {
ec: number
em: string
ltsm: number
srv_code: number
read_only: number
role: number
feeds: WebApiGroupNoticeFeed[]
group: {
group_id: number
class_ext: number
}
sta: number,
gln: number
tst: number,
ui: any
server_time: number
svrt: number
ad: number
}
interface GroupEssenceMsg {
group_code: string
msg_seq: number
msg_random: number
sender_uin: string
sender_nick: string
sender_time: number
add_digest_uin: string
add_digest_nick: string
add_digest_time: number
msg_content: any[]
can_be_removed: true
}
export interface GroupEssenceMsgRet {
retcode: number
retmsg: string
data: {
msg_list: GroupEssenceMsg[]
is_end: boolean
group_role: number
config_page_url: string
}
}
export class WebApi {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet> {
const {cookies: CookieValue, bkn: Bkn} = (await NTQQUserApi.getCookies('qun.qq.com'))
const url = 'https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=' + Bkn + '&group_code=' + GroupCode + '&page_start=' + page_start + '&page_limit=20';
let ret;
try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue });
} catch {
return undefined;
} }
//console.log(url, CookieValue);
constructor() { if (ret.retcode !== 0) {
return undefined;
} }
return ret;
}
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
log('webapi 获取群成员', GroupCode);
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
try {
let cachedData = WebGroupData.GroupData.get(GroupCode);
let cachedTime = WebGroupData.GroupTime.get(GroupCode);
public async addGroupDigest(groupCode: string, msgSeq: string){ if (!cachedTime || Date.now() - cachedTime > 1800 * 1000 || !cached) {
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 _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const res = await this.request(url) const _Skey = await NTQQUserApi.getSkey();
return await res.json() const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
} if (!_Skey || !_Pskey) {
return MemberData;
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;
} }
const Bkn = WebApi.genBkn(_Skey);
return (hash & 0x7FFFFFFF).toString(); const retList: Promise<WebApiGroupMemberRet>[] = [];
} const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=0&end=40&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
private async init(){ if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
if (!WebApi.bkn) { return [];
const group = groups[0]; } else {
WebApi.skey = (await NTQQApi.getSkey(group.groupName, group.groupCode)).data; for (const key in fastRet.mems) {
WebApi.bkn = this.genBkn(WebApi.skey); MemberData.push(fastRet.mems[key]);
let cookie = await NTQQApi.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秒
// });
// }
} }
//初始化获取PageNum
const PageNum = Math.ceil(fastRet.count / 40);
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret: Promise<WebApiGroupMemberRet> = RequestUtil.HttpGetJson<WebApiGroupMemberRet>('https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?st=' + (i - 1) * 40 + '&end=' + i * 40 + '&sort=1&gc=' + GroupCode + '&bkn=' + Bkn, 'POST', '', { 'Cookie': CookieValue });
retList.push(ret);
}
//批量等待
for (let i = 1; i <= PageNum; i++) {
const ret = await (retList[i]);
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
continue;
}
for (const key in ret.mems) {
MemberData.push(ret.mems[key]);
}
}
WebGroupData.GroupData.set(GroupCode, MemberData);
WebGroupData.GroupTime.set(GroupCode, Date.now());
} else {
MemberData = cachedData as Array<WebApiGroupMember>;
}
} catch {
return MemberData;
}
return MemberData;
}
// public static 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);
// return await res.json();
// }
static async setGroupNotice(GroupCode: string, Content: string = '') {
//https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=${bkn}
//qid=${群号}&bkn=${bkn}&text=${内容}&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: any = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
}
const Bkn = WebApi.genBkn(_Skey);
const data = 'qid=' + GroupCode + '&bkn=' + Bkn + '&text=' + Content + '&pinned=0&type=1&settings={"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}';
const url = 'https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?bkn=' + Bkn;
try {
ret = await RequestUtil.HttpGetJson<any>(url, 'GET', '', { 'Cookie': CookieValue });
return ret;
} catch (e) {
return undefined;
}
return undefined;
}
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'];
const _Skey = await NTQQUserApi.getSkey();
const CookieValue = 'p_skey=' + _Pskey + '; skey=' + _Skey + '; p_uin=o' + selfInfo.uin;
let ret: WebApiGroupNoticeRet | undefined = undefined;
//console.log(CookieValue);
if (!_Skey || !_Pskey) {
//获取Cookies失败
return undefined;
}
const Bkn = WebApi.genBkn(_Skey);
const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' + Bkn + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20';
try {
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': CookieValue });
if (ret?.ec !== 0) {
return undefined;
}
return ret;
} catch (e) {
return undefined;
}
return undefined;
}
static 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;
} }
private async request(url: string, method: "GET" | "POST" = "GET", headers: Record<string, string> = {}){ return (hash & 0x7FFFFFFF).toString();
}
await this.init(); //实现未缓存 考虑2h缓存
url += "&bkn=" + WebApi.bkn; static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
let _headers: Record<string, string> = { async function getDataInternal(Internal_groupCode: string, Internal_type: number) {
...this.defaultHeaders, ...headers, let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
"Cookie": WebApi.cookie, let res = '';
credentials: 'include' let resJson;
try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue });
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/);
if (match) {
resJson = JSON.parse(match[1].trim());
} }
log("request", url, _headers) if (Internal_type === 1) {
const options = { return resJson?.talkativeList;
method: method, } else {
headers: _headers return resJson?.actorList;
} }
return fetch(url, options) } catch (e) {
log('获取当前群荣耀失败', url, e);
}
return undefined;
} }
}
let HonorInfo: any = { group_id: groupCode };
const CookieValue = (await NTQQUserApi.getCookies('qun.qq.com')).cookies;
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 1);
if (!RetInternal) {
throw new Error('获取龙王信息失败');
}
HonorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
avatar: RetInternal[0]?.avatar,
nickname: RetInternal[0]?.name,
day_count: 0,
description: RetInternal[0]?.desc
}
HonorInfo.talkative_list = [];
for (const talkative_ele of RetInternal) {
HonorInfo.talkative_list.push({
user_id: talkative_ele?.uin,
avatar: talkative_ele?.avatar,
description: talkative_ele?.desc,
day_count: 0,
nickname: talkative_ele?.name
});
}
} catch (e) {
log(e);
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 2);
if (!RetInternal) {
throw new Error('获取群聊之火失败');
}
HonorInfo.performer_list = [];
for (const performer_ele of RetInternal) {
HonorInfo.performer_list.push({
user_id: performer_ele?.uin,
nickname: performer_ele?.name,
avatar: performer_ele?.avatar,
description: performer_ele?.desc
});
}
} catch (e) {
log(e);
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 3);
if (!RetInternal) {
throw new Error('获取群聊炽焰失败');
}
HonorInfo.legend_list = [];
for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({
user_id: legend_ele?.uin,
nickname: legend_ele?.name,
avatar: legend_ele?.avatar,
desc: legend_ele?.description
});
}
} catch (e) {
log('获取群聊炽焰失败', e);
}
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try {
let RetInternal = await getDataInternal(groupCode, 6);
if (!RetInternal) {
throw new Error('获取快乐源泉失败');
}
HonorInfo.emotion_list = [];
for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({
user_id: emotion_ele?.uin,
nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar,
desc: emotion_ele?.description
});
}
} catch (e) {
log('获取快乐源泉失败', e);
}
}
//冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
HonorInfo.strong_newbie_list = [];
}
return HonorInfo;
}
}

View File

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

View File

@@ -1,251 +1,372 @@
import { import {
AtType, AtType,
ElementType, ElementType,
PicType, FaceIndex,
SendArkElement, PicType,
SendFaceElement, SendArkElement,
SendFileElement, SendFaceElement,
SendPicElement, SendFileElement,
SendPttElement, SendMarketFaceElement,
SendReplyElement, SendPicElement,
SendTextElement, SendPttElement,
SendVideoElement SendReplyElement,
} from "./types"; SendTextElement,
import {promises as fs} from "node:fs"; SendVideoElement,
import ffmpeg from "fluent-ffmpeg" } from './types'
import {NTQQFileApi} from "./api/file"; import { promises as fs } from 'node:fs'
import {calculateFileMD5, encodeSilk, isGIF} from "../common/utils/file"; import ffmpeg from 'fluent-ffmpeg'
import {log} from "../common/utils/log"; import { NTQQFileApi } from './api/file'
import {defaultVideoThumb, getVideoInfo} from "../common/utils/video"; 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'
import { isNull } from '../common/utils'
import faceConfig from './face_config.json';
export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
static text(content: string): SendTextElement { static poke(groupCode: string, uin: string) {
return { return null
elementType: ElementType.TEXT, }
elementId: "",
textElement: { static text(content: string): SendTextElement {
content, return {
atType: AtType.notAt, elementType: ElementType.TEXT,
atUid: "", elementId: '',
atTinyId: "", textElement: {
atNtUid: "", content,
}, atType: AtType.notAt,
}; atUid: '',
atTinyId: '',
atNtUid: '',
},
}
}
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: '',
textElement: {
content: `@${atName}`,
atType,
atUid,
atTinyId: '',
atNtUid,
},
}
}
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return {
elementType: ElementType.REPLY,
elementId: '',
replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, // raw.msgId
senderUin: senderUin,
senderUinStr: senderUinStr,
},
}
}
static async pic(picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType)
if (fileSize === 0) {
throw '文件异常大小为0'
}
const maxMB = 30;
if (fileSize > 1024 * 1024 * 30){
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
}
const imageSize = await NTQQFileApi.getImageSize(picPath)
const picElement = {
md5HexStr: md5,
fileSize: fileSize.toString(),
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary,
}
log('图片信息', picElement)
return {
elementType: ElementType.PIC,
elementId: '',
picElement,
}
}
static async file(filePath: string, fileName: string = ''): Promise<SendFileElement> {
const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) {
throw '文件异常大小为0'
}
let element: SendFileElement = {
elementType: ElementType.FILE,
elementId: '',
fileElement: {
fileName: fileName || _fileName,
filePath: path,
fileSize: fileSize.toString(),
},
} }
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { return element
return { }
elementType: ElementType.TEXT,
elementId: "", static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
textElement: { try {
content: `@${atName}`, await fs.stat(filePath)
atType, } catch (e) {
atUid, throw `文件${filePath}异常,不存在`
atTinyId: "",
atNtUid,
},
};
} }
log('复制视频到QQ目录', filePath)
let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO)
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { log('复制视频到QQ目录完成', path)
return { if (fileSize === 0) {
elementType: ElementType.REPLY, throw '文件异常大小为0'
elementId: "",
replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, // raw.msgId
senderUin: senderUin,
senderUinStr: senderUinStr,
}
}
} }
const maxMB = 100;
static async pic(picPath: string, summary: string = ""): Promise<SendPicElement> { if (fileSize > 1024 * 1024 * maxMB) {
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC); throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
if (fileSize === 0) {
throw "文件异常大小为0";
}
const imageSize = await NTQQFileApi.getImageSize(picPath);
const picElement = {
md5HexStr: md5,
fileSize: fileSize,
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: 0,
fileUuid: "",
fileSubId: "",
thumbFileSize: 0,
summary,
};
return {
elementType: ElementType.PIC,
elementId: "",
picElement
};
} }
const pathLib = require('path')
static async file(filePath: string, fileName: string = ""): Promise<SendFileElement> { let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
const {md5, fileName: _fileName, path, fileSize} = await NTQQFileApi.uploadFile(filePath, ElementType.FILE); thumbDir = pathLib.dirname(thumbDir)
if (fileSize === 0) { // log("thumb 目录", thumb)
throw "文件异常大小为0"; let videoInfo = {
} width: 1920,
let element: SendFileElement = { height: 1080,
elementType: ElementType.FILE, time: 15,
elementId: "", format: 'mp4',
fileElement: { size: fileSize,
fileName: fileName || _fileName, filePath,
"filePath": path,
"fileSize": (fileSize).toString(),
}
}
return element;
} }
try {
videoInfo = await getVideoInfo(path)
log('视频信息', videoInfo)
} catch (e) {
log('获取视频信息失败', e)
}
const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumbDir, thumbFileName)
log('开始生成视频缩略图', filePath)
let completed = false
static async video(filePath: string, fileName: string = "", diyThumbPath: string = ""): Promise<SendVideoElement> { function useDefaultThumb() {
let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); if (completed) return
if (fileSize === 0) { log('获取视频封面失败,使用默认封面')
throw "文件异常大小为0"; fs.writeFile(thumbPath, defaultVideoThumb)
} .then(() => {
const pathLib = require("path"); resolve(thumbPath)
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) })
thumb = pathLib.dirname(thumb) .catch(reject)
// log("thumb 目录", thumb) }
let videoInfo ={
width: 1920, height: 1080, setTimeout(useDefaultThumb, 5000)
time: 15, ffmpeg(filePath)
format: "mp4", .on('end', () => {})
size: fileSize, .on('error', (err) => {
filePath if (diyThumbPath) {
}; fs.copyFile(diyThumbPath, thumbPath)
try { .then(() => {
videoInfo = await getVideoInfo(path); completed = true
log("视频信息", videoInfo) resolve(thumbPath)
}catch (e) { })
log("获取视频信息失败", e) .catch(reject)
} } else {
const createThumb = new Promise<string>((resolve, reject) => { useDefaultThumb()
const thumbFileName = `${md5}_0.png` }
const thumbPath = pathLib.join(thumb, thumbFileName)
ffmpeg(filePath)
.on("end", () => {
})
.on("error", (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({
timestamps: [0],
filename: thumbFileName,
folder: thumb,
size: videoInfo.width + "x" + videoInfo.height
}).on("end", () => {
resolve(thumbPath);
});
}) })
let thumbPath = new Map() .screenshots({
const _thumbPath = await createThumb; timestamps: [0],
const thumbSize = (await fs.stat(_thumbPath)).size; filename: thumbFileName,
// log("生成缩略图", _thumbPath) folder: thumbDir,
thumbPath.set(0, _thumbPath) size: videoInfo.width + 'x' + videoInfo.height,
const thumbMd5 = await calculateFileMD5(_thumbPath); })
let element: SendVideoElement = { .on('end', () => {
elementType: ElementType.VIDEO, log('生成视频缩略图', thumbPath)
elementId: "", completed = true
videoElement: { resolve(thumbPath)
fileName: fileName || _fileName, })
filePath: path, })
videoMd5: md5, let thumbPath = new Map()
thumbMd5, const _thumbPath = await createThumb
fileTime: videoInfo.time, log('生成视频缩略图', _thumbPath)
thumbPath: thumbPath, const thumbSize = (await fs.stat(_thumbPath)).size
thumbSize, // log("生成缩略图", _thumbPath)
thumbWidth: videoInfo.width, thumbPath.set(0, _thumbPath)
thumbHeight: videoInfo.height, const thumbMd5 = await calculateFileMD5(_thumbPath)
fileSize: "" + fileSize, let element: SendVideoElement = {
// fileUuid: "", elementType: ElementType.VIDEO,
// transferStatus: 0, elementId: '',
// progress: 0, videoElement: {
// invalidState: 0, fileName: fileName || _fileName,
// fileSubId: "", filePath: path,
// fileBizId: null, videoMd5: md5,
// originVideoMd5: "", thumbMd5,
// fileFormat: 2, fileTime: videoInfo.time,
// import_rich_media_context: null, thumbPath: thumbPath,
// sourceVideoCodecFormat: 2 thumbSize,
} thumbWidth: videoInfo.width,
} thumbHeight: videoInfo.height,
return element; fileSize: '' + fileSize,
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// sourceVideoCodecFormat: 2
},
} }
log('videoElement', element)
return element
}
static async ptt(pttPath: string): Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath); const { converted, path: silkPath, duration } = await encodeSilk(pttPath)
// log("生成语音", silkPath, duration); if (!silkPath) {
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT); throw '语音转换失败, 请检查语音文件是否正常'
if (fileSize === 0) {
throw "文件异常大小为0";
}
if (converted) {
fs.unlink(silkPath).then();
}
return {
elementType: ElementType.PTT,
elementId: "",
pttElement: {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
canConvert2Text: true,
waveAmplitudes: [
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
],
fileSubId: "",
playState: 1,
autoConvertText: 0,
}
};
} }
// log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT)
if (fileSize === 0) {
throw '文件异常大小为0'
}
if (converted) {
fs.unlink(silkPath).then()
}
return {
elementType: ElementType.PTT,
elementId: '',
pttElement: {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
canConvert2Text: true,
waveAmplitudes: [0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17],
fileSubId: '',
playState: 1,
autoConvertText: 0,
},
}
}
static face(faceId: number): SendFaceElement { static face(faceId: number): SendFaceElement {
return { // 从face_config.json中获取表情名称
elementType: ElementType.FACE, const sysFaces = faceConfig.sysface
elementId: "", const emojiFaces = faceConfig.emoji
faceElement: { const face = sysFaces.find((face) => face.QSid === faceId.toString())
faceIndex: faceId, faceId = parseInt(faceId.toString())
faceType: 1 // let faceType = parseInt(faceId.toString().substring(0, 1));
} let faceType = 1
} if (faceId >= 222){
faceType = 2
} }
if (face.AniStickerType){
faceType = 3;
}
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: faceId,
faceType,
faceText: face.QDes,
stickerId: face.AniStickerId,
stickerType: face.AniStickerType,
packId: face.AniStickerPackId,
sourceType: 1,
},
}
}
static ark(data: any): SendArkElement { static mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement {
return { return {
elementType: ElementType.ARK, elementType: ElementType.MFACE,
elementId: "", marketFaceElement: {
arkElement: { emojiPackageId,
bytesData: data, emojiId,
linkInfo: null, key,
subElementType: null faceName: faceName || mFaceCache.get(emojiId) || '[商城表情]',
} },
}
} }
} }
static dice(resultId: number | null): SendFaceElement {
// 实际测试并不能控制结果
// 随机1到6
if (isNull(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: FaceIndex.dice,
faceType: 3,
faceText: '[骰子]',
packId: '1',
stickerId: '33',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},
}
}
// 猜拳(石头剪刀布)表情
static rps(resultId: number | null): SendFaceElement {
// 实际测试并不能控制结果
if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: FaceIndex.RPS,
faceText: '[包剪锤]',
faceType: 3,
packId: '1',
stickerId: '34',
sourceType: 1,
stickerType: 2,
resultId: resultId.toString(),
surpriseId: '',
// "randomType": 1,
},
}
}
static ark(data: string): SendArkElement {
return {
elementType: ElementType.ARK,
elementId: '',
arkElement: {
bytesData: data,
linkInfo: null,
subElementType: null,
},
}
}
}

View File

@@ -1,28 +0,0 @@
import {log} from "../../../common/utils/log";
let pokeEngine: any = null
type PokeHandler = (id: string, isGroup: boolean)=>void
let pokeRecords: Record<string, number> = {}
export function registerPokeHandler(handler: PokeHandler){
if(!pokeEngine){
try {
pokeEngine = require("./ccpoke/poke-win32-x64.node")
pokeEngine.performHooks();
}catch (e) {
log("戳一戳引擎加载失败", e)
return
}
}
pokeEngine.setHandlerForPokeHook((id: string, isGroup: boolean)=>{
let existTime = pokeRecords[id]
if (existTime){
if (Date.now() - existTime < 1500){
return
}
}
pokeRecords[id] = Date.now()
handler(id, isGroup);
})
}

Binary file not shown.

3665
src/ntqqapi/face_config.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,374 +1,546 @@
import {BrowserWindow} from 'electron'; import { BrowserWindow } from 'electron'
import {NTQQApiClass} from "./ntcall"; import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import {NTQQMsgApi, sendMessagePool} from "./api/msg" import { NTQQMsgApi, sendMessagePool } from './api/msg'
import {ChatType, Group, RawMessage, User} from "./types"; import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User } from './types'
import {friends, groups, selfInfo, tempGroupCodeMap, uidMaps} from "../common/data"; import {
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; deleteGroup,
import {v4 as uuidv4} from "uuid" friends,
import {postOB11Event} from "../onebot11/server/postOB11Event"; getFriend,
import {getConfigUtil, HOOK_LOG} from "../common/config"; getGroupMember,
import fs from "fs"; groups, rawFriends,
import {dbUtil} from "../common/db"; selfInfo,
import {NTQQGroupApi} from "./api/group"; tempGroupCodeMap,
import {log} from "../common/utils/log"; uidMaps,
import {sleep} from "../common/utils/helper"; } from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { v4 as uuidv4 } from 'uuid'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config'
import fs from 'fs'
import { dbUtil } from '@/common/db'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils'
import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export let ReceiveCmdS = { export let ReceiveCmdS = {
UPDATE_MSG: "nodeIKernelMsgListener/onMsgInfoListUpdate", RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2',
UPDATE_ACTIVE_MSG: "nodeIKernelMsgListener/onActiveMsgInfoUpdate", UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`, UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate',
NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`, NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`,
SELF_SEND_MSG: "nodeIKernelMsgListener/onAddSendMsg", NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`,
USER_INFO: "nodeIKernelProfileListener/onProfileSimpleChanged", SELF_SEND_MSG: 'nodeIKernelMsgListener/onAddSendMsg',
USER_DETAIL_INFO: "nodeIKernelProfileListener/onProfileDetailInfoChanged", USER_INFO: 'nodeIKernelProfileListener/onProfileSimpleChanged',
GROUPS: "nodeIKernelGroupListener/onGroupListUpdate", USER_DETAIL_INFO: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
GROUPS_UNIX: "onGroupListUpdate", GROUPS: 'nodeIKernelGroupListener/onGroupListUpdate',
FRIENDS: "onBuddyListChange", GROUPS_STORE: 'onGroupListUpdate',
MEDIA_DOWNLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaDownloadComplete", GROUP_MEMBER_INFO_UPDATE: 'nodeIKernelGroupListener/onMemberInfoChange',
UNREAD_GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated", FRIENDS: 'onBuddyListChange',
GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupSingleScreenNotifies", MEDIA_DOWNLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaDownloadComplete',
FRIEND_REQUEST: "nodeIKernelBuddyListener/onBuddyReqChange", UNREAD_GROUP_NOTIFY: 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated',
SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged', GROUP_NOTIFY: 'nodeIKernelGroupListener/onGroupSingleScreenNotifies',
CACHE_SCAN_FINISH: "nodeIKernelStorageCleanListener/onFinishScan", FRIEND_REQUEST: 'nodeIKernelBuddyListener/onBuddyReqChange',
MEDIA_UPLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaUploadComplete", SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged',
SKEY_UPDATE: "onSkeyUpdate" CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: 'onSkeyUpdate',
} }
export type ReceiveCmd = typeof ReceiveCmdS[keyof typeof ReceiveCmdS] export type ReceiveCmd = (typeof ReceiveCmdS)[keyof typeof ReceiveCmdS]
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> { interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: { 0: {
"type": "request", type: 'request'
"eventName": NTQQApiClass, eventName: NTQQApiClass
"callbackId"?: string callbackId?: string
}, }
1: 1: {
{ cmdName: ReceiveCmd
cmdName: ReceiveCmd, cmdType: 'event'
cmdType: "event", payload: PayloadType
payload: PayloadType }[]
}[]
} }
let receiveHooks: Array<{ let receiveHooks: Array<{
method: ReceiveCmd[], method: ReceiveCmd[]
hookFunc: ((payload: any) => void | Promise<void>) hookFunc: (payload: any) => void | Promise<void>
id: string id: string
}> = []
let callHooks: Array<{
method: NTQQApiMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
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) // console.log("hookNTQQApiReceive", channel, args)
let isLogger = false let isLogger = false
try { try {
isLogger = args[0]?.eventName?.startsWith("ns-LoggerApi") isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) { } catch (e) {}
if (!isLogger) {
} try {
if (!isLogger) { HOOK_LOG && log(`received ntqq api message: ${channel}`, args)
try { } catch (e) {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args) log('hook log error', e, args)
}catch (e) { }
log("hook log error", e, args)
}
}
try {
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
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)
}
}).then()
}
}
}
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId;
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1]);
}).then()
delete hookApiCallbacks[callbackId];
}
}
}catch (e) {
log("hookNTQQApiReceive error", e.stack.toString(), args)
}
originalSend.call(window.webContents, channel, ...args);
} }
window.webContents.send = patchSend; try {
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
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)
}
}).then()
}
}
}
}
if (args[0]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args)
const callbackId = args[0].callbackId
if (hookApiCallbacks[callbackId]) {
// log("callback found")
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1])
}).then()
delete hookApiCallbacks[callbackId]
}
}
} catch (e) {
log('hookNTQQApiReceive error', e.stack.toString(), args)
}
originalSend.call(window.webContents, channel, ...args)
}
window.webContents.send = patchSend
} }
export function hookNTQQApiCall(window: BrowserWindow) { export function hookNTQQApiCall(window: BrowserWindow) {
// 监听调用NTQQApi // 监听调用NTQQApi
let webContents = window.webContents as any; let webContents = window.webContents as any
const ipc_message_proxy = webContents._events["-ipc-message"]?.[0] || webContents._events["-ipc-message"]; const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
const proxyIpcMsg = new Proxy(ipc_message_proxy, { const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
// console.log(thisArg, args); // console.log(thisArg, args);
let isLogger = false let isLogger = false
try { try {
isLogger = args[3][0].eventName.startsWith("ns-LoggerApi") isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) { } catch (e) {}
if (!isLogger) {
} try {
if (!isLogger) { HOOK_LOG && log('call NTQQ api', thisArg, args)
try{ } catch (e) {}
HOOK_LOG && log("call NTQQ api", thisArg, args); try {
}catch (e) { const _args: unknown[] = args[3][1]
const cmdName: NTQQApiMethod = _args[0] as NTQQApiMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
new Promise((resolve, reject) => {
try {
let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
;(_ as Promise<void>).then()
}
} catch (e) {
log('hook call error', e, _args)
} }
}).then()
} }
return target.apply(thisArg, args); })
} catch (e) {}
}
return target.apply(thisArg, args)
},
})
if (webContents._events['-ipc-message']?.[0]) {
webContents._events['-ipc-message'][0] = proxyIpcMsg
} else {
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)
}, },
}); })
if (webContents._events["-ipc-message"]?.[0]) { let ret = target.apply(thisArg, args)
webContents._events["-ipc-message"][0] = proxyIpcMsg; try {
} else { HOOK_LOG && log('call NTQQ invoke api return', ret)
webContents._events["-ipc-message"] = proxyIpcMsg; } catch (e) {}
} return ret
},
const ipc_invoke_proxy = webContents._events["-ipc-invoke"]?.[0] || webContents._events["-ipc-invoke"]; })
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { if (webContents._events['-ipc-invoke']?.[0]) {
apply(target, thisArg, args) { webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
// console.log(args); } else {
HOOK_LOG && log("call NTQQ invoke api", thisArg, args) webContents._events['-ipc-invoke'] = proxyIpcInvoke
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>(
const id = uuidv4() method: ReceiveCmd | ReceiveCmd[],
if (!Array.isArray(method)) { hookFunc: (payload: PayloadType) => void,
method = [method] ): string {
} const id = uuidv4()
receiveHooks.push({ if (!Array.isArray(method)) {
method, method = [method]
hookFunc, }
id receiveHooks.push({
}) method,
return id; hookFunc,
id,
})
return id
}
export function registerCallHook(
method: NTQQApiMethod | NTQQApiMethod[],
hookFunc: (callParams: unknown[]) => void | Promise<void>,
): void {
if (!Array.isArray(method)) {
method = [method]
}
callHooks.push({
method,
hookFunc,
})
} }
export function removeReceiveHook(id: string) { export function removeReceiveHook(id: string) {
const index = receiveHooks.findIndex(h => h.id === id) const index = receiveHooks.findIndex((h) => h.id === id)
receiveHooks.splice(index, 1); receiveHooks.splice(index, 1)
} }
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 (group.privilegeFlag === 0) {
NTQQMsgApi.activateGroupChat(group.groupCode).then((r) => { deleteGroup(group.groupCode)
activatedGroups.push(group.groupCode); continue
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
}).catch(log)
}
let existGroup = groups.find(g => g.groupCode == group.groupCode);
if (existGroup) {
Object.assign(existGroup, group);
} else {
groups.push(group);
existGroup = group;
}
if (needUpdate) {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode);
if (members) {
existGroup.members = members;
}
}
} }
log('update group', group)
// if (!activatedGroups.includes(group.groupCode)) {
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group })
.then((r) => {
// activatedGroups.push(group.groupCode);
// log(`激活群聊天窗口${group.groupName}(${group.groupCode})`, r)
// if (r.result !== 0) {
// setTimeout(() => NTQQMsgApi.activateGroupChat(group.groupCode).then(r => log(`再次激活群聊天窗口${group.groupName}(${group.groupCode})`, r)), 500);
// }else {
// }
})
.catch(log)
// }
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
Object.assign(existGroup, group)
} else {
groups.push(group)
existGroup = group
}
if (needUpdate) {
const members = await NTQQGroupApi.getGroupMembers(group.groupCode)
if (members) {
existGroup.members = members
}
}
}
} }
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) {
const oldMembers = existGroup.members; log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`)
const oldMembers = existGroup.members
await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时 await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode); const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = newMembers; group.members = newMembers
const newMembersSet = new Set<string>(); // 建立索引降低时间复杂度 const newMembersSet = new Set<string>() // 建立索引降低时间复杂度
for (const member of newMembers) { for (const member of newMembers) {
newMembersSet.add(member.uin); newMembersSet.add(member.uin)
} }
for (const member of oldMembers) { // 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
if (!newMembersSet.has(member.uin)) { let bot = await getGroupMember(group.groupCode, selfInfo.uin)
postOB11Event(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))); if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
break; continue
} }
} for (const member of oldMembers) {
} if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOb11Event(
new OB11GroupDecreaseEvent(
parseInt(group.groupCode),
parseInt(member.uin),
parseInt(member.uin),
'leave',
),
)
break
} }
}
} }
if (group.privilegeFlag === 0) {
updateGroups(newGroupList, false).then(); deleteGroup(group.groupCode)
} catch (e) { }
updateGroups(payload.groupList).then(); }
console.log(e);
} }
updateGroups(newGroupList, false).then()
} catch (e) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
} }
export async function startHook() {
// 群列表变动 // 群列表变动
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 {
if (process.platform == "win32") {
processGroupEvent(payload).then();
}
} }
}) else {
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS_UNIX, (payload) => { if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
})
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 {
if (process.platform != "win32") {
processGroupEvent(payload).then();
}
} }
}) else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
}
})
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) {
if (member.cardName != existMember.cardName) {
log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
postOb11Event(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
)
}
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: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, payload => { }>(ReceiveCmdS.FRIENDS, (payload) => {
rawFriends.length = 0;
rawFriends.push(...payload.data);
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) {
let existFriend = friends.find(f => f.uin == friend.uin) NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
if (!existFriend) { let existFriend = friends.find((f) => f.uin == friend.uin)
friends.push(friend) if (!existFriend) {
} else { friends.push(friend)
Object.assign(existFriend, friend)
}
} }
else {
Object.assign(existFriend, friend)
}
}
} }
}) })
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 // 保存一下uid
for (const message of payload.msgList) { for (const message of payload.msgList) {
const uid = message.senderUid; const uid = message.senderUid
const uin = message.senderUin; const uin = message.senderUin
if (uid && uin) { if (uid && uin) {
if (message.chatType === ChatType.temp){ if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then(receivedTempUinMap=>{ dbUtil.getReceivedTempUinMap().then((receivedTempUinMap) => {
if (!receivedTempUinMap[uin]){ if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid; receivedTempUinMap[uin] = uid
dbUtil.setReceivedTempUinMap(receivedTempUinMap) dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
} }
uidMaps[uid] = uin; })
} }
uidMaps[uid] = uin
}
} }
// 自动清理新消息文件 // 自动清理新消息文件
const {autoDeleteFile} = getConfigUtil().getConfig(); const { autoDeleteFile } = getConfigUtil().getConfig()
if (!autoDeleteFile) { if (!autoDeleteFile) {
return return
} }
for (const message of payload.msgList) { for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId) // log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then() // dbUtil.addMsg(message).then()
// 清理文件 // 清理文件
for (const msgElement of message.elements) { for (const msgElement of message.elements) {
setTimeout(() => { setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()] const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()] const videoThumbPath: string[] = [...msgElement.videoElement?.thumbPath.values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath] const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) { if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath)) pathList.push(...Object.values(msgElement.picElement.thumbPath))
} }
const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement const aioOpGrayTipElement = msgElement.grayTipElement?.aioOpGrayTipElement
if (aioOpGrayTipElement) { if (aioOpGrayTipElement) {
tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat; tempGroupCodeMap[aioOpGrayTipElement.peerUid] = aioOpGrayTipElement.fromGrpCodeOfTmpChat
} }
// log("需要清理的文件", pathList); // log("需要清理的文件", pathList);
for (const path of pathList) { for (const path of pathList) {
if (path) { if (path) {
fs.unlink(picPath, () => { fs.unlink(picPath, () => {
log("删除文件成功", path) log('删除文件成功', path)
}); })
} }
} }
}, getConfigUtil().getConfig().autoDeleteFileSecond * 1000) }, getConfigUtil().getConfig().autoDeleteFileSecond * 1000)
} }
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({msgRecord}) => { registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord; const message = msgRecord
const peerUid = message.peerUid; const peerUid = message.peerUid
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message); // log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq); // log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then() dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid] const sendCallback = sendMessagePool[peerUid]
if (sendCallback) { if (sendCallback) {
try { try {
sendCallback(message); sendCallback(message)
} catch (e) { } catch (e) {
log("receive self msg error", e.stack) log('receive self msg error', e.stack)
} }
} }
}) })
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()
}
}
}
})
registerCallHook(NTQQApiMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
log('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else {
// 检查是否好友
if (!(await getFriend(peerUid))) {
chatType = ChatType.temp
}
}
const peer = { peerUid, chatType }
await sleep(1000)
NTQQMsgApi.activateChat(peer).then((r) => {
log('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
}

View File

@@ -0,0 +1,19 @@
import * as os from "os";
import path from "node:path";
import fs from "fs";
export function getModuleWithArchName(moduleName: string) {
const systemPlatform = os.platform()
const cpuArch = os.arch()
return `${moduleName}-${systemPlatform}-${cpuArch}.node`
}
export function cpModule(moduleName: string) {
const currentDir = path.resolve(__dirname);
const fileName = `./${getModuleWithArchName(moduleName)}`
try {
fs.copyFileSync(path.join(currentDir, fileName), path.join(currentDir, `${moduleName}.node`));
} catch (e) {
}
}

Binary file not shown.

View File

@@ -0,0 +1,58 @@
import { log } from '../../../common/utils'
import { NTQQApi } from '../../ntcall'
import { cpModule } from '../cpmodule'
type PokeHandler = (id: string, isGroup: boolean) => void
type CrychicHandler = (event: string, id: string, isGroup: boolean) => void
let pokeRecords: Record<string, number> = {}
class Crychic {
private crychic: any = undefined
loadNode() {
if (!this.crychic) {
try {
cpModule('crychic')
this.crychic = require('./crychic.node')
this.crychic.init()
} catch (e) {
log('crychic加载失败', e)
}
}
}
registerPokeHandler(fn: PokeHandler) {
this.registerHandler((event, id, isGroup) => {
if (event === 'poke') {
let existTime = pokeRecords[id]
if (existTime) {
if (Date.now() - existTime < 1500) {
return
}
}
pokeRecords[id] = Date.now()
fn(id, isGroup)
}
})
}
registerHandler(fn: CrychicHandler) {
if (!this.crychic) return
this.crychic.setCryHandler(fn)
}
sendFriendPoke(friendUid: string) {
if (!this.crychic) return
this.crychic.sendFriendPoke(parseInt(friendUid))
NTQQApi.fetchUnitedCommendConfig().then()
}
sendGroupPoke(groupCode: string, memberUin: string) {
if (!this.crychic) return
this.crychic.sendGroupPoke(parseInt(memberUin), parseInt(groupCode))
NTQQApi.fetchUnitedCommendConfig().then()
}
}
export const crychic = new Crychic()

View File

@@ -0,0 +1,33 @@
import {cpModule} from "../cpmodule";
import { qqPkgInfo } from '@/common/utils/QQBasicInfo'
interface MoeHook {
GetRkey: () => string, // Return '&rkey=xxx'
HookRkey: (version: string) => string
}
class HookApi {
private readonly moeHook: MoeHook | null = null;
constructor() {
cpModule('MoeHoo');
try {
this.moeHook = require('./MoeHoo.node');
console.log("hook rkey qq version", this.moeHook!.HookRkey(qqPkgInfo.version));
console.log("hook rkey地址", this.moeHook!.HookRkey(qqPkgInfo.version));
} catch (e) {
console.log('加载 moehoo 失败', e);
}
}
getRKey(): string {
return this.moeHook?.GetRkey() || '';
}
isAvailable() {
return !!this.moeHook;
}
}
// export const hookApi = new HookApi();

View File

@@ -0,0 +1,19 @@
let Process = require('process')
let os = require('os')
Process.dlopenOrig = Process.dlopen
export const wrapperApi: any = {}
Process.dlopen = function(module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
let dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => {
let ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
},
})
}
return dlopenRet
}

View File

@@ -1,244 +1,228 @@
import {ipcMain} from "electron"; import { ipcMain } from 'electron'
import {hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook} from "./hook"; import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook'
import {v4 as uuidv4} from "uuid" import { v4 as uuidv4 } from 'uuid'
import {log} from "../common/utils/log"; import { log } from '../common/utils/log'
import {NTQQWindow, NTQQWindowApi, NTQQWindows} from "./api/window"; import { NTQQWindow, NTQQWindowApi, NTQQWindows } from './api/window'
import {WebApi} from "./api/webapi"; import { HOOK_LOG } from '../common/config'
import {HOOK_LOG} from "../common/config";
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = "ns-ntApi", NT_API = 'ns-ntApi',
FS_API = "ns-FsApi", FS_API = 'ns-FsApi',
OS_API = "ns-OsApi", OS_API = 'ns-OsApi',
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", SKEY_API = 'ns-SkeyApi',
GROUP_HOME_WORK = "ns-GroupHomeWork", GROUP_HOME_WORK = 'ns-GroupHomeWork',
GROUP_ESSENCE = "ns-GroupEssence", GROUP_ESSENCE = 'ns-GroupEssence',
} }
export enum NTQQApiMethod { export enum NTQQApiMethod {
RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact", TEST = 'NodeIKernelTipOffService/getPskey',
ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息 RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
HISTORY_MSG_998 = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelf", ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
SELF_INFO = "fetchAuthData", GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
FRIENDS = "nodeIKernelBuddyService/getBuddyList", DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
GROUPS = "nodeIKernelGroupService/getGroupList", ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio',
GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene",
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo",
USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo",
FILE_TYPE = "getFileType",
FILE_MD5 = "getFileMd5",
FILE_COPY = "copyFile",
IMAGE_SIZE = "getImageSizeFromPath",
FILE_SIZE = "getFileSize",
MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest",
KICK_MEMBER = "nodeIKernelGroupService/kickMember",
MUTE_MEMBER = "nodeIKernelGroupService/setMemberShutUp",
MUTE_GROUP = "nodeIKernelGroupService/setGroupShutUp",
SET_MEMBER_CARD = "nodeIKernelGroupService/modifyMemberCardName",
SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole",
PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin",
SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName",
SET_GROUP_TITLE = "nodeIKernelGroupService/modifyMemberSpecialTitle",
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan', LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths', SELF_INFO = 'fetchAuthData',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath', FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo', GROUPS = 'nodeIKernelGroupService/getGroupList',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo', GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo', GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow', USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
FILE_TYPE = 'getFileType',
FILE_MD5 = 'getFileMd5',
FILE_COPY = 'copyFile',
IMAGE_SIZE = 'getImageSizeFromPath',
FILE_SIZE = 'getFileSize',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
GET_SKEY = "nodeIKernelTipOffService/getPskey", SEND_MSG = 'nodeIKernelMsgService/sendMsg',
UPDATE_SKEY = "updatePskey" EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies',
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp',
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName',
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole',
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle',
ACTIVATE_MEMBER_LIST_CHANGE = 'nodeIKernelGroupListener/onMemberListChange',
ACTIVATE_MEMBER_INFO_CHANGE = 'nodeIKernelGroupListener/onMemberInfoChange',
GET_MSG_BOX_INFO = 'nodeIKernelMsgService/getABatchOfContactMsgBoxInfo',
GET_GROUP_ALL_INFO = 'nodeIKernelGroupService/getGroupAllInfo',
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader',
GET_PSKEY = 'nodeIKernelTipOffService/getPskey',
UPDATE_SKEY = 'updatePskey',
FETCH_UNITED_COMMEND_CONFIG = 'nodeIKernelUnitedConfigService/fetchUnitedCommendConfig', // 发包需要调用的
} }
enum NTQQApiChannel { enum NTQQApiChannel {
IPC_UP_2 = "IPC_UP_2", IPC_UP_2 = 'IPC_UP_2',
IPC_UP_3 = "IPC_UP_3", IPC_UP_3 = 'IPC_UP_3',
IPC_UP_1 = "IPC_UP_1", IPC_UP_1 = 'IPC_UP_1',
} }
interface NTQQApiParams { interface NTQQApiParams {
methodName: NTQQApiMethod | string, methodName: NTQQApiMethod | string
className?: NTQQApiClass, className?: NTQQApiClass
channel?: NTQQApiChannel, channel?: NTQQApiChannel
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[], args?: unknown[]
cbCmd?: ReceiveCmd | null, cbCmd?: ReceiveCmd | ReceiveCmd[] | null
cmdCB?: (payload: any) => boolean; cmdCB?: (payload: any) => boolean
afterFirstCmd?: boolean, // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number, timeoutSecond?: number
} }
export function callNTQQApi<ReturnType>(params: NTQQApiParams) { export function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let { let {
className, methodName, channel, args, className,
cbCmd, timeoutSecond: timeout, methodName,
classNameIsRegister, cmdCB, afterFirstCmd channel,
} = params; args,
className = className ?? NTQQApiClass.NT_API; cbCmd,
channel = channel ?? NTQQApiChannel.IPC_UP_2; timeoutSecond: timeout,
args = args ?? []; classNameIsRegister,
timeout = timeout ?? 5; cmdCB,
afterFirstCmd = afterFirstCmd ?? true; afterFirstCmd,
const uuid = uuidv4(); } = params
HOOK_LOG && log("callNTQQApi", channel, className, methodName, args, uuid) className = className ?? NTQQApiClass.NT_API
return new Promise((resolve: (data: ReturnType) => void, reject) => { channel = channel ?? NTQQApiChannel.IPC_UP_2
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) args = args ?? []
const _timeout = timeout * 1000 timeout = timeout ?? 5
let success = false afterFirstCmd = afterFirstCmd ?? true
let eventName = className + "-" + channel[channel.length - 1]; const uuid = uuidv4()
if (classNameIsRegister) { HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid)
eventName += "-register"; return new Promise((resolve: (data: ReturnType) => void, reject) => {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000
let success = false
let eventName = className + '-' + channel[channel.length - 1]
if (classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [methodName, ...args]
if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true
resolve(r)
}
}
else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!cmdCB) {
if (cmdCB(payload)) {
removeReceiveHook(hookId)
success = true
resolve(payload)
}
}
else {
removeReceiveHook(hookId)
success = true
resolve(payload)
}
})
}
!afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback()
} }
const apiArgs = [methodName, ...args] else {
if (!cbCmd) { success = true
// QQ后端会返回结果并且可以根据uuid识别 reject(`ntqq api call failed, ${result.errMsg}`)
hookApiCallbacks[uuid] = (r: ReturnType) => {
success = true
resolve(r)
};
} else {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB);
if (!!cmdCB) {
if (cmdCB(payload)) {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
} else {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
})
}
!afterFirstCmd && secondCallback();
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result?.result == 0 || result === undefined) {
afterFirstCmd && secondCallback();
} else {
success = true
reject(`ntqq api call failed, ${result.errMsg}`);
}
}
} }
setTimeout(() => { }
// log("ntqq api timeout", success, channel, className, methodName) }
if (!success) { setTimeout(() => {
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs); // log("ntqq api timeout", success, channel, className, methodName)
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`) if (!success) {
} log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs)
}, _timeout) reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
}
}, _timeout)
ipcMain.emit( ipcMain.emit(
channel, channel,
{}, {
{type: 'request', callbackId: uuid, eventName}, sender: {
apiArgs send: (..._args: unknown[]) => {
) },
}) },
},
{ type: 'request', callbackId: uuid, eventName },
apiArgs,
)
})
} }
export interface GeneralCallResult { export interface GeneralCallResult {
result: number, // 0: success result: number // 0: success
errMsg: string errMsg: string
} }
export class NTQQApi { export class NTQQApi {
static async call(className: NTQQApiClass, cmdName: string, args: any[],) { static async call(className: NTQQApiClass, cmdName: string, args: any[]) {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
className, className,
methodName: cmdName, methodName: cmdName,
args: [ args: [...args],
...args, })
] }
})
}
static async getSkey(groupName: string, groupCode: string): Promise<{data: string}> { static async fetchUnitedCommendConfig() {
return await NTQQWindowApi.openWindow<{data: string}>(NTQQWindows.GroupHomeWorkWindow, [{ return await callNTQQApi<GeneralCallResult>({
groupName, methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
groupCode, args: [
"source": "funcbar" {
}], ReceiveCmdS.SKEY_UPDATE, 1); groups: ['100243'],
// 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
// ]
// })
}
static async getPSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: "qun.qq.com"
}
]
})
}
static async addGroupDigest(groupCode: string, msgSeq: string) {
return await new WebApi().addGroupDigest(groupCode, msgSeq);
}
static async getGroupDigest(groupCode: string) {
return await new WebApi().getGroupDigest(groupCode);
}
}

View File

@@ -1,65 +1,66 @@
import {ChatType} from "./msg"; import { ChatType } from './msg'
export interface CacheScanResult { export interface CacheScanResult {
result: number, result: number
size: [ // 单位为字节 size: [
string, // 系统总存储空间 // 单位为字节
string, // 系统可用存储空间 string, // 系统存储空间
string, // 系统用存储空间 string, // 系统用存储空间
string, // QQ总大小 string, // 系统已用存储空间
string, // 「聊天与文件」大小 string, // QQ总大小
string, // 未知 string, // 「聊天与文件」大小
string, // 「缓存数据」大小 string, // 未知
string, // 「其他数据」大小 string, // 「缓存数据」大小
string, // 未知 string, // 「其他数据」大小
] string, // 未知
]
} }
export interface ChatCacheList { export interface ChatCacheList {
pageCount: number, pageCount: number
infos: ChatCacheListItem[] infos: ChatCacheListItem[]
} }
export interface ChatCacheListItem { export interface ChatCacheListItem {
chatType: ChatType, chatType: ChatType
basicChatCacheInfo: ChatCacheListItemBasic, basicChatCacheInfo: ChatCacheListItemBasic
guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容 guildChatCacheInfo: unknown[] // TODO: 没用过频道所以不知道这里边的详细内容
} }
export interface ChatCacheListItemBasic { export interface ChatCacheListItemBasic {
chatSize: string, chatSize: string
chatTime: string, chatTime: string
uid: string, uid: string
uin: string, uin: string
remarkName: string, remarkName: string
nickName: string, nickName: string
chatType?: ChatType, chatType?: ChatType
isChecked?: boolean isChecked?: boolean
} }
export enum CacheFileType { export enum CacheFileType {
IMAGE = 0, IMAGE = 0,
VIDEO = 1, VIDEO = 1,
AUDIO = 2, AUDIO = 2,
DOCUMENT = 3, DOCUMENT = 3,
OTHER = 4, OTHER = 4,
} }
export interface CacheFileList { export interface CacheFileList {
infos: CacheFileListItem[], infos: CacheFileListItem[]
} }
export interface CacheFileListItem { export interface CacheFileListItem {
fileSize: string, fileSize: string
fileTime: string, fileTime: string
fileKey: string, fileKey: string
elementId: string, elementId: string
elementIdStr: string, elementIdStr: string
fileType: CacheFileType, fileType: CacheFileType
path: string, path: string
fileName: string, fileName: string
senderId: string, senderId: string
previewPath: string, previewPath: string
senderName: string, senderName: string
isChecked?: boolean, isChecked?: boolean
} }

View File

@@ -1,55 +1,56 @@
import {QQLevel, Sex} from "./user"; import { QQLevel, Sex } from './user'
export interface Group { export interface Group {
groupCode: string, groupCode: string
maxMember: number, maxMember: number
memberCount: number, memberCount: number
groupName: string, groupName: string
groupStatus: 0, groupStatus: 0
memberRole: 2, memberRole: 2
isTop: boolean, isTop: boolean
toppedTimestamp: "0", toppedTimestamp: '0'
privilegeFlag: number, //65760 privilegeFlag: number //65760
isConf: boolean, isConf: boolean
hasModifyConfGroupFace: boolean, hasModifyConfGroupFace: boolean
hasModifyConfGroupName: boolean, hasModifyConfGroupName: boolean
remarkName: string, remarkName: string
hasMemo: boolean, hasMemo: boolean
groupShutupExpireTime: string, //"0", groupShutupExpireTime: string //"0",
personShutupExpireTime: string, //"0", personShutupExpireTime: string //"0",
discussToGroupUin: string, //"0", discussToGroupUin: string //"0",
discussToGroupMaxMsgSeq: number, discussToGroupMaxMsgSeq: number
discussToGroupTime: number, discussToGroupTime: number
groupFlagExt: number, //1073938496, groupFlagExt: number //1073938496,
authGroupType: number, //0, authGroupType: number //0,
groupCreditLevel: number, //0, groupCreditLevel: number //0,
groupFlagExt3: number, //0, groupFlagExt3: number //0,
groupOwnerId: { groupOwnerId: {
"memberUin": string, //"0", memberUin: string //"0",
"memberUid": string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q" memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
}, }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
} }
export enum GroupMemberRole { export enum GroupMemberRole {
normal = 2, normal = 2,
admin = 3, admin = 3,
owner = 4 owner = 4,
} }
export interface GroupMember { export interface GroupMember {
avatarPath: string; memberSpecialTitle: string
cardName: string; avatarPath: string
cardType: number; cardName: string
isDelete: boolean; cardType: number
nick: string; isDelete: boolean
qid: string; nick: string
remark: string; qid: string
role: GroupMemberRole; // 群主:4, 管理员:3群员:2 remark: string
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚 role: GroupMemberRole // 群主:4, 管理员:3群员:2
uid: string; // 加密的字符串 shutUpTime: number // 禁言时间,单位是什么暂时不清楚
uin: string; // QQ号 uid: string // 加密的字符串
isRobot: boolean; uin: string // QQ号
sex?: Sex isRobot: boolean
qqLevel?: QQLevel sex?: Sex
} qqLevel?: QQLevel
}

View File

@@ -1,7 +1,5 @@
export * from './user'
export * from './user'; export * from './group'
export * from './group'; export * from './msg'
export * from './msg'; export * from './notify'
export * from './notify'; export * from './cache'
export * from './cache';

View File

@@ -1,320 +1,417 @@
import {GroupMemberRole} from "./group"; import { GroupMemberRole } from './group'
import exp from 'constants'
export enum ElementType { export enum ElementType {
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
PTT = 4, PTT = 4,
VIDEO = 5, VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
ARK = 10, ARK = 10,
MFACE = 11,
} }
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT, elementType: ElementType.TEXT
elementId: "", elementId: ''
textElement: { textElement: {
content: string, content: string
atType: number, atType: number
atUid: string, atUid: string
atTinyId: string, atTinyId: string
atNtUid: string, atNtUid: string
} }
} }
export interface SendPttElement { export interface SendPttElement {
elementType: ElementType.PTT, elementType: ElementType.PTT
elementId: "", elementId: ''
pttElement: { pttElement: {
fileName: string, fileName: string
filePath: string, filePath: string
md5HexStr: string, md5HexStr: string
fileSize: number, fileSize: number
duration: number, // 单位是秒 duration: number // 单位是秒
formatType: number, formatType: number
voiceType: number, voiceType: number
voiceChangeType: number, voiceChangeType: number
canConvert2Text: boolean, canConvert2Text: boolean
waveAmplitudes: number[], waveAmplitudes: number[]
fileSubId: "", fileSubId: ''
playState: number, playState: number
autoConvertText: number, autoConvertText: number
} }
} }
export enum PicType { export enum PicType {
gif = 2000, gif = 2000,
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 {
elementType: ElementType.REPLY, elementType: ElementType.REPLY
elementId: "", elementId: ''
replyElement: { replyElement: {
replayMsgSeq: string, replayMsgSeq: string
replayMsgId: string, replayMsgId: string
senderUin: string, senderUin: string
senderUinStr: string, senderUinStr: string
} }
} }
export interface SendFaceElement { export interface SendFaceElement {
elementType: ElementType.FACE, elementType: ElementType.FACE
elementId: "", elementId: ''
faceElement: FaceElement faceElement: FaceElement
}
export interface SendMarketFaceElement {
elementType: ElementType.MFACE
marketFaceElement: MarketFaceElement
} }
export interface FileElement { export interface FileElement {
"fileMd5"?: "", fileMd5?: ''
"fileName": string, fileName: string
"filePath": string, filePath: string
"fileSize": string, fileSize: string
"picHeight"?: number, picHeight?: number
"picWidth"?: number, picWidth?: number
"picThumbPath"?: {}, picThumbPath?: {}
"file10MMd5"?: "", file10MMd5?: ''
"fileSha"?: "", fileSha?: ''
"fileSha3"?: "", fileSha3?: ''
"fileUuid"?: "", fileUuid?: ''
"fileSubId"?: "", fileSubId?: ''
"thumbFileSize"?: number thumbFileSize?: number
fileBizId?: number
} }
export interface SendFileElement { export interface SendFileElement {
elementType: ElementType.FILE elementType: ElementType.FILE
elementId: "", elementId: ''
fileElement: FileElement fileElement: FileElement
} }
export interface SendVideoElement { export interface SendVideoElement {
elementType: ElementType.VIDEO elementType: ElementType.VIDEO
elementId: "", elementId: ''
videoElement: VideoElement videoElement: VideoElement
} }
export interface SendArkElement { export interface SendArkElement {
elementType: ElementType.ARK, elementType: ElementType.ARK
elementId: "", elementId: ''
arkElement: ArkElement arkElement: ArkElement
} }
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement =
SendPicElement | SendReplyElement | SendFaceElement | SendFileElement | SendVideoElement | SendArkElement | SendTextElement
| SendPttElement
| SendPicElement
| SendReplyElement
| SendFaceElement
| SendMarketFaceElement
| SendFileElement
| SendVideoElement
| SendArkElement
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
atAll = 1, atAll = 1,
atUser = 2 atUser = 2,
} }
export enum ChatType { export enum ChatType {
friend = 1, friend = 1,
group = 2, group = 2,
temp = 100 temp = 100,
} }
export interface PttElement { export interface PttElement {
canConvert2Text: boolean; canConvert2Text: boolean
duration: number; // 秒数 duration: number // 秒数
fileBizId: null; fileBizId: null
fileId: number; // 0 fileId: number // 0
fileName: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr" fileName: string // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
filePath: string; // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr" filePath: string // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
fileSize: string; // "4261" fileSize: string // "4261"
fileSubId: string; // "0" fileSubId: string // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV" fileUuid: string // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1 formatType: string // 1
invalidState: number; // 0 invalidState: number // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6" md5HexStr: string // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0 playState: number // 0
progress: number; // 0 progress: number // 0
text: string; // "" text: string // ""
transferStatus: number; // 0 transferStatus: number // 0
translateStatus: number; // 0 translateStatus: number // 0
voiceChangeType: number; // 0 voiceChangeType: number // 0
voiceType: number; // 0 voiceType: number // 0
waveAmplitudes: number[]; waveAmplitudes: number[]
} }
export interface ArkElement { export interface ArkElement {
bytesData: string; bytesData: string
linkInfo: null, linkInfo: null
subElementType: null subElementType: null
} }
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/ picSubType: PicSubType
sourcePath: string; // 图片本地路径 picType: PicType // 有这玩意儿吗
thumbPath: Map<number, string>; originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
picWidth: number; originImageMd5?: string
picHeight: number; sourcePath: string // 图片本地路径
fileSize: number; thumbPath: Map<number, string>
fileName: string; picWidth: number
fileUuid: string; picHeight: number
md5HexStr?: string; fileSize: number
fileName: string
fileUuid: string
md5HexStr?: string
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
INVITE_NEW_MEMBER = 12, RECALL = 1,
INVITE_NEW_MEMBER = 12,
MEMBER_NEW_TITLE = 17,
} }
export interface GrayTipElement { export interface GrayTipElement {
subElementType: GrayTipElementSubType; subElementType: GrayTipElementSubType
revokeElement: { revokeElement: {
operatorRole: string; operatorRole: string
operatorUid: string; operatorUid: string
operatorNick: string; operatorNick: string
operatorRemark: string; operatorRemark: string
operatorMemRemark?: string; operatorMemRemark?: string
wording: string; // 自定义的撤回提示语 origMsgSenderUid?: string
} isSelfOperate?: boolean
aioOpGrayTipElement: TipAioOpGrayTipElement, wording: string // 自定义的撤回提示语
groupElement: TipGroupElement, }
xmlElement: { aioOpGrayTipElement: TipAioOpGrayTipElement
content: string; groupElement: TipGroupElement
} xmlElement: {
templId: string
content: string
}
jsonGrayTipElement: {
busiId: number
jsonStr: string
}
}
export enum FaceIndex {
dice = 358,
RPS = 359, // 石头剪刀布
} }
export interface FaceElement { export interface FaceElement {
faceIndex: number, faceIndex: number
faceType: 1 faceType: number
faceText?: string
packId?: string
stickerId?: string
sourceType?: number
stickerType?: number
resultId?: string
surpriseId?: string
randomType?: number
}
export interface MarketFaceElement {
emojiPackageId: number
faceName?: string
emojiId: string
key: string
} }
export interface VideoElement { export interface VideoElement {
"filePath": string, filePath: string
"fileName": string, fileName: string
"videoMd5"?: string, videoMd5?: string
"thumbMd5"?: string thumbMd5?: string
"fileTime"?: number, // second fileTime?: number // second
"thumbSize"?: number, // byte thumbSize?: number // byte
"fileFormat"?: number, // 2表示mp4 fileFormat?: number // 2表示mp4
"fileSize"?: string, // byte fileSize?: string // byte
"thumbWidth"?: number, thumbWidth?: number
"thumbHeight"?: number, thumbHeight?: number
"busiType"?: 0, // 未知 busiType?: 0 // 未知
"subBusiType"?: 0, // 未知 subBusiType?: 0 // 未知
"thumbPath"?: Map<number, any>, thumbPath?: Map<number, any>
"transferStatus"?: 0, // 未知 transferStatus?: 0 // 未知
"progress"?: 0, // 下载进度? progress?: 0 // 下载进度?
"invalidState"?: 0, // 未知 invalidState?: 0 // 未知
"fileUuid"?: string, // 可以用于下载链接? fileUuid?: string // 可以用于下载链接?
"fileSubId"?: "", fileSubId?: ''
"fileBizId"?: null, fileBizId?: null
"originVideoMd5"?: "", originVideoMd5?: ''
"import_rich_media_context"?: null, import_rich_media_context?: null
"sourceVideoCodecFormat"?: number sourceVideoCodecFormat?: number
} }
export interface TipAioOpGrayTipElement { // 这是什么提示来着? export interface MarkdownElement {
operateType: number, content: string
peerUid: string, }
fromGrpCodeOfTmpChat: 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 {
// 这是什么提示来着?
operateType: number
peerUid: string
fromGrpCodeOfTmpChat: string
} }
export enum TipGroupElementType { export enum TipGroupElementType {
memberIncrease = 1, memberIncrease = 1,
ban = 8 kicked = 3, // 被移出群
ban = 8,
} }
export interface TipGroupElement { export interface TipGroupElement {
"type": TipGroupElementType, // 1是表示有人加入群, 自己加入群也会收到这个 type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0, // 暂时不知 role: 0 // 暂时不知
"groupName": string, // 暂时获取不到 groupName: string // 暂时获取不到
"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
"memberAdd"?: { memberAdd?: {
"showType": 1, showType: 1
"otherAdd": null, otherAdd: null
"otherAddByOtherQRCode": null, otherAddByOtherQRCode: null
"otherAddByYourQRCode": null, otherAddByYourQRCode: null
"youAddByOtherQRCode": null, youAddByOtherQRCode: null
"otherInviteOther": null, otherInviteOther: null
"otherInviteYou": null, otherInviteYou: null
"youInviteOther": null youInviteOther: null
}, }
"shutUp"?: { shutUp?: {
"curTime": string, curTime: string
"duration": string, // 禁言时间,秒 duration: string // 禁言时间,秒
"admin": { admin: {
"uid": string, uid: string
"card": string, card: string
"name": string, name: string
"role": GroupMemberRole role: GroupMemberRole
},
"member": {
"uid": string
"card": string,
"name": string,
"role": GroupMemberRole
}
} }
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
} }
export interface MultiForwardMsgElement {
xmlContent: string // xml格式的消息内容
resId: string
fileName: string
}
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string
msgShortId?: number; // 自己维护的消息id msgType: number
msgTime: string; // 时间戳,秒 subMsgType: number
msgSeq: string; msgShortId?: number // 自己维护的消息id
senderUid: string; msgTime: string // 时间戳,秒
senderUin?: string; // 发送者QQ号 msgSeq: string
peerUid: string; // 群号 或者 QQ uid senderUid: string
peerUin: string; // 群号 或者 发送者QQ号 senderUin?: string // 发送者QQ号
sendNickName: string; peerUid: string // 群号 或者 QQ uid
sendMemberName?: string; // 发送者群名片 peerUin: string // 群号 或者 发送者QQ号
chatType: ChatType; sendNickName: string
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送 sendMemberName?: string // 发送者群名片
recallTime: string; // 撤回时间, "0"是没有撤回 chatType: ChatType
elements: { sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
elementId: string, recallTime: string // 撤回时间, "0"是没有撤回
elementType: ElementType; elements: {
replyElement: { elementId: string
senderUid: string; // 原消息发送者QQ号 elementType: ElementType
sourceMsgIsIncPic: boolean; // 原消息是否有图片 replyElement: {
sourceMsgText: string; senderUid: string // 原消息发送者QQ号
replayMsgSeq: string; // 消息的msgSeq可以通过这个找到源消息的msgId sourceMsgIsIncPic: boolean // 消息是否有图片
}; sourceMsgText: string
textElement: { replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
atType: AtType; }
atUid: string; // QQ号 textElement: {
content: string; atType: AtType
atNtUid: string; // uid atUid: string // QQ
}; content: string
picElement: PicElement; atNtUid: string // uid号
pttElement: PttElement; }
arkElement: ArkElement; picElement: PicElement
grayTipElement: GrayTipElement; pttElement: PttElement
faceElement: FaceElement; arkElement: ArkElement
videoElement: VideoElement; grayTipElement: GrayTipElement
fileElement: FileElement; faceElement: FaceElement
}[]; videoElement: VideoElement
} fileElement: FileElement
marketFaceElement: MarketFaceElement
inlineKeyboardElement: InlineKeyboardElement
markdownElement: MarkdownElement
multiForwardMsgElement: MultiForwardMsgElement
}[]
}

View File

@@ -1,64 +1,66 @@
export enum GroupNotifyTypes { export enum GroupNotifyTypes {
INVITE_ME = 1, INVITE_ME = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群 INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST = 7, JOIN_REQUEST_BY_INVITED = 5, // 有人邀请了别人入群
ADMIN_SET = 8, JOIN_REQUEST = 7,
ADMIN_UNSET = 12, ADMIN_SET = 8,
MEMBER_EXIT = 11, // 主动退出? KICK_MEMBER = 9,
MEMBER_EXIT = 11, // 主动退出
ADMIN_UNSET = 12, // 我被取消管理员
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员
} }
export interface GroupNotifies { export interface GroupNotifies {
doubt: boolean, doubt: boolean
nextStartSeq: string, nextStartSeq: string
notifies: GroupNotify[], notifies: GroupNotify[]
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
IGNORE = 0, IGNORE = 0,
WAIT_HANDLE = 1, WAIT_HANDLE = 1,
APPROVE = 2, APPROVE = 2,
REJECT = 3 REJECT = 3,
} }
export interface GroupNotify { export interface GroupNotify {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify time: number // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 唯一标识符转成数字再除以1000应该就是时间戳 seq: string // 唯一标识符转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes, type: GroupNotifyTypes
status: GroupNotifyStatus, // 0是已忽略1是未处理2是已同意 status: GroupNotifyStatus // 0是已忽略1是未处理2是已同意
group: { groupCode: string, groupName: string }, group: { groupCode: string; groupName: string }
user1: { uid: string, nickName: string }, // 被设置管理员的人 user1: { uid: string; nickName: string } // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者 user2: { uid: string; nickName: string } // 操作者
actionUser: { uid: string, nickName: string }, //未知 actionUser: { uid: string; nickName: string } //未知
actionTime: string, actionTime: string
invitationExt: { invitationExt: {
srcType: number, // 0?未知 srcType: number // 0?未知
groupCode: string, waitStatus: number groupCode: string
}, waitStatus: number
postscript: string, // 加群用户填写的验证信息 }
repeatSeqs: [], postscript: string // 加群用户填写的验证信息
warningTips: string repeatSeqs: []
warningTips: string
} }
export enum GroupRequestOperateTypes { export enum GroupRequestOperateTypes {
approve = 1, approve = 1,
reject = 2 reject = 2,
} }
export interface FriendRequest { export interface FriendRequest {
friendUid: string, friendUid: string
reqTime: string, // 时间戳,秒 reqTime: string // 时间戳,秒
extWords: string, // 申请人填写的验证消息 extWords: string // 申请人填写的验证消息
isUnread: boolean, isUnread: boolean
friendNick: string, friendNick: string
sourceId: number, sourceId: number
groupCode: string groupCode: string
} }
export interface FriendRequestNotify { export interface FriendRequestNotify {
data: { data: {
unreadNums: number, unreadNums: number
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }

View File

@@ -1,75 +1,84 @@
export enum Sex { export enum Sex {
male = 0, male = 0,
female = 2, female = 2,
unknown = 255, unknown = 255,
} }
export interface QQLevel { export interface QQLevel {
"crownNum": number, crownNum: number
"sunNum": number, sunNum: number
"moonNum": number, moonNum: number
"starNum": number starNum: number
} }
export interface User { export interface User {
uid: string; // 加密的字符串 uid: string // 加密的字符串
uin: string; // QQ号 uin: string // QQ号
nick: string; nick: string
avatarUrl?: string; avatarUrl?: string
longNick?: string; // 签名 longNick?: string // 签名
remark?: string; remark?: string
sex?: Sex; sex?: Sex
qqLevel?: QQLevel, qqLevel?: QQLevel
qid?: string qid?: string
"birthday_year"?: number, birthday_year?: number
"birthday_month"?: number, birthday_month?: number
"birthday_day"?: number, birthday_day?: number
"topTime"?: string, topTime?: string
"constellation"?: number, constellation?: number
"shengXiao"?: number, shengXiao?: number
"kBloodType"?: number, kBloodType?: number
"homeTown"?: string, //"0-0-0", homeTown?: string //"0-0-0",
"makeFriendCareer"?: number, makeFriendCareer?: number
"pos"?: string, pos?: string
"eMail"?: string eMail?: string
"phoneNum"?: string, phoneNum?: string
"college"?: string, college?: string
"country"?: string, country?: string
"province"?: string, province?: string
"city"?: string, city?: string
"postCode"?: string, postCode?: string
"address"?: string, address?: string
"isBlock"?: boolean, isBlock?: boolean
"isSpecialCareOpen"?: boolean, isSpecialCareOpen?: boolean
"isSpecialCareZone"?: boolean, isSpecialCareZone?: boolean
"ringId"?: string, ringId?: string
"regTime"?: number, regTime?: number
interest?: string, interest?: string
"labels"?: string[], labels?: string[]
"isHideQQLevel"?: number, isHideQQLevel?: number
"privilegeIcon"?: { privilegeIcon?: {
"jumpUrl": string, jumpUrl: string
"openIconList": unknown[], openIconList: unknown[]
"closeIconList": unknown[] closeIconList: unknown[]
}, }
"photoWall"?: { photoWall?: {
"picList": unknown[] picList: unknown[]
}, }
"vipFlag"?: boolean, vipFlag?: boolean
"yearVipFlag"?: boolean, yearVipFlag?: boolean
"svipFlag"?: boolean, svipFlag?: boolean
"vipLevel"?: number, vipLevel?: number
"status"?: number, status?: number
"qidianMasterFlag"?: number, qidianMasterFlag?: number
"qidianCrewFlag"?: number, qidianCrewFlag?: number
"qidianCrewFlag2"?: number, qidianCrewFlag2?: number
"extStatus"?: number, extStatus?: number
"recommendImgFlag"?: number, recommendImgFlag?: number
"disableEmojiShortCuts"?: number, disableEmojiShortCuts?: number
"pendantId"?: string, pendantId?: string
} }
export interface SelfInfo extends User { export interface SelfInfo extends User {
online?: boolean; online?: boolean
} }
export interface Friend extends User {} export interface Friend extends User {
}
export interface CategoryFriend {
categoryId: number;
categroyName: string;
categroyMbCount: number;
buddyList: User[]
}

View File

@@ -1,49 +1,49 @@
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/log"; import { log } from '../../common/utils/log'
class BaseAction<PayloadType, ReturnDataType> { class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName actionName: ActionName
protected async check(payload: PayloadType): Promise<BaseCheckResult> { protected async check(payload: PayloadType): Promise<BaseCheckResult> {
return { return {
valid: true, valid: true,
}
} }
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> { public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload); const result = await this.check(payload)
if (!result.valid) { if (!result.valid) {
return OB11Response.error(result.message, 400); return OB11Response.error(result.message, 400)
}
try {
const resData = await this._handle(payload);
return OB11Response.ok(resData);
} catch (e) {
log("发生错误", e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || "未知错误,可能操作超时", 200);
}
} }
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData)
} catch (e) {
log('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200)
}
}
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> { public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload) const result = await this.check(payload)
if (!result.valid) { if (!result.valid) {
return OB11Response.error(result.message, 1400) return OB11Response.error(result.message, 1400)
}
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo);
} catch (e) {
log("发生错误", e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}
} }
try {
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo)
} catch (e) {
log('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}
}
protected async _handle(payload: PayloadType): Promise<ReturnDataType> { protected async _handle(payload: PayloadType): Promise<ReturnDataType> {
throw `pleas override ${this.actionName} _handle`; throw `pleas override ${this.actionName} _handle`
} }
} }
export default BaseAction export default BaseAction

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
import {ActionName} from "./types";
import BaseAction from "./BaseAction";
import {dbUtil} from "../../common/db";
import {NTQQMsgApi} from "../../ntqqapi/api/msg";
interface Payload {
message_id: number
}
class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg
protected async _handle(payload: Payload) {
let msg = await dbUtil.getMsgByShortId(payload.message_id)
await NTQQMsgApi.recallMsg({
chatType: msg.chatType,
peerUid: msg.peerUid
}, [msg.msgId])
}
}
export default DeleteMsg

View File

@@ -1,102 +0,0 @@
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";
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> {
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)
const element = msg.elements.find(e=>e.fileElement)
log("找到了文件 element", element);
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.elementId, "", "", true)
await sleep(1000);
msg = await dbUtil.getMsgByLongId(cache.msgId)
log("下载完成后的msg", msg)
cache.filePath = msg?.elements.find(e=>e.fileElement)?.fileElement?.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,16 +0,0 @@
import {OB11User} from '../types';
import {OB11Constructor} from "../constructor";
import {friends} from "../../common/data";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
class GetFriendList extends BaseAction<null, OB11User[]> {
actionName = ActionName.GetFriendList
protected async _handle(payload: null) {
return OB11Constructor.friends(friends);
}
}
export default GetFriendList

View File

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

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,35 +0,0 @@
import {OB11GroupMember} from '../types';
import {getGroupMember} from "../../common/data";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {NTQQUserApi} from "../../ntqqapi/api/user";
import {log} from "../../common/utils/log";
import {isNull} from "../../common/utils/helper";
export interface PayloadType {
group_id: number
user_id: number
}
class GetGroupMemberInfo extends BaseAction<PayloadType, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo
protected async _handle(payload: PayloadType) {
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString())
if (member) {
if (isNull(member.sex)){
log("获取群成员详细信息")
let info = (await NTQQUserApi.getUserDetailInfo(member.uid))
log("群成员详细信息结果", info)
Object.assign(member, info);
}
return OB11Constructor.groupMember(payload.group_id.toString(), member)
} else {
throw (`群成员${payload.user_id}不存在`)
}
}
}
export default GetGroupMemberInfo

View File

@@ -1,29 +0,0 @@
import {OB11GroupMember} from '../types';
import {getGroup} from "../../common/data";
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {NTQQGroupApi} from "../../ntqqapi/api/group";
export interface PayloadType {
group_id: number
}
class GetGroupMemberList extends BaseAction<PayloadType, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList
protected async _handle(payload: PayloadType) {
const group = await getGroup(payload.group_id.toString());
if (group) {
if (!group.members?.length) {
group.members = await NTQQGroupApi.getGroupMembers(payload.group_id.toString())
}
return OB11Constructor.groupMembers(group);
} else {
throw (`${payload.group_id}不存在`)
}
}
}
export default GetGroupMemberList

View File

@@ -1,10 +0,0 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
export default class GetGuildList extends BaseAction<null, null> {
actionName = ActionName.GetGuildList
protected async _handle(payload: null): Promise<null> {
return null;
}
}

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
import {OB11Message} from '../types';
import {OB11Constructor} from "../constructor";
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {dbUtil} from "../../common/db";
export interface PayloadType {
message_id: number
}
export type ReturnDataType = OB11Message
class GetMsg extends BaseAction<PayloadType, OB11Message> {
actionName = ActionName.GetMsg
protected async _handle(payload: PayloadType) {
// log("history msg ids", Object.keys(msgHistory));
if (!payload.message_id) {
throw ("参数message_id不能为空")
}
let msg = await dbUtil.getMsgByShortId(payload.message_id)
if(!msg) {
msg = await dbUtil.getMsgByLongId(payload.message_id.toString())
}
if (!msg){
throw ("消息不存在")
}
return await OB11Constructor.message(msg)
}
}
export default GetMsg

View File

@@ -1,15 +0,0 @@
import {GetFileBase, GetFilePayload, GetFileResponse} from "./GetFile";
import {ActionName} from "./types";
interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
}
export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = super._handle(payload);
return res;
}
}

View File

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

View File

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

View File

@@ -1,14 +0,0 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
interface Payload{
message_id: number
}
export default class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null>{
actionName = ActionName.GoCQHTTP_MarkMsgAsRead
protected async _handle(payload: Payload): Promise<null> {
return null
}
}

View File

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

View File

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

View File

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

View File

@@ -1,511 +0,0 @@
import {
AtType,
ChatType,
ElementType,
Group,
RawMessage,
SendArkElement,
SendMessageElement
} from "../../ntqqapi/types";
import {
friends,
getFriend,
getGroup,
getGroupMember,
getUidByUin,
selfInfo,
} from "../../common/data";
import {
OB11MessageCustomMusic,
OB11MessageData,
OB11MessageDataType,
OB11MessageMixType,
OB11MessageNode,
OB11PostSendMsg
} from '../types';
import {Peer} from "../../ntqqapi/api/msg";
import {SendMsgElementConstructor} from "../../ntqqapi/constructor";
import BaseAction from "./BaseAction";
import {ActionName, BaseCheckResult} from "./types";
import * as fs from "node:fs";
import {decodeCQCode} from "../cqcode";
import {dbUtil} from "../../common/db";
import {ALLOW_SEND_TEMP_MSG} from "../../common/config";
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 checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (let msg of sendMsgList) {
if (msg["type"] && msg["data"]) {
let type = msg["type"];
let data = msg["data"];
if (type === "text" && !data["text"]) {
return 400;
} else if (["image", "voice", "record"].includes(type)) {
if (!data["file"]) {
return 400;
} else {
if (checkUri(data["file"])) {
return 200;
} else {
return 400;
}
}
} else if (type === "at" && !data["qq"]) {
return 400;
} else if (type === "reply" && !data["id"]) {
return 400;
}
} else {
return 400
}
}
return 200;
}
export interface ReturnDataType {
message_id: number
}
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
const messages = this.convertMessage2List(payload.message);
const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node)
if (fmNum && fmNum != messages.length) {
return {
valid: false,
message: "转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素"
}
}
if (payload.group_id && !(await getGroup(payload.group_id))) {
return {
valid: false,
message: `${payload.group_id}不存在`
}
}
if (payload.user_id && payload.message_type !== "group") {
if (!(await getFriend(payload.user_id))) {
if (!ALLOW_SEND_TEMP_MSG && !(await dbUtil.getReceivedTempUinMap())[payload.user_id.toString()]) {
return {
valid: false,
message: `不能发送临时消息`
}
}
}
}
return {
valid: true,
}
}
protected async _handle(payload: OB11PostSendMsg) {
const peer: Peer = {
chatType: ChatType.friend,
peerUid: ""
}
let isTempMsg = false;
let group: Group | undefined = undefined;
const genGroupPeer = async () => {
group = await getGroup(payload.group_id.toString())
peer.chatType = ChatType.group
// peer.name = group.name
peer.peerUid = group.groupCode
}
const genFriendPeer = () => {
const friend = friends.find(f => f.uin == payload.user_id.toString())
if (friend) {
// peer.name = friend.nickName
peer.peerUid = friend.uid
} else {
peer.chatType = ChatType.temp
const tempUserUid = getUidByUin(payload.user_id.toString())
if (!tempUserUid) {
throw (`找不到私聊对象${payload.user_id}`)
}
// peer.name = tempUser.nickName
isTempMsg = true;
peer.peerUid = tempUserUid;
}
}
if (payload?.group_id && payload.message_type === "group") {
await genGroupPeer()
} else if (payload?.user_id) {
genFriendPeer()
} else if (payload.group_id) {
await genGroupPeer()
} else {
throw ("发送消息参数错误, 请指定group_id或user_id")
}
const messages = this.convertMessage2List(payload.message);
if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) {
try {
const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group)
return {message_id: returnMsg.msgShortId}
} catch (e) {
throw ("发送转发消息失败 " + e.toString())
}
} else {
if (this.getSpecialMsgNum(payload, OB11MessageDataType.music)) {
const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic
if (music) {
const {url, audio, title, content, image} = music.data;
const selfPeer: Peer = {peerUid: selfInfo.uid, chatType: ChatType.friend}
// 搞不定!
// const musicMsg = await this.send(selfPeer, [this.genMusicElement(url, audio, title, content, image)], [], false)
// 转发
// const res = await NTQQApi.forwardMsg(selfPeer, peer, [musicMsg.msgId])
// log("转发音乐消息成功", res);
// return {message_id: musicMsg.msgShortId}
}
}
}
// log("send msg:", peer, sendElements)
const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group)
const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}));
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 {
if (Array.isArray(payload.message)) {
return payload.message.filter(msg => msg.type == msgType).length
}
return 0
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage> {
log("克隆的目标消息", msg)
let sendElements: SendMessageElement[] = [];
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement)
// Object.keys(ele).forEach((eleKey) => {
// if (eleKey.endsWith("Element")) {
// }
}
if (sendElements.length === 0) {
log("需要clone的消息无法解析将会忽略掉", msg)
}
log("克隆消息", sendElements)
try {
const nodeMsg = await NTQQMsgApi.sendMsg({
chatType: ChatType.friend,
peerUid: selfInfo.uid
}, sendElements, true);
await sleep(500);
return nodeMsg
} catch (e) {
log(e, "克隆转发消息失败,将忽略本条消息", msg);
}
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[], group: Group | undefined) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
let nodeMsgIds: string[] = []
// 先判断一遍是不是id和自定义混用
let needClone = messageNodes.filter(node => node.data.id).length && messageNodes.filter(node => !node.data.id).length
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
let nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
let nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId));
if (!needClone) {
nodeMsgIds.push(nodeMsg.msgId)
} else {
if (nodeMsg.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(nodeMsg)
if (cloneMsg) {
nodeMsgIds.push(cloneMsg.msgId)
}
}
}
} else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const {
sendElements,
deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group);
log("开始生成转发节点", sendElements);
let sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0;
for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = []
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++;
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++;
} else {
sendElementsSplit[splitIndex].push(ele)
}
log(sendElementsSplit)
}
// log("分割后的转发节点", sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await this.send(selfPeer, eles, [], true);
nodeMsgIds.push(nodeMsg.msgId)
await sleep(500);
log("转发节点生成成功", nodeMsg.msgId);
}
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}));
} catch (e) {
log("生成转发消息节点失败", e)
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
let nodeMsgArray: Array<RawMessage> = []
let srcPeer: Peer = null;
let needSendSelf = false;
for (const [index, msgId] of nodeMsgIds.entries()) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId)
if (nodeMsg) {
nodeMsgArray.push(nodeMsg)
if (!srcPeer) {
srcPeer = {chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid}
} else if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
srcPeer = selfPeer
}
}
}
log("nodeMsgArray", nodeMsgArray);
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
if (needSendSelf) {
log("需要克隆转发消息");
for (const [index, msg] of nodeMsgArray.entries()) {
if (msg.peerUid !== selfInfo.uid) {
const cloneMsg = await this.cloneMsg(msg)
if (cloneMsg) {
nodeMsgIds[index] = cloneMsg.msgId
}
}
}
}
// elements之间用换行符分隔
// let _sendForwardElements: SendMessageElement[] = []
// for(let i = 0; i < sendForwardElements.length; i++){
// _sendForwardElements.push(sendForwardElements[i])
// _sendForwardElements.push(SendMsgElementConstructor.text("\n\n"))
// }
// const nodeMsg = await NTQQApi.sendMsg(selfPeer, _sendForwardElements, true);
// nodeIds.push(nodeMsg.msgId)
// await sleep(500);
// 开发转发
try {
log("开发转发", nodeMsgIds)
return await NTQQMsgApi.multiForwardMsg(srcPeer, destPeer, nodeMsgIds)
} catch (e) {
log("forward failed", e)
return null;
}
}
private async createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) {
let sendElements: SendMessageElement[] = []
let deleteAfterSentFiles: string[] = []
for (let sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) {
continue
}
switch (sendMsg.type) {
case OB11MessageDataType.text: {
const text = sendMsg.data?.text;
if (text) {
sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text))
}
}
break;
case OB11MessageDataType.at: {
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 || ""));
}
}
}
} break;
case OB11MessageDataType.json: {
sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data))
}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 {
const musicJson = {
app: 'com.tencent.structmsg',
config: {
ctime: 1709689928,
forward: 1,
token: '5c1e4905f926dd3a64a4bd3841460351',
type: 'normal'
},
extra: {app_type: 1, appid: 100497308, uin: selfInfo.uin},
meta: {
news: {
action: '',
android_pkg_name: '',
app_type: 1,
appid: 100497308,
ctime: 1709689928,
desc: content || title,
jumpUrl: url,
musicUrl: audio,
preview: image,
source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
source_url: '',
tag: 'QQ音乐',
title: title,
uin: selfInfo.uin,
}
},
prompt: content || title,
ver: '0.0.0.1',
view: 'news'
}
return SendMsgElementConstructor.ark(musicJson)
}
}
export default SendMsg

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
import BaseAction from "./BaseAction";
import {getGroupMember} from "../../common/data";
import {ActionName} from "./types";
import {NTQQGroupApi} from "../../ntqqapi/api/group";
interface Payload {
group_id: number,
user_id: number,
duration: number
}
export default class SetGroupBan extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupBan
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if (!member) {
throw `群成员${payload.user_id}不存在`
}
await NTQQGroupApi.banMember(payload.group_id.toString(),
[{uid: member.uid, timeStamp: parseInt(payload.duration.toString())}])
return null
}
}

View File

@@ -1,23 +0,0 @@
import BaseAction from "./BaseAction";
import {getGroupMember} from "../../common/data";
import {ActionName} from "./types";
import {NTQQGroupApi} from "../../ntqqapi/api/group";
interface Payload {
group_id: number,
user_id: number,
card: string
}
export default class SetGroupCard extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupCard
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if (!member) {
throw `群成员${payload.user_id}不存在`
}
await NTQQGroupApi.setMemberCard(payload.group_id.toString(), member.uid, payload.card || "")
return null
}
}

View File

@@ -1,23 +0,0 @@
import BaseAction from "./BaseAction";
import {getGroupMember} from "../../common/data";
import {ActionName} from "./types";
import {NTQQGroupApi} from "../../ntqqapi/api/group";
interface Payload {
group_id: number,
user_id: number,
reject_add_request: boolean
}
export default class SetGroupKick extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupKick
protected async _handle(payload: Payload): Promise<null> {
const member = await getGroupMember(payload.group_id, payload.user_id)
if (!member) {
throw `群成员${payload.user_id}不存在`
}
await NTQQGroupApi.kickMember(payload.group_id.toString(), [member.uid], !!payload.reject_add_request);
return null
}
}

View File

@@ -1,22 +0,0 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {NTQQGroupApi} from "../../ntqqapi/api/group";
import {log} from "../../common/utils/log";
interface Payload {
group_id: number,
is_dismiss: boolean
}
export default class SetGroupLeave extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupLeave
protected async _handle(payload: Payload): Promise<any> {
try {
await NTQQGroupApi.quitGroup(payload.group_id.toString())
} catch (e) {
log("退群失败", e)
throw e
}
}
}

View File

@@ -1,18 +0,0 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
import {NTQQGroupApi} from "../../ntqqapi/api/group";
interface Payload {
group_id: number,
group_name: string
}
export default class SetGroupName extends BaseAction<Payload, null> {
actionName = ActionName.SetGroupName
protected async _handle(payload: Payload): Promise<null> {
await NTQQGroupApi.setGroupName(payload.group_id.toString(), payload.group_name)
return null
}
}

View File

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

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 { checkFileReceived, log, sleep, uri2local } from '@/common/utils'
import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types'
import { FileCache } from '@/common/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, elementId: string): VideoElement | FileElement {
let element = msg.elements.find((e) => e.elementId === elementId)
if (!element) {
throw new Error('element not found')
}
return element.fileElement
}
private async download(cache: FileCache, file: string) {
log('需要调用 NTQQ 下载文件api')
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg) {
log('找到了文件 msg', msg)
let element = this.getElement(msg, cache.elementId)
log('找到了文件 element', element)
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '', true)
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}
}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let cache = await dbUtil.getFileCache(payload.file)
if (!cache) {
throw new Error('file not found')
}
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
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 {
await this.download(cache, payload.file)
}
} else {
// 没有url的可能是私聊文件或者群文件需要自己下载
await this.download(cache, payload.file)
}
}
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

@@ -0,0 +1,6 @@
import { GetFileBase } from './GetFile'
import { ActionName } from '../types'
export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage
}

View File

@@ -0,0 +1,25 @@
import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types'
import {decodeSilk} from "@/common/utils/audio";
import { getConfigUtil } from '@/common/config'
import path from 'node:path'
import fs from 'node:fs'
interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
}
export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord
protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = await super._handle(payload)
res.file = await decodeSilk(res.file, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){
res.base64 = fs.readFileSync(res.file, 'base64')
}
return res
}
}

View File

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

@@ -1,39 +1,46 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {OB11Message, OB11User} from "../../types"; import { OB11Message, OB11User } from '../../types'
import {groups} from "../../../common/data"; import { groups } from '../../../common/data'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {ChatType} from "../../../ntqqapi/types"; import { ChatType } from '../../../ntqqapi/types'
import {dbUtil} from "../../../common/db"; import { dbUtil } from '../../../common/db'
import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; import { NTQQMsgApi } from '../../../ntqqapi/api/msg'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import {log} from "../../../common/utils"; import { log } from '../../../common/utils'
interface Payload { interface Payload {
group_id: number group_id: number
message_seq: number, message_seq: number
count: number count: number
} }
interface Response{ interface Response {
messages: OB11Message[] messages: OB11Message[]
} }
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> { export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const group = groups.find(group => group.groupCode === payload.group_id.toString()) const group = groups.find((group) => group.groupCode === payload.group_id.toString())
if (!group) { if (!group) {
throw `${payload.group_id}不存在` 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}
} }
} 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 }
}
}

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