Compare commits

...

260 Commits

Author SHA1 Message Date
idranme
26fc0c68b2 Merge pull request #337 from LLOneBot/dev
3.29.1
2024-08-14 19:00:42 +08:00
idranme
c1d7aa7aed chore: v3.29.1 2024-08-14 18:59:27 +08:00
idranme
6aa44bdd79 fix: /get_image 2024-08-14 18:20:39 +08:00
idranme
77f3bfc5c5 Merge pull request #335 from LLOneBot/dev
3.29.0
2024-08-13 22:11:17 +08:00
idranme
2715552814 chore: v3.29.0 2024-08-13 22:08:36 +08:00
idranme
8ed0e6c1be fix 2024-08-13 21:59:13 +08:00
idranme
260a0be184 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-08-13 19:31:10 +08:00
idranme
6582ffe964 fix: msg 2024-08-13 19:29:22 +08:00
linyuchen
f8e231b8b8 chore: v3.28.7
fix: CPU占用过高
fix: 好友列表变动hook失败
2024-08-13 19:09:13 +08:00
idranme
a0f5cc0e36 Merge pull request #333 from LLOneBot/dev
Update README.md
2024-08-12 15:01:26 +08:00
idranme
277c2a9b67 Update README.md 2024-08-12 15:00:41 +08:00
idranme
874acdd7fe Merge pull request #331 from LLOneBot/dev
3.28.6
2024-08-12 00:03:25 +08:00
idranme
b2b996df9c chore: v3.28.6 2024-08-12 00:01:39 +08:00
idranme
4427774c2d fix: multiForwardMsg 2024-08-12 00:01:06 +08:00
idranme
41c04faa05 Merge pull request #330 from LLOneBot/dev
3.28.5
2024-08-11 20:01:29 +08:00
idranme
6ad4492f01 chore: v3.28.5 2024-08-11 20:00:47 +08:00
idranme
d52f16bc88 opt 2024-08-11 19:42:44 +08:00
idranme
2b0179acd1 opt 2024-08-11 18:10:27 +08:00
idranme
f540f324a1 Merge pull request #329 from LLOneBot/dev
3.28.4
2024-08-11 12:21:37 +08:00
idranme
128f40a51d chore: v3.28.4 2024-08-11 12:17:47 +08:00
idranme
c815e0ca6b sync 2024-08-11 12:16:53 +08:00
idranme
1da720e0a7 sync 2024-08-11 02:43:14 +08:00
idranme
1472c9c949 opt 2024-08-11 00:23:17 +08:00
idranme
4678253815 sync 2024-08-11 00:18:54 +08:00
idranme
e1176e18cd Merge pull request #328 from LLOneBot/dev
3.28.3
2024-08-10 23:19:09 +08:00
idranme
107f02f21f chore: 3.28.3 2024-08-10 23:17:38 +08:00
idranme
51f8db3a83 opt 2024-08-10 22:31:14 +08:00
idranme
25691a4124 sync 2024-08-10 22:09:35 +08:00
idranme
40f03e6401 sync 2024-08-10 21:34:28 +08:00
idranme
9f89094978 sync 2024-08-10 20:36:15 +08:00
idranme
04f837145c sync 2024-08-10 18:14:33 +08:00
idranme
6126920830 sync 2024-08-10 17:17:19 +08:00
idranme
5c219aa003 opt 2024-08-09 22:32:54 +08:00
idranme
ce5cf82339 Merge pull request #325 from LLOneBot/dev
3.28.2
2024-08-09 18:10:50 +08:00
idranme
6931277e33 chore: v3.28.2 2024-08-09 18:07:50 +08:00
idranme
be1b9c21c1 feat: support for at message segment specifying name 2024-08-09 18:02:52 +08:00
idranme
b02cd3af00 Create .editorconfig 2024-08-09 16:46:08 +08:00
idranme
22dcbac16f Merge pull request #324 from LLOneBot/dev
fix ci
2024-08-09 16:06:44 +08:00
idranme
44faedd6c0 fix ci 2024-08-09 16:05:51 +08:00
idranme
fb3b673e63 Merge pull request #323 from LLOneBot/dev
fix ci
2024-08-09 15:53:42 +08:00
idranme
4e377f86d1 fix ci 2024-08-09 15:53:04 +08:00
idranme
e8bd98020b Merge pull request #322 from LLOneBot/dev
v3.28.1
2024-08-09 15:49:29 +08:00
idranme
c520034934 chore: v3.28.1 2024-08-09 15:47:57 +08:00
idranme
5d5fd403b8 fix: filtering at segments when sending private chat messages 2024-08-09 15:44:18 +08:00
idranme
1fc02229df sync 2024-08-09 15:40:08 +08:00
idranme
6c8d3db3a4 opt 2024-08-09 14:26:30 +08:00
idranme
c5b69561af sync 2024-08-09 14:20:59 +08:00
idranme
b5bffff941 fix 2024-08-07 23:17:13 +08:00
idranme
1a2cdc8c0e opt 2024-08-07 22:08:47 +08:00
idranme
50ab62f103 opt: config 2024-08-07 21:39:26 +08:00
idranme
5005d83ce0 opt: audio encoding and decoding 2024-08-07 04:22:51 +08:00
idranme
d7e40e488c Update README.md
LLAPI 已删库
2024-08-06 22:31:39 +08:00
idranme
4958e22770 Update README.md 2024-08-06 22:28:49 +08:00
idranme
a5e3f94228 chore: deps 2024-08-06 22:26:21 +08:00
idranme
9e57b2c17e Update publish.yml 2024-08-06 14:51:17 +08:00
idranme
e1ff366e10 clean 2024-08-06 02:32:28 +08:00
idranme
6b03b01a24 Merge pull request #319 from LLOneBot/dev
chore: v3.28.0
2024-08-06 02:08:51 +08:00
idranme
18f01b7f21 chore: v3.28.0 2024-08-06 02:08:00 +08:00
idranme
897f691d6c make ts happy 2024-08-06 01:47:51 +08:00
idranme
a9902d9109 sync 2024-08-05 22:49:48 +08:00
idranme
5d78fdd6a4 fix 2024-08-05 22:07:04 +08:00
idranme
72eb013371 fix 2024-08-05 20:44:28 +08:00
idranme
808777c044 fix: import path 2024-08-05 19:18:15 +08:00
idranme
a2d1379866 sync 2024-08-05 19:09:41 +08:00
idranme
c41a8556fa Change description 2024-08-05 00:23:41 +08:00
idranme
fa2df2a3cd opt 2024-08-04 23:11:59 +08:00
idranme
b28dd3a723 Update publish.yml 2024-08-04 22:44:20 +08:00
idranme
6ffa41e0d6 prioritise local versions 2024-08-04 22:14:07 +08:00
idranme
85df3794e8 optimise 2024-08-04 22:07:55 +08:00
idranme
4bee2ba062 reduce icon size 2024-08-04 20:35:31 +08:00
idranme
4bf992c4a9 chore: deps 2024-08-04 20:31:29 +08:00
idranme
898e856150 poke require >=25765 2024-08-04 20:22:07 +08:00
idranme
c86797afc8 chore: remove unused eslint 2024-08-04 19:54:32 +08:00
idranme
799593b788 chore: support yarn berry 2024-08-04 19:48:17 +08:00
idranme
74d9a083aa Update README.md 2024-08-04 19:28:13 +08:00
idranme
cae525429a Update README.md 2024-08-04 19:21:44 +08:00
idranme
cc0d1e2a9b Merge pull request #316 from idranme/uuid
refa: deps
2024-08-04 18:36:01 +08:00
idranme
34ecfcfa16 Merge branch 'dev' into uuid 2024-08-04 18:35:11 +08:00
idranme
79c5041216 Merge pull request #318 from LLOneBot/dev 2024-08-04 18:07:03 +08:00
idranme
8fb53260ab chore: v3.27.4 2024-08-04 10:05:03 +00:00
idranme
07d9ac823a Merge pull request #317 from LLOneBot/dev
chore: v3.27.4
2024-08-04 17:48:24 +08:00
idranme
b571ef434c chore 2024-08-02 20:50:34 +00:00
idranme
c1f4dcd6a6 chore 2024-08-02 20:40:50 +00:00
idranme
4c5befbe44 chore 2024-08-02 20:39:26 +00:00
linyuchen
296cd4d0a3 Merge pull request #315 from idranme/main
feat: at segment add name
2024-08-02 23:11:14 +08:00
linyuchen
e77a2ca34a Merge pull request #311 from cnxysoft/dev
BUG修复
2024-08-02 23:09:35 +08:00
idranme
f3af0d18bc refa: deps 2024-08-02 12:00:13 +00:00
idranme
406e3c7e6b opt 2024-08-02 10:49:30 +00:00
idranme
3f5ca8ebfa chore 2024-08-02 10:31:37 +00:00
idranme
6e8389e833 chore 2024-08-02 10:26:18 +00:00
idranme
71aedca4c6 feat: the name attribute of the at message segment 2024-08-02 10:23:48 +00:00
Alen
6410689549 BUG修复
尝试修复设精事件shortId和senderId
2024-08-01 21:56:44 +08:00
linyuchen
6d0e2269cc Merge pull request #304 from cnxysoft/dev
功能更新
2024-07-28 14:52:58 +08:00
linyuchen
2e28fc678c Merge branch 'dev' into dev 2024-07-28 14:52:17 +08:00
linyuchen
8204f4407f Merge pull request #300 from super1207/dev
Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev
2024-07-26 09:57:37 +08:00
Alen
9f1d4c4db2 功能修改
修改群管变更事件获取渠道,让所有群角色都能收到群管变更通知
2024-07-25 17:25:40 +08:00
Alen
8ba47635d3 功能更新
1.增加设精事件上报(目前上报的shortId经常出错,实际消息体却是正确的,待解决)
2.增加设精/取消设精api接口
3.poke事件增加raw信息上报
2024-07-25 01:02:48 +08:00
Alen
5fa2427c51 修改poke事件
新增poke事件支持上传raw信息
2024-07-24 19:04:07 +08:00
Alen
aa8739d016 Merge remote-tracking branch 'upstream/main' into dev 2024-07-24 11:48:55 +08:00
super1207
79f0329da7 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-07-20 18:01:30 +08:00
super1207
7a33a36f44 add get_event api 2024-07-20 17:58:00 +08:00
linyuchen
808424d08e Merge branch 'main' into dev 2024-07-20 17:08:59 +08:00
linyuchen
d0967785de chore: v3.27.3 2024-07-20 16:58:03 +08:00
linyuchen
eccabb8189 Merge pull request #299 from Natsukage/main
fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
2024-07-20 15:25:27 +08:00
夏影
c9374ff515 fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
Added logic to skip name-value pairs in encodeCQCode when value cannot be converted to string, preventing errors caused by undefined values. This ensures the function can handle such cases gracefully and continue processing other valid data.
2024-07-20 00:49:34 +08:00
Alen
92c4889924 Merge remote-tracking branch 'upstream/main' 2024-07-16 23:19:32 +08:00
linyuchen
f9454039a1 fix: old poke event 2024-07-16 21:52:15 +08:00
linyuchen
bc4511e175 chore: v3.27.2 2024-07-16 21:43:50 +08:00
linyuchen
f191103f99 Merge pull request #294 from cnxysoft/dev
修复戳一戳
2024-07-16 21:38:17 +08:00
linyuchen
408463f63b Merge branch 'dev' into dev 2024-07-16 21:21:50 +08:00
Alen
fb96c4272e 修复戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 21:01:19 +08:00
Alen
c6b302d5a8 修复好友戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 20:27:44 +08:00
linyuchen
1dd468e2ff fix: #290 2024-07-13 16:25:00 +08:00
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
183 changed files with 16898 additions and 14288 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
insert_final_newline = true
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

View File

@@ -1,39 +1,38 @@
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@v4
- name: setup node - name: setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v4
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 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 }}

15
.gitignore vendored
View File

@@ -1,6 +1,15 @@
node_modules/ .yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
package-lock.json package-lock.json
dist/ yarn.lock
out/ node_modules
dist
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

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
nodeLinker: node-modules

View File

@@ -1,6 +1,9 @@
# LLOneBot
# LLOneBot API LiteLoaderQQNT 插件,实现 OneBot 11 协议,用以 QQ 机器人开发
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
> [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息**
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG群<https://t.me/+nLZEnpne-pQ1OWFl>
@@ -10,43 +13,28 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 设置界面 ## 设置界面
<img src="./doc/image/setting.png" width="500px" alt="图片名称"/> <img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例 ## HTTP 调用示例
![](doc/image/example.jpg) <img src="./doc/image/example.jpg" width="500px" alt="HTTP调用示例"/>
## 支持的 api 和功能详情 ## 支持的 API
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录
- [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息
- [x] 文件消息
- [x] 群禁言事件上报
- [x] 优化加群成功事件上报
- [x] 清理缓存api
- [ ] 无头模式
- [ ] 框架对接文档
## onebot11文档
<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) - [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
* [chronocat](https://github.com/chrononeko/chronocat/) - [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [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,83 @@
import cp from 'vite-plugin-cp'; import cp from 'vite-plugin-cp'
import "./scripts/gen-version" import path from 'node:path'
import './scripts/gen-manifest'
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',
'@minatojs/sql.js',
]
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',
} },
}, minify: true,
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/crychic/crychic-win32-x64.node', dest: 'dist/main/'},
]
})]
}, },
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.webp', 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

BIN
icon.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
icon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,11 +1,11 @@
{ {
"manifest_version": 4, "manifest_version": 4,
"type": "extension", "type": "extension",
"name": "LLOneBot v3.21.0", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "使你的NTQQ支持OneBot11协议进行QQ机器人开发, 不支持商店在线更新", "description": "实现 OneBot 11 协议,用以 QQ 机器人开发",
"version": "3.21.0", "version": "3.29.1",
"icon": "./icon.jpg", "icon": "./icon.webp",
"authors": [ "authors": [
{ {
"name": "linyuchen", "name": "linyuchen",
@@ -13,7 +13,7 @@
} }
], ],
"repository": { "repository": {
"repo": "linyuchen/LiteLoaderQQNT-OneBotApi", "repo": "LLOneBot/LLOneBot",
"branch": "main", "branch": "main",
"release": { "release": {
"tag": "latest", "tag": "latest",

6782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,44 +2,43 @@
"name": "llonebot", "name": "llonebot",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"description": "NTQQLiteLoaderOneBotApi", "description": "",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"build": "electron-vite build", "build": "electron-vite build",
"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", "@minatojs/driver-sqlite": "^4.4.1",
"express": "^4.18.2", "compressing": "^1.10.1",
"file-type": "^19.0.0", "cordis": "^3.17.9",
"fluent-ffmpeg": "^2.1.2", "cors": "^2.8.5",
"level": "^8.0.1", "express": "^4.19.2",
"silk-wasm": "^3.3.4", "fast-xml-parser": "^4.4.1",
"utf-8-validate": "^6.0.3", "file-type": "^19.4.0",
"uuid": "^9.0.1", "fluent-ffmpeg": "^2.1.3",
"ws": "^8.16.0" "minato": "^3.4.3",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.20", "@types/cors": "^2.8.17",
"@types/fluent-ffmpeg": "^2.1.24", "@types/express": "^4.17.21",
"@types/node": "^20.11.24", "@types/fluent-ffmpeg": "^2.1.25",
"@types/uuid": "^9.0.8", "@types/node": "^20.14.15",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^6.4.0", "electron": "^29.1.4",
"electron": "^29.0.1", "electron-vite": "^2.3.0",
"electron-vite": "^2.0.0", "typescript": "^5.5.4",
"eslint": "^8.0.1", "vite": "^5.4.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-promise": "^6.0.0",
"ts-node": "^10.9.2",
"typescript": "*",
"vite": "^5.1.4",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
} },
"packageManager": "yarn@4.4.0"
} }

38
scripts/gen-manifest.ts Normal file
View File

@@ -0,0 +1,38 @@
import { version } from '../src/version'
import { writeFileSync } from 'node:fs'
const manifest = {
manifest_version: 4,
type: 'extension',
name: 'LLOneBot',
slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用以 QQ 机器人开发',
version,
icon: './icon.webp',
authors: [
{
name: 'linyuchen',
link: 'https://github.com/linyuchen'
}
],
repository: {
repo: 'LLOneBot/LLOneBot',
branch: 'main',
release: {
tag: 'latest',
name: 'LLOneBot.zip'
}
},
platform: [
'win32',
'linux',
'darwin'
],
injects: {
renderer: './renderer/index.js',
main: './main/main.cjs',
preload: './preload/preload.cjs'
}
}
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2))

View File

@@ -1,22 +0,0 @@
import fs from 'fs'
import path from 'path'
import { version } from '../src/version'
const manifestPath = path.join(__dirname, '../manifest.json')
function readManifest (): any {
if (fs.existsSync(manifestPath)) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
}
}
function writeManifest (manifest: any) {
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
}
const manifest = readManifest()
if (version !== manifest.version) {
manifest.version = version
manifest.name = `LLOneBot v${version}`
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'] as string, { valueEncoding: 'json' })
async function getGroupNotify() { async function getGroupNotify() {
let keys = await db.keys().all(); let keys = await db.keys().all()
let result = [] let result: string[] = []
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,98 +1,102 @@
import fs from "fs"; import fs from 'node:fs'
import fsPromise from "fs/promises"; import { Config, OB11Config } from './types'
import {Config, OB11Config} from './types'; import { mergeNewProperties } from './utils/helper'
import path from 'node:path'
import { getSelfUin } from './data'
import { DATA_DIR } from './utils'
import {mergeNewProperties} from "./utils/helper"; export const HOOK_LOG = false
import path from "node:path";
import {selfInfo} from "./data";
import {DATA_DIR} from "./utils";
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,
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_${getSelfUin()}.json`)
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }

View File

@@ -1,104 +1,130 @@
import { import {
type Friend, type Friend,
type FriendRequest, type Group,
type Group, type GroupMember,
type GroupMember, type SelfInfo,
type SelfInfo
} from '../ntqqapi/types' } from '../ntqqapi/types'
import {type FileCache, type LLOneBotError} from './types' import { 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, NTQQUserApi } from '../ntqqapi/api'
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true
}
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 const llonebotError: LLOneBotError = { export const llonebotError: LLOneBotError = {
ffmpegError: '', ffmpegError: '',
httpServerError: '', httpServerError: '',
wsServerError: '', wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误' otherError: 'LLOneBot 未能正常启动,请检查日志查看错误',
} }
// 群号 -> 群成员map(uid=>GroupMember)
export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> { export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid.toString()) ? "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: any) {
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) let members = groupMembers.get(groupQQ)
if (group) { if (!members) {
const filterKey = isNumeric(memberUinOrUid) ? "uin" : "uid" try {
const filterValue = memberUinOrUid members = await NTQQGroupApi.getGroupMembers(groupQQ)
let filterFunc: (member: GroupMember) => boolean = member => member[filterKey] === filterValue // 更新群成员列表
let member = group.members?.find(filterFunc) groupMembers.set(groupQQ, members)
if (!member) {
try {
const _members = await NTQQGroupApi.getGroupMembers(groupQQ)
if (_members.length > 0) {
group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
}
return member
} }
return null catch (e) {
return null
}
}
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUid)) {
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUid)
} else {
member = members!.get(memberUinOrUid)
}
return member
}
let member = getMember()
if (!member) {
members = await NTQQGroupApi.getGroupMembers(groupQQ)
member = getMember()
}
return member
} }
export async function refreshGroupMembers(groupQQ: string) { const selfInfo: SelfInfo = {
const group = groups.find(group => group.groupCode === groupQQ) uid: '',
if (group) { uin: '',
group.members = await NTQQGroupApi.getGroupMembers(groupQQ) nick: '',
} online: true,
} }
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号 export async function getSelfNick(force = false): Promise<string> {
if ((!selfInfo.nick || force) && selfInfo.uid) {
export function getUidByUin(uin: string) { const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
for (const uid in uidMaps) { if (userInfo) {
if (uidMaps[uid] === uin) { selfInfo.nick = userInfo.nick
return uid return userInfo.nick
}
} }
}
return selfInfo.nick
} }
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号 export function getSelfInfo() {
return selfInfo
}
export function setSelfInfo(data: Partial<SelfInfo>) {
Object.assign(selfInfo, data)
}
export function getSelfUid() {
return selfInfo['uid']
}
export function getSelfUin() {
return selfInfo['uin']
}

View File

@@ -1,277 +0,0 @@
import {Level} from "level";
import {type GroupNotify, RawMessage} from "../ntqqapi/types";
import {DATA_DIR} from "./utils";
import {selfInfo} from "./data";
import {FileCache} from "./types";
import {log} from "./utils/log";
type ReceiveTempUinMap = Record<string, string>;
class DBUtil {
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_SEQ_ID = "msg_seq_id_";
public readonly DB_KEY_PREFIX_FILE = "file_";
public readonly DB_KEY_PREFIX_GROUP_NOTIFY = "group_notify_";
private readonly DB_KEY_RECEIVED_TEMP_UIN_MAP = "received_temp_uin_map";
public db: Level;
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number;
/*
* 数据库结构
* msg_id_101231230999: {} // 长id: RawMessage
* msg_short_id_1: 101231230999 // 短id: 长id
* msg_seq_id_1: 101231230999 // 序列id: 长id
* file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */
constructor() {
let initCount = 0;
new Promise((resolve, reject) => {
const initDB = () => {
initCount++;
// if (initCount > 50) {
// return reject("init db fail")
// }
try {
if (!selfInfo.uin) {
setTimeout(initDB, 300);
return
}
const DB_PATH = DATA_DIR + `/msg_${selfInfo.uin}`;
this.db = new Level(DB_PATH, {valueEncoding: 'json'});
console.log("llonebot init db success")
resolve(null)
} catch (e) {
console.log("init db fail", e.stack.toString())
setTimeout(initDB, 300);
}
}
initDB();
}).then()
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
}
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
}
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();

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 'node: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 = 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: any) {
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: any) {
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,106 +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 | undefined
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 = 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: any) {
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) => {
maxPayload: 1024 * 1024 * 1024 log('ws server close failed!', err)
}); })
llonebotError.wsServerError = '' this.ws = null
}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())
})
})
}
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,13 +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 {
imageRKey?: string; enableLLOB: boolean
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval?: number // ms
@@ -26,7 +28,8 @@ export interface Config {
autoDeleteFile?: boolean autoDeleteFile?: boolean
autoDeleteFileSecond?: number autoDeleteFileSecond?: number
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
enablePoke?: boolean musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean
} }
export interface LLOneBotError { export interface LLOneBotError {
@@ -38,10 +41,10 @@ export interface LLOneBotError {
export interface FileCache { export interface FileCache {
fileName: string fileName: string
filePath: string
fileSize: string fileSize: string
fileUuid?: string msgId: string
url?: string peerUid: string
msgId?: string chatType: number
downloadFunc?: () => Promise<void> elementId: string
elementType: number
} }

View File

@@ -0,0 +1,232 @@
import { NodeIQQNTWrapperSession } from '@/ntqqapi/wrapper'
import { randomUUID } from 'node:crypto'
interface Internal_MapKey {
timeout: number
createtime: number
func: (...arg: any[]) => any
checker: ((...args: any[]) => boolean) | undefined
}
export class ListenerClassBase {
[key: string]: string
}
export interface ListenerIBase {
new(listener: any): ListenerClassBase
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/EventTask.ts#L20
export class NTEventWrapper {
private ListenerMap: { [key: string]: ListenerIBase } | undefined//ListenerName-Unique -> Listener构造函数
private WrapperSession: NodeIQQNTWrapperSession | undefined//WrapperSession
private ListenerManger: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>() //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, Internal_MapKey>>>()//tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
constructor() {
}
createProxyDispatch(ListenerMainName: string) {
const current = this
return new Proxy({}, {
get(target: any, prop: any, receiver: any) {
// console.log('get', prop, typeof target[prop])
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
return (...args: any[]) => {
current.dispatcherListener.apply(current, [ListenerMainName, prop, ...args]).then()
}
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver)
}
})
}
init({ ListenerMap, WrapperSession }: { ListenerMap: { [key: string]: typeof ListenerClassBase }, WrapperSession: NodeIQQNTWrapperSession }) {
this.ListenerMap = ListenerMap
this.WrapperSession = WrapperSession
}
createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/')
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
}
if (eventNameArr.length > 1) {
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '')
const eventName = eventNameArr[1]
//getNodeIKernelGroupListener,GroupService
//console.log('2', eventName)
const services = (this.WrapperSession as unknown as eventType)[serviceName]()
let event = services[eventName]
//重新绑定this
event = event.bind(services)
if (event) {
return event as T
}
return undefined
}
}
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
const ListenerType = this.ListenerMap![listenerMainName]
let Listener = this.ListenerManger.get(listenerMainName + uniqueCode)
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName))
const ServiceSubName = listenerMainName.match(/^NodeIKernel(.*?)Listener$/)![1]
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener'
const addfunc = this.createEventFunction<(listener: T) => number>(Service)
addfunc!(Listener as T)
//console.log(addfunc!(Listener as T))
this.ListenerManger.set(listenerMainName + uniqueCode, Listener)
}
return Listener as T
}
//统一回调清理事件
async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args)
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout)
if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid)
return
}
if (task.checker && task.checker(...args)) {
task.func(...args)
}
})
}
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.createEventFunction<EventType>(EventName)
let complete = false
const Timeouter = setTimeout(() => {
if (!complete) {
reject(new Error('NTEvent EventName:' + EventName + ' timeout'))
}
}, timeout)
const retData = await EventFunc!(...args)
complete = true
resolve(retData)
})
}
async RegisterListen<ListenerType extends (...args: any[]) => void>(ListenerName = '', waitTimes = 1, timeout = 5000, checker: (...args: Parameters<ListenerType>) => boolean) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
const databack = () => {
if (complete == 0) {
reject(new Error(' ListenerName:' + ListenerName + ' timeout'))
} else {
resolve(retData!)
}
}
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: Parameters<ListenerType>) => {
complete++
retData = args
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.createListenerFunction(ListenerMainName)
})
}
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>
(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, checker: (...args: Parameters<ListenerType>) => boolean, ...args: Parameters<EventType>) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const id = randomUUID()
let complete = 0
let retData: Parameters<ListenerType> | undefined = undefined
let retEvent: any = {}
const databack = () => {
if (complete == 0) {
reject(new Error('Timeout: NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'))
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!])
}
}
const ListenerNameList = ListenerName.split('/')
const ListenerMainName = ListenerNameList[0]
const ListenerSubName = ListenerNameList[1]
const Timeouter = setTimeout(databack, timeout)
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: any[]) => {
complete++
//console.log('func', ...args)
retData = args as Parameters<ListenerType>
if (complete >= waitTimes) {
clearTimeout(Timeouter)
databack()
}
}
}
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map())
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map())
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak)
this.createListenerFunction(ListenerMainName)
const EventFunc = this.createEventFunction<EventType>(EventName)
retEvent = await EventFunc!(...(args as any[]))
})
}
}
export const NTEventDispatch = new NTEventWrapper()
// 示例代码 快速创建事件
// let NTEvent = new NTEventWrapper()
// let TestEvent = NTEvent.CreatEventFunction<(force: boolean) => Promise<Number>>('NodeIKernelProfileLikeService/GetTest')
// if (TestEvent) {
// TestEvent(true)
// }
// 示例代码 快速创建监听Listener类
// let NTEvent = new NTEventWrapper()
// NTEvent.CreatListenerFunction<NodeIKernelMsgListener>('NodeIKernelMsgListener', 'core')
// 调用接口
//let NTEvent = new NTEventWrapper()
//let ret = await NTEvent.CallNormalEvent<(force: boolean) => Promise<Number>, (data1: string, data2: number) => void>('NodeIKernelProfileLikeService/GetTest', 'NodeIKernelMsgListener/onAddSendMsg', 1, 3000, true)
// 注册监听 解除监听
// NTEventDispatch.RigisterListener('NodeIKernelMsgListener/onAddSendMsg','core',cb)
// NTEventDispatch.UnRigisterListener('NodeIKernelMsgListener/onAddSendMsg','core')
// let GetTest = NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode)
// GetTest('test')
// always模式
// NTEventDispatch.CreatEvent('NodeIKernelProfileLikeService/GetTest','NodeIKernelMsgListener/onAddSendMsg',Mode,(...args:any[])=>{ console.log(args) })

View File

@@ -0,0 +1,154 @@
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from './table'
import { DATA_DIR } from './index'
import Database, { Tables } from 'minato'
import SQLite from '@minatojs/driver-sqlite'
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
import { FileCache } from '../types'
interface SQLiteTables extends Tables {
message: {
shortId: number
msgId: string
chatType: number
peerUid: string
}
file: FileCache
}
interface MsgIdAndPeerByShortId {
MsgId: string
Peer: Peer
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L84
class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number>
private msgIdMap: LimitedHashTable<string, number>
private db: Database<SQLiteTables> | undefined
constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap)
this.msgDataMap = new LimitedHashTable<string, number>(maxMap)
}
async init(uin: string) {
const dbDir = path.join(DATA_DIR, 'database')
if (!fs.existsSync(dbDir)) {
await fsPromise.mkdir(dbDir)
}
const database = new Database<SQLiteTables>()
await database.connect(SQLite, {
path: path.join(dbDir, `${uin}.db`)
})
database.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
database.extend('file', {
fileName: 'string',
fileSize: 'string',
msgId: 'string(24)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileName'
})
this.db = database
}
async getRecentMsgIds(Peer: Peer, size: number): Promise<string[]> {
const heads = this.msgIdMap.getHeads(size)
if (!heads) {
return []
}
const data: (MsgIdAndPeerByShortId | undefined)[] = []
for (const t of heads) {
data.push(await MessageUnique.getMsgIdAndPeerByShortId(t.value))
}
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid)
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined)
}
createMsg(peer: Peer, msgId: string): number | undefined {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(key).digest()
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f
const shortId = hash.readInt32BE(0)
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId)
// if (isExist && isExist === msgId) {
// return shortId
// }
this.msgIdMap.set(msgId, shortId)
this.msgDataMap.set(key, shortId)
this.db?.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgIdAndPeerByShortId(shortId: number): Promise<MsgIdAndPeerByShortId | undefined> {
const data = this.msgDataMap.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
}
return { MsgId: msgId, Peer: peer }
}
const items = await this.db?.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
MsgId: msgId,
Peer: {
chatType,
peerUid,
guildId: '',
}
}
}
return undefined
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId)
}
async getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId)
if (!shortId) return undefined
return await this.getMsgIdAndPeerByShortId(shortId)
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize)
this.msgDataMap.resize(maxSize)
}
addFileCache(data: FileCache) {
return this.db?.upsert('file', [data], 'fileName')
}
getFileCache(fileName: string) {
return this.db?.get('file', { fileName })
}
}
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper()

View File

@@ -0,0 +1,52 @@
import path from 'node:path'
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
}
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',
export function getBuildVersion(): number {
return +qqPkgInfo.buildVersion
}

View File

@@ -1,131 +1,119 @@
import fs from "fs"; import path from 'node:path'
import {encode, getDuration, getWavFileInfo, isWav} from "silk-wasm"; import ffmpeg from 'fluent-ffmpeg'
import fsPromise from "fs/promises"; import fsPromise from 'node:fs/promises'
import {log} from "./log"; import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm'
import path from "node:path"; import { log } from './log'
import {DATA_DIR, TEMP_DIR} from "./index"; import { TEMP_DIR } from './index'
import {v4 as uuidv4} from "uuid"; import { getConfigUtil } from '../config'
import {getConfigUtil} from "../config"; import { randomUUID } from 'node:crypto'
import {spawn} from "node:child_process" import { Readable } from 'node:stream'
interface FFmpegOptions {
input?: string[]
output?: string[]
}
type Input = string | Readable
function convert(input: Input, options: FFmpegOptions): Promise<Buffer>
function convert(input: Input, options: FFmpegOptions, outputPath: string): Promise<string>
function convert(input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer> | Promise<string> {
return new Promise<any>((resolve, reject) => {
const chunks: Buffer[] = []
let command = ffmpeg(input)
.on('error', err => {
log(`FFmpeg处理转换出错: `, err.message)
reject(err)
})
.on('end', () => {
if (!outputPath) {
resolve(Buffer.concat(chunks))
} else {
resolve(outputPath)
}
})
if (options.input) {
command = command.inputOptions(options.input)
}
if (options.output) {
command = command.outputOptions(options.output)
}
const ffmpegPath = getConfigUtil().getConfig().ffmpeg
if (ffmpegPath) {
command = command.setFfmpegPath(ffmpegPath)
}
if (!outputPath) {
const stream = command.pipe()
stream.on('data', chunk => {
chunks.push(chunk)
})
} else {
command.save(outputPath)
}
})
}
export async function encodeSilk(filePath: string) { export async function encodeSilk(filePath: string) {
function getFileHeader(filePath: string) { try {
// 定义要读取的字节数 const file = await fsPromise.readFile(filePath)
const bytesToRead = 7; if (!isSilk(file)) {
try { log(`语音文件${filePath}需要转换成silk`)
const buffer = fs.readFileSync(filePath, { let result: EncodeResult
encoding: null, const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
flag: "r", if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
}); result = await encode(file, 0)
} else {
const fileHeader = buffer.toString("hex", 0, bytesToRead); const input = await convert(filePath, {
return fileHeader; output: [
} catch (err) { '-ar 24000',
console.error("读取文件错误:", err); '-ac 1',
return; '-f s16le'
} ]
} })
result = await encode(input, 24000)
async function isWavFile(filePath: string) { }
return isWav(fs.readFileSync(filePath)); const pttPath = path.join(TEMP_DIR, randomUUID())
} await fsPromise.writeFile(pttPath, result.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
async function guessDuration(pttPath: string) { return {
const pttFileInfo = await fsPromise.stat(pttPath) converted: true,
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s path: pttPath,
duration = Math.floor(duration) duration: result.duration / 1000,
duration = Math.max(1, duration) }
log(`通过文件大小估算语音的时长:`, duration) } else {
return duration const silk = file
} let duration = 1
try {
// function verifyDuration(oriDuration: number, guessDuration: number) { duration = getDuration(silk) / 1000
// // 单位都是秒 } catch (e: any) {
// if (oriDuration - guessDuration > 10) { log('获取语音文件时长失败, 默认为1秒', filePath, e.stack)
// return guessDuration }
// } return {
// oriDuration = Math.max(1, oriDuration) converted: false,
// return oriDuration path: filePath,
// } duration,
// async function getAudioSampleRate(filePath: string) { }
// try {
// const mm = await import('music-metadata');
// const metadata = await mm.parseFile(filePath);
// log(`${filePath}采样率`, metadata.format.sampleRate);
// return metadata.format.sampleRate;
// } catch (error) {
// log(`${filePath}采样率获取失败`, error.stack);
// // console.error(error);
// }
// }
try {
const pttPath = path.join(TEMP_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换成silk`)
const _isWav = await isWavFile(filePath);
const 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 = fs.readFileSync(filePath)
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 = fs.readFileSync(filePath);
let duration = 0;
try {
duration = getDuration(silk) / 1000
} catch (e) {
log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
duration = await guessDuration(filePath);
}
return {
converted: false,
path: filePath,
duration,
};
}
} catch (error) {
log("convert silk failed", error.stack);
return {};
} }
} catch (error: any) {
log('convert silk failed', error.stack)
return {}
}
}
type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
export async function decodeSilk(inputFilePath: string, outFormat: OutFormat = 'mp3') {
const silk = await fsPromise.readFile(inputFilePath)
const { data } = await decode(silk, 24000)
const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath))
const outFilePath = tmpPath + `.${outFormat}`
const pcmFilePath = tmpPath + '.pcm'
await fsPromise.writeFile(pcmFilePath, data)
return convert(pcmFilePath, {
input: [
'-f s16le',
'-ar 24000',
'-ac 1'
]
}, outFilePath)
} }

View File

@@ -1,258 +1,241 @@
import fs from "fs"; import fs from 'node:fs'
import fsPromise from "fs/promises"; import fsPromise from 'node:fs/promises'
import crypto from "crypto"; import path from 'node:path'
import util from "util"; import { log, TEMP_DIR } from './index'
import path from "node:path"; import * as fileType from 'file-type'
import {v4 as uuidv4} from "uuid"; import { randomUUID, createHash } from 'node:crypto'
import {log, TEMP_DIR} from "./index";
import {dbUtil} from "../db";
import * as fileType from "file-type";
import {net} from "electron";
export function isGIF(path: string) { 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, 200)
} }
} }
check(); check()
}); })
} }
export async function file2base64(path: string) { export async function file2base64(path: string) {
const readFile = util.promisify(fs.readFile); let result = {
let result = { err: '',
err: "", data: '',
data: "" }
} try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try { try {
// 读取文件内容 await checkFileReceived(path, 5000)
// if (!fs.existsSync(path)){ } catch (e: any) {
// path = path.replace("\\Ori\\", "\\Thumb\\"); result.err = e.toString()
// } return result
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; const data = await fsPromise.readFile(path)
// 转换为Base64编码
result.data = data.toString('base64')
} catch (err: any) {
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 = 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 url: string
let url: string; let headers: Record<string, string> = {
let headers: Record<string, string> = { 'User-Agent':
"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" '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") { if (typeof options === 'string') {
url = options; url = options
} else { } else {
url = options.url; url = options.url
if (options.headers) { if (options.headers) {
if (typeof options.headers === "string") { if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers); headers = JSON.parse(options.headers)
} else { } else {
headers = options.headers; headers = options.headers
} }
}
} }
const fetchRes = await net.fetch(url, headers); }
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`) const fetchRes = await fetch(url, { headers })
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
const blob = await fetchRes.blob(); return Buffer.from(await fetchRes.arrayBuffer())
let buffer = await blob.arrayBuffer();
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 = 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 = randomUUID()
} }
let filePath = path.join(TEMP_DIR, fileName) let filePath = path.join(TEMP_DIR, fileName)
let url = null; let url: URL | null = null
try { try {
url = new URL(uri); url = new URL(uri)
} catch (e) { } catch (e: any) {
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')
await fsPromise.writeFile(filePath, buffer)
} catch (e: any) {
res.errMsg = `base64文件下载失败,` + e.toString()
return res
}
} else if (url.protocol == 'http:' || url.protocol == 'https:') {
// 下载文件
let buffer: Buffer | null = null
try {
buffer = await httpDownload(uri)
} catch (e: any) {
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, randomUUID() + fileName)
await fsPromise.writeFile(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
}
}
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 {
const ext = (await fileType.fileTypeFromFile(filePath))?.ext
if (ext) {
log('获取文件类型', ext, filePath)
await fsPromise.rename(filePath, filePath + `.${ext}`)
filePath += `.${ext}`
res.fileName += `.${ext}`
res.ext = ext
}
} catch (e) {
// log("获取文件类型失败", filePath,e.stack)
}
}
res.success = true
res.path = filePath
return res
} }
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: unknown) {
return value === undefined || value === null; return value === undefined || value === null
} }
/** /**
@@ -52,17 +52,118 @@ 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
}
}
export function CacheClassFuncAsync(ttl = 3600 * 1000, customKey = '') {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true }) {
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, { expiry: number; value: any }>()
const originalMethod = descriptor.value
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key)
}
})
const cachedValue = cache.get(key)
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value
}
const result = await originalMethod.apply(this, args)
if (!checker(...args, result)) {
return result //丢弃缓存
}
cache.set(key, { expiry: Date.now() + ttl, value: result })
return result
}
}
return logExecutionTime
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr)
const low = BigInt(lowStr)
const highHex = high.toString(16).padStart(16, '0')
const lowHex = low.toString(16).padStart(16, '0')
const combinedHex = highHex + lowHex
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`
return uuid
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '')
const high = BigInt('0x' + hex.substring(0, 16))
const low = BigInt('0x' + hex.substring(16))
return { high: high.toString(), low: low.toString() }
}
}

View File

@@ -1,18 +1,14 @@
import path from "node:path"; import path from 'node:path'
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: string = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR = path.join(DATA_DIR, "temp"); export const TEMP_DIR: string = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin; export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin
if (!fs.existsSync(TEMP_DIR)) { export { getVideoInfo } from './video'
fs.mkdirSync(TEMP_DIR, {recursive: true}); export { checkFfmpeg } from './video'
} export { encodeSilk } from './audio'
export {getVideoInfo} from "./video";
export {checkFfmpeg} from "./video";
export {encodeSilk} from "./audio";

View File

@@ -1,37 +1,35 @@
import {selfInfo} from "../data"; import { getSelfInfo } 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 date = new Date()
const logFileName = `llonebot-${date.toLocaleString("zh-CN")}.log`.replace(/\//g, "-").replace(/:/g, "-"); const logFileName = `llonebot-${date.toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
const logDir = path.join(DATA_DIR, "logs"); const logDir = path.join(DATA_DIR, 'logs')
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, {recursive: true}); 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);
}
const selfInfo = getSelfInfo()
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 + ' '
const userInfo = selfInfo.uin ? `${selfInfo.nick}(${selfInfo.uin})` : "" }
let logMsg = ""; let currentDateTime = new Date().toLocaleString()
for (let msgItem of msg) { logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// 判断是否是对象 // sendLog(...msg);
if (typeof msgItem === "object") { // console.log(msg)
let obj = JSON.parse(JSON.stringify(msgItem)); fs.appendFile(path.join(logDir, logFileName), logMsg, () => {})
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();

72
src/common/utils/table.ts Normal file
View File

@@ -0,0 +1,72 @@
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5
export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map()
private valueToKey: Map<V, K> = new Map()
private maxSize: number
constructor(maxSize: number) {
this.maxSize = maxSize
}
resize(count: number) {
this.maxSize = count
}
set(key: K, value: V): void {
this.keyToValue.set(key, value)
this.valueToKey.set(value, key)
while (this.keyToValue.size !== this.valueToKey.size) {
console.log('keyToValue.size !== valueToKey.size Error Atom')
this.keyToValue.clear()
this.valueToKey.clear()
}
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value
this.valueToKey.delete(this.keyToValue.get(oldestKey)!)
this.keyToValue.delete(oldestKey)
}
}
getValue(key: K): V | undefined {
return this.keyToValue.get(key)
}
getKey(value: V): K | undefined {
return this.valueToKey.get(value)
}
deleteByValue(value: V): void {
const key = this.valueToKey.get(value)
if (key !== undefined) {
this.keyToValue.delete(key)
this.valueToKey.delete(value)
}
}
deleteByKey(key: K): void {
const value = this.keyToValue.get(key)
if (value !== undefined) {
this.keyToValue.delete(key)
this.valueToKey.delete(value)
}
}
getKeyList(): K[] {
return Array.from(this.keyToValue.keys())
}
//获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined {
const keyList = this.getKeyList()
if (keyList.length === 0) {
return undefined
}
const result: { key: K; value: V }[] = []
const listSize = Math.min(size, keyList.length)
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i]
result.push({ key, value: this.keyToValue.get(key)! })
}
return result
}
}

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 {
releasePage = (await httpDownload(mirrorGithub + "/LLOneBot/LLOneBot/releases")).toString();
// log("releasePage", releasePage);
if (releasePage === "error") return "";
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0];
} catch {
}
return "";
try {
releasePage = (await httpDownload(mirrorGithub + '/LLOneBot/LLOneBot/releases')).toString()
// log("releasePage", releasePage);
if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch {}
return ''
} }

File diff suppressed because one or more lines are too long

2
src/global.d.ts vendored
View File

@@ -3,6 +3,6 @@ import { type LLOneBot } from './preload'
declare global { declare global {
interface Window { interface Window {
llonebot: LLOneBot llonebot: LLOneBot
LiteLoader: any LiteLoader: Record<string, any>
} }
} }

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,479 +1,457 @@
// 运行在 Electron 主进程 下的插件入口 // 运行在 Electron 主进程 下的插件入口
import {BrowserWindow, dialog, ipcMain} from 'electron'; import { BrowserWindow, dialog, ipcMain } from 'electron'
import * as fs from 'node:fs'; import path from 'node:path'
import {Config} from "../common/types"; import fs from 'node:fs'
import { Config } from '../common/types'
import { import {
CHANNEL_CHECK_VERSION, CHANNEL_CHECK_VERSION,
CHANNEL_ERROR, CHANNEL_ERROR,
CHANNEL_GET_CONFIG, CHANNEL_GET_CONFIG,
CHANNEL_LOG, 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} from "../common/utils"; import { DATA_DIR, TEMP_DIR } from '../common/utils'
import { import {
friendRequests, getGroupMember,
getFriend, llonebotError,
getGroup, setSelfInfo,
getGroupMember, groups, getSelfInfo,
llonebotError, getSelfUid,
refreshGroupMembers, getSelfUin
selfInfo, } from '../common/data'
uidMaps import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
} from "../common/data"; import { OB11Constructor } from '../onebot11/constructor'
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor";
import { import {
ChatType, FriendRequestNotify,
FriendRequestNotify, GroupNotifies,
GroupMemberRole, GroupNotifyTypes,
GroupNotifies, RawMessage,
GroupNotifyTypes, BuddyReqType,
RawMessage } from '../ntqqapi/types'
} from "../ntqqapi/types"; import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import {ob11HTTPServer} from "../onebot11/server/http"; import { postOb11Event } from '../onebot11/server/post-ob11-event'
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent"; import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent"; import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import {postOB11Event} from "../onebot11/server/postOB11Event"; import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import { MessageUnique } from '../common/utils/MessageUnique'
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent"; import { setConfig } from './setConfig'
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest"; import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest"; import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import * as path from "node:path"; import { log } from '../common/utils/log'
import {dbUtil} from "../common/db"; import { getConfigUtil } from '../common/config'
import {setConfig} from "./setConfig"; import { checkFfmpeg } from '../common/utils/video'
import {NTQQUserApi} from "../ntqqapi/api/user"; import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import {NTQQGroupApi} from "../ntqqapi/api/group"; import '../ntqqapi/wrapper'
import {crychic} from "../ntqqapi/external/crychic"; import { NTEventDispatch } from '../common/utils/EventTask'
import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent"; import { wrapperConstructor, getSession } from '../ntqqapi/wrapper'
import {checkNewVersion, upgradeLLOneBot} from "../common/utils/upgrade"; import { Peer } from '../ntqqapi/types'
import {log} from "../common/utils/log";
import {getConfigUtil} from "../common/config";
import {checkFfmpeg} from "../common/utils/video";
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
let running = false; let mainWindow: BrowserWindow | null = null
let mainWindow: BrowserWindow | null = null;
// 加载插件时触发 // 加载插件时触发
function onLoad() { function 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,视频尺寸可能异常'
log("查询llonebot错误信息", error); let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
return error; let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
}) error = error.replace('\n\n', '\n')
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { error = error.trim()
const config = getConfigUtil().getConfig() log('查询llonebot错误信息', error)
return config; return error
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => { ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
if (!ask) { const config = getConfigUtil().getConfig()
setConfig(config).then().catch(e => { return config
log("保存设置失败", e.stack) })
}); 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) => {
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)
const peer: Peer = {
chatType: message.chatType,
peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
OB11Constructor.message(message)
.then((msg) => {
if (!debug && msg.message.length === 0) {
return return
}
const isSelfMsg = msg.user_id.toString() === getSelfUin()
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)
} }
dialog.showMessageBox(mainWindow, { })
type: 'question', OB11Constructor.PrivateEvent(message).then((privateEvent) => {
buttons: ['确认', '取消'], log(message)
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认" if (privateEvent) {
title: '确认保存', // log("post private event", privateEvent);
message: '是否保存?', postOb11Event(privateEvent)
detail: 'LLOneBot配置已更改是否保存' }
}).then(result => { })
if (result.response === 0) { // OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
setConfig(config).then().catch(e => { // log(message)
log("保存设置失败", e.stack) // if (friendAddEvent) {
}); // // log("post friend add event", friendAddEvent);
} else { // postOb11Event(friendAddEvent)
} // }
}).catch(err => { // })
log("保存设置询问弹窗错误", err); }
}); }
async function startReceiveHook() {
startHook()
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList)
} catch (e: any) {
log('report message error: ', e.stack.toString())
}
}) })
const recallMsgIds: string[] = [] // 避免重复上报
ipcMain.on(CHANNEL_LOG, (event, arg) => { registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
log(arg); for (const message of payload.msgList) {
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
continue
}
recallMsgIds.push(message.msgId)
const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId)
if (!oriMessageId) {
continue
}
OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => {
if (recallEvent) {
//log('post recall event', recallEvent)
postOb11Event(recallEvent)
}
})
}
}
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
async function postReceiveMsg(msgList: RawMessage[]) { const { reportSelfMessage } = getConfigUtil().getConfig()
const {debug, reportSelfMessage} = getConfigUtil().getConfig(); if (!reportSelfMessage) {
for (let message of msgList) { return
}
// log("收到新消息", message.msgId, message.msgSeq) // log("reportSelfMessage", payload)
// if (message.senderUin !== selfInfo.uin){ try {
message.msgShortId = await dbUtil.addMsg(message); await postReceiveMsg([payload.msgRecord])
// } } catch (e: any) {
log('report self message error: ', e.stack.toString())
OB11Constructor.message(message).then((msg) => { }
if (debug) { })
msg.raw = message; registerReceiveHook<{
} else { doubt: boolean
if (msg.message.length === 0) { oldestUnreadSeq: string
return unreadCount: number
} }>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
} if (payload.unreadCount) {
const isSelfMsg = msg.user_id.toString() == selfInfo.uin // log("开始获取群通知详情")
if (isSelfMsg && !reportSelfMessage) { let notify: GroupNotifies
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) {
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());
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断
// 撤回消息上报
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then();
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId);
postOB11Event(friendRecallEvent);
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId
)
postOB11Event(groupRecallEvent);
}
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
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
}
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) {
try {
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);
if (existNotify) {
continue
}
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";
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
postOB11Event(groupAdminNoticeEvent, true);
} else {
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
}
} 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";
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin), parseInt(operatorId), subType)
postOB11Event(groupDecreaseEvent, true);
} catch (e) {
log("获取群通知的成员信息失败", notify, e.stack.toString())
}
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
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) {
// 可能有群管理员变动
}
})
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)
llonebotError.otherError = "";
startTime = Date.now();
dbUtil.getReceivedTempUinMap().then(m => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key;
}
})
startReceiveHook().then();
NTQQGroupApi.getGroups(true).then()
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;
const init = async () => {
try { try {
log("start get self info") notify = await NTQQGroupApi.getGroupNotifies()
const _ = await NTQQUserApi.getSelfInfo();
log("get self info api result:", _);
Object.assign(selfInfo, _);
selfInfo.nick = selfInfo.uin;
} catch (e) { } catch (e) {
log("retry get self info", e); // log("获取群通知详情失败", e);
return
} }
if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin; const notifies = notify.notifies.slice(0, payload.unreadCount)
selfInfo.uid = globalThis.authData?.uid; // log("获取群通知详情完成", notifies, payload);
selfInfo.nick = selfInfo.uin;
} for (const notify of notifies) {
log("self info", selfInfo, globalThis.authData); try {
if (selfInfo.uin) { notify.time = Date.now()
async function getUserNick() { const notifyTime = parseInt(notify.seq) / 1000
try { if (notifyTime < startTime) {
getSelfNickCount++; continue
const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid));
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
return
}
} catch (e) {
log("get self nickname failed", e.stack);
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000);
}
} }
log('收到群通知', notify)
getUserNick().then() const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
start().then(); if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
} else { log('有成员退出通知', notify)
setTimeout(init, 1000) 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'
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1.uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
} catch (e: any) {
log('获取群通知的成员信息失败', notify, e.stack.toString())
}
}
else if ([GroupNotifyTypes.JOIN_REQUEST, GroupNotifyTypes.JOIN_REQUEST_BY_INVITED].includes(notify.type)) {
log('有加群请求')
let requestQQ = ''
try {
// uid-->uin
requestQQ = (await NTQQUserApi.getUinByUid(notify.user1.uid))
if (isNaN(parseInt(requestQQ))) {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin
}
} catch (e) {
log('获取加群人QQ号失败 Uid:', notify.user1.uid, e)
}
let invitorId: string
if (notify.type == GroupNotifyTypes.JOIN_REQUEST_BY_INVITED) {
// groupRequestEvent.sub_type = 'invite'
try {
// uid-->uin
invitorId = (await NTQQUserApi.getUinByUid(notify.user2.uid))
if (isNaN(parseInt(invitorId))) {
invitorId = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid)).uin
}
} catch (e) {
invitorId = ''
log('获取邀请人QQ号失败 Uid:', notify.user2.uid, e)
}
}
const groupRequestEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(requestQQ) || 0,
flag,
notify.postscript,
invitorId! === undefined ? undefined : +invitorId,
'add'
)
postOb11Event(groupRequestEvent)
}
else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log('收到邀请我加群通知')
const userId = (await NTQQUserApi.getUinByUid(notify.user2.uid)) || ''
const groupInviteEvent = new OB11GroupRequestEvent(
parseInt(notify.group.groupCode),
parseInt(userId),
flag,
undefined,
undefined,
'invite'
)
postOb11Event(groupInviteEvent)
}
} catch (e: any) {
log('解析群通知失败', e.stack.toString())
}
} }
} }
setTimeout(init, 1000); else if (payload.doubt) {
} // 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
continue
}
let userId = 0
try {
const requesterUin = await NTQQUserApi.getUinByUid(req.friendUid)
userId = parseInt(requesterUin!)
} catch (e) {
log('获取加好友者QQ号失败', e)
}
const flag = req.friendUid + '|' + req.reqTime
const comment = req.extWords
const friendRequestEvent = new OB11FriendRequestEvent(
userId,
comment,
flag
)
postOb11Event(friendRequestEvent)
}
})
}
let startTime = 0 // 毫秒
async function start(uid: string, uin: string) {
log('llonebot pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
llonebotError.otherError = ''
startTime = Date.now()
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! })
MessageUnique.init(uin)
log('start activate group member info')
// 下面两个会导致CPU占用过高QQ卡死
// NTQQGroupApi.activateMemberInfoChange().then().catch(log)
// NTQQGroupApi.activateMemberListChange().then().catch(log)
startReceiveHook().then()
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort)
}
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start()
}
if (config.ob11.enableHttpHeart) {
httpHeart.start()
}
log('LLOneBot start')
}
const intervalId = setInterval(() => {
const current = getSelfInfo()
if (!current.uin) {
setSelfInfo({
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
nick: current.uin,
})
}
if (current.uin && getSession()) {
clearInterval(intervalId)
start(current.uid, current.uin)
}
}, 600)
}
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) { if (getSelfUid()) {
return return
} }
mainWindow = window; mainWindow = window
log("window create", window.webContents.getURL().toString()) log('window create', window.webContents.getURL().toString())
try { try {
hookNTQQApiCall(window); hookNTQQApiCall(window)
hookNTQQApiReceive(window); hookNTQQApiReceive(window)
} catch (e) { } catch (e: any) {
log("LLOneBot hook error: ", e.toString()) 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,60 +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, log} 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 {
log("反向ws地址有变化, 重启反向ws服务") ob11ReverseWebsockets.stop()
ob11ReverseWebsockets.restart();
} else {
for (const newHost of config.ob11.wsHosts) {
if (!oldConfig.ob11.wsHosts.includes(newHost)) {
log("反向ws地址有变化, 重启反向ws服务")
ob11ReverseWebsockets.restart();
break;
}
}
}
} }
log("old config", oldConfig) }
log("配置已更新", config) if (config.ob11.enableWsReverse) {
checkFfmpeg(config.ffmpeg).then() // 判断反向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,236 +1,449 @@
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,
import fs from "fs"; PicElement,
import {ReceiveCmdS} from "../hook"; } from '../types'
import {log} from "../../common/utils/log"; import path from 'node:path'
import fs from 'node:fs'
import { ReceiveCmdS } from '../hook'
import { log, TEMP_DIR } from '@/common/utils'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { NodeIKernelSearchService } from '@/ntqqapi/services'
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 }>({ const session = getSession()
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath] return (await session?.getRichMediaService().getVideoPlayUrlV2(peer,
}) msgId,
} elementId,
0,
{ downSourceType: 1, triggerType: 1 }))?.urlResult?.domainUrl[0]?.url!
}
static async getFileMd5(filePath: string) { static async getFileType(filePath: string) {
return await callNTQQApi<string>({ return fileTypeFromFile(filePath)
className: NTQQApiClass.FS_API, }
methodName: NTQQApiMethod.FILE_MD5,
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, elementSubType: number = 0) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const md5 = await NTQQFileApi.getFileMd5(filePath); const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext let ext = (await NTQQFileApi.getFileType(filePath))?.ext || ''
if (ext) { if (ext) {
ext = "." + ext ext = '.' + ext
} else { }
ext = "" let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) {
fileName += ext
}
const session = getSession()
const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
})
await fsPromise.copyFile(filePath, mediaPath!)
const fileSize = (await fsPromise.stat(filePath)).size
return {
md5: fileMd5,
fileName,
path: mediaPath!,
fileSize,
ext
}
}
static async downloadMedia(
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbPath: string,
sourcePath: string,
timeout = 1000 * 60 * 2,
force = false
) {
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) {
if (force) {
try {
await fsPromise.unlink(sourcePath)
} catch (e) {
//
} }
let fileName = `${path.basename(filePath)}`; } else {
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) {
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(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 return sourcePath
}
}
const data = await NTEventDispatch.CallNormalEvent<
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
}
)
let filePath = data[1].filePath
if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath)
// 下载路径是下载文件夹的相对路径
}
return filePath
}
static async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number; height: number }>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.IMAGE_SIZE,
args: [filePath],
})
}
static async getImageUrl(element: PicElement) {
if (!element) {
return ''
}
const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr
if (url) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = UrlParse.searchParams.get('appid')
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey')
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url
}
const rkeyData = await rkeyManager.getRkey()
UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`
} 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获取失败', element)
return ''
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/core/src/apis/file.ts#L149
static async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) {
let GroupData: any[] | undefined
let BuddyData: any[] | undefined
if (peer.chatType === ChatType.group) {
GroupData =
[{
groupCode: peer.peerUid,
isConf: false,
hasModifyConfGroupFace: true,
hasModifyConfGroupName: true,
groupName: 'LLOneBot.Cached',
remark: 'LLOneBot.Cached',
}];
} else if (peer.chatType === ChatType.friend) {
BuddyData = [{
category_name: 'LLOneBot.Cached',
peerUid: peer.peerUid,
peerUin: peer.peerUid,
remark: 'LLOneBot.Cached',
}]
} else {
return undefined
} }
static async getImageSize(filePath: string) { const session = getSession()
return await callNTQQApi<{ width: number, height: number }>({ return session?.getSearchService().addSearchHistory({
className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath] type: 4,
}) contactList: [],
id: -1,
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: peer.chatType,
buddyChatInfo: BuddyData || [],
discussChatInfo: [],
groupChatInfo: GroupData || [],
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: msgId,
msgSeq: msgSeq,
msgTime: Math.floor(Date.now() / 1000).toString(),
senderUid: senderUid,
senderNick: 'LLOneBot.Cached',
senderRemark: 'LLOneBot.Cached',
senderCard: 'LLOneBot.Cached',
elemId: elemId,
elemType: elemType,
fileSize: fileSize,
filePath: '',
fileName: fileName,
hits: [{
start: 12,
end: 14,
}],
},
],
})
}
static async searchfile(keys: string[]) {
type EventType = NodeIKernelSearchService['searchFileWithKeywords']
interface OnListener {
searchId: string,
hasMore: boolean,
resultItems: {
chatType: ChatType,
buddyChatInfo: any[],
discussChatInfo: any[],
groupChatInfo:
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}[],
dataLineChatInfo: any[],
tmpChatInfo: any[],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: number,
fileSize: string,
filePath: string,
fileName: string,
hits:
{
start: number,
end: number
}[]
}[]
} }
const Event = NTEventDispatch.createEventFunction<EventType>('NodeIKernelSearchService/searchFileWithKeywords')
let id = ''
const Listener = NTEventDispatch.RegisterListen<(params: OnListener) => void>
(
'NodeIKernelSearchListener/onSearchFileKeywordsResult',
1,
20000,
(params) => id !== '' && params.searchId == id,
)
id = await Event!(keys, 12)
const [ret] = await Listener
return ret
}
} }
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[] = []) { })
return await callNTQQApi<GeneralCallResult>({ }
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [{
chats,
fileKeys
}, null]
});
}
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR,
args: [
{
chats,
fileKeys,
},
null,
],
})
}
} }

View File

@@ -1,61 +1,126 @@
import {Friend, FriendRequest} from "../types"; import { Friend, FriendV2 } 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 { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { CacheClassFuncAsyncExtend } from '@/common/utils/helper'
import { LimitedHashTable } from '@/common/utils/table'
export class NTQQFriendApi{ export class NTQQFriendApi {
static async getFriends(forced = false) { /** >=26702 应使用 getBuddyV2 */
const data = await callNTQQApi<{ static async getFriends(forced = false) {
data: { const data = await callNTQQApi<{
categoryId: number, data: {
categroyName: string, categoryId: number
categroyMbCount: number, categroyName: string
buddyList: Friend[] categroyMbCount: number
}[] 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[] = []; })
for (const fData of data.data) { // log('获取好友列表', data)
_friends.push(...fData.buddyList) let _friends: Friend[] = []
} for (const fData of data.data) {
return _friends _friends.push(...fData.buddyList)
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
}, null]
})
}
static async handleFriendRequest(flag: string, accept: boolean,) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
throw `flat: ${flag}, 对应的好友请求不存在`
}
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
"approvalInfo": {
"friendUid": request.friendUid,
"reqTime": request.reqTime,
accept
}
}
]
})
delete friendRequests[flag];
return result;
} }
return _friends
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0,
},
},
null,
],
})
}
static async handleFriendRequest(flag: string, accept: boolean) {
const data = flag.split('|')
if (data.length < 2) {
return
}
const friendUid = data[0]
const reqTime = data[1]
const session = getSession()
return session?.getBuddyService().approvalFriendRequest({
friendUid,
reqTime,
accept
})
}
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = []
const session = getSession()
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data.values())
}
@CacheClassFuncAsyncExtend(3600 * 1000, 'getBuddyIdMap', () => true)
static async getBuddyIdMapCache(refresh = false): Promise<LimitedHashTable<string, string>> {
return await NTQQFriendApi.getBuddyIdMap(refresh)
}
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const uids: string[] = []
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000)
const session = getSession()
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
);
data.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
//console.log('getBuddyIdMap', retMap.getValue)
return retMap
}
static async getBuddyV2ExWithCate(refresh = false) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const session = getSession()
const buddyService = session?.getBuddyService()
const buddyListV2 = refresh ? (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data : (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data
uids.push(
...buddyListV2?.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName })
})
return item.buddyUids
})!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
}
static async isBuddy(uid: string): Promise<boolean> {
const session = getSession()
return session?.getBuddyService().isBuddy(uid)!
}
} }

View File

@@ -1,216 +1,223 @@
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types"; import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } from '../types'
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import {uidMaps} from "../../common/data"; import { NTQQWindowApi, NTQQWindows } from './window'
import {dbUtil} from "../../common/db"; import { getSession } from '../wrapper'
import {log} from "../../common/utils/log"; import { NTEventDispatch } from '@/common/utils/EventTask'
import {NTQQWindowApi, NTQQWindows} from "./window"; import { NodeIKernelGroupListener } from '../listeners'
export class NTQQGroupApi{ export class NTQQGroupApi {
static async getGroups(forced = false) { static async activateMemberListChange() {
let cbCmd = ReceiveCmdS.GROUPS return await callNTQQApi<GeneralCallResult>({
if (process.platform != "win32") { methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE,
cbCmd = ReceiveCmdS.GROUPS_STORE 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): Promise<Group[]> {
type ListenerType = NodeIKernelGroupListener['onGroupListUpdate']
const [, , groupList] = await NTEventDispatch.CallNormalEvent
<(force: boolean) => Promise<any>, ListenerType>
(
'NodeIKernelGroupService/getGroupList',
'NodeIKernelGroupListener/onGroupListUpdate',
1,
5000,
(updateType) => true,
forced
)
return groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const session = getSession()
const groupService = session?.getGroupService()
const sceneId = groupService?.createMemberListScene(groupQQ, 'groupMemberList_MainWindow')
const result = await groupService?.getNextMemberList(sceneId!, undefined, num)
if (result?.errCode !== 0) {
throw ('获取群成员列表出错,' + result?.errMsg)
}
return result.result.infos
}
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(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|')
const groupCode = flagitem[0]
const seq = flagitem[1]
const type = parseInt(flagitem[2])
const session = getSession()
return session?.getGroupService().operateSysNotify(
false,
{
'operateType': operateType, // 2 拒绝
'targetMsg': {
'seq': seq, // 通知序列号
'type': type,
'groupCode': groupCode,
'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
} }
const result = await callNTQQApi<{ })
updateType: number, }
groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd})
return result.groupList
}
static async getGroupMembers(groupQQ: string, num = 3000): 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) static async quitGroup(groupQQ: string) {
for (const member of members) { const session = getSession()
uidMaps[member.uid] = member.uin; return session?.getGroupService().quitGroup(groupQQ)
} }
// log(uidMaps);
// log("members info", values); static async kickMember(
log(`get group ${groupQQ} members success`) groupQQ: string,
return members kickUids: string[],
} catch (e) { refuseForever = false,
log(`get group ${groupQQ} members failed`, e) kickReason = '',
return [] ) {
const session = getSession()
return session?.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
}
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言
const session = getSession()
return session?.getGroupService().setMemberShutUp(groupQQ, memList)
}
static async banGroup(groupQQ: string, shutUp: boolean) {
const session = getSession()
return session?.getGroupService().setGroupShutUp(groupQQ, shutUp)
}
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
const session = getSession()
return session?.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
}
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
const session = getSession()
return session?.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
}
static async setGroupName(groupQQ: string, groupName: string) {
const session = getSession()
return session?.getGroupService().modifyGroupName(groupQQ, groupName, false)
}
static async getGroupAtAllRemainCount(groupCode: string) {
return await callNTQQApi<
GeneralCallResult & {
atInfo: {
canAtAll: boolean
RemainAtAllCountForUin: number
RemainAtAllCountForGroup: number
atTimesMsg: string
canNotAtAllMsg: ''
} }
} }
static async getGroupNotifies() { >({
// 获取管理员变更 methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT,
// 加群通知,退出通知,需要管理员权限 args: [
callNTQQApi<GeneralCallResult>({ {
methodName: ReceiveCmdS.GROUP_NOTIFY, groupCode,
classNameIsRegister: true, },
}).then() null,
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 getGroupAtAllRemainCount(groupCode: string){ static async getGroupRemainAtTimes(GroupCode: string) {
return await callNTQQApi<GeneralCallResult & {"atInfo":{"canAtAll": boolean,"RemainAtAllCountForUin": number,"RemainAtAllCountForGroup": number,"atTimesMsg": string,"canNotAtAllMsg":""}}>({ const session = getSession()
methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT, return session?.getGroupService().getGroupRemainAtTimes(GroupCode)!
args: [ }
{
groupCode
}, null
]
})
}
// 头衔不可用 // 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) { 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) {
static publishGroupBulletin(groupQQ: string, title: string, content: string) { }
static async removeGroupEssence(GroupCode: string, msgId: string) {
const session = getSession()
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData?.msgList[0].msgRandom!),
msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
} }
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().removeGroupEssence(param)
}
static async addGroupEssence(GroupCode: string, msgId: string) {
const session = getSession()
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData?.msgList[0].msgRandom!),
msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
}
} }

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,221 +1,293 @@
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall'
import {ChatType, RawMessage, SendMessageElement} from "../types"; import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import {dbUtil} from "../../common/db"; import { getSelfNick, getSelfUid } from '../../common/data'
import {selfInfo} from "../../common/data"; import { getBuildVersion } from '../../common/utils'
import {ReceiveCmdS, registerReceiveHook} from "../hook"; import { getSession } from '@/ntqqapi/wrapper'
import {log} from "../../common/utils/log"; import { NTEventDispatch } from '@/common/utils/EventTask'
import {sleep} from "../../common/utils/helper";
import {isQQ998} from "../../common/utils";
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ""
}
export class NTQQMsgApi { export class NTQQMsgApi {
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { static async getTempChatInfo(chatType: ChatType2, peerUid: string) {
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({ const session = getSession()
methodName: NTQQApiMethod.GET_MULTI_MSG, return session?.getMsgService().getTempChatInfo(chatType, peerUid)!
args: [{ }
peer,
rootMsgId,
parentMsgId
}, null]
})
}
static async activateChat(peer: Peer) { static async prepareTempChat(toUserUid: string, GroupCode: string, nickname: string) {
// await this.fetchRecentContact(); //By Jadx/Ida Mlikiowa
// await sleep(500); let TempGameSession = {
return await callNTQQApi<GeneralCallResult>({ nickname: '',
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW, gameAppId: '',
args: [{peer, cnt: 20}, null] selfTinyId: '',
}) peerRoleId: '',
} peerOpenId: '',
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,
},
relativeMoveCount: 0,
listType: 2, // 1普通消息2群助手内的消息
count: 200,
fetchOld: true,
},
}
]
})
} }
const session = getSession()
return session?.getMsgService().prepareTempChat({
chatType: ChatType2.KCHATTYPETEMPC2CFROMGROUP,
peerUid: toUserUid,
peerNickname: nickname,
fromGroupCode: GroupCode,
sig: '',
selfPhone: '',
selfUid: getSelfUid(),
gameSession: TempGameSession
})
}
static async recallMsg(peer: Peer, msgIds: string[]) { static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
return await callNTQQApi({ // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
methodName: NTQQApiMethod.RECALL_MSG, // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
args: [{ // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
peer, emojiId = emojiId.toString()
msgIds const session = getSession()
}, null] return session?.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
}) }
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
const session = getSession()
return session?.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)!
}
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 getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession()
//Mlikiowa 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得
return await session?.getMsgService().getMsgsByMsgId(peer, msgIds)!
}
static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
const session = getSession()
// 消息时间从旧到新
return session?.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)!
}
static async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession()
return await session?.getMsgService().recallMsg({
chatType: peer.chatType,
peerUid: peer.peerUid
}, msgIds)
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
function generateMsgId() {
const timestamp = Math.floor(Date.now() / 1000)
const random = Math.floor(Math.random() * Math.pow(2, 32))
const buffer = Buffer.alloc(8)
buffer.writeUInt32BE(timestamp, 0)
buffer.writeUInt32BE(random, 4)
const msgId = BigInt("0x" + buffer.toString('hex')).toString()
return msgId
} }
// 此处有采用Hack方法 利用数据返回正确得到对应消息
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], // 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同
waitComplete = true, timeout = 10000) { // 谨慎采用 目前测试暂无问题 Developer.Mlikiowa
const peerUid = peer.peerUid let msgId: string
try {
// 等待上一个相同的peer发送完 msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime())
let checkLastSendUsingTime = 0; } catch (error) {
const waitLastSend = async () => { //if (!napCatCore.session.getMsgService()['generateMsgUniqueId'])
if (checkLastSendUsingTime > timeout) { //兜底识别策略V2
throw ("发送超时") msgId = generateMsgId()
} }
let lastSending = sendMessagePool[peer.peerUid] peer.guildId = msgId
if (lastSending) { const data = await NTEventDispatch.CallNormalEvent<
// log("有正在发送的消息,等待中...") (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
await sleep(500); (msgList: RawMessage[]) => void
checkLastSendUsingTime += 500; >(
return await waitLastSend(); 'NodeIKernelMsgService/sendMsg',
} else { 'NodeIKernelMsgListener/onMsgInfoListUpdate',
return; 1,
} timeout,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
} }
await waitLastSend(); return false
},
'0',
peer,
msgElements,
new Map()
)
const retMsg = data[1].find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
}
})
return retMsg!
}
let sentMessage: RawMessage = null; static async sendMsgV2(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { function generateMsgId() {
delete sendMessagePool[peerUid]; const timestamp = Math.floor(Date.now() / 1000)
sentMessage = rawMessage; const random = Math.floor(Math.random() * Math.pow(2, 32))
const buffer = Buffer.alloc(8)
buffer.writeUInt32BE(timestamp, 0)
buffer.writeUInt32BE(random, 4)
const msgId = BigInt('0x' + buffer.toString('hex')).toString()
return msgId
}
// 此处有采用Hack方法 利用数据返回正确得到对应消息
// 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同
// 谨慎采用 目前测试暂无问题 Developer.Mlikiowa
let msgId: string
try {
msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime())
} catch (error) {
//if (!napCatCore.session.getMsgService()['generateMsgUniqueId'])
//兜底识别策略V2
msgId = generateMsgId().toString()
}
let data = await NTEventDispatch.CallNormalEvent<
(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/sendMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
timeout,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.msgId === msgId && msgRecord.sendStatus === 2) {
return true
}
} }
return false
},
msgId,
peer,
msgElements,
new Map()
)
const retMsg = data[1].find(msgRecord => {
if (msgRecord.msgId === msgId) {
return true
}
})
return retMsg!
}
let checkSendCompleteUsingTime = 0; static async getMsgUnique(chatType: number, time: string) {
const checkSendComplete = async (): Promise<RawMessage> => { const session = getSession()
if (sentMessage) { if (getBuildVersion() >= 26702) {
if (waitComplete) { return session?.getMsgService().generateMsgUniqueId(chatType, time)!
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) { }
return sentMessage return session?.getMsgService().getMsgUniqueId(time)!
} }
} else {
return sentMessage static async getServerTime() {
} const session = getSession()
// log(`给${peerUid}发送消息成功`) return session?.getMSFService().getServerTime()!
} }
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) { static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
throw ('发送超时') const session = getSession()
} return session?.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])!
await sleep(500) }
return await checkSendComplete()
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await getSelfNick()
const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
})
const selfUid = getSelfUid()
let data = await NTEventDispatch.CallNormalEvent<
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/multiForwardMsgWithComment',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
} }
return false
callNTQQApi({ },
methodName: NTQQApiMethod.SEND_MSG, msgInfos,
args: [{ srcPeer,
msgId: "0", destPeer,
peer, msgElements, [],
msgAttributeInfos: new Map(), new Map()
}, null] )
}).then() for (let msg of data[1]) {
return await checkSendComplete() const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
continue
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
continue
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) {
return msg
}
} }
throw new Error('转发消息超时')
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.FORWARD_MSG, const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
args: [ chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
{ filterMsgType: [],
msgIds: msgIds, filterSendersUid: [],
srcContact: srcPeer, filterMsgToTime: '0',
dstContacts: [ filterMsgFromTime: '0',
destPeer isReverseOrder: false,
], isIncludeCurrent: true,
commentElements: [], pageLimit: 1,
msgAttributeInfos: new Map() })
}, return ret!
null, }
]
})
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
const msgInfos = msgIds.map(id => { const session = getSession()
return {msgId: id, senderShowName: selfInfo.nick} return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)!
}) }
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));
}
})
})
}
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) {
const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isIncludeCurrent: true,
pageLimit: count,
})
return ret!
}
static async getSingleMsg(peer: Peer, seq: string) {
const session = getSession()
return await session?.getMsgService().getSingleMsg(peer, seq)!
}
} }

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

@@ -0,0 +1,64 @@
//远端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,117 +1,299 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall'
import {SelfInfo, User} from "../types"; import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types'
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {uidMaps} from "../../common/data"; import { friends, groupMembers, getSelfUin } from '@/common/data'
import {NTQQWindowApi, NTQQWindows} from "./window"; import { CacheClassFuncAsync, log, getBuildVersion } from '@/common/utils'
import {isQQ998, sleep} from "../../common/utils"; import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NTQQFriendApi } from './friend'
let userInfoCache: Record<string, User> = {}; // uid: User export class NTQQUserApi {
static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [
{
path: filePath,
},
null,
],
timeoutSecond: 10, // 10秒不一定够
})
}
export class NTQQUserApi{ static async getSelfInfo() {
static async setQQAvatar(filePath: string) { return await callNTQQApi<SelfInfo>({
return await callNTQQApi<GeneralCallResult>({ className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SET_QQ_AVATAR, methodName: NTQQApiMethod.SELF_INFO,
args: [{ timeoutSecond: 2,
path:filePath })
}, null], }
timeoutSecond: 10 // 10秒不一定够
}); 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)
}
/** 26702 */
static async fetchUserDetailInfo(uid: string) {
type EventService = NodeIKernelProfileService['fetchUserDetailInfo']
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
1,
5000,
(profile) => profile.uid === uid,
'BuddyProfileStore',
[uid],
UserDetailSource.KSERVER,
[ProfileBizType.KALL]
)
const RetUser: User = {
...profile.simpleInfo.coreInfo,
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
...profile.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel,
pendantId: ''
}
return RetUser
}
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) {
if (getBuildVersion() >= 26702) {
return NTQQUserApi.fetchUserDetailInfo(uid)
}
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo']
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile) => profile.uid === uid,
uid,
[0]
)
return profile
}
// 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 uin = getSelfUin()
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + (await NTQQUserApi.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + 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 NTQQUserApi.getClientKey()
if (clientKeyData.result !== 0) {
throw new Error('获取clientKey失败')
}
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin()
+ '&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
}
@CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) {
const ClientKeyData = await NTQQUserApi.forceFetchClientKey()
const uin = getSelfUin()
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27'
const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl)
return cookies
}
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 getPSkey(domains: string[]): Promise<Map<string, string>> {
}) const session = getSession()
const res = await session?.getTipOffService().getPskey(domains, true)
if (res?.result !== 0) {
throw new Error(`获取Pskey失败: ${res?.errMsg}`)
} }
static async getUserInfo(uid: string) { return res.domainPskeyMap
const result = await callNTQQApi<{ profiles: Map<string, User> }>({ }
methodName: NTQQApiMethod.USER_INFO,
args: [{force: true, uids: [uid]}, undefined], static async getClientKey() {
cbCmd: ReceiveCmdS.USER_INFO const session = getSession()
}) return await session?.getTicketService().forceFetchClientKey('')!
return result.profiles.get(uid) }
}
static async getUserDetailInfo(uid: string, getLevel=false) { static async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> {
// this.getUserInfo(uid); const session = getSession()
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO return session?.getProfileLikeService().setBuddyProfileLike({
const fetchInfo = async ()=>{ friendUid: uid,
const result = await callNTQQApi<{ info: User }>({ sourceId: 71,
methodName, doLikeCount: count,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO, doLikeTollCount: 0
afterFirstCmd: false, })!
cmdCB: (payload) => { }
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload) static async getUidByUinV1(Uin: string) {
return success const session = getSession()
}, // 通用转换开始尝试
args: [ let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
{ // Uid 好友转
uid if (!uid) {
}, friends.forEach((t) => {
null if (t.uin == Uin) {
] uid = t.uid
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
} }
// 首次请求两次才能拿到的等级信息 })
if (!userInfoCache[uid] && getLevel) { }
await fetchInfo() //Uid 群友列表转
await sleep(1000); if (!uid) {
for (let groupMembersList of groupMembers.values()) {
for (let GroupMember of groupMembersList.values()) {
if (GroupMember.uin == Uin) {
uid = GroupMember.uid
}
} }
let userInfo = await fetchInfo() }
userInfoCache[uid] = userInfo
return userInfo
} }
if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
if (unveifyUid.indexOf('*') == -1) {
uid = unveifyUid
}
}
return uid
}
static async getPSkey() { static async getUidByUinV2(Uin: string) {
return await callNTQQApi<string>({ const session = getSession()
className: NTQQApiClass.GROUP_HOME_WORK, let uid = (await session?.getProfileService().getUidByUin('FriendsServiceImpl', [Uin]))?.get(Uin)
methodName: NTQQApiMethod.UPDATE_SKEY, if (uid) return uid
args: [ uid = (await session?.getGroupService().getUidByUins([Uin]))?.uids.get(Uin)
{ if (uid) return uid
domain: "qun.qq.com" uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
} if (uid) return uid
] console.log((await NTQQFriendApi.getBuddyIdMapCache(true)))
}) uid = (await NTQQFriendApi.getBuddyIdMapCache(true)).getValue(Uin)//从Buddy缓存获取Uid
} if (uid) return uid
static async getSkey(groupName: string, groupCode: string): Promise<{data: string}> { uid = (await NTQQFriendApi.getBuddyIdMap(true)).getValue(Uin)
return await NTQQWindowApi.openWindow<{data: string}>(NTQQWindows.GroupHomeWorkWindow, [{ if (uid) return uid
groupName, let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(Uin)).detail.uid//从QQ Native 特殊转换
groupCode, if (unveifyUid.indexOf('*') == -1) uid = unveifyUid
"source": "funcbar" //if (uid) return uid
}], ReceiveCmdS.SKEY_UPDATE, 1); return uid
// 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 getUidByUin(Uin: string) {
if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUidByUinV2(Uin)
}
return await NTQQUserApi.getUidByUinV1(Uin)
}
static async getUserDetailInfoByUinV2(Uin: string) {
return await NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUinV2>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
)
}
static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
)
}
static async getUinByUidV1(Uid: string) {
const ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin',
5000,
[Uid]
)
let uin = ret.uinInfo.get(Uid)
if (!uin) {
//从Buddy缓存获取Uin
friends.forEach((t) => {
if (t.uid == Uid) {
uin = t.uin
}
})
}
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
}
return uin
}
static async getUinByUidV2(Uid: string) {
const session = getSession()
let uin = (await session?.getProfileService().getUinByUid('FriendsServiceImpl', [Uid]))?.get(Uid)
if (uin) return uin
uin = (await session?.getGroupService().getUinByUids([Uid]))?.uins.get(Uid)
if (uin) return uin
uin = (await session?.getUixConvertService().getUin([Uid]))?.uinInfo.get(Uid)
if (uin) return uin
uin = (await NTQQFriendApi.getBuddyIdMapCache(true)).getKey(Uid) //从Buddy缓存获取Uin
if (uin) return uin
uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(Uid)
if (uin) return uin
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
return uin
}
static async getUinByUid(Uid: string) {
if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUinByUidV2(Uid)
}
return await NTQQUserApi.getUinByUidV1(Uid)
}
@CacheClassFuncAsync(3600 * 1000, 'ClientKey')
static async forceFetchClientKey() {
const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')!
}
} }

View File

@@ -1,86 +1,366 @@
import {groups} from "../../common/data"; import { getSelfUin } from '@/common/data'
import {log} from "../../common/utils"; import { log } from '@/common/utils/log'
import {NTQQUserApi} from "./user"; import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request'
import { CacheClassFuncAsync } from '@/common/utils/helper'
export class WebApi{ export enum WebHonorType {
private static bkn: string; ALL = 'all',
private static skey: string; TALKACTIVE = 'talkative',
private static pskey: string; PERFROMER = 'performer',
private static cookie: string LEGEND = 'legend',
private defaultHeaders: Record<string,string> = { STORONGE_NEWBI = 'strong_newbie',
"User-Agent": "QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0" EMOTION = 'emotion'
} }
constructor() { export interface WebApiGroupMember {
uin: number
} role: number
g: number
public async addGroupDigest(groupCode: string, msgSeq: string){ join_time: number
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` last_speak_time: number
const res = await this.request(url) lv: {
return await res.json() point: number
} level: number
}
public async getGroupDigest(groupCode: string){ card: 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` tags: string
const res = await this.request(url) flag: number
log(res.headers) nick: string
return await res.json() qage: number
} rm: number
}
private genBkn(sKey: string){
sKey = sKey || ""; interface WebApiGroupMemberRet {
let hash = 5381; ec: number
errcode: number
for (let i = 0; i < sKey.length; i++) { em: string
const code = sKey.charCodeAt(i); cache: number
hash = hash + (hash << 5) + code; adm_num: number
} levelname: any
mems: WebApiGroupMember[]
return (hash & 0x7FFFFFFF).toString(); count: number
} svr_time: number
private async init(){ max_count: number
if (!WebApi.bkn) { search_count: number
const group = groups[0]; extmode: number
WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data; }
WebApi.bkn = this.genBkn(WebApi.skey);
let cookie = await NTQQUserApi.getPSkey(); export interface WebApiGroupNoticeFeed {
const pskeyRegex = /p_skey=([^;]+)/; u: number//发送者
const match = cookie.match(pskeyRegex); fid: string//fid
const pskeyValue = match ? match[1] : null; pubt: number//时间
WebApi.pskey = pskeyValue; msg: {
if (cookie.indexOf("skey=;") !== -1) { text: string
cookie = cookie.replace("skey=;", `skey=${WebApi.skey};`); text_face: string
} title: string,
WebApi.cookie = cookie; pics?: {
// for(const kv of WebApi.cookie.split(";")){ id: string,
// const [key, value] = kv.split("="); w: string,
// } h: string
// log("set cookie", key, value) }[]
// await session.defaultSession.cookies.set({ }
// url: 'https://qun.qq.com', // 你要请求的域名 type: number
// name: key.trim(), fn: number
// value: value.trim(), cn: number
// expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间例如设置为当前时间之后的300秒 vn: number
// }); settings: {
// } is_show_edit_card: number
} remind_ts: number
} tip_window_type: number
confirm_required: number
private async request(url: string, method: "GET" | "POST" = "GET", headers: Record<string, string> = {}){ }
read_num: number
await this.init(); is_read: number
url += "&bkn=" + WebApi.bkn; is_all_confirm: number
let _headers: Record<string, string> = { }
...this.defaultHeaders, ...headers,
"Cookie": WebApi.cookie, export interface WebApiGroupNoticeRet {
credentials: 'include' ec: number
} em: string
log("request", url, _headers) ltsm: number
const options = { srv_code: number
method: method, read_only: number
headers: _headers role: number
} feeds: WebApiGroupNoticeFeed[]
return fetch(url, options) 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 | undefined> {
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: GroupEssenceMsgRet
try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(url, 'GET', '', { 'Cookie': CookieValue })
} catch {
return undefined
}
//console.log(url, CookieValue)
if (ret.retcode !== 0) {
return undefined
}
return ret
}
@CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members')
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
//logDebug('webapi 获取群成员', GroupCode)
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
try {
const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com')
const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ')
const Bkn = WebApi.genBkn(CookiesObject.skey)
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 });
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return []
} else {
for (const key in fastRet.mems) {
MemberData.push(fastRet.mems[key])
}
}
//初始化获取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])
}
}
} 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' + getSelfUin()
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
}
}
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' + getSelfUin()
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
}
}
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;
}
return (hash & 0x7FFFFFFF).toString();
}
//实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
let res = '';
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());
}
if (Internal_type === 1) {
return resJson?.talkativeList;
} else {
return resJson?.actorList;
}
} 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 = 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,256 +1,371 @@
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, 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 {encodeSilk} from "../common/utils/audio"; 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 poke(groupCode: string, uin: string) {
return null
}
static poke(groupCode: string, uin: string){ static text(content: string): SendTextElement {
return null return {
elementType: ElementType.TEXT,
elementId: '',
textElement: {
content,
atType: AtType.notAt,
atUid: '',
atTinyId: '',
atNtUid: '',
},
} }
static text(content: string): SendTextElement { }
return {
elementType: ElementType.TEXT, static at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement {
elementId: "", return {
textElement: { elementType: ElementType.TEXT,
content, elementId: '',
atType: AtType.notAt, textElement: {
atUid: "", content: display,
atTinyId: "", atType,
atNtUid: "", atUid,
}, atTinyId: '',
}; atNtUid,
},
} }
}
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.REPLY,
elementId: "", elementId: '',
textElement: { replyElement: {
content: `@${atName}`, replayMsgSeq: msgSeq, // raw.msgSeq
atType, replayMsgId: msgId, // raw.msgId
atUid, senderUin: senderUin,
atTinyId: "", senderUinStr: senderUinStr,
atNtUid, },
},
};
} }
}
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { static async pic(picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> {
return { const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType)
elementType: ElementType.REPLY, if (fileSize === 0) {
elementId: "", throw '文件异常大小为0'
replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, // raw.msgId
senderUin: senderUin,
senderUinStr: senderUinStr,
}
}
} }
const maxMB = 30;
static async pic(picPath: string, summary: string = "", subType: 0|1=0): Promise<SendPicElement> { if (fileSize > 1024 * 1024 * 30) {
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType); throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
if (fileSize === 0) {
throw "文件异常大小为0";
}
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,
};
} }
const imageSize = await NTQQFileApi.getImageSize(picPath)
static async file(filePath: string, fileName: string = ""): Promise<SendFileElement> { const picElement = {
const {md5, fileName: _fileName, path, fileSize} = await NTQQFileApi.uploadFile(filePath, ElementType.FILE); md5HexStr: md5,
if (fileSize === 0) { fileSize: fileSize.toString(),
throw "文件异常大小为0"; picWidth: imageSize.width,
} picHeight: imageSize.height,
let element: SendFileElement = { fileName: fileName,
elementType: ElementType.FILE, sourcePath: path,
elementId: "", original: true,
fileElement: { picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
fileName: fileName || _fileName, picSubType: subType,
"filePath": path, fileUuid: '',
"fileSize": (fileSize).toString(), fileSubId: '',
} thumbFileSize: 0,
} summary,
return element;
} }
log('图片信息', picElement)
return {
elementType: ElementType.PIC,
elementId: '',
picElement,
}
}
static async video(filePath: string, fileName: string = "", diyThumbPath: string = ""): Promise<SendVideoElement> { static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> {
let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE)
if (fileSize === 0) { if (fileSize === 0) {
throw "文件异常,大小为0"; throw '文件异常,大小为 0'
} }
const pathLib = require("path"); const element: SendFileElement = {
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) elementType: ElementType.FILE,
thumb = pathLib.dirname(thumb) elementId: '',
// log("thumb 目录", thumb) fileElement: {
let videoInfo = { fileName: fileName || _fileName,
width: 1920, height: 1080, folderId: folderId,
time: 15, filePath: path!,
format: "mp4", fileSize: fileSize.toString(),
size: fileSize, },
filePath }
}; return element
try { }
videoInfo = await getVideoInfo(path);
log("视频信息", videoInfo) static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
} catch (e) { try {
log("获取视频信息失败", e) await fs.stat(filePath)
} } catch (e) {
const createThumb = new Promise<string>((resolve, reject) => { throw `文件${filePath}异常,不存在`
const thumbFileName = `${md5}_0.png` }
const thumbPath = pathLib.join(thumb, thumbFileName) log('复制视频到QQ目录', filePath)
ffmpeg(filePath) let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO)
.on("end", () => {
}) log('复制视频到QQ目录完成', path)
.on("error", (err) => { if (fileSize === 0) {
log("获取视频封面失败,使用默认封面", err) throw '文件异常大小为0'
if (diyThumbPath) { }
fs.copyFile(diyThumbPath, thumbPath).then(() => { const maxMB = 100;
resolve(thumbPath); if (fileSize > 1024 * 1024 * maxMB) {
}).catch(reject) throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} else { }
fs.writeFile(thumbPath, defaultVideoThumb).then(() => { const pathLib = require('path')
resolve(thumbPath); let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
}).catch(reject) thumbDir = pathLib.dirname(thumbDir)
} // log("thumb 目录", thumb)
}) let videoInfo = {
.screenshots({ width: 1920,
timestamps: [0], height: 1080,
filename: thumbFileName, time: 15,
folder: thumb, format: 'mp4',
size: videoInfo.width + "x" + videoInfo.height size: fileSize,
}).on("end", () => { filePath,
resolve(thumbPath); }
}); 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
function useDefaultThumb() {
if (completed) return
log('获取视频封面失败,使用默认封面')
fs.writeFile(thumbPath, defaultVideoThumb)
.then(() => {
resolve(thumbPath)
})
.catch(reject)
}
setTimeout(useDefaultThumb, 5000)
ffmpeg(filePath)
.on('error', (err) => {
if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath)
.then(() => {
completed = true
resolve(thumbPath)
})
.catch(reject)
} else {
useDefaultThumb()
}
}) })
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,53 +0,0 @@
import {log} from "../../../common/utils";
import {NTQQApi} from "../../ntcall";
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 {
this.crychic = require("./crychic-win32-x64.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()

3665
src/ntqqapi/face_config.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,310 +1,384 @@
import {BrowserWindow} from 'electron'; import type { BrowserWindow } from 'electron'
import {NTQQApiClass} from "./ntcall"; import { NTQQApiClass, NTQQApiMethod } from './ntcall'
import {NTQQMsgApi, sendMessagePool} from "./api/msg" import { NTQQMsgApi } from './api/msg'
import {ChatType, Group, GroupMember, GroupMemberRole, RawMessage, User} from "./types"; import {
import {friends, getGroupMember, groups, selfInfo, tempGroupCodeMap, uidMaps} from "../common/data"; CategoryFriend,
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; ChatType,
import {v4 as uuidv4} from "uuid" FriendV2,
import {postOB11Event} from "../onebot11/server/postOB11Event"; Group,
import {getConfigUtil, HOOK_LOG} from "../common/config"; GroupMember,
import fs from "fs"; GroupMemberRole,
import {dbUtil} from "../common/db"; RawMessage,
import {NTQQGroupApi} from "./api/group"; SimpleInfo, User,
import {log} from "../common/utils/log"; } from './types'
import {sleep} from "../common/utils/helper"; import {
import {OB11Constructor} from "../onebot11/constructor"; deleteGroup,
friends,
getFriend,
getGroupMember,
groups,
getSelfUin,
setSelfInfo
} from '@/common/data'
import { OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil, HOOK_LOG } from '@/common/config'
import fs from 'node:fs'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique'
import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
export let ReceiveCmdS = { export let ReceiveCmdS = {
RECENT_CONTACT: "nodeIKernelRecentContactListener/onRecentContactListChangedVer2", RECENT_CONTACT: 'nodeIKernelRecentContactListener/onRecentContactListChangedVer2',
UPDATE_MSG: "nodeIKernelMsgListener/onMsgInfoListUpdate", UPDATE_MSG: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
UPDATE_ACTIVE_MSG: "nodeIKernelMsgListener/onActiveMsgInfoUpdate", UPDATE_ACTIVE_MSG: 'nodeIKernelMsgListener/onActiveMsgInfoUpdate',
NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`, NEW_MSG: `nodeIKernelMsgListener/onRecvMsg`,
NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`, NEW_ACTIVE_MSG: `nodeIKernelMsgListener/onRecvActiveMsg`,
SELF_SEND_MSG: "nodeIKernelMsgListener/onAddSendMsg", SELF_SEND_MSG: 'nodeIKernelMsgListener/onAddSendMsg',
USER_INFO: "nodeIKernelProfileListener/onProfileSimpleChanged", USER_INFO: 'nodeIKernelProfileListener/onProfileSimpleChanged',
USER_DETAIL_INFO: "nodeIKernelProfileListener/onProfileDetailInfoChanged", USER_DETAIL_INFO: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
GROUPS: "nodeIKernelGroupListener/onGroupListUpdate", GROUPS: 'nodeIKernelGroupListener/onGroupListUpdate',
GROUPS_STORE: "onGroupListUpdate", GROUPS_STORE: 'onGroupListUpdate',
GROUP_MEMBER_INFO_UPDATE: "nodeIKernelGroupListener/onMemberInfoChange", GROUP_MEMBER_INFO_UPDATE: 'nodeIKernelGroupListener/onMemberInfoChange',
FRIENDS: "onBuddyListChange", FRIENDS: 'onBuddyListChange',
MEDIA_DOWNLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaDownloadComplete", MEDIA_DOWNLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaDownloadComplete',
UNREAD_GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated", UNREAD_GROUP_NOTIFY: 'nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated',
GROUP_NOTIFY: "nodeIKernelGroupListener/onGroupSingleScreenNotifies", GROUP_NOTIFY: 'nodeIKernelGroupListener/onGroupSingleScreenNotifies',
FRIEND_REQUEST: "nodeIKernelBuddyListener/onBuddyReqChange", FRIEND_REQUEST: 'nodeIKernelBuddyListener/onBuddyReqChange',
SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged', SELF_STATUS: 'nodeIKernelProfileListener/onSelfStatusChanged',
CACHE_SCAN_FINISH: "nodeIKernelStorageCleanListener/onFinishScan", CACHE_SCAN_FINISH: 'nodeIKernelStorageCleanListener/onFinishScan',
MEDIA_UPLOAD_COMPLETE: "nodeIKernelMsgListener/onRichMediaUploadComplete", MEDIA_UPLOAD_COMPLETE: 'nodeIKernelMsgListener/onRichMediaUploadComplete',
SKEY_UPDATE: "onSkeyUpdate" 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: any) {
log('hook error', ntQQApiMethodName, e.stack.toString())
}
}).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: any) {
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)
} catch (e) { }
try {
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 { try {
HOOK_LOG && log("call NTQQ api", thisArg, args); let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
(_ as Promise<void>).then()
}
} catch (e) { } 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 = randomUUID()
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.groupCode)
// if (!activatedGroups.includes(group.groupCode)) { if (group.privilegeFlag === 0) {
NTQQMsgApi.activateChat({peerUid: group.groupCode, chatType: ChatType.group}).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)
NTQQMsgApi.activateChat({ peerUid: group.groupCode, chatType: ChatType.group }).then().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 = Array.from(members.values())
}
}
}
} }
async function processGroupEvent(payload: { groupList: Group[] }) { async function processGroupEvent(payload: { groupList: Group[] }) {
try { try {
const newGroupList = payload.groupList; const newGroupList = payload.groupList
for (const group of newGroupList) { for (const group of newGroupList) {
let existGroup = groups.find(g => g.groupCode == group.groupCode); let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) { if (existGroup) {
if (existGroup.memberCount > group.memberCount) { if (existGroup.memberCount > group.memberCount) {
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`); log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`)
const oldMembers = existGroup.members; const oldMembers = existGroup.members
await sleep(200); // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时 await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode); const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = newMembers; group.members = Array.from(newMembers.values())
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[1].uin)
} }
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
let bot = await getGroupMember(group.groupCode, selfInfo.uin)
if (bot.role == GroupMemberRole.admin || bot.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfInfo.uin) {
postOB11Event(new OB11GroupDecreaseEvent(parseInt(group.groupCode), parseInt(member.uin), parseInt(member.uin), "leave"));
break;
}
}
}
// 判断bot是否是管理员如果是管理员不需要从这里得知有人退群这里的退群无法得知是主动退群还是被踢
const selfUin = getSelfUin()
const bot = await getGroupMember(group.groupCode, selfUin)
if (bot?.role == GroupMemberRole.admin || bot?.role == GroupMemberRole.owner) {
continue
}
for (const member of oldMembers) {
if (!newMembersSet.has(member.uin) && member.uin != selfUin) {
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(); }
log("更新群信息错误", e.stack.toString());
} }
updateGroups(newGroupList, false).then()
} catch (e: any) {
updateGroups(payload.groupList).then()
log('更新群信息错误', e.stack.toString())
}
} }
// 群列表变动 export async function startHook() {
registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then();
} else {
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) {
updateGroups(payload.groupList).then();
} else {
if (process.platform != "win32") {
processGroupEvent(payload).then();
}
}
})
registerReceiveHook<{ // 群列表变动
groupCode: string, registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS, (payload) => {
dataSource: number, // updateType 3是群列表变动2是群成员变动
// log("群列表变动", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform == 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{ groupList: Group[]; updateType: number }>(ReceiveCmdS.GROUPS_STORE, (payload) => {
// updateType 3是群列表变动2是群成员变动
// log("群列表变动, store", payload.updateType, payload.groupList)
if (payload.updateType != 2) {
updateGroups(payload.groupList).then()
}
else {
if (process.platform != 'win32') {
processGroupEvent(payload).then()
}
}
})
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember> members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => { }>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode; const groupCode = payload.groupCode
const members = Array.from(payload.members.values()); const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members) // log("群成员信息变动", groupCode, members)
for (const member of members) { for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin); const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) { if (existMember) {
Object.assign(existMember, member); 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),
)
} else if (member.role != existMember.role) {
log('有管理员变动通知')
const groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent(
member.role == GroupMemberRole.admin ? 'set' : 'unset',
parseInt(groupCode),
parseInt(member.uin)
)
postOb11Event(groupAdminNoticeEvent, true)
} }
Object.assign(existMember, member)
}
} }
// const existGroup = groups.find(g => g.groupCode == groupCode); // const existGroup = groups.find(g => g.groupCode == groupCode);
// if (existGroup) { // if (existGroup) {
@@ -320,137 +394,146 @@ registerReceiveHook<{
// } // }
// } // }
// } // }
}) })
// 好友列表变动 // 好友列表变动
registerReceiveHook<{ registerReceiveHook<{
data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, payload => { }>(ReceiveCmdS.FRIENDS, (payload) => {
for (const fData of payload.data) { // log("onBuddyListChange", payload)
const _friends = fData.buddyList; // let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
for (let friend of _friends) { type V2data = {userSimpleInfos: Map<string, SimpleInfo>}
NTQQMsgApi.activateChat({peerUid: friend.uid, chatType: ChatType.friend}).then() let friendList: User[] = [];
let existFriend = friends.find(f => f.uin == friend.uin) if ((payload as any).userSimpleInfos) {
if (!existFriend) { // friendListV2 = payload as any
friends.push(friend) friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
} else { return {
Object.assign(existFriend, friend) ...v.coreInfo,
}
} }
})
} }
}) else{
for (const fData of payload.data) {
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => { friendList.push(...fData.buddyList)
// 保存一下uid }
for (const message of payload.msgList) {
const uid = message.senderUid;
const uin = message.senderUin;
if (uid && uin) {
if (message.chatType === ChatType.temp) {
dbUtil.getReceivedTempUinMap().then(receivedTempUinMap => {
if (!receivedTempUinMap[uin]) {
receivedTempUinMap[uin] = uid;
dbUtil.setReceivedTempUinMap(receivedTempUinMap)
}
})
}
uidMaps[uid] = uin;
}
} }
log('好友列表变动', friendList)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
let existFriend = friends.find((f) => f.uin == friend.uin)
if (!existFriend) {
friends.push(friend)
}
else {
Object.assign(existFriend, friend)
}
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
// 自动清理新消息文件 // 自动清理新消息文件
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
if (aioOpGrayTipElement) {
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)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({msgRecord}) => {
const message = msgRecord;
const peerUid = message.peerUid;
// log("收到自己发送成功的消息", Object.keys(sendMessagePool), message);
// log("收到自己发送成功的消息", message.msgId, message.msgSeq);
dbUtil.addMsg(message).then()
const sendCallback = sendMessagePool[peerUid]
if (sendCallback) {
try {
sendCallback(message);
} catch (e) {
log("receive self msg error", e.stack)
}
}
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
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()
} }
} }
}, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000)
}
} }
}) })
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const { msgId, chatType, peerUid } = msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
setSelfInfo({
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,240 @@
import { Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/ntqqapi/types'
interface IGroupListener {
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): void
onGroupExtListUpdate(...args: unknown[]): void
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]): void
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]): void
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): void
onGroupDetailInfoChange(...args: unknown[]): void
onGroupAllInfoChange(...args: unknown[]): void
onGroupsMsgMaskResult(...args: unknown[]): void
onGroupConfMemberChange(...args: unknown[]): void
onGroupBulletinChange(...args: unknown[]): void
onGetGroupBulletinListResult(...args: unknown[]): void
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>,
finish: boolean,
hasRobot: boolean
}): void
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>): void
onSearchMemberChange(...args: unknown[]): void
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]): void
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]): void
onGroupStatisticInfoChange(...args: unknown[]): void
onJoinGroupNotify(...args: unknown[]): void
onShutUpMemberListChanged(...args: unknown[]): void
onGroupBulletinRemindNotify(...args: unknown[]): void
onGroupFirstBulletinNotify(...args: unknown[]): void
onJoinGroupNoVerifyFlag(...args: unknown[]): void
onGroupArkInviteStateResult(...args: unknown[]): void
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void
}
export interface NodeIKernelGroupListener extends IGroupListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IGroupListener): NodeIKernelGroupListener
}
export class GroupListener implements IGroupListener {
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void {
}
onGetGroupBulletinListResult(...args: unknown[]) {
}
onGroupAllInfoChange(...args: unknown[]) {
}
onGroupBulletinChange(...args: unknown[]) {
}
onGroupBulletinRemindNotify(...args: unknown[]) {
}
onGroupArkInviteStateResult(...args: unknown[]) {
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
}
onGroupConfMemberChange(...args: unknown[]) {
}
onGroupDetailInfoChange(...args: unknown[]) {
}
onGroupExtListUpdate(...args: unknown[]) {
}
onGroupFirstBulletinNotify(...args: unknown[]) {
}
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
}
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
}
onGroupsMsgMaskResult(...args: unknown[]) {
}
onGroupStatisticInfoChange(...args: unknown[]) {
}
onJoinGroupNotify(...args: unknown[]) {
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
}
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean,
hasRobot: boolean
}) {
}
onSearchMemberChange(...args: unknown[]) {
}
onShutUpMemberListChanged(...args: unknown[]) {
}
}
export class DebugGroupListener implements IGroupListener {
onGroupMemberLevelInfoChange(...args: unknown[]): void {
console.log('onGroupMemberLevelInfoChange:', ...args)
}
onGetGroupBulletinListResult(...args: unknown[]) {
console.log('onGetGroupBulletinListResult:', ...args)
}
onGroupAllInfoChange(...args: unknown[]) {
console.log('onGroupAllInfoChange:', ...args)
}
onGroupBulletinChange(...args: unknown[]) {
console.log('onGroupBulletinChange:', ...args)
}
onGroupBulletinRemindNotify(...args: unknown[]) {
console.log('onGroupBulletinRemindNotify:', ...args)
}
onGroupArkInviteStateResult(...args: unknown[]) {
console.log('onGroupArkInviteStateResult:', ...args)
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
console.log('onGroupBulletinRichMediaDownloadComplete:', ...args)
}
onGroupConfMemberChange(...args: unknown[]) {
console.log('onGroupConfMemberChange:', ...args)
}
onGroupDetailInfoChange(...args: unknown[]) {
console.log('onGroupDetailInfoChange:', ...args)
}
onGroupExtListUpdate(...args: unknown[]) {
console.log('onGroupExtListUpdate:', ...args)
}
onGroupFirstBulletinNotify(...args: unknown[]) {
console.log('onGroupFirstBulletinNotify:', ...args)
}
onGroupListUpdate(...args: unknown[]) {
console.log('onGroupListUpdate:', ...args)
}
onGroupNotifiesUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUpdated:', ...args)
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
console.log('onGroupBulletinRichMediaProgressUpdate:', ...args)
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
console.log('onGroupNotifiesUnreadCountUpdated:', ...args)
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
console.log('onGroupSingleScreenNotifies:')
}
onGroupsMsgMaskResult(...args: unknown[]) {
console.log('onGroupsMsgMaskResult:', ...args)
}
onGroupStatisticInfoChange(...args: unknown[]) {
console.log('onGroupStatisticInfoChange:', ...args)
}
onJoinGroupNotify(...args: unknown[]) {
console.log('onJoinGroupNotify:', ...args)
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
console.log('onJoinGroupNoVerifyFlag:', ...args)
}
onMemberInfoChange(groupCode: string, changeType: number, members: Map<string, GroupMember>) {
console.log('onMemberInfoChange:', groupCode, changeType, members)
}
onMemberListChange(...args: unknown[]) {
console.log('onMemberListChange:', ...args)
}
onSearchMemberChange(...args: unknown[]) {
console.log('onSearchMemberChange:', ...args)
}
onShutUpMemberListChanged(...args: unknown[]) {
console.log('onShutUpMemberListChanged:', ...args)
}
}

View File

@@ -0,0 +1,514 @@
import { ChatType, RawMessage } from '@/ntqqapi/types'
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string,
msgElementId: string,
msgId: string,
fileId: string,
fileProgress: string, // '0'
fileSpeed: string, // '0'
fileErrCode: string, // '0'
fileErrMsg: string,
fileDownType: number, // 暂时未知
thumbSize: number,
filePath: string,
totalSize: string,
trasferStatus: number,
step: number,
commonFileInfo: unknown | null,
fileSrvErrCode: string,
clientMsg: string,
businessId: number,
userTotalSpacePerDay: unknown | null,
userUsedSpacePerDay: unknown | null
}
export interface onGroupFileInfoUpdateParamType {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: Array<any>
allFileCount: string
nextIndex: string
reqId: string
}
// {
// sessionType: 1,
// chatType: 100,
// peerUid: 'u_PVQ3tl6K78xxxx',
// groupCode: '809079648',
// fromNick: '拾xxxx,
// sig: '0x'
// }
export interface TempOnRecvParams {
sessionType: number,//1
chatType: ChatType,//100
peerUid: string,//uid
groupCode: string,//gc
fromNick: string,//gc name
sig: string,
}
export interface IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage): void
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): void
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): void
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): void
onContactUnreadCntUpdate(hashMap: unknown): void
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): void
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): void
onEmojiDownloadComplete(emojiNotifyInfo: unknown): void
onEmojiResourceUpdate(emojiResourceInfo: unknown): void
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFileMsgCome(arrayList: unknown): void
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onFirstViewGroupGuildMapping(arrayList: unknown): void
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): void
onGroupFileInfoAdd(groupItem: unknown): void
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType): void
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): void
onGroupTransferInfoAdd(groupItem: unknown): void
onGroupTransferInfoUpdate(groupFileListResult: unknown): void
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): void
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): void
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): void
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): void
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): void
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): void
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): void
onInputStatusPush(inputStatusInfo: unknown): void
onKickedOffLine(kickedInfo: unknown): void
onLineDev(arrayList: unknown): void
onLogLevelChanged(j2: unknown): void
onMsgAbstractUpdate(arrayList: unknown): void
onMsgBoxChanged(arrayList: unknown): void
onMsgDelete(contact: unknown, arrayList: unknown): void
onMsgEventListUpdate(hashMap: unknown): void
onMsgInfoListAdd(arrayList: unknown): void
onMsgInfoListUpdate(msgList: RawMessage[]): void
onMsgQRCodeStatusChanged(i2: unknown): void
onMsgRecall(i2: unknown, str: unknown, j2: unknown): void
onMsgSecurityNotify(msgRecord: unknown): void
onMsgSettingUpdate(msgSetting: unknown): void
onNtFirstViewMsgSyncEnd(): void
onNtMsgSyncEnd(): void
onNtMsgSyncStart(): void
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): void
onRecvGroupGuildFlag(i2: unknown): void
onRecvMsg(...arrayList: unknown[]): void
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): void
onRecvOnlineFileMsg(arrayList: unknown): void
onRecvS2CMsg(arrayList: unknown): void
onRecvSysMsg(arrayList: unknown): void
onRecvUDCFlag(i2: unknown): void
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): void
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): void
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): void
onSearchGroupFileInfoUpdate(searchGroupFileResult:
{
result: {
retCode: number,
retMsg: string,
clientWording: string
},
syncCookie: string,
totalMatchCount: number,
ownerMatchCount: number,
isEnd: boolean,
reqId: number,
item: Array<{
groupCode: string,
groupName: string,
uploaderUin: string,
uploaderName: string,
matchUin: string,
matchWords: Array<unknown>,
fileNameHits: Array<{
start: number,
end: number
}>,
fileModelId: string,
fileId: string,
fileName: string,
fileSize: string,
busId: number,
uploadTime: number,
modifyTime: number,
deadTime: number,
downloadTimes: number,
localPath: string
}>
}): void
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): void
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): void
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): void
onUnreadCntAfterFirstView(hashMap: unknown): void
onUnreadCntUpdate(hashMap: unknown): void
onUserChannelTabStatusChanged(z: unknown): void
onUserOnlineStatusChanged(z: unknown): void
onUserTabStatusChanged(arrayList: unknown): void
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): void
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]): void
onMsgWithRichLinkInfoUpdate(...args: unknown[]): void
onRedTouchChanged(...args: unknown[]): void
// 第一次发现于Win 9.9.9 23159
onBroadcastHelperProgerssUpdate(...args: unknown[]): void
}
export interface NodeIKernelMsgListener extends IKernelMsgListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IKernelMsgListener): NodeIKernelMsgListener
}
export class MsgListener implements IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage) {
}
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown) {
}
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown) {
}
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown) {
}
onContactUnreadCntUpdate(hashMap: unknown) {
}
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown) {
}
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown) {
}
onEmojiDownloadComplete(emojiNotifyInfo: unknown) {
}
onEmojiResourceUpdate(emojiResourceInfo: unknown) {
}
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFileMsgCome(arrayList: unknown) {
}
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onFirstViewGroupGuildMapping(arrayList: unknown) {
}
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown) {
}
onGroupFileInfoAdd(groupItem: unknown) {
}
onGroupFileInfoUpdate(groupFileListResult: onGroupFileInfoUpdateParamType) {
}
onGroupGuildUpdate(groupGuildNotifyInfo: unknown) {
}
onGroupTransferInfoAdd(groupItem: unknown) {
}
onGroupTransferInfoUpdate(groupFileListResult: unknown) {
}
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown) {
}
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown) {
}
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown) {
}
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown) {
}
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown) {
}
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown) {
}
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown) {
}
onInputStatusPush(inputStatusInfo: unknown) {
}
onKickedOffLine(kickedInfo: unknown) {
}
onLineDev(arrayList: unknown) {
}
onLogLevelChanged(j2: unknown) {
}
onMsgAbstractUpdate(arrayList: unknown) {
}
onMsgBoxChanged(arrayList: unknown) {
}
onMsgDelete(contact: unknown, arrayList: unknown) {
}
onMsgEventListUpdate(hashMap: unknown) {
}
onMsgInfoListAdd(arrayList: unknown) {
}
onMsgInfoListUpdate(msgList: RawMessage[]) {
}
onMsgQRCodeStatusChanged(i2: unknown) {
}
onMsgRecall(i2: unknown, str: unknown, j2: unknown) {
}
onMsgSecurityNotify(msgRecord: unknown) {
}
onMsgSettingUpdate(msgSetting: unknown) {
}
onNtFirstViewMsgSyncEnd() {
}
onNtMsgSyncEnd() {
}
onNtMsgSyncStart() {
}
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
}
onRecvGroupGuildFlag(i2: unknown) {
}
onRecvMsg(arrayList: RawMessage[]) {
}
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown) {
}
onRecvOnlineFileMsg(arrayList: unknown) {
}
onRecvS2CMsg(arrayList: unknown) {
}
onRecvSysMsg(arrayList: unknown) {
}
onRecvUDCFlag(i2: unknown) {
}
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) {
}
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown) {
}
onRichMediaUploadComplete(fileTransNotifyInfo: unknown) {
}
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown) {
}
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown) {
}
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown) {
}
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams) {
}
onUnreadCntAfterFirstView(hashMap: unknown) {
}
onUnreadCntUpdate(hashMap: unknown) {
}
onUserChannelTabStatusChanged(z: unknown) {
}
onUserOnlineStatusChanged(z: unknown) {
}
onUserTabStatusChanged(arrayList: unknown) {
}
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
}
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]) {
}
onMsgWithRichLinkInfoUpdate(...args: unknown[]) {
}
onRedTouchChanged(...args: unknown[]) {
}
// 第一次发现于Win 9.9.9-23159
onBroadcastHelperProgerssUpdate(...args: unknown[]) {
}
}

View File

@@ -0,0 +1,44 @@
import { User, UserDetailInfoListenerArg } from '@/ntqqapi/types'
interface IProfileListener {
onProfileSimpleChanged(...args: unknown[]): void
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void
onProfileDetailInfoChanged(profile: User): void
onStatusUpdate(...args: unknown[]): void
onSelfStatusChanged(...args: unknown[]): void
onStrangerRemarkChanged(...args: unknown[]): void
}
export interface NodeIKernelProfileListener extends IProfileListener {
new(listener: IProfileListener): NodeIKernelProfileListener
}
export class ProfileListener implements IProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]) {
}
onProfileDetailInfoChanged(profile: User) {
}
onStatusUpdate(...args: unknown[]) {
}
onSelfStatusChanged(...args: unknown[]) {
}
onStrangerRemarkChanged(...args: unknown[]) {
}
}

View File

@@ -0,0 +1,3 @@
export * from './NodeIKernelProfileListener'
export * from './NodeIKernelGroupListener'
export * from './NodeIKernelMsgListener'

View File

@@ -1,211 +1,226 @@
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 { log } from '../common/utils/log'
import {v4 as uuidv4} from "uuid" import { HOOK_LOG } from '../common/config'
import {log} from "../common/utils/log"; import { randomUUID } from 'node:crypto'
import {NTQQWindow, NTQQWindowApi, NTQQWindows} from "./api/window";
import {WebApi} from "./api/webapi";
import {HOOK_LOG} from "../common/config";
export enum NTQQApiClass { export enum NTQQApiClass {
NT_API = "ns-ntApi", NT_API = 'ns-ntApi',
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',
ACTIVE_CHAT_PREVIEW = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息 RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_HISTORY = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息 ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelf", ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
GET_MULTI_MSG = "nodeIKernelMsgService/getMultiMsg", HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio',
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = "fetchAuthData", SELF_INFO = 'fetchAuthData',
FRIENDS = "nodeIKernelBuddyService/getBuddyList", FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = "nodeIKernelGroupService/getGroupList",
GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene",
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList",
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",
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",
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",
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan', GROUPS = 'nodeIKernelGroupService/getGroupList',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths', GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath', GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath', GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo', USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo', USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo', 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',
OPEN_EXTRA_WINDOW = 'openExternalWindow', RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
SEND_MSG = 'nodeIKernelMsgService/sendMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia',
GET_SKEY = "nodeIKernelTipOffService/getPskey", FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
UPDATE_SKEY = "updatePskey", 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',
FETCH_UNITED_COMMEND_CONFIG = "nodeIKernelUnitedConfigService/fetchUnitedCommendConfig" // 发包需要调用的 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 = randomUUID()
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,
{ {
sender: { sender: {
send: (..._args: unknown[]) => { send: (..._args: unknown[]) => {
}, },
}, },
}, },
{type: 'request', callbackId: uuid, eventName}, { type: 'request', callbackId: uuid, eventName },
apiArgs 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 fetchUnitedCommendConfig() { static async fetchUnitedCommendConfig() {
return await callNTQQApi<GeneralCallResult>({ return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG, methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
args:[ args: [
{ {
groups: ['100243'] groups: ['100243'],
} },
] ],
}) })
} }
} }

View File

@@ -0,0 +1,125 @@
import { GeneralCallResult } from './common'
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export interface NodeIKernelBuddyService {
// 26702 以上
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
data: Array<{
categoryId: number,
categorySortId: number,
categroyName: string,
categroyMbCount: number,
onlineCount: number,
buddyUids: Array<string>
}>
}>
//26702 以上
getBuddyListFromCache(callFrom: string): Promise<Array<
{
categoryId: number,//9999应该跳过 那是兜底数据吧
categorySortId: number,//排序方式
categroyName: string,//分类名
categroyMbCount: number,//不懂
onlineCount: number,//在线数目
buddyUids: Array<string>//Uids
}>>
addKernelBuddyListener(listener: any): number
getAllBuddyCount(): number
removeKernelBuddyListener(listener: unknown): void
getBuddyList(nocache: boolean): Promise<GeneralCallResult>
getBuddyNick(uid: number): string
getBuddyRemark(uid: number): string
setBuddyRemark(uid: number, remark: string): void
getAvatarUrl(uid: number): string
isBuddy(uid: string): boolean
getCategoryNameWithUid(uid: number): string
getTargetBuddySetting(uid: number): unknown
getTargetBuddySettingByType(uid: number, type: number): unknown
getBuddyReqUnreadCnt(): number
getBuddyReq(): unknown
delBuddyReq(uid: number): void
clearBuddyReqUnreadCnt(): void
reqToAddFriends(uid: number, msg: string): void
setSpacePermission(uid: number, permission: number): void
approvalFriendRequest(arg: {
friendUid: string
reqTime: string
accept: boolean
}): Promise<void>
delBuddy(uid: number): void
delBatchBuddy(uids: number[]): void
getSmartInfos(uid: number): unknown
setBuddyCategory(uid: number, category: number): void
setBatchBuddyCategory(uids: number[], category: number): void
addCategory(category: string): void
delCategory(category: string): void
renameCategory(oldCategory: string, newCategory: string): void
resortCategory(categorys: string[]): void
pullCategory(uid: number, category: string): void
setTop(uid: number, isTop: boolean): void
SetSpecialCare(uid: number, isSpecialCare: boolean): void
setMsgNotify(uid: number, isNotify: boolean): void
hasBuddyList(): boolean
setBlock(uid: number, isBlock: boolean): void
isBlocked(uid: number): boolean
modifyAddMeSetting(setting: unknown): void
getAddMeSetting(): unknown
getDoubtBuddyReq(): unknown
getDoubtBuddyUnreadNum(): number
approvalDoubtBuddyReq(uid: number, isAgree: boolean): void
delDoubtBuddyReq(uid: number): void
delAllDoubtBuddyReq(): void
reportDoubtBuddyReqUnread(): void
getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,249 @@
import { NodeIKernelGroupListener } from '@/ntqqapi/listeners'
import {
GroupExtParam,
GroupMember,
GroupMemberRole,
GroupNotifyTypes,
GroupRequestOperateTypes,
} from '@/ntqqapi/types'
import { GeneralCallResult } from './common'
//高版本的接口不应该随意使用 使用应该严格进行pr审核 同时部分ipc中未出现的接口不要过于依赖 应该做好数据兜底
export interface NodeIKernelGroupService {
getMemberCommonInfo(Req: {
groupCode: string,
startUin: string,
identifyFlag: string,
uinList: string[],
memberCommonFilter: {
memberUin: number,
uinFlag: number,
uinFlagExt: number,
uinMobileFlag: number,
shutUpTime: number,
privilege: number,
},
memberNum: number,
filterMethod: string,
onlineFlag: string,
realSpecialTitleFlag: number
}): Promise<unknown>
//26702
getGroupMemberLevelInfo(groupCode: string): Promise<unknown>
//26702
getGroupHonorList(groupCodes: Array<string>): unknown
getUinByUids(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uins: Map<string, string>
}>
getUidByUins(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uids: Map<string, string>
}>
//26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
//26702(其实更早 但是我不知道)
getGroupLatestEssenceList(groupCode: string): Promise<unknown>
//26702(其实更早 但是我不知道)
shareDigest(Req: {
appId: string,
appType: number,
msgStyle: number,
recvUin: string,
sendType: number,
clientInfo: {
platform: number
},
richMsg: {
usingArk: boolean,
title: string,
summary: string,
url: string,
pictureUrl: string,
brief: string
}
}): Promise<unknown>
//26702(其实更早 但是我不知道)
isEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
queryCachedEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//26702(其实更早 但是我不知道)
fetchGroupEssenceList(Req: { groupCode: string, pageStart: number, pageLimit: number }, Arg: unknown): Promise<unknown>
//26702
getAllMemberList(groupCode: string, forceFetch: boolean): Promise<{
errCode: number,
errMsg: string,
result: {
ids: Array<{
uid: string,
index: number//0
}>,
infos: {},
finish: true,
hasRobot: false
}
}>
setHeader(uid: string, path: string): unknown
addKernelGroupListener(listener: NodeIKernelGroupListener): number
removeKernelGroupListener(listenerId: unknown): void
createMemberListScene(groupCode: string, scene: string): string
destroyMemberListScene(SceneId: string): void
//About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string}
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number, errMsg: string,
result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean }
}>
getPrevMemberList(): unknown
monitorMemberList(): unknown
searchMember(sceneId: string, keywords: string[]): unknown
getMemberInfo(group_id: string, uids: string[], forceFetch: boolean): Promise<GeneralCallResult>
//getMemberInfo [ '56729xxxx', [ 'u_4Nj08cwW5Hxxxxx' ], true ]
kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>
modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void
modifyMemberCardName(groupCode: string, uid: string, cardName: string): void
getTransferableMemberInfo(groupCode: string): unknown//获取整个群的
transferGroup(uid: string): void
getGroupList(force: boolean): Promise<GeneralCallResult>
getGroupExtList(force: boolean): Promise<GeneralCallResult>
getGroupDetailInfo(groupCode: string): unknown
getMemberExtInfo(param: GroupExtParam): Promise<unknown>//req
getGroupAllInfo(): unknown
getDiscussExistInfo(): unknown
getGroupConfMember(): unknown
getGroupMsgMask(): unknown
getGroupPortrait(): void
modifyGroupName(groupCode: string, groupName: string, arg: false): void
modifyGroupRemark(groupCode: string, remark: string): void
modifyGroupDetailInfo(groupCode: string, arg: unknown): void
setGroupMsgMask(groupCode: string, arg: unknown): void
changeGroupShieldSettingTemp(groupCode: string, arg: unknown): void
inviteToGroup(arg: unknown): void
inviteMembersToGroup(args: unknown[]): void
inviteMembersToGroupWithMsg(args: unknown): void
createGroup(arg: unknown): void
createGroupWithMembers(arg: unknown): void
quitGroup(groupCode: string): void
destroyGroup(groupCode: string): void
//获取单屏群通知列表
getSingleScreenNotifies(force: boolean, start_seq: string, num: number): Promise<GeneralCallResult>
clearGroupNotifies(groupCode: string): void
getGroupNotifiesUnreadCount(unknown: Boolean): Promise<GeneralCallResult>
clearGroupNotifiesUnreadCount(groupCode: string): void
operateSysNotify(
doubt: boolean,
operateMsg: {
operateType: GroupRequestOperateTypes, // 2 拒绝
targetMsg: {
seq: string, // 通知序列号
type: GroupNotifyTypes,
groupCode: string,
postscript: string
}
}): Promise<void>
setTop(groupCode: string, isTop: boolean): void
getGroupBulletin(groupCode: string): unknown
deleteGroupBulletin(groupCode: string, seq: string): void
publishGroupBulletin(groupCode: string, pskey: string, data: any): Promise<GeneralCallResult>
publishInstructionForNewcomers(groupCode: string, arg: unknown): void
uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<GeneralCallResult & {
errCode: number
picInfo?: {
id: string,
width: number,
height: number
}
}>
downloadGroupBulletinRichMedia(groupCode: string): unknown
getGroupBulletinList(groupCode: string): unknown
getGroupStatisticInfo(groupCode: string): unknown
getGroupRemainAtTimes(groupCode: string): number
getJoinGroupNoVerifyFlag(groupCode: string): unknown
getGroupArkInviteState(groupCode: string): unknown
reqToJoinGroup(groupCode: string, arg: unknown): void
setGroupShutUp(groupCode: string, shutUp: boolean): void
getGroupShutUpMemberList(groupCode: string): unknown[]
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>
getGroupRecommendContactArkJson(groupCode: string): unknown
getJoinGroupLink(groupCode: string): unknown
modifyGroupExtInfo(groupCode: string, arg: unknown): void
//需要提前判断是否存在 高版本新增
addGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>
//需要提前判断是否存在 高版本新增
removeGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,3 @@
export interface NodeIKernelMSFService {
getServerTime(): string
}

View File

@@ -0,0 +1,744 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { NodeIKernelMsgListener } from '@/ntqqapi/listeners/NodeIKernelMsgListener'
import { GeneralCallResult } from './common'
export interface QueryMsgsParams {
chatInfo: Peer,
filterMsgType: [],
filterSendersUid: string[],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi {
errMsg: string
result: number
tmpChatInfo?: TmpChatInfo
}
export interface TmpChatInfo {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string
addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>): Promise<GeneralCallResult>
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>
addKernelMsgImportToolListener(arg: Object): unknown
removeKernelMsgListener(args: unknown): unknown
addKernelTempChatSigListener(...args: unknown[]): unknown
removeKernelTempChatSigListener(...args: unknown[]): unknown
setAutoReplyTextList(AutoReplyText: Array<unknown>, i2: number): unknown
getAutoReplyTextList(...args: unknown[]): unknown
getOnLineDev(): void
kickOffLine(DevInfo: Object): unknown
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
fetchStatusMgrInfo(): unknown
fetchStatusUnitedConfigInfo(): unknown
getOnlineStatusSmallIconBasePath(): unknown
getOnlineStatusSmallIconFileNameByUrl(Url: string): unknown
downloadOnlineStatusSmallIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusBigIconBasePath(): unknown
downloadOnlineStatusBigIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusCommonPath(arg: string): unknown
getOnlineStatusCommonFileNameByUrl(Url: string): unknown
downloadOnlineStatusCommonByUrl(arg0: string, arg1: string): unknown
// this.tokenType = i2
// this.apnsToken = bArr
// this.voipToken = bArr2
// this.profileId = str
setToken(arg: Object): unknown
switchForeGround(): unknown
switchBackGround(arg: Object): unknown
//hex
setTokenForMqq(token: string): unknown
switchForeGroundForMqq(...args: unknown[]): unknown
switchBackGroundForMqq(...args: unknown[]): unknown
getMsgSetting(...args: unknown[]): unknown
setMsgSetting(...args: unknown[]): unknown
addSendMsg(...args: unknown[]): unknown
cancelSendMsg(...args: unknown[]): unknown
switchToOfflineSendMsg(peer: Peer, MsgId: string): unknown
reqToOfflineSendMsg(...args: unknown[]): unknown
refuseReceiveOnlineFileMsg(peer: Peer, MsgId: string): unknown
resendMsg(...args: unknown[]): unknown
recallMsg(...args: unknown[]): unknown
reeditRecallMsg(...args: unknown[]): unknown
//调用请检查除开commentElements其余参数不能为null
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
forwardMsgWithComment(...args: unknown[]): unknown
forwardSubMsgWithComment(...args: unknown[]): unknown
forwardRichMsgInVist(...args: unknown[]): unknown
forwardFile(...args: unknown[]): unknown
//Array<Msg>, Peer from, Peer to
multiForwardMsg(...args: unknown[]): unknown
multiForwardMsgWithComment(...args: unknown[]): unknown
deleteRecallMsg(...args: unknown[]): unknown
deleteRecallMsgForLocal(...args: unknown[]): unknown
addLocalGrayTipMsg(...args: unknown[]): unknown
addLocalJsonGrayTipMsg(...args: unknown[]): unknown
addLocalJsonGrayTipMsgExt(...args: unknown[]): unknown
IsLocalJsonTipValid(...args: unknown[]): unknown
addLocalAVRecordMsg(...args: unknown[]): unknown
addLocalTofuRecordMsg(...args: unknown[]): unknown
addLocalRecordMsg(Peer: Peer, msgId: string, ele: MessageElement, attr: Array<any> | number, front: boolean): Promise<unknown>
deleteMsg(Peer: Peer, msgIds: Array<string>): Promise<any>
updateElementExtBufForUI(...args: unknown[]): unknown
updateMsgRecordExtPbBufForUI(...args: unknown[]): unknown
startMsgSync(...args: unknown[]): unknown
startGuildMsgSync(...args: unknown[]): unknown
isGuildChannelSync(...args: unknown[]): unknown
getMsgUniqueId(UniqueId: string): string
isMsgMatched(...args: unknown[]): unknown
getOnlineFileMsgs(...args: unknown[]): unknown
getAllOnlineFileMsgs(...args: unknown[]): unknown
getLatestDbMsgs(peer: Peer, cnt: number): Promise<unknown>
getLastMessageList(peer: Peer[]): Promise<unknown>
getAioFirstViewLatestMsgs(peer: Peer, num: number): unknown
//deprecated 从9.9.15-26702版本开始该接口已经废弃请使用getMsgsEx
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
// this.$peer = contact
// this.$msgTime = j2
// this.$clientSeq = j3
// this.$cnt = i2
getMsgsWithMsgTimeAndClientSeqForC2C(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsWithStatus(params: {
peer: Peer
msgId: string
msgTime: unknown
cnt: unknown
queryOrder: boolean
isIncludeSelf: boolean
appid: unknown
}): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqRange(peer: Peer, startSeq: string, endSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsByMsgId(peer: Peer, ids: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getRecallMsgsByMsgId(peer: Peer, MsgId: string[]): Promise<unknown>
getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSourceOfReplyMsg(peer: Peer, MsgId: string, SourceSeq: string): unknown
getSourceOfReplyMsgV2(peer: Peer, RootMsgId: string, ReplyMsgId: string): unknown
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
//cnt clientSeq?并不是吧
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown
queryMsgsWithFilter(...args: unknown[]): unknown
/**
* @deprecated 该函数已被标记为废弃,请使用新的替代方法。
* 使用过滤条件查询消息列表的版本2接口。
*
* 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
* 函数返回一个Promise解析为查询结果的未知类型对象。
*
* @param MsgId 消息ID用于特定消息的查询。
* @param MsgTime 消息时间,用于指定消息的时间范围。
* @param param 查询参数对象,包含详细的过滤条件和分页信息。
* @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
* @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
* @param param.filterSendersUid 需要过滤的发送者用户ID数组。
* @param param.filterMsgFromTime 查询消息的起始时间。
* @param param.filterMsgToTime 查询消息的结束时间。
* @param param.pageLimit 每页的消息数量限制。
* @param param.isReverseOrder 是否按时间顺序倒序返回消息。
* @param param.isIncludeCurrent 是否包含当前页码。
* @returns 返回一个Promise解析为查询结果的未知类型对象。
*/
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>
// this.chatType = i2
// this.peerUid = str
// this.chatInfo = new ChatInfo()
// this.filterMsgType = new ArrayList<>()
// this.filterSendersUid = new ArrayList<>()
// this.chatInfo = chatInfo
// this.filterMsgType = arrayList
// this.filterSendersUid = arrayList2
// this.filterMsgFromTime = j2
// this.filterMsgToTime = j3
// this.pageLimit = i2
// this.isReverseOrder = z
// this.isIncludeCurrent = z2
//queryMsgsWithFilterEx(0L, 0L, 0L, new QueryMsgsParams(new ChatInfo(2, str), new ArrayList(), new ArrayList(), 0L, 0L, 250, false, true))
queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
//queryMsgsWithFilterEx(this.$msgId, this.$msgTime, this.$msgSeq, this.$param)
queryFileMsgsDesktop(...args: unknown[]): unknown
setMsgRichInfoFlag(...args: unknown[]): unknown
queryPicOrVideoMsgs(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<unknown>
queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown
queryEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryTroopEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryMsgsAndAbstractsWithFilter(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): unknown
setFocusOnGuild(...args: unknown[]): unknown
setFocusSession(...args: unknown[]): unknown
enableFilterUnreadInfoNotify(...args: unknown[]): unknown
enableFilterMsgAbstractNotify(...args: unknown[]): unknown
onScenesChangeForSilenceMode(...args: unknown[]): unknown
getContactUnreadCnt(...args: unknown[]): unknown
getUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntTabInfo(...args: unknown[]): unknown
getAllGuildUnreadCntInfo(...args: unknown[]): unknown
getAllJoinGuildCnt(...args: unknown[]): unknown
getAllDirectSessionUnreadCntInfo(...args: unknown[]): unknown
getCategoryUnreadCntInfo(...args: unknown[]): unknown
getGuildFeedsUnreadCntInfo(...args: unknown[]): unknown
setUnVisibleChannelCntInfo(...args: unknown[]): unknown
setUnVisibleChannelTypeCntInfo(...args: unknown[]): unknown
setVisibleGuildCntInfo(...args: unknown[]): unknown
setMsgRead(peer: Peer): Promise<GeneralCallResult>
setAllC2CAndGroupMsgRead(): Promise<unknown>
setGuildMsgRead(...args: unknown[]): unknown
setAllGuildMsgRead(...args: unknown[]): unknown
setMsgReadAndReport(...args: unknown[]): unknown
setSpecificMsgReadAndReport(...args: unknown[]): unknown
setLocalMsgRead(...args: unknown[]): unknown
setGroupGuildMsgRead(...args: unknown[]): unknown
getGuildGroupTransData(...args: unknown[]): unknown
setGroupGuildBubbleRead(...args: unknown[]): unknown
getGuildGroupBubble(...args: unknown[]): unknown
fetchGroupGuildUnread(...args: unknown[]): unknown
setGroupGuildFlag(...args: unknown[]): unknown
setGuildUDCFlag(...args: unknown[]): unknown
setGuildTabUserFlag(...args: unknown[]): unknown
setBuildMode(flag: number/*0 1 3*/): unknown
setConfigurationServiceData(...args: unknown[]): unknown
setMarkUnreadFlag(...args: unknown[]): unknown
getChannelEventFlow(...args: unknown[]): unknown
getMsgEventFlow(...args: unknown[]): unknown
getRichMediaFilePathForMobileQQSend(...args: unknown[]): unknown
getRichMediaFilePathForGuild(arg: {
md5HexStr: string,
fileName: string,
elementType: ElementType,
elementSubType: number,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
}): string
assembleMobileQQRichMediaFilePath(...args: unknown[]): unknown
getFileThumbSavePathForSend(...args: unknown[]): unknown
getFileThumbSavePath(...args: unknown[]): unknown
//猜测居多
translatePtt2Text(MsgId: string, Peer: {}, MsgElement: {}): unknown
setPttPlayedState(...args: unknown[]): unknown
// NodeIQQNTWrapperSession fetchFavEmojiList [
// "",
// 48,
// true,
// true
// ]
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: Array<{
uin: string,
emoId: number,
emoPath: string,
isExist: boolean,
resId: string,
url: string,
md5: string,
emoOriginalPath: string,
thumbPath: string,
RomaingType: string,
isAPNG: false,
isMarkFace: false,
eId: string,
epId: string,
ocrWord: string,
modifyWord: string,
exposeNum: number,
clickNum: number,
desc: string
}>
}>
addFavEmoji(...args: unknown[]): unknown
fetchMarketEmoticonList(...args: unknown[]): unknown
fetchMarketEmoticonShowImage(...args: unknown[]): unknown
fetchMarketEmoticonAioImage(...args: unknown[]): unknown
fetchMarketEmotionJsonFile(...args: unknown[]): unknown
getMarketEmoticonPath(...args: unknown[]): unknown
getMarketEmoticonPathBySync(...args: unknown[]): unknown
fetchMarketEmoticonFaceImages(...args: unknown[]): unknown
fetchMarketEmoticonAuthDetail(...args: unknown[]): unknown
getFavMarketEmoticonInfo(...args: unknown[]): unknown
addRecentUsedFace(...args: unknown[]): unknown
getRecentUsedFaceList(...args: unknown[]): unknown
getMarketEmoticonEncryptKeys(...args: unknown[]): unknown
downloadEmojiPic(...args: unknown[]): unknown
deleteFavEmoji(...args: unknown[]): unknown
modifyFavEmojiDesc(...args: unknown[]): unknown
queryFavEmojiByDesc(...args: unknown[]): unknown
getHotPicInfoListSearchString(...args: unknown[]): unknown
getHotPicSearchResult(...args: unknown[]): unknown
getHotPicHotWords(...args: unknown[]): unknown
getHotPicJumpInfo(...args: unknown[]): unknown
getEmojiResourcePath(...args: unknown[]): unknown
JoinDragonGroupEmoji(JoinDragonGroupEmojiReq: any/*joinDragonGroupEmojiReq*/): unknown
getMsgAbstracts(...args: unknown[]): unknown
getMsgAbstract(...args: unknown[]): unknown
getMsgAbstractList(...args: unknown[]): unknown
getMsgAbstractListBySeqRange(...args: unknown[]): unknown
refreshMsgAbstracts(...args: unknown[]): unknown
refreshMsgAbstractsByGuildIds(...args: unknown[]): unknown
getRichMediaElement(...args: unknown[]): unknown
cancelGetRichMediaElement(...args: unknown[]): unknown
refuseGetRichMediaElement(...args: unknown[]): unknown
switchToOfflineGetRichMediaElement(...args: unknown[]): unknown
downloadRichMedia(...args: unknown[]): unknown
getFirstUnreadMsgSeq(args: {
peerUid: string
guildId: string
}): unknown
getFirstUnreadCommonMsg(...args: unknown[]): unknown
getFirstUnreadAtmeMsg(...args: unknown[]): unknown
getFirstUnreadAtallMsg(...args: unknown[]): unknown
getNavigateInfo(...args: unknown[]): unknown
getChannelFreqLimitInfo(...args: unknown[]): unknown
getRecentUseEmojiList(...args: unknown[]): unknown
getRecentEmojiList(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number,
errMsg: string,
emojiLikesList:
Array<{
tinyId: string,
nickName: string,
headUrl: string
}>,
cookie: string,
isLastPage: boolean,
isFirstPage: boolean
}>
setMsgEmojiLikesForRole(...args: unknown[]): unknown
clickInlineKeyboardButton(...args: unknown[]): unknown
setCurOnScreenMsg(...args: unknown[]): unknown
setCurOnScreenMsgForMsgEvent(...args: unknown[]): unknown
getMiscData(key: string): unknown
setMiscData(key: string, value: string): unknown
getBookmarkData(...args: unknown[]): unknown
setBookmarkData(...args: unknown[]): unknown
sendShowInputStatusReq(ChatType: number, EventType: number, toUid: string): Promise<unknown>
queryCalendar(...args: unknown[]): unknown
queryFirstMsgSeq(peer: Peer, ...args: unknown[]): unknown
queryRoamCalendar(...args: unknown[]): unknown
queryFirstRoamMsg(...args: unknown[]): unknown
fetchLongMsg(peer: Peer, msgId: string): unknown
fetchLongMsgWithCb(...args: unknown[]): unknown
setIsStopKernelFetchLongMsg(...args: unknown[]): unknown
insertGameResultAsMsgToDb(...args: unknown[]): unknown
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
setDraft(...args: unknown[]): unknown
getDraft(...args: unknown[]): unknown
deleteDraft(...args: unknown[]): unknown
getRecentHiddenSesionList(...args: unknown[]): unknown
setRecentHiddenSession(...args: unknown[]): unknown
delRecentHiddenSession(...args: unknown[]): unknown
getCurHiddenSession(...args: unknown[]): unknown
setCurHiddenSession(...args: unknown[]): unknown
setReplyDraft(...args: unknown[]): unknown
getReplyDraft(...args: unknown[]): unknown
deleteReplyDraft(...args: unknown[]): unknown
getFirstUnreadAtMsg(peer: Peer): unknown
clearMsgRecords(...args: unknown[]): unknown//设置已读后调用我觉得比较好 清理记录 现在别了
IsExistOldDb(...args: unknown[]): unknown
canImportOldDbMsg(...args: unknown[]): unknown
setPowerStatus(z: boolean): unknown
canProcessDataMigration(...args: unknown[]): unknown
importOldDbMsg(...args: unknown[]): unknown
stopImportOldDbMsgAndroid(...args: unknown[]): unknown
isMqqDataImportFinished(...args: unknown[]): unknown
getMqqDataImportTableNames(...args: unknown[]): unknown
getCurChatImportStatusByUin(...args: unknown[]): unknown
getDataImportUserLevel(): unknown
getMsgQRCode(...args: unknown[]): unknown
getGuestMsgAbstracts(...args: unknown[]): unknown
getGuestMsgByRange(...args: unknown[]): unknown
getGuestMsgAbstractByRange(...args: unknown[]): unknown
registerSysMsgNotification(...args: unknown[]): unknown
unregisterSysMsgNotification(...args: unknown[]): unknown
enterOrExitAio(...args: unknown[]): unknown
// this.peerUid = ""
// this.peerNickname = ""
// this.fromGroupCode = ""
// this.sig = new byte[0]
// this.selfUid = ""
// this.selfPhone = ""
// this.chatType = i2
// this.peerUid = str
// this.peerNickname = str2
// this.fromGroupCode = str3
// this.sig = bArr
// this.selfUid = str4
// this.selfPhone = str5
// this.gameSession = tempChatGameSession
prepareTempChat(args: unknown): unknown//主动临时消息 不做
sendSsoCmdReqByContend(cmd: string, param: string): Promise<unknown>
//chattype,uid->Promise<any>
getTempChatInfo(ChatType: number, Uid: string): Promise<TmpChatInfoApi>
setContactLocalTop(...args: unknown[]): unknown
switchAnonymousChat(...args: unknown[]): unknown
renameAnonyChatNick(...args: unknown[]): unknown
getAnonymousInfo(...args: unknown[]): unknown
updateAnonymousInfo(...args: unknown[]): unknown
sendSummonMsg(peer: Peer, MsgElement: unknown, MsgAttributeInfo: unknown): Promise<unknown>//频道的东西
outputGuildUnreadInfo(...args: unknown[]): unknown
checkMsgWithUrl(...args: unknown[]): unknown
checkTabListStatus(...args: unknown[]): unknown
getABatchOfContactMsgBoxInfo(...args: unknown[]): unknown
insertMsgToMsgBox(peer: Peer, msgId: string, arg: 2006): unknown
isHitEmojiKeyword(...args: unknown[]): unknown
getKeyWordRelatedEmoji(...args: unknown[]): unknown
recordEmoji(...args: unknown[]): unknown
fetchGetHitEmotionsByWord(args: Object): Promise<unknown>//表情推荐?
deleteAllRoamMsgs(...args: unknown[]): unknown//漫游消息?
packRedBag(...args: unknown[]): unknown
grabRedBag(...args: unknown[]): unknown
pullDetail(...args: unknown[]): unknown
selectPasswordRedBag(...args: unknown[]): unknown
pullRedBagPasswordList(...args: unknown[]): unknown
requestTianshuAdv(...args: unknown[]): unknown
tianshuReport(...args: unknown[]): unknown
tianshuMultiReport(...args: unknown[]): unknown
GetMsgSubType(a0: number, a1: number): unknown
setIKernelPublicAccountAdapter(...args: unknown[]): unknown
//tempChatGameSession有关
createUidFromTinyId(fromTinyId: string, toTinyId: string): unknown
dataMigrationGetDataAvaiableContactList(...args: unknown[]): unknown
dataMigrationGetMsgList(...args: unknown[]): unknown
dataMigrationStopOperation(...args: unknown[]): unknown
//新的希望
dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{
extensionData: string//"Hex"
extraData: string //""
chatType: number
chatUin: string
msgType: number
msgTime: string
msgSeq: string
msgRandom: string
}>, DataMigrationResourceInfo: {
extraData: string
filePath: string
fileSize: string
msgRandom: string
msgSeq: string
msgSubType: number
msgType: number
}): unknown
dataMigrationGetResourceLocalDestinyPath(...args: unknown[]): unknown
dataMigrationSetIOSPathPrefix(...args: unknown[]): unknown
getServiceAssistantSwitch(...args: unknown[]): unknown
setServiceAssistantSwitch(...args: unknown[]): unknown
setSubscribeFolderUsingSmallRedPoint(...args: unknown[]): unknown
clearGuildNoticeRedPoint(...args: unknown[]): unknown
clearFeedNoticeRedPoint(...args: unknown[]): unknown
clearFeedSquareRead(...args: unknown[]): unknown
IsC2CStyleChatType(...args: unknown[]): unknown
IsTempChatType(uin: number): unknown//猜的
getGuildInteractiveNotification(...args: unknown[]): unknown
getGuildNotificationAbstract(...args: unknown[]): unknown
setFocusOnBase(...args: unknown[]): unknown
queryArkInfo(...args: unknown[]): unknown
queryUserSecQuality(...args: unknown[]): unknown
getGuildMsgAbFlag(...args: unknown[]): unknown
getGroupMsgStorageTime(): unknown//这是嘛啊
}

View File

@@ -0,0 +1,22 @@
import { BuddyProfileLikeReq } from '../types'
import { GeneralCallResult } from './common'
export interface NodeIKernelProfileLikeService {
addKernelProfileLikeListener(listener: NodeIKernelProfileLikeService): void
removeKernelProfileLikeListener(listener: unknown): void
setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number }
getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
'info': {
'userLikeInfos': Array<any>,
'friendMaxVotes': number,
'start': number
}
}>
getProfileLikeScidResourceInfo(...args: unknown[]): void
isNull(): boolean
}

View File

@@ -0,0 +1,106 @@
import { AnyCnameRecord } from 'node:dns'
import { SimpleInfo } from '../types'
import { GeneralCallResult } from './common'
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}
export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string,string>>//uin->uid
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string,string>>
// {
// coreInfo: CoreInfo,
// baseInfo: BaseInfo,
// status: null,
// vasInfo: null,
// relationFlags: null,
// otherFlags: null,
// intimate: null
// }
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>
addKernelProfileListener(listener: any): number
removeKernelProfileListener(listenerId: number): void
prepareRegionConfig(...args: unknown[]): unknown
getLocalStrangerRemark(): Promise<AnyCnameRecord>
enumCountryOptions(): Array<string>
enumProvinceOptions(Country: string): Array<string>
enumCityOptions(Country: string, Province: string): unknown
enumAreaOptions(...args: unknown[]): unknown
//SimpleInfo
// this.uid = ""
// this.uid = str
// this.uin = j2
// this.isBuddy = z
// this.coreInfo = coreInfo
// this.baseInfo = baseInfo
// this.status = statusInfo
// this.vasInfo = vasInfo
// this.relationFlags = relationFlag
// this.otherFlags = otherFlag
// this.intimate = intimate
modifySelfProfile(...args: unknown[]): Promise<unknown>
modifyDesktopMiniProfile(param: any): Promise<GeneralCallResult>
setNickName(NickName: string): Promise<unknown>
setLongNick(longNick: string): Promise<unknown>
setBirthday(...args: unknown[]): Promise<unknown>
setGander(...args: unknown[]): Promise<unknown>
setHeader(arg: string): Promise<unknown>
setRecommendImgFlag(...args: unknown[]): Promise<unknown>
getUserSimpleInfo(force: boolean, uids: string[],): Promise<unknown>
getUserDetailInfo(uid: string): Promise<unknown>
getUserDetailInfoWithBizInfo(uid: string, Biz: any[]): Promise<GeneralCallResult>
getUserDetailInfoByUin(uin: string): Promise<any>
getZplanAvatarInfos(args: string[]): Promise<unknown>
getStatus(uid: string): Promise<unknown>
startStatusPolling(isForceReset: boolean): Promise<unknown>
getSelfStatus(): Promise<unknown>
setdisableEmojiShortCuts(...args: unknown[]): unknown
getProfileQzonePicInfo(uid: string, type: number, force: boolean): Promise<unknown>
//profileService.getCoreInfo("UserRemarkServiceImpl::getStrangerRemarkByUid", arrayList)
getCoreInfo(name: string, arg: any[]): unknown
//m429253e12.getOtherFlag("FriendListInfoCache_getKernelDataAndPutCache", new ArrayList<>())
isNull(): boolean
}

View File

@@ -0,0 +1,270 @@
import { GetFileListParam, MessageElement, Peer } from '../types'
import { GeneralCallResult } from './common'
export enum UrlFileDownloadType {
KUNKNOWN,
KURLFILEDOWNLOADPRIVILEGEICON,
KURLFILEDOWNLOADPHOTOWALL,
KURLFILEDOWNLOADQZONE,
KURLFILEDOWNLOADCOMMON,
KURLFILEDOWNLOADINSTALLAPP
}
export enum RMBizTypeEnum {
KUNKNOWN,
KC2CFILE,
KGROUPFILE,
KC2CPIC,
KGROUPPIC,
KDISCPIC,
KC2CVIDEO,
KGROUPVIDEO,
KC2CPTT,
KGROUPPTT,
KFEEDCOMMENTPIC,
KGUILDFILE,
KGUILDPIC,
KGUILDPTT,
KGUILDVIDEO
}
export interface CommonFileInfo {
bizType: number
chatType: number
elemId: string
favId: string
fileModelId: string
fileName: string
fileSize: string
md5: string
md510m: string
msgId: string
msgTime: string
parent: string
peerUid: string
picThumbPath: Array<string>
sha: string
sha3: string
subId: string
uuid: string
}
export interface NodeIKernelRichMediaService {
//getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb)
// public enum VideoCodecFormatType {
// KCODECFORMATH264,
// KCODECFORMATH265,
// KCODECFORMATH266,
// KCODECFORMATAV1
// }
// public enum VideoRequestWay {
// KUNKNOW,
// KHAND,
// KAUTO
// }
getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise<unknown>
//exParams (RMReqExParams)
// this.downSourceType = i2
// this.triggerType = i3
//peer, msgId, elemId, videoCodecFormat, exParams
// 1 0 频道在用
// 1 1
// 0 2
// public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007
// public static final int KDOWNSOURCETYPEAIOINNER = 1
// public static final int KDOWNSOURCETYPEBIGSCREEN = 2
// public static final int KDOWNSOURCETYPEHISTORY = 3
// public static final int KDOWNSOURCETYPEUNKNOWN = 0
// public static final int KTRIGGERTYPEAUTO = 1
// public static final int KTRIGGERTYPEMANUAL = 0
getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & {
urlResult: {
v4IpUrl: [],
v6IpUrl: [],
domainUrl: Array<{
url: string,
isHttps: boolean,
httpsDomain: string
}>,
videoCodecFormat: number
}
}>
getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown
// this.senderUid = ""
// this.peerUid = ""
// this.guildId = ""
// this.elem = new MsgElement()
// this.downloadType = i2
// this.thumbSize = i3
// this.msgId = j2
// this.msgRandom = j3
// this.msgSeq = j4
// this.msgTime = j5
// this.chatType = i4
// this.senderUid = str
// this.peerUid = str2
// this.guildId = str3
// this.elem = msgElement
// this.useHttps = num
getVideoPlayUrlInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): Promise<unknown>
//arg双端number
isFileExpired(arg: number): unknown
deleteGroupFolder(GroupCode: string, FolderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>
//参数与getVideoPlayUrlInVisit一样
downloadRichMediaInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): unknown
//arg3为“”
downloadFileForModelId(peer: Peer, ModelId: string[], arg3: string): unknown
//第三个参数 Array<Type>
// this.fileId = ""
// this.fileName = ""
// this.fileId = str
// this.fileName = str2
// this.fileSize = j2
// this.fileModelId = j3
downloadFileForFileUuid(peer: Peer, uuid: string, arg3: {
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}[]): Promise<unknown>
downloadFileByUrlList(fileDownloadTyp: UrlFileDownloadType, urlList: Array<string>): unknown
downloadFileForFileInfo(fileInfo: CommonFileInfo[], savePath: string): unknown
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }>
downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown
createGroupFolder(arg1: unknown, arg2: unknown): unknown
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
deleteGroupFolder(arg1: unknown, arg2: unknown): unknown
deleteTransferInfo(arg1: unknown, arg2: unknown): unknown
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown
cancelUrlDownload(arg: unknown): unknown
updateOnlineVideoElemStatus(arg: unknown): unknown
getGroupSpace(arg: unknown): unknown
getGroupFileList(groupCode: string, params: GetFileListParam): Promise<GeneralCallResult & {
groupSpaceResult: {
retCode: number
retMsg: string
clientWording: string
totalSpace: number
usedSpace: number
allUpload: boolean
}
}>
getGroupFileInfo(arg1: unknown, arg2: unknown): unknown
getGroupTransferList(arg1: unknown, arg2: unknown): unknown
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
transGroupFile(arg1: unknown, arg2: unknown): unknown
searchGroupFile(
keywords: Array<string>,
param: {
groupIds: Array<string>,
fileType: number,
context: string,
count: number,
sortType: number,
groupNames: Array<string>
}): Promise<unknown>
searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & {
transGroupFileResult: {
result: any
successFileIdList: Array<any>
failFileIdList: Array<any>
}
}>
translateEnWordToZn(words: string[]): Promise<GeneralCallResult & { words: string[] }>
getScreenOCR(path: string): Promise<unknown>
batchGetGroupFileCount(Gids: Array<string>): Promise<GeneralCallResult & { groupCodes: Array<string>, groupFileCounts: Array<number> }>
queryPicDownloadSize(arg: unknown): unknown
searchGroupFile(arg1: unknown, arg2: unknown): unknown
searchMoreGroupFile(arg: unknown): unknown
cancelSearcheGroupFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown
onlyDownloadFile(peer: Peer, arg2: unknown, arg3: Array<{
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}
>): unknown
onlyUploadFile(arg1: unknown, arg2: unknown): unknown
isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown
uploadRMFileWithoutMsg(arg: {
bizType: RMBizTypeEnum,
filePath: string,
peerUid: string,
transferId: string
useNTV2: string
}): Promise<unknown>
isNull(): boolean
}

View File

@@ -0,0 +1,128 @@
import { ChatType } from '../types'
export interface NodeIKernelSearchService {
addKernelSearchListener(...args: any[]): unknown// needs 1 arguments
removeKernelSearchListener(...args: any[]): unknown// needs 1 arguments
searchStranger(...args: any[]): unknown// needs 3 arguments
searchGroup(...args: any[]): unknown// needs 1 arguments
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown
cancelSearchLocalInfo(...args: any[]): unknown// needs 3 arguments
searchBuddyChatInfo(...args: any[]): unknown// needs 2 arguments
searchMoreBuddyChatInfo(...args: any[]): unknown// needs 1 arguments
cancelSearchBuddyChatInfo(...args: any[]): unknown// needs 3 arguments
searchContact(...args: any[]): unknown// needs 2 arguments
searchMoreContact(...args: any[]): unknown// needs 1 arguments
cancelSearchContact(...args: any[]): unknown// needs 3 arguments
searchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoSortType(...args: any[]): unknown// needs 3 arguments
resetSearchGroupChatInfoFilterMembers(...args: any[]): unknown// needs 3 arguments
searchMoreGroupChatInfo(...args: any[]): unknown// needs 1 arguments
cancelSearchGroupChatInfo(...args: any[]): unknown// needs 3 arguments
searchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
searchMoreChatsWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchChatsWithKeywords(...args: any[]): unknown// needs 3 arguments
searchChatMsgs(...args: any[]): unknown// needs 2 arguments
searchMoreChatMsgs(...args: any[]): unknown// needs 1 arguments
cancelSearchChatMsgs(...args: any[]): unknown// needs 3 arguments
searchMsgWithKeywords(...args: any[]): unknown// needs 2 arguments
searchMoreMsgWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchMsgWithKeywords(...args: any[]): unknown// needs 3 arguments
searchFileWithKeywords(keywords: string[], source: number): Promise<string>// needs 2 arguments
searchMoreFileWithKeywords(...args: any[]): unknown// needs 1 arguments
cancelSearchFileWithKeywords(...args: any[]): unknown// needs 3 arguments
searchAtMeChats(...args: any[]): unknown// needs 3 arguments
searchMoreAtMeChats(...args: any[]): unknown// needs 1 arguments
cancelSearchAtMeChats(...args: any[]): unknown// needs 3 arguments
searchChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
searchMoreChatAtMeMsgs(...args: any[]): unknown// needs 1 arguments
cancelSearchChatAtMeMsgs(...args: any[]): unknown// needs 3 arguments
addSearchHistory(param: {
type: number,//4
contactList: [],
id: number,//-1
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: ChatType,
buddyChatInfo: Array<{ category_name: string, peerUid: string, peerUin: string, remark: string }>,
discussChatInfo: [],
groupChatInfo: Array<
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}>,
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: string,//3
fileSize: string,
filePath: string,
fileName: string,
hits: Array<
{
start: 12,
end: 14
}
>
}
]
}): Promise<{
result: number,
errMsg: string,
id?: number
}>
removeSearchHistory(...args: any[]): unknown// needs 1 arguments
searchCache(...args: any[]): unknown// needs 3 arguments
clearSearchCache(...args: any[]): unknown// needs 1 arguments
}

View File

@@ -0,0 +1,11 @@
import { forceFetchClientKeyRetType } from './common'
export interface NodeIKernelTicketService {
addKernelTicketListener(listener: unknown): void
removeKernelTicketListener(listenerId: unknown): void
forceFetchClientKey(arg: string): Promise<forceFetchClientKeyRetType>
isNull(): boolean
}

View File

@@ -0,0 +1,19 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelTipOffService {
addKernelTipOffListener(listener: unknown): void
removeKernelTipOffListener(listenerId: unknown): void
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
getPskey(domainList: string[], nocache: boolean): Promise<GeneralCallResult & { domainPskeyMap: Map<string, string> }> //2
tipOffSendJsData(args: unknown[]): Promise<unknown> //2
tipOffMsgs(args: unknown[]): Promise<unknown> //1
encodeUinAesInfo(args: unknown[]): Promise<unknown> //2
isNull(): boolean
}

View File

@@ -0,0 +1,5 @@
export interface NodeIKernelUixConvertService {
getUin(uid: string[]): Promise<{ uinInfo: Map<string, string> }>
getUid(uin: string[]): Promise<{ uidInfo: Map<string, string> }>
}

View File

@@ -0,0 +1,16 @@
export enum GeneralCallResultStatus {
OK = 0
// ERROR = 1
}
export interface GeneralCallResult {
result: GeneralCallResultStatus
errMsg: string
}
export interface forceFetchClientKeyRetType extends GeneralCallResult {
url: string
keyIndex: string
clientKey: string
expireTime: string
}

View File

@@ -0,0 +1,11 @@
export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService'
export * from './NodeIKernelGroupService'
export * from './NodeIKernelProfileLikeService'
export * from './NodeIKernelMsgService'
export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService'
export * from './NodeIKernelSearchService'

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,56 +1,63 @@
import {QQLevel, Sex} from "./user"; import { QQLevel, Sex } from './user'
export enum GroupListUpdateType {
REFRESHALL,
GETALL,
MODIFIED,
REMOVE
}
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 {
memberSpecialTitle: string; memberSpecialTitle: string
avatarPath: string; avatarPath: string
cardName: string; cardName: string
cardType: number; cardType: number
isDelete: boolean; isDelete: boolean
nick: string; nick: string
qid: string; qid: string
remark: string; remark: string
role: GroupMemberRole; // 群主:4, 管理员:3群员:2 role: GroupMemberRole // 群主:4, 管理员:3群员:2
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚 shutUpTime: number // 禁言时间,单位是什么暂时不清楚
uid: string; // 加密的字符串 uid: string // 加密的字符串
uin: string; // QQ号 uin: string // QQ号
isRobot: boolean; isRobot: boolean
sex?: Sex sex?: Sex
qqLevel?: QQLevel 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,410 +1,540 @@
import {GroupMemberRole} from "./group"; import { GroupMemberRole } from './group'
import exp from "constants";
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
}
export enum ElementType { export enum ElementType {
TEXT = 1, UNKNOWN = 0,
PIC = 2, TEXT = 1,
FILE = 3, PIC = 2,
PTT = 4, FILE = 3,
VIDEO = 5, PTT = 4,
FACE = 6, VIDEO = 5,
REPLY = 7, FACE = 6,
ARK = 10, REPLY = 7,
WALLET = 9,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10,
MFACE = 11,
LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
} }
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT, elementType: ElementType.TEXT
elementId: "", elementId: ''
textElement: { textElement: TextElement
content: string,
atType: number,
atUid: string,
atTinyId: 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 { export enum PicSubType {
normal = 0, // 普通图片,大图 normal = 0, // 普通图片,大图
face = 1 // 表情包小图 face = 1, // 表情包小图
} }
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC, elementType: ElementType.PIC
elementId: "", elementId: ''
picElement: { picElement: {
md5HexStr: string, md5HexStr: string
fileSize: number | string, 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: PicSubType, 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: ReplyElement
replayMsgSeq: string,
replayMsgId: string,
senderUin: 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 TextElement {
content: string
atType: number
atUid: string
atTinyId: string
atNtUid: string
}
export interface ReplyElement {
replayMsgSeq: string
replayMsgId: string
senderUin: string
senderUinStr: string
} }
export interface FileElement { export interface FileElement {
"fileMd5"?: "", fileMd5?: string
"fileName": string, fileName: string
"filePath": string, filePath: string
"fileSize": string, fileSize: string
"picHeight"?: number, picHeight?: number
"picWidth"?: number, picWidth?: number
"picThumbPath"?: {}, folderId?: string
"file10MMd5"?: "", picThumbPath?: Map<number, string>
"fileSha"?: "", file10MMd5?: string
"fileSha3"?: "", fileSha?: string
"fileUuid"?: "", fileSha3?: string
"fileSubId"?: "", fileUuid?: string
"thumbFileSize"?: number, fileSubId?: string
fileBizId?: 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,
}
// 来自Android分析
export enum ChatType2 {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
} }
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 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/, 带download参数的是https://multimedia.nt.qq.com.cn picSubType: PicSubType
originImageMd5?: string; picType: PicType // 有这玩意儿吗
sourcePath: string; // 图片本地路径 originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
thumbPath: Map<number, string>; originImageMd5?: string
picWidth: number; sourcePath: string // 图片本地路径
picHeight: number; thumbPath: Map<number, string>
fileSize: number; picWidth: number
fileName: string; picHeight: number
fileUuid: string; fileSize: number
md5HexStr?: string; fileName: string
fileUuid: string
md5HexStr?: string
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
INVITE_NEW_MEMBER = 12, RECALL = 1,
MEMBER_NEW_TITLE = 17 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: {
jsonGrayTipElement: { templId: string
jsonStr: 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 { export interface MarketFaceElement {
"itemType": 6, emojiPackageId: number
"faceInfo": 1, faceName?: string
"emojiPackageId": 203875, emojiId: string
"subType": 3, key: string
"mediaType": 0,
"imageWidth": 200,
"imageHeight": 200,
"faceName": string,
"emojiId": "094d53bd1c9ac5d35d04b08e8a6c992c",
"key": "a8b1dd0aebc8d910",
"param": null,
"mobileParam": null,
"sourceType": null,
"startTime": null,
"endTime": null,
"emojiType": 1,
"hasIpProduct": null,
"voiceItemHeightArr": null,
"sourceName": null,
"sourceJumpUrl": null,
"sourceTypeName": null,
"backColor": null,
"volumeColor": null,
"staticFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c_aio.png",
"dynamicFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c",
"supportSize": [
{
"width": 300,
"height": 300
},
{
"width": 200,
"height": 200
}
],
"apngSupportSize": null
} }
export interface VideoElement { export interface VideoElement {
"filePath": string, filePath: string
"fileName": string, fileName: string
"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 MarkdownElement { export interface MarkdownElement {
content: string, content: string
} }
export interface InlineKeyboardElementRowButton{ export interface InlineKeyboardElementRowButton {
"id": "", id: ''
"label": string, label: string
"visitedLabel": string, visitedLabel: string
"style": 1, // 未知 style: 1 // 未知
"type": 2, // 未知 type: 2 // 未知
"clickLimit": 0, // 未知 clickLimit: 0 // 未知
"unsupportTips": "请升级新版手机QQ", unsupportTips: '请升级新版手机QQ'
"data": string, data: string
"atBotShowChannelList": false, atBotShowChannelList: false
"permissionType": 2, permissionType: 2
"specifyRoleIds": [], specifyRoleIds: []
"specifyTinyids": [], specifyTinyids: []
"isReply": false, isReply: false
"anchor": 0, anchor: 0
"enter": false, enter: false
"subscribeDataTemplateIds": [] subscribeDataTemplateIds: []
} }
export interface InlineKeyboardElement { export interface InlineKeyboardElement {
rows: [{ rows: [
buttons: InlineKeyboardElementRowButton[] {
}] buttons: InlineKeyboardElementRowButton[]
},
]
} }
export interface TipAioOpGrayTipElement { // 这是什么提示来着? export interface TipAioOpGrayTipElement {
operateType: number, // 这是什么提示来着?
peerUid: string, operateType: number
fromGrpCodeOfTmpChat: string, peerUid: string
fromGrpCodeOfTmpChat: string
} }
export enum TipGroupElementType { export enum TipGroupElementType {
memberIncrease = 1, memberIncrease = 1,
kicked = 3, // 被移出群 kicked = 3, // 被移出群
ban = 8 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, 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{ export interface MultiForwardMsgElement {
xmlContent: string, // xml格式的消息内容 xmlContent: string // xml格式的消息内容
resId: string, resId: string
fileName: 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 msgRandom: string
peerUin: string; // 群号 或者 发送者QQ号 senderUid: string
sendNickName: string; senderUin?: string // 发送者QQ号
sendMemberName?: string; // 发送者群名片 peerUid: string // 群号 或者 QQ uid
chatType: ChatType; peerUin: string // 群号 或者 发送者QQ号
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送 guildId: string
recallTime: string; // 撤回时间, "0"是没有撤回 sendNickName: string
elements: { sendMemberName?: string // 发送者群名片
elementId: string, chatType: ChatType
elementType: ElementType; sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
replyElement: { recallTime: string // 撤回时间, "0"是没有撤回
senderUid: string; // 原消息发送者QQ号 records: RawMessage[]
sourceMsgIsIncPic: boolean; // 原消息是否有图片 elements: {
sourceMsgText: string; elementId: string
replayMsgSeq: string; // 源消息的msgSeq可以通过这个找到源消息的msgId elementType: ElementType
}; replyElement: {
textElement: { sourceMsgIdInRecords: string
atType: AtType; senderUid: string // 原消息发送者QQ号
atUid: string; // QQ号 sourceMsgIsIncPic: boolean // 原消息是否有图片
content: string; sourceMsgText: string
atNtUid: string; // uid号 replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
}; }
picElement: PicElement; textElement: {
pttElement: PttElement; atType: AtType
arkElement: ArkElement; atUid: string // QQ号
grayTipElement: GrayTipElement; content: string
faceElement: FaceElement; atNtUid: string // uid号
videoElement: VideoElement; }
fileElement: FileElement; picElement: PicElement
marketFaceElement: MarketFaceElement; pttElement: PttElement
inlineKeyboardElement: InlineKeyboardElement; arkElement: ArkElement
markdownElement: MarkdownElement; grayTipElement: GrayTipElement
multiForwardMsgElement: MultiForwardMsgElement; faceElement: FaceElement
}[]; videoElement: VideoElement
fileElement: FileElement
marketFaceElement: MarketFaceElement
inlineKeyboardElement: InlineKeyboardElement
markdownElement: MarkdownElement
multiForwardMsgElement: MultiForwardMsgElement
}[]
}
export interface Peer {
chatType: ChatType
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string
}
export interface MessageElement {
elementType: ElementType
elementId: string
extBufForUI: string //"0x"
textElement?: TextElement
faceElement?: FaceElement
marketFaceElement?: MarkdownElement
replyElement?: ReplyElement
picElement?: PicElement
pttElement?: PttElement
videoElement?: VideoElement
grayTipElement?: GrayTipElement
arkElement?: ArkElement
fileElement?: FileElement
liveGiftElement?: null
markdownElement?: MarkdownElement
structLongMsgElement?: any
multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: any
walletElement?: null
inlineKeyboardElement?: InlineKeyboardElement
textGiftElement?: null //????
calendarElement?: any
yoloGameResultElement?: any
avRecordElement?: any
structMsgElement?: null
faceBubbleElement?: any
shareLocationElement?: any
tofuRecordElement?: any
taskTopMsgElement?: any
recommendedMsgElement?: any
actionBarElement?: any
} }

View File

@@ -1,65 +1,124 @@
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,
KICK_MEMBER = 9, ADMIN_SET = 8,
MEMBER_EXIT = 11, // 主动退出 KICK_MEMBER = 9,
ADMIN_UNSET = 12, 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 enum BuddyReqType {
KMEINITIATOR,
KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
friendUid: string, isInitiator?: boolean
reqTime: string, // 时间戳,秒 isDecide: boolean
extWords: string, // 申请人填写的验证消息 friendUid: string
isUnread: boolean, reqType: BuddyReqType
friendNick: string, reqTime: string // 时间戳,秒
sourceId: number, extWords: string // 申请人填写的验证消息
groupCode: string isUnread: boolean
friendNick: string
sourceId: number
groupCode: string
} }
export interface FriendRequestNotify { export interface FriendRequestNotify {
data: { data: {
unreadNums: number, unreadNums: number
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
}
export enum MemberExtSourceType {
DEFAULTTYPE = 0,
TITLETYPE = 1,
NEWGROUPTYPE = 2,
}
export interface GroupExtParam {
groupCode: string
seq: string
beginUin: string
dataTime: string
uinList: Array<string>
uinNum: string
groupType: string
richCardNameVer: string
sourceType: MemberExtSourceType
memberExtFilter: {
memberLevelInfoUin: number
memberLevelInfoPoint: number
memberLevelInfoActiveDay: number
memberLevelInfoLevel: number
memberLevelInfoName: number
levelName: number
dataTime: number
userShowFlag: number
sysShowFlag: number
timeToUpdate: number
nickName: number
specialTitle: number
levelNameNew: number
userShowFlagNew: number
msgNeedField: number
cmdUinFlagExt3Grocery: number
memberIcon: number
memberInfoSeq: number
}
} }

View File

@@ -1,75 +1,343 @@
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[]
}
export interface CoreInfo {
uid: string
uin: string
nick: string
remark: string
}
export interface BaseInfo {
qid: string
longNick: string
birthday_year: number
birthday_month: number
birthday_day: number
age: number
sex: number
eMail: string
phoneNum: string
categoryId: number
richTime: number
richBuffer: string
}
interface MusicInfo {
buf: string
}
interface VideoBizInfo {
cid: string
tvUrl: string
synchType: string
}
interface VideoInfo {
name: string
}
interface ExtOnlineBusinessInfo {
buf: string
customStatus: any
videoBizInfo: VideoBizInfo
videoInfo: VideoInfo
}
interface ExtBuffer {
buf: string
}
interface UserStatus {
uid: string
uin: string
status: number
extStatus: number
batteryStatus: number
termType: number
netType: number
iconType: number
customStatus: any
setTime: string
specialFlag: number
abiFlag: number
eNetworkType: number
showName: string
termDesc: string
musicInfo: MusicInfo
extOnlineBusinessInfo: ExtOnlineBusinessInfo
extBuffer: ExtBuffer
}
interface PrivilegeIcon {
jumpUrl: string
openIconList: any[]
closeIconList: any[]
}
interface VasInfo {
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean
vipLevel: number
bigClub: boolean
bigClubLevel: number
nameplateVipType: number
grayNameplateFlag: number
superVipTemplateId: number
diyFontId: number
pendantId: number
pendantDiyId: number
faceId: number
vipFont: number
vipFontType: number
magicFont: number
fontEffect: number
newLoverDiamondFlag: number
extendNameplateId: number
diyNameplateIDs: any[]
vipStartFlag: number
vipDataFlag: number
gameNameplateId: string
gameLastLoginTime: string
gameRank: number
gameIconShowFlag: boolean
gameCardId: string
vipNameColorId: string
privilegeIcon: PrivilegeIcon
}
export interface SimpleInfo {
uid?: string
uin?: string
coreInfo: CoreInfo
baseInfo: BaseInfo
status: UserStatus | null
vasInfo: VasInfo | null
relationFlags: RelationFlags | null
otherFlags: any | null
intimate: any | null
}
interface RelationFlags {
topTime: string
isBlock: boolean
isMsgDisturb: boolean
isSpecialCareOpen: boolean
isSpecialCareZone: boolean
ringId: string
isBlocked: boolean
recommendImgFlag: number
disableEmojiShortCuts: number
qidianMasterFlag: number
qidianCrewFlag: number
qidianCrewFlag2: number
isHideQQLevel: number
isHidePrivilegeIcon: number
}
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt {
constellation: number
shengXiao: number
kBloodType: number
homeTown: string
makeFriendCareer: number
pos: string
college: string
country: string
province: string
city: string
postCode: string
address: string
regTime: number
interest: string
labels: any[]
qqLevel: QQLevel
}
interface Pic {
picId: string
picTime: number
picUrlMap: Record<string, string>
}
interface PhotoWall {
picList: Pic[]
}
export interface UserDetailInfoListenerArg {
uid: string
uin: string
simpleInfo: SimpleInfo
commonExt: CommonExt
photoWall: PhotoWall
}
export interface BuddyProfileLikeReq {
friendUids: string[]
basic: number
vote: number
favorite: number
userProfile: number
type: number
start: number
limit: number
}
export interface UserDetailInfoByUinV2 {
result: number
errMsg: string
detail: {
uid: string
uin: string
simpleInfo: SimpleInfo
commonExt: CommonExt
photoWall: null
}
}
export interface UserDetailInfoByUin {
result: number
errMsg: string
info: {
uid: string //这个没办法用
qid: string
uin: string
nick: string
remark: string
longNick: string
avatarUrl: string
birthday_year: number
birthday_month: number
birthday_day: number
sex: number //0
topTime: string
constellation: number
shengXiao: number
kBloodType: number
homeTown: string
makeFriendCareer: number
pos: string
eMail: string
phoneNum: string
college: string
country: string
province: string
city: string
postCode: string
address: string
isBlock: boolean
isSpecialCareOpen: boolean
isSpecialCareZone: boolean
ringId: string
regTime: number
interest: string
termType: number
labels: any[]
qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number }
isHideQQLevel: number
privilegeIcon: { jumpUrl: string, openIconList: any[], closeIconList: any[] }
isHidePrivilegeIcon: number
photoWall: { picList: any[] }
vipFlag: boolean
yearVipFlag: boolean
svipFlag: boolean
vipLevel: number
status: number
qidianMasterFlag: number
qidianCrewFlag: number
qidianCrewFlag2: number
extStatus: number
recommendImgFlag: number
disableEmojiShortCuts: number
pendantId: string
vipNameColorId: string
}
}

94
src/ntqqapi/wrapper.ts Normal file
View File

@@ -0,0 +1,94 @@
import {
NodeIKernelBuddyService,
NodeIKernelGroupService,
NodeIKernelProfileService,
NodeIKernelProfileLikeService,
NodeIKernelMSFService,
NodeIKernelMsgService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services'
import os from 'node:os'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
[key: string]: any
getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService
getProfileLikeService(): NodeIKernelProfileLikeService
getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
getSearchService(): NodeIKernelSearchService
}
export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
export interface WrapperConstructor {
[key: string]: any
NodeIKernelBuddyListener?: any
NodeIKernelGroupListener?: any
NodeQQNTWrapperUtil?: any
NodeIKernelMsgListener?: any
NodeIQQNTWrapperEngine?: any
NodeIGlobalAdapter?: any
NodeIDependsAdapter?: any
NodeIDispatcherAdapter?: any
NodeIKernelSessionListener?: any
NodeIKernelLoginService?: any
NodeIKernelLoginListener?: any
NodeIKernelProfileService?: any
NodeIKernelProfileListener?: any
}
const wrapperApi: WrapperApi = {}
export const wrapperConstructor: WrapperConstructor = {}
const constructor = [
'NodeIKernelBuddyListener',
'NodeIKernelGroupListener',
'NodeQQNTWrapperUtil',
'NodeIKernelMsgListener',
'NodeIQQNTWrapperEngine',
'NodeIGlobalAdapter',
'NodeIDependsAdapter',
'NodeIDispatcherAdapter',
'NodeIKernelSessionListener',
'NodeIKernelLoginService',
'NodeIKernelLoginListener',
'NodeIKernelProfileService',
'NodeIKernelProfileListener',
]
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) {
const 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) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
}
})
if (constructor.includes(export_name)) {
wrapperConstructor[export_name] = module.exports[export_name]
}
}
return dlopenRet
}
export function getSession() {
return wrapperApi['NodeIQQNTWrapperSession']
}

View File

@@ -1,49 +1,49 @@
import {ActionName, BaseCheckResult} from "./types" import { ActionName, BaseCheckResult } from './types'
import {OB11Response} from "./OB11Response" 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> { abstract class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName abstract 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: any) {
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: any) {
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,32 +1,32 @@
import {OB11Return} from '../types'; import { OB11Return } from '../types'
import {isNull} from "../../common/utils/helper"; import { isNull } from '../../common/utils/helper'
export class OB11Response { export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> { static res<T>(data: T, status: string, retcode: number, message: string = ''): OB11Return<T> {
return { return {
status: status, status: status,
retcode: retcode, retcode: retcode,
data: data, data: data,
message: message, message: message,
wording: message, wording: message,
echo: null echo: null,
}
} }
}
static ok<T>(data: T, echo: any = null) { static ok<T>(data: T, echo: any = null) {
let res = OB11Response.res<T>(data, "ok", 0) let res = OB11Response.res<T>(data, 'ok', 0)
if (!isNull(echo)) { if (!isNull(echo)) {
res.echo = echo; res.echo = echo
}
return res;
} }
return res
}
static error(err: string, retcode: number, echo: any = null) { static error(err: string, retcode: number, echo: any = null) {
let res = OB11Response.res(null, "failed", retcode, err) let res = OB11Response.res(null, 'failed', retcode, err)
if (!isNull(echo)) { if (!isNull(echo)) {
res.echo = echo; res.echo = echo
}
return res;
} }
return res
}
} }

View File

@@ -1,110 +1,144 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import fs from "fs/promises"; import fsPromise from 'node:fs/promises'
import {dbUtil} from "../../../common/db"; import { getConfigUtil } from '@/common/config'
import {getConfigUtil} from "../../../common/config"; import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api'
import {log, sleep, uri2local} from "../../../common/utils"; import { ActionName } from '../types'
import {NTQQFileApi} from "../../../ntqqapi/api/file"; import { UUIDConverter } from '@/common/utils/helper'
import {ActionName} from "../types"; import { Peer, ChatType, ElementType } from '@/ntqqapi/types'
import {FileElement, RawMessage, VideoElement} from "../../../ntqqapi/types"; import { MessageUnique } from '@/common/utils/MessageUnique'
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名或者fileUuid file: string // 文件名或者fileUuid
} }
export interface GetFileResponse { export interface GetFileResponse {
file?: string // path file?: string // path
url?: string url?: string
file_size?: string file_size?: string
file_name?: string file_name?: string
base64?: string base64?: string
} }
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { // forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44
private getElement(msg: RawMessage): {id: string, element: VideoElement | FileElement}{ protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let element = msg.elements.find(e=>e.fileElement) const { enableLocalFile2Url } = getConfigUtil().getConfig()
if (!element){ let UuidData: {
element = msg.elements.find(e=>e.videoElement) high: string
return {id: element.elementId, element: element.videoElement} low: string
} | undefined
try {
UuidData = UUIDConverter.decode(payload.file)
if (UuidData) {
const peerUin = UuidData.high
const msgId = UuidData.low
const isGroup: boolean = !!(await NTQQGroupApi.getGroups(false)).find(e => e.groupCode == peerUin)
let peer: Peer | undefined
//识别Peer
if (isGroup) {
peer = { chatType: ChatType.group, peerUid: peerUin }
} }
return {id: element.elementId, element: element.fileElement} const PeerUid = await NTQQUserApi.getUidByUinV2(peerUin)
} if (PeerUid) {
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { const isBuddy = await NTQQFriendApi.isBuddy(PeerUid)
const cache = await dbUtil.getFileCache(payload.file) if (isBuddy) {
const {autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond} = getConfigUtil().getConfig() peer = { chatType: ChatType.friend, peerUid: PeerUid }
if (!cache) { } else {
throw new Error('file not found') peer = { chatType: ChatType.temp, peerUid: PeerUid }
}
} }
if (cache.downloadFunc) { if (!peer) {
await cache.downloadFunc() throw new Error('chattype not support')
} }
try { const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId])
await fs.access(cache.filePath, fs.constants.F_OK) if (msgList.msgList.length === 0) {
} catch (e) { throw new Error('msg not found')
log("file not found", e)
if (cache.url){
const downloadResult = await uri2local(cache.url)
if (downloadResult.success) {
cache.filePath = downloadResult.path
dbUtil.addFileCache(payload.file, cache).then()
} else {
throw new Error("file download failed. " + downloadResult.errMsg)
}
}
else{
// 没有url的可能是私聊文件或者群文件需要自己下载
log("需要调用 NTQQ 下载文件api")
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg){
log("找到了文件 msg", msg)
let element = this.getElement(msg);
log("找到了文件 element", element);
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
element.id, "", "", true)
await sleep(1000);
msg = await dbUtil.getMsgByLongId(cache.msgId)
log("下载完成后的msg", msg)
cache.filePath = this.getElement(msg).element.filePath
dbUtil.addFileCache(payload.file, cache).then()
}
}
}
} }
let res: GetFileResponse = { const msg = msgList.msgList[0]
file: cache.filePath, const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT)
url: cache.url, if (!findEle) {
file_size: cache.fileSize, throw new Error('element not found')
file_name: cache.fileName
} }
if (enableLocalFile2Url) { const downloadPath = await NTQQFileApi.downloadMedia(msgId, msg.chatType, msg.peerUid, findEle.elementId, '', '')
if (!cache.url) { const fileSize = findEle?.videoElement?.fileSize || findEle?.fileElement?.fileSize || findEle?.pttElement?.fileSize || '0'
try{ const fileName = findEle?.videoElement?.fileName || findEle?.fileElement?.fileName || findEle?.pttElement?.fileName || ''
res.base64 = await fs.readFile(cache.filePath, 'base64') const res: GetFileResponse = {
}catch (e) { file: downloadPath,
throw new Error("文件下载失败. " + e) url: downloadPath,
} file_size: fileSize,
} file_name: fileName,
} }
// if (autoDeleteFile) { if (enableLocalFile2Url && downloadPath) {
// setTimeout(() => { try {
// fs.unlink(cache.filePath) res.base64 = await fsPromise.readFile(downloadPath, 'base64')
// }, autoDeleteFileSecond * 1000) } catch (e) {
// } throw new Error('文件下载失败. ' + e)
}
}
//不手动删除?文件持久化了
return res return res
}
} catch {
} }
const fileCache = await MessageUnique.getFileCache(String(payload.file))
if (fileCache?.length) {
const downloadPath = await NTQQFileApi.downloadMedia(
fileCache[0].msgId,
fileCache[0].chatType,
fileCache[0].peerUid,
fileCache[0].elementId,
'',
''
)
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: fileCache[0].fileSize,
file_name: fileCache[0].fileName,
}
const peer: Peer = {
chatType: fileCache[0].chatType,
peerUid: fileCache[0].peerUid,
guildId: ''
}
if (fileCache[0].elementType === ElementType.PIC) {
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (msgList.msgList.length === 0) {
throw new Error('msg not found')
}
const msg = msgList.msgList[0]
const findEle = msg.elements.find(e => e.elementId === fileCache[0].elementId)
if (!findEle) {
throw new Error('element not found')
}
res.url = await NTQQFileApi.getImageUrl(findEle.picElement)
} else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
}
if (enableLocalFile2Url && downloadPath && res.file === res.url) {
try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64')
} catch (e) {
throw new Error('文件下载失败. ' + e)
}
}
//不手动删除?文件持久化了
return res
}
throw new Error('file not found')
}
} }
export default class GetFile extends GetFileBase { export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile actionName = ActionName.GetFile
protected async _handle(payload: {file_id: string, file: string}): Promise<GetFileResponse> { protected async _handle(payload: { file_id: string; file: string }): Promise<GetFileResponse> {
if (!payload.file_id) { if (!payload.file_id) {
throw new Error('file_id 不能为空') throw new Error('file_id 不能为空')
}
payload.file = payload.file_id
return super._handle(payload);
} }
payload.file = payload.file_id
return super._handle(payload)
}
} }

View File

@@ -1,7 +1,13 @@
import {GetFileBase} from "./GetFile"; import { GetFileBase } from './GetFile'
import {ActionName} from "../types"; import { ActionName } from '../types'
export default class GetImage extends GetFileBase { export default class GetImage extends GetFileBase {
actionName = ActionName.GetImage actionName = ActionName.GetImage
protected async _handle(payload: { file: string }) {
if (!payload.file) {
throw new Error('参数 file 不能为空')
}
return super._handle(payload)
}
} }

View File

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

@@ -0,0 +1,27 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload {
message_id: number | string
}
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg;
protected async _handle(payload: Payload): Promise<any> {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
return await NTQQGroupApi.removeGroupEssence(
msg.Peer.peerUid,
msg.MsgId,
)
}
}

View File

@@ -1,73 +1,73 @@
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 fsPromise from 'fs/promises'
import {calculateFileMD5, httpDownload, TEMP_DIR} from "../../../common/utils"; import path from 'node:path'
import {v4 as uuid4} from "uuid"; import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils'
import { randomUUID } from 'node:crypto'
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(); const name = payload.name ? path.basename(payload.name) : randomUUID()
const filePath = joinPath(TEMP_DIR, name); const filePath = path.join(TEMP_DIR, name)
if (payload.base64) { if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64') await fsPromise.writeFile(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}) const buffer = await httpDownload({ url: payload.url, headers: headers })
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary'); await fsPromise.writeFile(filePath, buffer)
} 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 = path.join(TEMP_DIR, md5)
} await fsPromise.rename(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

@@ -1,39 +1,51 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {OB11ForwardMessage, OB11Message, OB11MessageData} from "../../types"; import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'
import {NTQQMsgApi, Peer} from "../../../ntqqapi/api"; import { NTQQMsgApi } from '@/ntqqapi/api'
import {dbUtil} from "../../../common/db"; import { OB11Constructor } from '../../constructor'
import {OB11Constructor} from "../../constructor"; import { ActionName } from '../types'
import {ActionName} from "../types"; import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: string; // long msg id message_id: string // long msg idgocq
id?: string // long msg id, onebot11
} }
interface Response{ interface Response {
messages: (OB11Message & {content: OB11MessageData})[] messages: (OB11Message & { content: OB11MessageData })[]
} }
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any>{ export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload): Promise<any> {
const rootMsg = await dbUtil.getMsgByLongId(payload.message_id) const msgId = payload.id || payload.message_id
if (!rootMsg){ if (!msgId) {
throw Error("msg not found") throw Error('message_id不能为空')
}
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}
} }
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId)
const rootMsg = await MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || +msgId)
if (!rootMsg) {
throw Error('msg not found')
}
const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId)
if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg)
}
const msgList = data.msgList
const messages = await Promise.all(
msgList.map(async (msg) => {
const resMsg = await OB11Constructor.message(msg)
resMsg.message_id = MessageUnique.createMsg({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId)!
return resMsg
}),
)
messages.map(v => {
const msg = v as Partial<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 } from '../../types'
import {groups} from "../../../common/data"; import { ActionName } from '../types'
import {ActionName} from "../types"; import { ChatType } from '@/ntqqapi/types'
import {ChatType} from "../../../ntqqapi/types"; import { NTQQMsgApi } from '@/ntqqapi/api/msg'
import {dbUtil} from "../../../common/db"; import { OB11Constructor } from '../../constructor'
import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; import { RawMessage } from '@/ntqqapi/types'
import {OB11Constructor} from "../../constructor"; import { MessageUnique } from '@/common/utils/MessageUnique'
import {log} from "../../../common/utils";
interface Payload { interface Payload {
group_id: number group_id: number | string
message_seq: number, message_seq?: number
count: number count?: number
reverseOrder?: boolean
} }
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 count = payload.count || 20
if (!group) { const isReverseOrder = payload.reverseOrder || true
throw `${payload.group_id}不存在` const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() }
} let msgList: RawMessage[]
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || "0" // 包含 message_seq 0
// log("startMsgId", startMsgId) if (!payload.message_seq) {
let msgList = (await NTQQMsgApi.getMsgHistory({chatType: ChatType.group, peerUid: group.groupCode}, startMsgId, parseInt(payload.count?.toString()) || 20)).msgList msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count)).msgList
await Promise.all(msgList.map(async msg => { } else {
msg.msgShortId = await dbUtil.addMsg(msg) const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId
})) if (!startMsgId) throw `消息${payload.message_seq}不存在`
const ob11MsgList = await Promise.all(msgList.map(msg=>OB11Constructor.message(msg))) msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList
return {"messages": ob11MsgList}
} }
if (isReverseOrder) msgList.reverse()
await Promise.all(
msgList.map(async msg => {
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
})
)
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg)))
return { messages: ob11MsgList }
}
} }

View File

@@ -1,20 +1,59 @@
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {OB11User} from "../../types"; import { OB11User } from '../../types'
import {getUidByUin, uidMaps} from "../../../common/data"; import { OB11Constructor } from '../../constructor'
import {OB11Constructor} from "../../constructor"; import { ActionName } from '../types'
import {ActionName} from "../types"; import { NTQQUserApi } from '../../../ntqqapi/api/user'
import {NTQQUserApi} from "../../../ntqqapi/api/user"; import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel'
interface Payload {
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { user_id: number | string
actionName = ActionName.GoCQHTTP_GetStrangerInfo }
protected async _handle(payload: { user_id: number }): Promise<OB11User> { export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> {
const user_id = payload.user_id.toString() actionName = ActionName.GoCQHTTP_GetStrangerInfo
const uid = getUidByUin(user_id)
if (!uid) { protected async _handle(payload: Payload): Promise<OB11User> {
throw new Error("查无此人") if (!(getBuildVersion() >= 26702)) {
} const user_id = payload.user_id.toString()
return OB11Constructor.stranger(await NTQQUserApi.getUserDetailInfo(uid, true)) const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id)
} const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick,
sex: OB11UserSex.unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
} else {
const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0,
uid: ''
}
return ret
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data)
}
}
} }

View File

@@ -0,0 +1,17 @@
import BaseAction from '../BaseAction'
import { handleQuickOperation, QuickOperation, QuickOperationEvent } from '../quick-operation'
import { log } from '@/common/utils'
import { ActionName } from '../types'
interface Payload{
context: QuickOperationEvent,
operation: QuickOperation
}
export class GoCQHTTHandleQuickOperation extends BaseAction<Payload, null>{
actionName = ActionName.GoCQHTTP_HandleQuickOperation
protected async _handle(payload: Payload): Promise<null> {
handleQuickOperation(payload.context, payload.operation).then().catch(log);
return null
}
}

View File

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

View File

@@ -0,0 +1,26 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api/group'
import { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload {
message_id: number | string
}
export default class GoCQHTTPSetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetEssenceMsg;
protected async _handle(payload: Payload): Promise<any> {
if (!payload.message_id) {
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
return await NTQQGroupApi.addGroupEssence(
msg.Peer.peerUid,
msg.MsgId
)
}
}

View File

@@ -0,0 +1,72 @@
import fs from 'node:fs'
import BaseAction from '../BaseAction'
import { getGroup } from '@/common/data'
import { ActionName } from '../types'
import { SendMsgElementConstructor } from '@/ntqqapi/constructor'
import { ChatType, SendFileElement } from '@/ntqqapi/types'
import { uri2local } from '@/common/utils'
import { Peer } from '@/ntqqapi/types'
import { sendMsg } from '../msg/SendMsg'
import { NTQQUserApi, NTQQFriendApi } from '@/ntqqapi/api'
interface Payload {
user_id: number | string
group_id?: number | string
file: string
name: string
folder?: string
folder_id?: string
}
export class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id?.toString()!)
if (!group) {
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
}
const downloadResult = await uri2local(file)
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name, payload.folder_id)
await sendMsg({ chatType: ChatType.group, peerUid: group.groupCode }, [sendFileEle], [], true)
return null
}
}
export class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadPrivateFile
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString())
if (!peerUid) {
throw `私聊${payload.user_id}不存在`
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid)
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid }
}
throw '缺少参数 user_id'
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload)
let file = payload.file
if (fs.existsSync(file)) {
file = `file://${file}`
}
const downloadResult = await uri2local(file)
if (!downloadResult.success) {
throw new Error(downloadResult.errMsg)
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name)
await sendMsg(peer, [sendFileEle], [], true)
return null
}
}

View File

@@ -1,37 +0,0 @@
import BaseAction from "../BaseAction";
import {getGroup} from "../../../common/data";
import {ActionName} from "../types";
import {SendMsgElementConstructor} from "../../../ntqqapi/constructor";
import {ChatType, SendFileElement} from "../../../ntqqapi/types";
import fs from "fs";
import {NTQQMsgApi} from "../../../ntqqapi/api/msg";
import {uri2local} from "../../../common/utils";
interface Payload{
group_id: number
file: string
name: string
folder: string
}
export default class GoCQHTTPUploadGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_UploadGroupFile
protected async _handle(payload: Payload): Promise<null> {
const group = await getGroup(payload.group_id.toString());
if (!group){
throw new Error(`群组${payload.group_id}不存在`)
}
let file = payload.file;
if (fs.existsSync(file)){
file = `file://${file}`
}
const downloadResult = await uri2local(file);
if (downloadResult.errMsg){
throw new Error(downloadResult.errMsg)
}
let sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
await NTQQMsgApi.sendMsg({chatType: ChatType.group, peerUid: group.groupCode}, [sendFileEle]);
return null
}
}

View File

@@ -0,0 +1,24 @@
import { GroupEssenceMsgRet, WebApi } from '@/ntqqapi/api'
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
interface PayloadType {
group_id: number
pages?: number
}
export class GetGroupEssence extends BaseAction<PayloadType, GroupEssenceMsgRet | void> {
actionName = ActionName.GoCQHTTP_GetEssenceMsg
protected async _handle(payload: PayloadType) {
throw '此 api 暂不支持'
const ret = await WebApi.getGroupEssenceMsg(payload.group_id.toString(), payload.pages?.toString() || '0')
if (!ret) {
throw new Error('获取失败')
}
// ret.map((item) => {
//
// })
return ret
}
}

View File

@@ -0,0 +1,23 @@
import { WebApi, WebHonorType } from '@/ntqqapi/api'
import { ActionName } from '../types'
import BaseAction from '../BaseAction'
interface Payload {
group_id: number
type?: WebHonorType
}
export class GetGroupHonorInfo extends BaseAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo
protected async _handle(payload: Payload) {
// console.log(await NTQQUserApi.getRobotUinRange())
if (!payload.group_id) {
throw '缺少参数group_id'
}
if (!payload.type) {
payload.type = WebHonorType.ALL
}
return await WebApi.getGroupHonorInfo(payload.group_id.toString(), payload.type)
}
}

View File

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

View File

@@ -1,22 +1,28 @@
import {OB11Group} from '../../types'; import { OB11Group } from '../../types'
import {OB11Constructor} from "../../constructor"; import { OB11Constructor } from '../../constructor'
import {groups} from "../../../common/data"; import { groups } from '../../../common/data'
import BaseAction from "../BaseAction"; import BaseAction from '../BaseAction'
import {ActionName} from "../types"; import { ActionName } from '../types'
import {NTQQGroupApi} from "../../../ntqqapi/api"; import { NTQQGroupApi } from '../../../ntqqapi/api'
import {log} from "../../../common/utils"; import { log } from '../../../common/utils'
interface Payload {
no_cache: boolean | string
}
class GetGroupList extends BaseAction<null, OB11Group[]> { class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList actionName = ActionName.GetGroupList
protected async _handle(payload: null) { protected async _handle(payload: Payload) {
// if (groups.length === 0) { if (groups.length === 0 || payload?.no_cache === true || payload?.no_cache === 'true') {
// const groups = await NTQQGroupApi.getGroups(true) try {
// log("get groups", groups) const groups = await NTQQGroupApi.getGroups(true)
// } log('强制刷新群列表, 数量:', groups.length)
return OB11Constructor.groups(groups); return OB11Constructor.groups(groups)
} catch (e) {}
} }
return OB11Constructor.groups(groups)
}
} }
export default GetGroupList export default GetGroupList

View File

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

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