Compare commits

...

236 Commits

Author SHA1 Message Date
idranme
ac07c98ae1 Merge pull request #434 from LLOneBot/dev
release: 3.33.2
2024-09-20 23:00:09 +08:00
idranme
6a19d6f234 chore: v3.33.2 2024-09-20 22:57:00 +08:00
idranme
ab0b8ae663 feat: get_essence_msg_list API 2024-09-20 22:55:29 +08:00
idranme
96aa5e264a refactor 2024-09-20 22:13:26 +08:00
idranme
15b85f735d fix: friend_add event 2024-09-20 19:08:22 +08:00
idranme
4dd6d12168 feat 2024-09-20 18:00:32 +08:00
idranme
44febed486 optimize 2024-09-20 03:19:43 +08:00
idranme
6c66dab3dc Merge pull request #433 from LLOneBot/dev
release: 3.33.1
2024-09-19 18:31:01 +08:00
idranme
0f7939fe5e chore: v3.33.1 2024-09-19 18:29:16 +08:00
idranme
73a2b4e35f fix 2024-09-19 18:29:12 +08:00
idranme
936b1d911c Merge pull request #428 from LLOneBot/dev
release: 3.33.0
2024-09-18 20:59:57 +08:00
idranme
58817d1c02 chore: v3.33.0 2024-09-18 20:53:28 +08:00
idranme
2c24422478 feat: support setting remark when agreeing to a friend request 2024-09-18 20:47:45 +08:00
idranme
c2a723380a fix: get_group_member_list API 2024-09-18 19:35:58 +08:00
idranme
156bbaea33 feat: get_group_files_by_folder API 2024-09-18 17:22:09 +08:00
idranme
6c485634e1 feat: get_friend_msg_history API 2024-09-18 16:56:15 +08:00
idranme
f39a9aeafb feat: fetch_custom_face API 2024-09-18 16:11:08 +08:00
idranme
1160cd4b26 feat: fetch_emoji_like API 2024-09-18 15:49:37 +08:00
idranme
9a7ff523dd optimize 2024-09-18 14:07:42 +08:00
idranme
f49995ea97 refactor 2024-09-17 21:04:36 +08:00
idranme
1876dd29ac Merge pull request #423 from LLOneBot/dev
release: 3.32.8
2024-09-17 11:59:57 +08:00
idranme
9944b53266 chore: v3.32.8 2024-09-17 11:55:50 +08:00
idranme
9a791e3a21 fix 2024-09-17 02:17:16 +08:00
idranme
64c5eb6c04 Merge pull request #422 from LLOneBot/dev
release: 3.32.7
2024-09-16 20:48:15 +08:00
idranme
e5750786cb chore: v3.32.7 2024-09-16 20:46:14 +08:00
idranme
18cb46ade5 fix 2024-09-16 20:43:18 +08:00
idranme
e39c89a441 fix 2024-09-16 19:01:59 +08:00
idranme
476d498e44 Merge pull request #417 from LLOneBot/dev
release: 3.32.6
2024-09-15 17:48:35 +08:00
idranme
55446538de chore: v3.32.6 2024-09-15 17:43:10 +08:00
idranme
b965f50653 fix: friend_add event 2024-09-15 16:14:36 +08:00
idranme
2d354c5eda optimize 2024-09-15 14:08:02 +08:00
idranme
536999f296 feat: support for sending contact message segment 2024-09-14 20:13:45 +08:00
idranme
cad09b2ed1 fix 2024-09-14 19:56:46 +08:00
idranme
6be0c11ca2 refactor 2024-09-13 22:58:21 +08:00
idranme
b03bcf9a7c Merge pull request #415 from LLOneBot/dev
release: 3.32.5
2024-09-13 18:59:37 +08:00
idranme
d4f9629af2 chore: v3.32.5 2024-09-13 18:54:56 +08:00
idranme
6d47e2ee80 chore 2024-09-13 18:52:45 +08:00
idranme
506bddb21a chore: style 2024-09-13 17:30:56 +08:00
idranme
91c689baf8 fix 2024-09-13 14:56:30 +08:00
idranme
b7938aaab8 refactor 2024-09-12 22:39:14 +08:00
idranme
b1a892cf4e chore 2024-09-12 18:29:18 +08:00
idranme
9284fc7e8a Merge pull request #413 from LLOneBot/dev
3.32.4
2024-09-12 18:14:23 +08:00
idranme
ceb063143a chore: v3.32.4 2024-09-12 18:11:01 +08:00
idranme
ed55a5a54c optimize 2024-09-12 17:52:21 +08:00
idranme
2c4fdbfa6a fix: upload_group_file API 2024-09-12 16:46:38 +08:00
idranme
1132495eb3 fix: check for updates 2024-09-12 01:11:17 +08:00
idranme
2ac2c68435 chore 2024-09-11 22:13:11 +08:00
idranme
6477366ba6 chore
chore
2024-09-11 21:35:50 +08:00
idranme
1d63473a04 Merge pull request #411 from LLOneBot/dev
3.32.3
2024-09-11 20:52:52 +08:00
idranme
692ba5163e chore: v3.32.3 2024-09-11 20:52:16 +08:00
idranme
8bcea090ec Merge pull request #410 from LLOneBot/main
merge branch
2024-09-11 20:50:38 +08:00
idranme
67568a662d feat: profile_like event 2024-09-11 20:48:32 +08:00
linyuchen
66b3706524 ♻️refactor: rkey server url 2024-09-10 17:35:14 +08:00
idranme
6d82dd1dad chore 2024-09-10 17:16:47 +08:00
idranme
20f2e66b11 chore
chore

chore
2024-09-10 16:59:12 +08:00
idranme
efefb963c0 Merge pull request #409 from LLOneBot/main
merge branch
2024-09-10 15:32:58 +08:00
linyuchen
92d780cf70 fix: download file add referer 2024-09-10 15:01:50 +08:00
idranme
ce6886011f Merge pull request #407 from LLOneBot/dev
3.32.1
2024-09-10 13:44:03 +08:00
idranme
319b275e4f chore: v3.32.1 2024-09-10 13:39:12 +08:00
idranme
9738c3f63c feat: GetProfileLike 2024-09-10 13:36:00 +08:00
idranme
3de054600c chore 2024-09-09 17:06:33 +08:00
idranme
da0ebd3f80 refactor 2024-09-08 23:37:20 +08:00
idranme
d5682a9788 Merge pull request #406 from LLOneBot/dev
3.32.0
2024-09-08 21:11:52 +08:00
idranme
a1298b1c93 optimize 2024-09-08 21:09:03 +08:00
idranme
6fac490d66 chore: v3.32.0 2024-09-08 20:54:32 +08:00
idranme
dc25d83778 feat 2024-09-08 20:50:22 +08:00
idranme
1bcdbba29a feat 2024-09-08 15:56:05 +08:00
idranme
9ecfb6ea0c optimize 2024-09-08 01:18:14 +08:00
idranme
5c68d4de84 Merge pull request #404 from LLOneBot/dev
3.31.10
2024-09-07 21:55:04 +08:00
idranme
c1c22e872e chore: v3.31.10 2024-09-07 21:52:34 +08:00
idranme
709c0b6f7b fix 2024-09-07 21:51:17 +08:00
idranme
dd34286b43 refactor 2024-09-07 17:45:36 +08:00
idranme
80487e31f5 chore 2024-09-07 03:03:33 +08:00
idranme
bdf1c7fd5f chore 2024-09-07 02:56:59 +08:00
idranme
285fc1d33d Merge pull request #398 from LLOneBot/dev
3.31.9
2024-09-06 23:12:47 +08:00
idranme
e14ba108fc chore: v3.31.9 2024-09-06 23:06:43 +08:00
idranme
e0be0bcc77 fix 2024-09-06 23:04:17 +08:00
idranme
c24fa6cea1 chore: improve code quality
chore: improve code quality
2024-09-06 23:04:17 +08:00
idranme
04b2a323a7 chore: improve code quality 2024-09-06 23:03:04 +08:00
idranme
970f1a98ec chore: improve code quality
chore: improve code quality
2024-09-06 23:03:04 +08:00
idranme
3064a6eb7c chore: improve code quality 2024-09-06 22:59:14 +08:00
idranme
f93e2b5a95 chore: improve code quality
chore: improve code quality
2024-09-06 22:59:14 +08:00
idranme
e185e700b7 chore: improve code quality
chore: improve code quality

chore: improve code quality
2024-09-06 22:58:00 +08:00
idranme
eae6e09e22 optimize 2024-09-05 17:16:42 +08:00
idranme
e204bb0957 Merge pull request #395 from LLOneBot/dev
3.31.8
2024-09-05 15:00:57 +08:00
idranme
ed546ace3d chore: v3.31.8 2024-09-05 15:00:05 +08:00
idranme
3c79cffa42 optimize 2024-09-05 14:58:52 +08:00
idranme
acce444dee optimize 2024-09-05 02:26:42 +08:00
idranme
f359e3ea9d fix 2024-09-05 02:20:23 +08:00
idranme
fe99da985f Merge pull request #394 from LLOneBot/dev
3.31.7
2024-09-04 20:35:08 +08:00
idranme
58d5de572c chore: v3.31.7 2024-09-04 20:32:44 +08:00
idranme
b2088824cc feat 2024-09-04 17:15:41 +08:00
idranme
fffa664400 fix: reply message segment 2024-09-04 16:18:48 +08:00
idranme
02e5222f92 feat: SendGroupNotice 2024-09-04 15:42:10 +08:00
idranme
18d253edf6 fix: GroupMsgEmojiLikeEvent 2024-09-04 13:13:49 +08:00
idranme
da8b5e2429 chore 2024-09-04 13:12:39 +08:00
idranme
502be69bc5 feat: SetOnlineStatus 2024-09-04 01:23:25 +08:00
idranme
273d4133eb refactor 2024-09-04 00:44:41 +08:00
idranme
44bfc5aab9 optimize 2024-09-03 21:46:26 +08:00
idranme
050c9d9b54 fix 2024-09-03 21:43:18 +08:00
idranme
7904f45c20 Merge pull request #392 from LLOneBot/dev
3.31.6
2024-09-03 18:38:07 +08:00
idranme
1afdad1452 chore: v3.31.6 2024-09-03 18:34:30 +08:00
idranme
cd930c43b6 feat: GetGroupRootFiles 2024-09-03 15:14:05 +08:00
idranme
b7efbdf239 fix: ws 2024-09-03 13:16:25 +08:00
idranme
56706f3838 chore 2024-09-03 01:24:21 +08:00
idranme
387c9dcb52 refactor 2024-09-03 01:04:16 +08:00
idranme
a7bb55b31c chore 2024-09-02 19:53:18 +08:00
idranme
fbf09e1db4 chore 2024-09-02 19:48:17 +08:00
idranme
9b98f8f33d optimize 2024-09-02 19:30:23 +08:00
idranme
727f399de6 fix: GetGroupMsgHistory 2024-09-02 19:24:27 +08:00
idranme
e03b82fb44 optimize: ci 2024-09-02 18:28:21 +08:00
idranme
ba413b9581 Merge pull request #390 from LLOneBot/dev
3.31.5
2024-09-02 16:42:35 +08:00
idranme
abcec99ce0 chore: v3.31.5 2024-09-02 16:39:36 +08:00
idranme
a7da7ab598 optimize 2024-09-02 01:58:31 +08:00
idranme
5cc8a2b96e fix 2024-09-02 01:46:08 +08:00
idranme
f0d8c851d4 optimize 2024-09-02 01:24:15 +08:00
idranme
828b20e0e8 optimize 2024-09-02 01:05:58 +08:00
idranme
3570349fcd optimize 2024-09-02 00:42:35 +08:00
idranme
ad74854e42 fix 2024-09-01 20:28:12 +08:00
idranme
15e7afed62 Merge pull request #385 from LLOneBot/dev
3.31.4
2024-09-01 18:50:38 +08:00
idranme
bf71328650 chore: v3.31.4 2024-09-01 18:50:09 +08:00
idranme
b3299ba1e3 chore 2024-09-01 15:39:37 +08:00
idranme
d36ea93e63 refactor 2024-09-01 15:26:34 +08:00
idranme
0bd3f8f1a2 feat 2024-09-01 15:26:11 +08:00
idranme
4bf79e021e Merge pull request #383 from LLOneBot/dev
3.31.3
2024-09-01 00:36:41 +08:00
idranme
2dac109e58 chore: v3.31.3 2024-09-01 00:34:08 +08:00
idranme
2637a5da6d chore 2024-08-31 22:59:42 +08:00
idranme
f8b2be246f optimize 2024-08-31 22:55:26 +08:00
idranme
44921e85ad chore 2024-08-31 19:46:35 +08:00
idranme
388e016365 optimize 2024-08-31 19:41:48 +08:00
idranme
a2056a43f3 fix 2024-08-31 01:29:44 +08:00
idranme
a249e0b581 Merge pull request #381 from LLOneBot/dev
3.31.2
2024-08-30 12:47:18 +08:00
idranme
f7343332d7 chore: v3.31.2 2024-08-30 12:46:03 +08:00
idranme
bf17d46157 fix 2024-08-30 12:38:39 +08:00
idranme
3e3f792035 optimize 2024-08-30 03:09:34 +08:00
idranme
d7cc5d68a7 refactor 2024-08-30 02:52:21 +08:00
idranme
64a8efb8df optimize 2024-08-30 02:51:56 +08:00
idranme
6af31c48c4 fix 2024-08-29 20:48:08 +08:00
idranme
6954551cb7 feat 2024-08-29 18:06:53 +08:00
idranme
c71885a29e refactor 2024-08-28 23:57:11 +08:00
idranme
183eab2cf4 optimize 2024-08-28 17:13:26 +08:00
idranme
c0b682606c Merge pull request #378 from LLOneBot/dev
3.31.1
2024-08-28 16:09:35 +08:00
idranme
8564630c4d Update manifest.json 2024-08-28 16:07:58 +08:00
idranme
abd5a12708 chore: v3.31.1 2024-08-28 16:07:31 +08:00
idranme
234167f305 fix 2024-08-28 16:06:40 +08:00
idranme
da75f59d0d fix 2024-08-28 15:40:08 +08:00
idranme
eaf96ac3fc Merge pull request #376 from LLOneBot/dev
fix
2024-08-28 10:45:50 +08:00
idranme
2491de9af8 fix 2024-08-28 02:45:17 +00:00
idranme
01f8987e1e Merge pull request #375 from LLOneBot/dev
3.31.0
2024-08-28 10:28:27 +08:00
idranme
4a9bebbc9c chore: v3.31.0 2024-08-28 10:27:05 +08:00
idranme
6be6151d73 fix 2024-08-28 10:25:17 +08:00
idranme
738b0a96a0 chore 2024-08-28 06:52:29 +08:00
idranme
7cb94cb8b8 refactor 2024-08-28 06:49:46 +08:00
idranme
5501980ab3 refactor 2024-08-28 04:48:07 +08:00
idranme
bc3c8b1259 Merge pull request #374 from LLOneBot/main
merge
2024-08-28 04:45:33 +08:00
idranme
61e63efbd8 Merge pull request #373 from itzdrli/main
Fix typo in LICENSE file
2024-08-27 22:01:30 +08:00
itzdrli
28770d5995 Fix typo in LICENSE file 2024-08-27 13:01:14 +00:00
idranme
67d3dfb3cf Merge pull request #367 from LLOneBot/dev
3.30.5
2024-08-25 23:09:44 +08:00
idranme
afe8392a1e chore: v3.30.5 2024-08-25 23:07:33 +08:00
idranme
c1f5c5cd58 fix 2024-08-25 20:00:13 +08:00
idranme
85001a40da Merge pull request #366 from LLOneBot/dev
3.30.4
2024-08-23 17:05:03 +08:00
idranme
867a05c85a chore: v3.30.4 2024-08-23 17:03:58 +08:00
idranme
d8a63f6561 fix 2024-08-23 17:02:31 +08:00
idranme
e9fb9d1b30 Update publish.yml 2024-08-23 16:08:59 +08:00
idranme
b4fc987537 Merge pull request #365 from LLOneBot/dev
3.30.3
2024-08-23 13:40:59 +08:00
idranme
d0ccf53d88 chore: v3.30.3 2024-08-23 13:39:26 +08:00
idranme
d5ca94569d fix 2024-08-23 13:32:58 +08:00
idranme
bf72685501 Merge pull request #363 from LLOneBot/dev
3.30.2
2024-08-23 00:30:48 +08:00
idranme
c07467b670 chore: v3.30.2 2024-08-23 00:08:52 +08:00
idranme
ea164fb048 fix: friend list 2024-08-22 23:47:15 +08:00
idranme
0c0ad9a616 Merge pull request #362 from LLOneBot/dev
3.30.1
2024-08-22 20:41:32 +08:00
idranme
7bb4808e2d chore: v3.30.1 2024-08-22 20:18:16 +08:00
idranme
3f7592d06d opt 2024-08-22 20:17:28 +08:00
idranme
2f341fcf43 fix 2024-08-22 18:16:08 +08:00
idranme
9c59e5903e Merge pull request #360 from LLOneBot/dev
3.30.0
2024-08-22 12:41:06 +08:00
idranme
339ba409ee chore: v3.30.0 2024-08-22 12:37:43 +08:00
idranme
099da66661 fix: poke event 2024-08-22 12:32:09 +08:00
idranme
adcde6e49e fix 2024-08-22 06:37:28 +08:00
idranme
b3b8f9cd72 fix 2024-08-22 06:23:35 +08:00
idranme
8b57ebd7de fix: adaptation 27187 2024-08-22 05:45:02 +08:00
idranme
1afaeb0396 fix: adaptation 27187 2024-08-22 03:34:42 +08:00
idranme
235a986253 fix: adaptation 27187 2024-08-22 02:48:01 +08:00
idranme
b16bea9548 fix: adaptation 27187 2024-08-22 02:01:44 +08:00
idranme
7897034d13 opt 2024-08-22 00:42:12 +08:00
idranme
eabe891838 opt 2024-08-21 23:36:35 +08:00
idranme
75d3fc27f0 chore: remove unused methods 2024-08-21 22:51:00 +08:00
idranme
111bb4dd88 fix: adaptation 27187 2024-08-21 22:14:52 +08:00
idranme
f8bf60a3a0 Merge pull request #357 from cnxysoft/dev
fix: Linux上报
2024-08-21 17:50:42 +08:00
Alen
7c22eb3376 fix: Linux上报 2024-08-21 17:42:33 +08:00
Alen
7e1f7ac7f5 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-21 10:56:00 +08:00
idranme
4ea02676f7 Merge pull request #354 from LLOneBot/dev
3.29.6
2024-08-21 00:29:41 +08:00
idranme
ddefb4c194 chore: v3.29.6 2024-08-21 00:27:47 +08:00
idranme
2792fa4776 fix 2024-08-21 00:14:15 +08:00
idranme
c37858e2f9 opt 2024-08-20 21:13:27 +08:00
idranme
59a11faa7f Merge pull request #352 from LLOneBot/dev
3.29.5
2024-08-19 17:40:30 +08:00
idranme
3b3795c946 chore: v3.29.5 2024-08-19 17:38:42 +08:00
idranme
ff18937828 fix 2024-08-19 17:29:58 +08:00
idranme
65d02d7f21 Merge pull request #351 from LLOneBot/main
merge
2024-08-19 12:59:10 +08:00
idranme
9cb8ba017e Merge pull request #350 from snsin09/nocache
ws修复必须no_cache参数
2024-08-19 12:55:27 +08:00
yota
1e579858b8 ws修复必须no_cache参数 2024-08-19 09:47:24 +08:00
idranme
db0c800851 Merge pull request #347 from LLOneBot/dev
3.29.4
2024-08-18 21:09:15 +08:00
idranme
e912911dd8 chore: v3.29.4 2024-08-18 21:04:30 +08:00
idranme
2245d0d3de fix 2024-08-18 20:58:26 +08:00
idranme
a56eac0251 Merge pull request #345 from LLOneBot/main
merge
2024-08-18 16:45:02 +08:00
linyuchen
8be0562c19 Merge pull request #344 from LLOneBot/linyuchen-patch-1
Fix: typo
2024-08-17 23:46:12 +08:00
linyuchen
f4c77f3e20 Fix: typo 2024-08-17 23:45:41 +08:00
linyuchen
508e6f2928 Merge pull request #342 from gfhdhytghd/patch-1
Update LICENSE
2024-08-17 16:20:50 +08:00
lin
9353cb0432 Update LICENSE
修改许可证以在法律层面上禁止宣传
2024-08-17 14:21:27 +08:00
idranme
816e07f47c Merge pull request #341 from LLOneBot/dev
3.29.3
2024-08-16 22:27:41 +08:00
idranme
46b1e8e67d chore: v3.29.3 2024-08-16 22:25:17 +08:00
idranme
8542594181 fix 2024-08-16 21:58:05 +08:00
idranme
0d7aa9bd2c fix 2024-08-16 21:28:43 +08:00
idranme
a47ee4c3e4 fix 2024-08-16 09:53:23 +08:00
idranme
0182803ae1 Merge pull request #339 from LLOneBot/dev
3.29.2
2024-08-15 11:14:35 +08:00
idranme
94c1aea6df chore: v3.29.2 2024-08-15 10:57:15 +08:00
idranme
d143dc043c fix 2024-08-15 10:31:51 +08:00
idranme
3f4b0b44cf feat: cache recalled message content 2024-08-14 23:04:15 +08:00
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
Alen
4efcf5b520 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 19:56:48 +08:00
Alen
9ff6ff7cab Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 16:09:24 +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
Alen
594a421163 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-09 22:15:54 +08:00
Alen
b748d84e8a Merge branch 'dev' of https://github.com/cnxysoft/LLOneBot into dev 2024-08-07 15:06:19 +08:00
Alen
e8d83d2958 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-07 15:06:11 +08:00
Alen
cdb34ffe61 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 22:15:48 +08:00
Alen
a45c56bd85 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 10:09:12 +08:00
Alen
bb07ebd5d7 Merge branch 'main' into dev 2024-08-05 10:07:28 +08:00
176 changed files with 9327 additions and 8554 deletions

7
.gitattributes vendored Normal file
View File

@@ -0,0 +1,7 @@
* text eol=lf
*.png -text
*.jpg -text
*.ico -text
*.gif -text
*.webp -text

View File

@@ -7,9 +7,6 @@ body:
attributes: attributes:
value: | value: |
欢迎来到 LLOneBot 的 Issue Tracker请填写以下表格来提交 Bug。 欢迎来到 LLOneBot 的 Issue Tracker请填写以下表格来提交 Bug。
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
- type: input - type: input
id: system-version id: system-version
attributes: attributes:
@@ -40,8 +37,6 @@ body:
label: OneBot 客户端 label: OneBot 客户端
description: 连接至 LLOneBot 的客户端版本信息 description: 连接至 LLOneBot 的客户端版本信息
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
validations:
required: true
- type: textarea - type: textarea
id: what-happened id: what-happened
attributes: attributes:

View File

@@ -1,4 +1,4 @@
name: 'publish' name: Publish
on: on:
push: push:
tags: tags:
@@ -8,31 +8,36 @@ jobs:
build-and-publish: build-and-publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: setup node - name: Setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
- 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: Compress
run: | run: |
sudo apt install zip -y sudo apt install zip -y
cd ./dist/ cd ./dist/
zip -r ../LLOneBot.zip ./* zip -r ../LLOneBot.zip ./*
- name: publish - name: Extract version from tag
id: get-version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
- name: Release
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 }}
name: LLOneBot v${{ steps.get-version.outputs.VERSION }}

3
.gitignore vendored
View File

@@ -11,5 +11,6 @@ node_modules
dist dist
out out
.idea/
.DS_Store .DS_Store
.idea
.vscode

View File

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

View File

@@ -1,4 +1,4 @@
MIT License MIT Without Public Social Media Promotion License
Copyright (c) 2024 LLOneBot Copyright (c) 2024 LLOneBot
@@ -19,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
You may use this software in accordance with the above terms, but you are not
allowed to promote this project or your projects based on this project on any
public social media.

View File

@@ -1,11 +1,11 @@
# LLOneBot # LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 协议,用 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: B站,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** > 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息
TG群<https://t.me/+nLZEnpne-pQ1OWFl> TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
## 安装方法 ## 安装方法
@@ -15,10 +15,6 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
<img src="./doc/image/setting.png" width="400px" alt="设置界面"/> <img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例
<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>
@@ -29,11 +25,12 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 鸣谢 ## 鸣谢
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
- [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html) - [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
- [chronocat](https://github.com/chrononeko/chronocat) - [Chronocat](https://github.com/chrononeko/chronocat)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) - [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm) - [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): An Implementation of NTQQ Protocol

View File

@@ -1,25 +1,19 @@
import cp from 'vite-plugin-cp' import cp from 'vite-plugin-cp'
import path from 'node:path' import path from 'node:path'
import './scripts/gen-manifest' import './scripts/gen-manifest'
import type { ElectronViteConfig } from 'electron-vite'
const external = [ const external = [
'silk-wasm', 'silk-wasm',
'ws', 'ws',
'level', '@minatojs/sql.js',
'classic-level',
'abstract-level',
'level-supports',
'level-transcoder',
'module-error',
'catering',
'node-gyp-build',
] ]
function genCpModule(module: string) { function genCpModule(module: string) {
return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false } return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }
} }
let config = { const config: ElectronViteConfig = {
main: { main: {
build: { build: {
outDir: 'dist/main', outDir: 'dist/main',
@@ -37,7 +31,6 @@ let config = {
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
'./lib-cov/fluent-ffmpeg': './lib/fluent-ffmpeg',
}, },
}, },
plugins: [ plugins: [
@@ -46,9 +39,6 @@ let config = {
...external.map(genCpModule), ...external.map(genCpModule),
{ src: './manifest.json', dest: 'dist' }, { src: './manifest.json', dest: 'dist' },
{ src: './icon.webp', dest: 'dist' }, { src: './icon.webp', dest: 'dist' },
// { src: './src/ntqqapi/native/crychic/crychic-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-win32-x64.node', dest: 'dist/main/' },
// { src: './src/ntqqapi/native/moehook/MoeHoo-linux-x64.node', dest: 'dist/main/' },
], ],
}), }),
], ],

View File

@@ -3,8 +3,8 @@
"type": "extension", "type": "extension",
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 协议,用 QQ 机器人开发", "description": "实现 OneBot 11 协议,用 QQ 机器人开发",
"version": "3.28.6", "version": "3.33.2",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -2,41 +2,47 @@
"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/Documents/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\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"",
"format": "prettier -cw .", "format": "prettier -cw .",
"check": "tsc" "check": "tsc",
"compile:proto": "pbjs --no-create --no-convert --no-encode --no-verify -t static-module -w es6 -p src/ntqqapi/proto -o src/ntqqapi/proto/compiled.js systemMessage.proto profileLikeTip.proto && pbts -o src/ntqqapi/proto/compiled.d.ts src/ntqqapi/proto/compiled.js"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@minatojs/driver-sqlite": "^4.6.0",
"compressing": "^1.10.1", "compressing": "^1.10.1",
"cordis": "^3.18.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.19.2", "cosmokit": "^1.6.2",
"fast-xml-parser": "^4.4.1", "express": "^5.0.0",
"file-type": "^19.4.0", "fast-xml-parser": "^4.5.0",
"file-type": "^19.5.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"level": "^8.0.1", "minato": "^3.6.0",
"protobufjs": "^7.4.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.25", "@types/fluent-ffmpeg": "^2.1.26",
"@types/node": "^20.14.15", "@types/node": "^20.14.15",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"electron": "^29.1.4", "electron": "^31.4.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"typescript": "^5.5.4", "protobufjs-cli": "^1.1.3",
"vite": "^5.4.0", "typescript": "^5.6.2",
"vite": "^5.4.6",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.0" "packageManager": "yarn@4.4.1"
} }

View File

@@ -6,7 +6,7 @@ const manifest = {
type: 'extension', type: 'extension',
name: 'LLOneBot', name: 'LLOneBot',
slug: 'LLOneBot', slug: 'LLOneBot',
description: '实现 OneBot 11 协议,用 QQ 机器人开发', description: '实现 OneBot 11 协议,用 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [
@@ -35,4 +35,4 @@ const manifest = {
} }
} }
writeFileSync('manifest.json', JSON.stringify(manifest, null, 2)) writeFileSync('manifest.json', JSON.stringify(manifest, null, 2))

View File

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

View File

@@ -1,5 +1,6 @@
export const CHANNEL_GET_CONFIG = 'llonebot_get_config' export const CHANNEL_GET_CONFIG = 'llonebot_get_config'
export const CHANNEL_SET_CONFIG = 'llonebot_set_config' export const CHANNEL_SET_CONFIG = 'llonebot_set_config'
export const CHANNEL_SET_CONFIG_CONFIRMED = 'llonebot_set_config_confirmed'
export const CHANNEL_LOG = 'llonebot_log' export const CHANNEL_LOG = 'llonebot_log'
export const CHANNEL_ERROR = 'llonebot_error' export const CHANNEL_ERROR = 'llonebot_error'
export const CHANNEL_UPDATE = 'llonebot_update' export const CHANNEL_UPDATE = 'llonebot_update'

View File

@@ -1,13 +1,8 @@
import fs from 'node:fs' import fs from 'node:fs'
import { Config, OB11Config } from './types'
import { mergeNewProperties } from './utils/helper'
import path from 'node:path' import path from 'node:path'
import { getSelfUin } from './data' import { Config, OB11Config } from './types'
import { DATA_DIR } from './utils' import { selfInfo, DATA_DIR } from './globalVars'
import { mergeNewProperties } from './utils/misc'
export const HOOK_LOG = false
export const ALLOW_SEND_TEMP_MSG = false
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string private readonly configPath: string
@@ -26,7 +21,7 @@ export class ConfigUtil {
} }
reloadConfig(): Config { reloadConfig(): Config {
let ob11Default: OB11Config = { const ob11Default: OB11Config = {
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: '', httpSecret: '',
@@ -38,9 +33,9 @@ export class ConfigUtil {
enableWsReverse: false, enableWsReverse: false,
messagePostFormat: 'array', messagePostFormat: 'array',
enableHttpHeart: false, enableHttpHeart: false,
enableQOAutoQuote: false listenLocalhost: false
} }
let defaultConfig: Config = { const defaultConfig: Config = {
enableLLOB: true, enableLLOB: true,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
@@ -52,6 +47,7 @@ export class ConfigUtil {
autoDeleteFile: false, autoDeleteFile: false,
autoDeleteFileSecond: 60, autoDeleteFileSecond: 60,
musicSignUrl: '', musicSignUrl: '',
msgCacheExpire: 120
} }
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
@@ -70,7 +66,6 @@ export class ConfigUtil {
this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http') this.checkOldConfig(jsonData.ob11, jsonData, 'httpPort', 'http')
this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts') this.checkOldConfig(jsonData.ob11, jsonData, 'httpHosts', 'hosts')
this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort') this.checkOldConfig(jsonData.ob11, jsonData, 'wsPort', 'wsPort')
// console.log("get config", jsonData);
this.config = jsonData this.config = jsonData
return this.config return this.config
} }
@@ -82,21 +77,21 @@ export class ConfigUtil {
} }
private checkOldConfig( private checkOldConfig(
currentConfig: Config | OB11Config, currentConfig: OB11Config,
oldConfig: Config | OB11Config, oldConfig: Config,
currentKey: string, currentKey: 'httpPort' | 'httpHosts' | 'wsPort',
oldKey: string, oldKey: 'http' | 'hosts' | 'wsPort',
) { ) {
// 迁移旧的配置到新配置,避免用户重新填写配置 // 迁移旧的配置到新配置,避免用户重新填写配置
const oldValue = oldConfig[oldKey] const oldValue = oldConfig[oldKey]
if (oldValue) { if (oldValue) {
currentConfig[currentKey] = oldValue Object.assign(currentConfig, { [currentKey]: oldValue })
delete oldConfig[oldKey] delete oldConfig[oldKey]
} }
} }
} }
export function getConfigUtil() { export function getConfigUtil() {
const configFilePath = path.join(DATA_DIR, `config_${getSelfUin()}.json`) const configFilePath = path.join(DATA_DIR, `config_${selfInfo.uin}.json`)
return new ConfigUtil(configFilePath) return new ConfigUtil(configFilePath)
} }

View File

@@ -1,130 +0,0 @@
import {
type Friend,
type Group,
type GroupMember,
type SelfInfo,
} from '../ntqqapi/types'
import { type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { log } from './utils/log'
import { isNumeric } from './utils/helper'
import { NTQQFriendApi, NTQQUserApi } from '../ntqqapi/api'
export let groups: Group[] = []
export let friends: Friend[] = []
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
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> {
let filterKey = isNumeric(uinOrUid.toString()) ? 'uin' : 'uid'
let filterValue = uinOrUid
let friend = friends.find((friend) => friend[filterKey] === filterValue.toString())
if (!friend) {
try {
const _friends = await NTQQFriendApi.getFriends(true)
friend = _friends.find((friend) => friend[filterKey] === filterValue.toString())
if (friend) {
friends.push(friend)
}
} catch (e: any) {
log('刷新好友列表失败', e.stack.toString())
}
}
return friend
}
export async function getGroup(qq: string): Promise<Group | undefined> {
let group = groups.find((group) => group.groupCode === qq.toString())
if (!group) {
try {
const _groups = await NTQQGroupApi.getGroups(true)
group = _groups.find((group) => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
}
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) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
let members = groupMembers.get(groupQQ)
if (!members) {
try {
members = await NTQQGroupApi.getGroupMembers(groupQQ)
// 更新群成员列表
groupMembers.set(groupQQ, members)
}
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
}
const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export async function getSelfNick(force = false): Promise<string> {
if (!selfInfo.nick || force) {
const userInfo = await NTQQUserApi.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
selfInfo.nick = userInfo.nick
return userInfo.nick
}
}
return selfInfo.nick
}
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,253 +0,0 @@
import { Level } from 'level'
import { type GroupNotify, RawMessage } from '../ntqqapi/types'
import { DATA_DIR } from './utils'
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 | undefined
public cache: Record<string, RawMessage | string | FileCache | GroupNotify | ReceiveTempUinMap> = {} // <msg_id_ | msg_short_id_ | msg_seq_id_><id>: RawMessage
private currentShortId: number | undefined
/*
* 数据库结构
* msg_id_101231230999: {} // 长id: RawMessage
* msg_short_id_1: 101231230999 // 短id: 长id
* msg_seq_id_1: 101231230999 // 序列id: 长id
* file_7827DBAFJFW2323.png: {} // 文件名: FileCache
* */
constructor() {
}
init(uin: string) {
const DB_PATH = DATA_DIR + `/msg_${uin}`
this.db = new Level(DB_PATH, { valueEncoding: 'json' })
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 | undefined> {
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: any) {
log('getMsgByShortId db error', e.stack.toString())
}
}
async getMsgByLongId(longId: string): Promise<RawMessage | undefined> {
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 | undefined> {
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: any) {
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: RawMessage | undefined = 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
}
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
this.addCache(msg)
// 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: RawMessage | undefined = 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 {
const id = 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: any) {
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 {
const 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 {
const data = await this.db?.get(key)
return JSON.parse(data!)
} catch (e) {
// log("getGroupNotify db error", e.stack.toString())
}
}
}
export const dbUtil = new DBUtil()

22
src/common/globalVars.ts Normal file
View File

@@ -0,0 +1,22 @@
import { LLOneBotError } from './types'
import { SelfInfo } from '../ntqqapi/types'
import path from 'node:path'
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
otherError: 'LLOneBot 未能正常启动,请检查日志查看错误',
}
export const DATA_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.data
export const TEMP_DIR: string = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR: string = global.LiteLoader.plugins['LLOneBot'].path.plugin
export const LOG_DIR = path.join(DATA_DIR, 'logs')
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}

View File

@@ -1,119 +0,0 @@
import express, { Express, Request, Response } from 'express'
import http from 'node:http'
import cors from 'cors'
import { log } from '../utils/log'
import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase {
name: string = 'LLOneBot'
private readonly expressAPP: Express
private server: http.Server | null = null
constructor() {
this.expressAPP = express()
// 添加 CORS 中间件
this.expressAPP.use(cors())
this.expressAPP.use(express.urlencoded({ extended: true, limit: '5000mb' }))
this.expressAPP.use((req, res, next) => {
// 兼容处理没有带content-type的请求
// log("req.headers['content-type']", req.headers['content-type'])
req.headers['content-type'] = 'application/json'
const originalJson = express.json({ limit: '5000mb' })
// 调用原始的express.json()处理器
originalJson(req, res, (err) => {
if (err) {
log('Error parsing JSON:', err)
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)
}
if (serverToken && clientToken != serverToken) {
return res.status(403).send(JSON.stringify({ message: 'token verify failed!' }))
}
next()
}
start(port: number) {
try {
this.expressAPP.get('/', (req: Request, res: Response) => {
res.send(`${this.name} 已启动`)
})
this.listen(port)
llonebotError.httpServerError = ''
} catch (e: any) {
log('HTTP服务启动失败', e.toString())
llonebotError.httpServerError = 'HTTP服务启动失败, ' + e.toString()
}
}
stop() {
llonebotError.httpServerError = ''
if (this.server) {
this.server.close()
this.server = null
}
}
restart(port: number) {
this.stop()
this.start(port)
}
abstract handleFailed(res: Response, payload: any, err: any): void
registerRouter(method: 'post' | 'get' | string, url: string, handler: RegisterHandler) {
if (!url.startsWith('/')) {
url = '/' + url
}
if (!this.expressAPP[method]) {
const err = `${this.name} register router failed${method} not exist`
log(err)
throw err
}
this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => {
let payload = req.body
if (method == 'get') {
payload = req.query
} 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())
}
})
}
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,93 +0,0 @@
import { WebSocket, WebSocketServer } from 'ws'
import urlParse from 'url'
import { IncomingMessage } from 'node:http'
import { log } from '../utils/log'
import { getConfigUtil } from '../config'
import { llonebotError } from '../data'
class WebsocketClientBase {
private wsClient: WebSocket | undefined
constructor() { }
send(msg: string) {
if (this.wsClient && this.wsClient.readyState == WebSocket.OPEN) {
this.wsClient.send(msg)
}
}
onMessage(msg: string) { }
}
export class WebsocketServerBase {
private ws: WebSocketServer | null = null
constructor() {
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())
})
})
}
stop() {
llonebotError.wsServerError = ''
this.ws?.close((err) => {
log('ws server close failed!', err)
})
this.ws = null
}
restart(port: number) {
this.stop()
this.start(port)
}
authorize(wsClient: WebSocket, req) {
let token = getConfigUtil().getConfig().token
const url = req.url.split('?').shift()
log('ws connect', url)
let clientToken: string = ''
const authHeader = req.headers['authorization']
if (authHeader) {
clientToken = authHeader.split('Bearer ').pop()
log('receive ws header token', clientToken)
} else {
const parsedUrl = urlParse.parse(req.url, true)
const urlToken = parsedUrl.query.access_token
if (urlToken) {
if (Array.isArray(urlToken)) {
clientToken = urlToken[0]
} else {
clientToken = urlToken
}
log('receive ws url token', clientToken)
}
}
if (token && clientToken != token) {
this.authorizeFailed(wsClient)
return wsClient.close()
}
}
authorizeFailed(wsClient: WebSocket) { }
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) { }
onMessage(wsClient: WebSocket, url: string, msg: string) { }
sendHeart() { }
}

View File

@@ -10,17 +10,24 @@ export interface OB11Config {
enableWsReverse?: boolean enableWsReverse?: boolean
messagePostFormat?: 'array' | 'string' messagePostFormat?: 'array' | 'string'
enableHttpHeart?: boolean enableHttpHeart?: boolean
enableQOAutoQuote: boolean // 快速操作回复自动引用原消息 /**
* 快速操作回复自动引用原消息
* @deprecated
*/
enableQOAutoQuote?: boolean
listenLocalhost: boolean
} }
export interface CheckVersion { export interface CheckVersion {
result: boolean result: boolean
version: string version: string
} }
export interface Config { export interface Config {
enableLLOB: boolean enableLLOB: boolean
ob11: OB11Config ob11: OB11Config
token?: string token?: string
heartInterval?: number // ms heartInterval: number // ms
enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64 enableLocalFile2Url?: boolean // 开启后本地文件路径图片会转成http链接, 语音会转成base64
debug?: boolean debug?: boolean
reportSelfMessage?: boolean reportSelfMessage?: boolean
@@ -30,6 +37,14 @@ export interface Config {
ffmpeg?: string // ffmpeg路径 ffmpeg?: string // ffmpeg路径
musicSignUrl?: string musicSignUrl?: string
ignoreBeforeLoginMsg?: boolean ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */
msgCacheExpire?: number
/** @deprecated */
http?: string
/** @deprecated */
hosts?: string[]
/** @deprecated */
wsPort?: string
} }
export interface LLOneBotError { export interface LLOneBotError {
@@ -41,11 +56,22 @@ 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
elementId: string elementId: string
downloadFunc?: () => Promise<void> elementType: number
}
export interface FileCacheV2 {
fileName: string
fileSize: string
fileUuid: string
msgId: string
msgTime: number
peerUid: string
chatType: number
elementId: string
elementType: number
} }

View File

@@ -1,231 +0,0 @@
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
}
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
}
CreatEventFunction<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
}
}
CreatListenerFunction<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.CreatEventFunction<(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.CreatEventFunction<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.CreatListenerFunction(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.CreatListenerFunction(ListenerMainName)
const EventFunc = this.CreatEventFunction<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

@@ -1,52 +0,0 @@
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

@@ -2,11 +2,10 @@ import path from 'node:path'
import ffmpeg from 'fluent-ffmpeg' import ffmpeg from 'fluent-ffmpeg'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm' import { decode, encode, getDuration, getWavFileInfo, isWav, isSilk, EncodeResult } from 'silk-wasm'
import { log } from './log' import { TEMP_DIR } from '../globalVars'
import { TEMP_DIR } from './index'
import { getConfigUtil } from '../config'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Readable } from 'node:stream' import { Readable } from 'node:stream'
import { Context } from 'cordis'
interface FFmpegOptions { interface FFmpegOptions {
input?: string[] input?: string[]
@@ -15,14 +14,14 @@ interface FFmpegOptions {
type Input = string | Readable type Input = string | Readable
function convert(input: Input, options: FFmpegOptions): Promise<Buffer> function convert(ctx: Context, input: Input, options: FFmpegOptions): Promise<Buffer>
function convert(input: Input, options: FFmpegOptions, outputPath: string): Promise<string> function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath: string): Promise<string>
function convert(input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer> | Promise<string> { function convert(ctx: Context, input: Input, options: FFmpegOptions, outputPath?: string): Promise<Buffer | string> {
return new Promise<any>((resolve, reject) => { return new Promise((resolve, reject) => {
const chunks: Buffer[] = [] const chunks: Buffer[] = []
let command = ffmpeg(input) let command = ffmpeg(input)
.on('error', err => { .on('error', err => {
log(`FFmpeg处理转换出错: `, err.message) ctx.logger.error(`FFmpeg处理转换出错: `, err.message)
reject(err) reject(err)
}) })
.on('end', () => { .on('end', () => {
@@ -38,7 +37,7 @@ function convert(input: Input, options: FFmpegOptions, outputPath?: string): Pro
if (options.output) { if (options.output) {
command = command.outputOptions(options.output) command = command.outputOptions(options.output)
} }
const ffmpegPath = getConfigUtil().getConfig().ffmpeg const ffmpegPath: string | undefined = ctx.config.ffmpeg
if (ffmpegPath) { if (ffmpegPath) {
command = command.setFfmpegPath(ffmpegPath) command = command.setFfmpegPath(ffmpegPath)
} }
@@ -53,17 +52,17 @@ function convert(input: Input, options: FFmpegOptions, outputPath?: string): Pro
}) })
} }
export async function encodeSilk(filePath: string) { export async function encodeSilk(ctx: Context, filePath: string) {
try { try {
const file = await fsPromise.readFile(filePath) const file = await fsPromise.readFile(filePath)
if (!isSilk(file)) { if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`) ctx.logger.info(`语音文件${filePath}需要转换成silk`)
let result: EncodeResult let result: EncodeResult
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000] const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) { if (isWav(file) && allowSampleRate.includes(getWavFileInfo(file).fmt.sampleRate)) {
result = await encode(file, 0) result = await encode(file, 0)
} else { } else {
const input = await convert(filePath, { const input = await convert(ctx, filePath, {
output: [ output: [
'-ar 24000', '-ar 24000',
'-ac 1', '-ac 1',
@@ -74,7 +73,7 @@ export async function encodeSilk(filePath: string) {
} }
const pttPath = path.join(TEMP_DIR, randomUUID()) const pttPath = path.join(TEMP_DIR, randomUUID())
await fsPromise.writeFile(pttPath, result.data) await fsPromise.writeFile(pttPath, result.data)
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration) ctx.logger.info(`语音文件${filePath}转换成功!`, pttPath, `时长:`, result.duration)
return { return {
converted: true, converted: true,
path: pttPath, path: pttPath,
@@ -85,8 +84,8 @@ export async function encodeSilk(filePath: string) {
let duration = 1 let duration = 1
try { try {
duration = getDuration(silk) / 1000 duration = getDuration(silk) / 1000
} catch (e: any) { } catch (e) {
log('获取语音文件时长失败, 默认为1秒', filePath, e.stack) ctx.logger.warn('获取语音文件时长失败, 默认为1秒', filePath, (e as Error).stack)
} }
return { return {
converted: false, converted: false,
@@ -94,26 +93,26 @@ export async function encodeSilk(filePath: string) {
duration, duration,
} }
} }
} catch (error: any) { } catch (err) {
log('convert silk failed', error.stack) ctx.logger.error('convert silk failed', (err as Error).stack)
return {} return {}
} }
} }
type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' type OutFormat = 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
export async function decodeSilk(inputFilePath: string, outFormat: OutFormat = 'mp3') { export async function decodeSilk(ctx: Context, inputFilePath: string, outFormat: OutFormat) {
const silk = await fsPromise.readFile(inputFilePath) const silk = await fsPromise.readFile(inputFilePath)
const { data } = await decode(silk, 24000) const { data } = await decode(silk, 24000)
const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath)) const tmpPath = path.join(TEMP_DIR, path.basename(inputFilePath))
const outFilePath = tmpPath + `.${outFormat}` const outFilePath = tmpPath + `.${outFormat}`
const pcmFilePath = tmpPath + '.pcm' const pcmFilePath = tmpPath + '.pcm'
await fsPromise.writeFile(pcmFilePath, data) await fsPromise.writeFile(pcmFilePath, data)
return convert(pcmFilePath, { return convert(ctx, pcmFilePath, {
input: [ input: [
'-f s16le', '-f s16le',
'-ar 24000', '-ar 24000',
'-ac 1' '-ac 1'
] ]
}, outFilePath) }, outFilePath)
} }

View File

@@ -1,10 +1,10 @@
import fs from 'node:fs' import fs from 'node:fs'
import fsPromise from 'node:fs/promises' import fsPromise from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { log, TEMP_DIR } from './index' import { TEMP_DIR } from '../globalVars'
import { dbUtil } from '../db'
import * as fileType from 'file-type'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { fileTypeFromFile } from 'file-type'
export function isGIF(path: string) { export function isGIF(path: string) {
const buffer = Buffer.alloc(4) const buffer = Buffer.alloc(4)
@@ -33,31 +33,6 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
}) })
} }
export async function file2base64(path: string) {
let result = {
err: '',
data: '',
}
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000)
} catch (e: any) {
result.err = e.toString()
return result
}
const data = await 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) => {
// 创建一个流式读取器 // 创建一个流式读取器
@@ -82,154 +57,153 @@ export function calculateFileMD5(filePath: string): Promise<string> {
}) })
} }
export interface HttpDownloadOptions { export enum FileUriType {
url: string Unknown = 0,
headers?: Record<string, string> | string FileURL = 1,
RemoteURL = 2,
OneBotBase64 = 3,
DataURL = 4,
Path = 5
} }
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
let url: string
let headers: Record<string, string> = {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
}
if (typeof options === 'string') {
url = options
} else {
url = options.url
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers)
} else {
headers = options.headers
}
}
}
const fetchRes = await fetch(url, { headers })
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`)
return Buffer.from(await fetchRes.arrayBuffer()) export function checkUriType(uri: string): { type: FileUriType } {
if (uri.startsWith('base64://')) {
return { type: FileUriType.OneBotBase64 }
}
if (uri.startsWith('data:')) {
return { type: FileUriType.DataURL }
}
if (uri.startsWith('http://') || uri.startsWith('https://')) {
return { type: FileUriType.RemoteURL }
}
if (uri.startsWith('file://')) {
return { type: FileUriType.FileURL }
}
try {
if (fs.existsSync(uri)) return { type: FileUriType.Path }
} catch { }
return { type: FileUriType.Unknown }
}
interface FetchFileRes {
data: Buffer
url: string
}
export async function fetchFile(url: string, headersInit?: Record<string, string>): Promise<FetchFileRes> {
const headers = new Headers({
'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',
'Host': new URL(url).hostname,
...headersInit
})
let raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
if (raw.status === 403 && !headers.has('Referer')) {
headers.set('Referer', url)
raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause
}
throw err
})
}
if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`)
return {
data: Buffer.from(await raw.arrayBuffer()),
url: raw.url
}
} }
type Uri2LocalRes = { type Uri2LocalRes = {
success: boolean success: boolean
errMsg: string errMsg: string
fileName: string fileName: string
ext: string
path: string path: string
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, fileName: string | null = null): Promise<Uri2LocalRes> { export async function uri2local(uri: string, filename?: string, needExt?: boolean): Promise<Uri2LocalRes> {
let res = { const { type } = checkUriType(uri)
success: false,
errMsg: '', if (type === FileUriType.FileURL) {
fileName: '', const filePath = fileURLToPath(uri)
ext: '', const fileName = path.basename(filePath)
path: '', return { success: true, errMsg: '', fileName, path: filePath, isLocal: true }
isLocal: false,
}
if (!fileName) {
fileName = randomUUID()
}
let filePath = path.join(TEMP_DIR, fileName)
let url: URL | null = null
try {
url = new URL(uri)
} catch (e: any) {
res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在`
return res
} }
// log("uri protocol", url.protocol, uri); if (type === FileUriType.Path) {
if (url.protocol == 'base64:') { const fileName = path.basename(uri)
// base64转成文件 return { success: true, errMsg: '', fileName, path: uri, isLocal: true }
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
}
} else {
const cache = await dbUtil.getFileCache(uri)
if (cache) {
filePath = cache.filePath
} else {
filePath = uri
}
}
res.isLocal = true
} }
// else{
// res.errMsg = `不支持的file协议,` + url.protocol if (type === FileUriType.RemoteURL) {
// return res
// }
// if (isGIF(filePath) && !res.isLocal) {
// await fs.rename(filePath, filePath + ".gif");
// filePath += ".gif";
// }
if (!res.isLocal && !res.ext) {
try { try {
const ext = (await fileType.fileTypeFromFile(filePath))?.ext const res = await fetchFile(uri)
if (ext) { const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
log('获取文件类型', ext, filePath) if (match?.[1]) {
await fsPromise.rename(filePath, filePath + `.${ext}`) filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_')
filePath += `.${ext}` } else {
res.fileName += `.${ext}` filename ??= randomUUID()
res.ext = ext
} }
let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data)
if (needExt && !path.extname(filePath)) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e) { } catch (e) {
// log("获取文件类型失败", filePath,e.stack) const errMsg = `${uri} 下载失败, ${(e as Error).message}`
return { success: false, errMsg, fileName: '', path: '', isLocal: false }
} }
} }
res.success = true
res.path = filePath if (type === FileUriType.OneBotBase64) {
return res filename ??= randomUUID()
let filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
}
if (type === FileUriType.DataURL) {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri)
if (capture) {
filename ??= randomUUID()
const [, _type, base64] = capture
let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await fileTypeFromFile(filePath))?.ext
filename += `.${ext}`
await fsPromise.rename(filePath, `${filePath}.${ext}`)
filePath = `${filePath}.${ext}`
}
return { success: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
}
}
return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false }
} }
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 (const 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()) {

View File

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

View File

@@ -1,18 +1,7 @@
import path from 'node:path'
import fs from 'fs'
export * from './file' export * from './file'
export * from './helper' export * from './misc'
export * from './log' export * from './legacyLog'
export * from './qqlevel' export * from './misc'
export * from './QQBasicInfo'
export * from './upgrade' export * from './upgrade'
export const DATA_DIR = global.LiteLoader.plugins['LLOneBot'].path.data export { getVideoInfo, checkFfmpeg } from './video'
export const TEMP_DIR = path.join(DATA_DIR, 'temp')
export const PLUGIN_DIR = global.LiteLoader.plugins['LLOneBot'].path.plugin
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true })
}
export { getVideoInfo } from './video'
export { checkFfmpeg } from './video'
export { encodeSilk } from './audio' export { encodeSilk } from './audio'

View File

@@ -0,0 +1,42 @@
import fs from 'fs'
import path from 'node:path'
import { getConfigUtil } from '../config'
import { LOG_DIR } from '../globalVars'
import { Dict } from 'cosmokit'
function truncateString(obj: Dict | null, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...'
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength)
}
})
}
return obj
}
export const logFileName = `llonebot-${new Date().toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
export function log(...msg: unknown[]) {
if (!getConfigUtil().getConfig().log) {
return
}
let logMsg = ''
for (const msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === 'object') {
logMsg += JSON.stringify(truncateString(msgItem)) + ' '
continue
}
logMsg += msgItem + ' '
}
const currentDateTime = new Date().toLocaleString()
logMsg = `${currentDateTime} ${logMsg}\n\n`
fs.appendFile(path.join(LOG_DIR, logFileName), logMsg, () => { })
}

View File

@@ -1,35 +0,0 @@
import { getSelfInfo } from '../data'
import fs from 'fs'
import path from 'node:path'
import { DATA_DIR, truncateString } from './index'
import { getConfigUtil } from '../config'
const date = new Date()
const logFileName = `llonebot-${date.toLocaleString('zh-CN')}.log`.replace(/\//g, '-').replace(/:/g, '-')
const logDir = path.join(DATA_DIR, 'logs')
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return //console.log(...msg);
}
const 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 + ' '
}
let currentDateTime = new Date().toLocaleString()
logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n\n`
// sendLog(...msg);
// console.log(msg)
fs.appendFile(path.join(logDir, logFileName), logMsg, () => {})
}

39
src/common/utils/misc.ts Normal file
View File

@@ -0,0 +1,39 @@
import { QQLevel } from '@/ntqqapi/types'
import { Dict, isNullable } from 'cosmokit'
export function isNumeric(str: string) {
return /^\d+$/.test(str)
}
export function calcQQLevel(level: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum
}
/** QQ Build Version */
export function getBuildVersion(): number {
const version: string = globalThis.LiteLoader.versions.qqnt
return +version.split('-')[1]
}
/** 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 */
export function mergeNewProperties(newObj: Dict, oldObj: Dict) {
Object.keys(newObj).forEach((key) => {
// 如果老对象不存在当前属性,则直接复制
if (!oldObj.hasOwnProperty(key)) {
oldObj[key] = newObj[key]
} else {
// 如果老对象和新对象的当前属性都是对象,则递归合并
if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
mergeNewProperties(newObj[key], oldObj[key])
} else if (typeof oldObj[key] === 'object' || typeof newObj[key] === 'object') {
// 属性冲突,有一方不是对象,直接覆盖
oldObj[key] = newObj[key]
}
}
})
}
export function filterNullable<T>(array: T[]) {
return array.filter(e => !isNullable(e)) as NonNullable<T>[]
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { log } from './log' import { Context } from 'cordis'
export interface IdMusicSignPostData { export interface IdMusicSignPostData {
type: 'qq' | '163' type: 'qq' | '163'
@@ -19,7 +19,7 @@ export type MusicSignPostData = IdMusicSignPostData | CustomMusicSignPostData
export class MusicSign { export class MusicSign {
private readonly url: string private readonly url: string
constructor(url: string) { constructor(protected ctx: Context, url: string) {
this.url = url this.url = url
} }
@@ -31,7 +31,7 @@ export class MusicSign {
}) })
if (!resp.ok) throw new Error(resp.statusText) if (!resp.ok) throw new Error(resp.statusText)
const data = await resp.text() const data = await resp.text()
log('音乐消息生成成功', data) this.ctx.logger.info('音乐消息生成成功', data)
return data return data
} }
} }

View File

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

View File

@@ -1,71 +1,71 @@
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/MessageUnique.ts#L5
export class LimitedHashTable<K, V> { export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map() private keyToValue: Map<K, V> = new Map()
private valueToKey: Map<V, K> = new Map() private valueToKey: Map<V, K> = new Map()
private maxSize: number private maxSize: number
constructor(maxSize: number) { constructor(maxSize: number) {
this.maxSize = maxSize this.maxSize = maxSize
} }
resize(count: number) { resize(count: number) {
this.maxSize = count this.maxSize = count
} }
set(key: K, value: V): void { set(key: K, value: V): void {
this.keyToValue.set(key, value) this.keyToValue.set(key, value)
this.valueToKey.set(value, key) this.valueToKey.set(value, key)
while (this.keyToValue.size !== this.valueToKey.size) { while (this.keyToValue.size !== this.valueToKey.size) {
console.log('keyToValue.size !== valueToKey.size Error Atom') this.keyToValue.clear()
this.keyToValue.clear() this.valueToKey.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)
}
} }
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 { getValue(key: K): V | undefined {
return this.keyToValue.get(key) return this.keyToValue.get(key)
} }
getKey(value: V): K | undefined { getKey(value: V): K | undefined {
return this.valueToKey.get(value) return this.valueToKey.get(value)
} }
deleteByValue(value: V): void { deleteByValue(value: V): void {
const key = this.valueToKey.get(value) const key = this.valueToKey.get(value)
if (key !== undefined) { if (key !== undefined) {
this.keyToValue.delete(key) this.keyToValue.delete(key)
this.valueToKey.delete(value) this.valueToKey.delete(value)
}
} }
}
deleteByKey(key: K): void { deleteByKey(key: K): void {
const value = this.keyToValue.get(key) const value = this.keyToValue.get(key)
if (value !== undefined) { if (value !== undefined) {
this.keyToValue.delete(key) this.keyToValue.delete(key)
this.valueToKey.delete(value) this.valueToKey.delete(value)
}
} }
}
getKeyList(): K[] { getKeyList(): K[] {
return Array.from(this.keyToValue.keys()) return Array.from(this.keyToValue.keys())
} }
//获取最近刚写入的几个值 //获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined { getHeads(size: number): { key: K, value: V }[] | undefined {
const keyList = this.getKeyList() const keyList = this.getKeyList()
if (keyList.length === 0) { if (keyList.length === 0) {
return undefined 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
} }
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,23 +1,26 @@
import { version } from '../../version' import path from 'node:path'
import * as path from 'node:path'
import * as fs from 'node:fs'
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.'
import compressing from 'compressing' import compressing from 'compressing'
import { writeFile } from 'node:fs/promises'
import { version } from '../../version'
import { copyFolder, log, fetchFile } from '.'
import { PLUGIN_DIR, TEMP_DIR } from '../globalVars'
const downloadMirrorHosts = ['https://mirror.ghproxy.com/'] const downloadMirrorHosts = ['https://ghp.ci/']
const checkVersionMirrorHosts = ['https://kkgithub.com'] const releasesMirrorHosts = ['https://kkgithub.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 latest version', latestVersion)
const currentVersion: string[] = version.split('.') const currentVersion = version.split('.')
log('llonebot current version', currentVersion) //log('llonebot current version', currentVersion)
for (let k of [0, 1, 2]) { for (const k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) { const latest = parseInt(latestVersion[k])
const current = parseInt(currentVersion[k])
if (latest > current) {
log('') log('')
return { result: true, version: latestVersionText } return { result: true, version: latestVersionText }
} else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) { } else if (latest < current) {
break break
} }
} }
@@ -27,14 +30,14 @@ export async function checkNewVersion() {
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 res = await fetchFile(mirrorGithub + downloadUrl)
fs.writeFileSync(filePath, buffer) await writeFile(filePath, res.data)
downloadSuccess = true downloadSuccess = true
break break
} catch (e) { } catch (e) {
@@ -46,14 +49,14 @@ export async function upgradeLLOneBot() {
return false return false
} }
const temp_ver_dir = path.join(TEMP_DIR, 'LLOneBot' + latestVersion) const temp_ver_dir = path.join(TEMP_DIR, 'LLOneBot' + latestVersion)
let uncompressedPromise = async function () { const uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => { return new Promise<boolean>(resolve => {
compressing.zip compressing.zip
.uncompress(filePath, temp_ver_dir) .uncompress(filePath, temp_ver_dir)
.then(() => { .then(() => {
resolve(true) resolve(true)
}) })
.catch((reason: any) => { .catch(reason => {
log('llonebot upgrade failed, ', reason) log('llonebot upgrade failed, ', reason)
if (reason?.errno == -4082) { if (reason?.errno == -4082) {
resolve(true) resolve(true)
@@ -72,26 +75,34 @@ export async function upgradeLLOneBot() {
} }
export async function getRemoteVersion() { export async function getRemoteVersion() {
let Version = '' for (const mirror of releasesMirrorHosts) {
for (let i = 0; i < checkVersionMirrorHosts.length; i++) { const version = await getRemoteVersionByReleasesMirror(mirror)
let mirrorGithub = checkVersionMirrorHosts[i] if (version) {
let tVersion = await getRemoteVersionByMirror(mirrorGithub) return version
if (tVersion && tVersion != '') { }
Version = tVersion }
break for (const mirror of downloadMirrorHosts) {
const version = await getRemoteVersionByDownloadMirror(mirror)
if (version) {
return version
} }
} }
return Version
}
export async function getRemoteVersionByMirror(mirrorGithub: string) {
let releasePage = 'error'
try {
releasePage = (await httpDownload(mirrorGithub + '/LLOneBot/LLOneBot/releases')).toString()
// log("releasePage", releasePage);
if (releasePage === 'error') return ''
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch {}
return '' return ''
} }
export async function getRemoteVersionByDownloadMirror(mirrorGithub: string) {
try {
const source = 'https://raw.githubusercontent.com/LLOneBot/LLOneBot/main/src/version.ts'
const page = (await fetchFile(mirrorGithub + source)).data.toString()
return page.match(/(\d+\.\d+\.\d+)/)?.[0]
} catch (e) {
log(e?.toString())
}
}
export async function getRemoteVersionByReleasesMirror(mirrorGithub: string) {
try {
const page = (await fetchFile(mirrorGithub + '/LLOneBot/LLOneBot/releases')).data.toString()
return page.match(new RegExp('(?<=(tag/v)).*?(?=("))'))?.[0]
} catch { }
}

File diff suppressed because one or more lines are too long

11
src/global.d.ts vendored
View File

@@ -1,8 +1,9 @@
import { type LLOneBot } from './preload' import type { LLOneBot } from './preload'
import { Dict } from 'cosmokit'
declare global { declare global {
interface Window { var llonebot: LLOneBot
llonebot: LLOneBot var LiteLoader: Dict
LiteLoader: Record<string, any> var authData: Dict | undefined
} var navigation: Dict | undefined
} }

View File

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

35
src/main/log.ts Normal file
View File

@@ -0,0 +1,35 @@
import path from 'node:path'
import { Context, Logger } from 'cordis'
import { appendFile } from 'node:fs'
import { LOG_DIR, selfInfo } from '@/common/globalVars'
import { noop } from 'cosmokit'
interface Config {
enable: boolean
filename: string
}
export default class Log {
static name = 'logger'
constructor(ctx: Context, cfg: Config) {
Logger.targets.splice(0, Logger.targets.length)
let enable = cfg.enable
const file = path.join(LOG_DIR, cfg.filename)
const target: Logger.Target = {
colors: 0,
record: (record: Logger.Record) => {
if (!enable) {
return
}
const dateTime = new Date(record.timestamp).toLocaleString()
const content = `${dateTime} [${record.type}] ${selfInfo.nick}(${selfInfo.uin}) | ${record.name} ${record.content}\n\n`
appendFile(file, content, noop)
},
}
Logger.targets.push(target)
ctx.on('llonebot/config-updated', input => {
enable = input.log!
})
}
}

View File

@@ -1,8 +1,9 @@
// 运行在 Electron 主进程 下的插件入口 import path from 'node:path'
import Log from './log'
import Core from '../ntqqapi/core'
import OneBot11Adapter from '../onebot11/adapter'
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import fs from 'node:fs' import { Config as LLOBConfig } from '../common/types'
import { Config } from '../common/types'
import { import {
CHANNEL_CHECK_VERSION, CHANNEL_CHECK_VERSION,
CHANNEL_ERROR, CHANNEL_ERROR,
@@ -11,55 +12,60 @@ import {
CHANNEL_SELECT_FILE, CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
CHANNEL_UPDATE, CHANNEL_UPDATE,
CHANNEL_SET_CONFIG_CONFIRMED
} from '../common/channels' } from '../common/channels'
import { ob11WebsocketServer } from '../onebot11/server/ws/WebsocketServer' import { getBuildVersion } from '../common/utils'
import { DATA_DIR } from '../common/utils' import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook'
import {
getGroupMember,
llonebotError,
setSelfInfo,
getSelfInfo,
getSelfUid,
getSelfUin
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import {
FriendRequestNotify,
GroupNotifies,
GroupNotifyTypes,
RawMessage,
BuddyReqType,
} from '../ntqqapi/types'
import { httpHeart, ob11HTTPServer } from '../onebot11/server/http'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { ob11ReverseWebsockets } from '../onebot11/server/ws/ReverseWebsocket'
import { OB11GroupRequestEvent } from '../onebot11/event/request/OB11GroupRequest'
import { OB11FriendRequestEvent } from '../onebot11/event/request/OB11FriendRequest'
import path from 'node:path'
import { dbUtil } from '../common/db'
import { setConfig } from './setConfig'
import { NTQQUserApi, NTQQGroupApi } from '../ntqqapi/api'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { log } from '../common/utils/log'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../onebot11/event/notice/OB11GroupDecreaseEvent' import { getSession } from '../ntqqapi/wrapper'
import '../ntqqapi/wrapper' import { Context } from 'cordis'
import { NTEventDispatch } from '../common/utils/EventTask' import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars'
import { wrapperConstructor, getSession } from '../ntqqapi/wrapper' import { log, logFileName } from '../common/utils/legacyLog'
import {
NTQQFileApi,
NTQQFileCacheApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQMsgApi,
NTQQUserApi,
NTQQWebApi,
NTQQWindowApi
} from '../ntqqapi/api'
import { mkdir } from 'node:fs/promises'
import { existsSync, mkdirSync } from 'node:fs'
import Database from 'minato'
import SQLiteDriver from '@minatojs/driver-sqlite'
import Store from './store'
declare module 'cordis' {
interface Events {
'llonebot/config-updated': (input: LLOBConfig) => void
}
}
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => { if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true })
}
if (!existsSync(LOG_DIR)) {
mkdirSync(LOG_DIR)
}
ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
return checkNewVersion() return checkNewVersion()
}) })
ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => {
ipcMain.handle(CHANNEL_UPDATE, async () => {
return upgradeLLOneBot() return upgradeLLOneBot()
}) })
ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => {
ipcMain.handle(CHANNEL_SELECT_FILE, async () => {
const selectPath = new Promise<string>((resolve, reject) => { const selectPath = new Promise<string>((resolve, reject) => {
dialog dialog
.showOpenDialog({ .showOpenDialog({
@@ -72,11 +78,9 @@ function onLoad() {
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() } else {
// config.ffmpeg = path.join(result.filePaths[0]); resolve('')
// getConfigUtil().setConfig(config);
} }
resolve('')
}) })
.catch((err) => { .catch((err) => {
reject(err) reject(err)
@@ -89,377 +93,147 @@ function onLoad() {
return '' return ''
} }
}) })
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true }) ipcMain.handle(CHANNEL_ERROR, async () => {
}
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => {
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常' llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常'
let { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError const { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
error = error.replace('\n\n', '\n') error = error.replace('\n\n', '\n')
error = error.trim() error = error.trim()
log('查询llonebot错误信息', error) log('查询 LLOneBot 错误信息', error)
return error return error
}) })
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => {
ipcMain.handle(CHANNEL_GET_CONFIG, async () => {
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
return config return config
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => {
if (!ask) { ipcMain.handle(CHANNEL_SET_CONFIG, (_event, ask: boolean, config: LLOBConfig) => {
setConfig(config) return new Promise<boolean>(resolve => {
.then() if (!ask) {
.catch((e) => { getConfigUtil().setConfig(config)
log('保存设置失败', e.stack) log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
resolve(true)
return
}
dialog
.showMessageBox(mainWindow!, {
type: 'question',
buttons: ['确认', '取消'],
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
title: '确认保存',
message: '是否保存?',
detail: 'LLOneBot配置已更改是否保存',
}) })
return .then((result) => {
} if (result.response === 0) {
dialog getConfigUtil().setConfig(config)
.showMessageBox(mainWindow!, { log('配置已更新', config)
type: 'question', checkFfmpeg(config.ffmpeg).then()
buttons: ['确认', '取消'], resolve(true)
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认" }
title: '确认保存', })
message: '是否保存?', .catch((err) => {
detail: 'LLOneBot配置已更改是否保存', log('保存设置询问弹窗错误', err)
}) resolve(false)
.then((result) => { })
if (result.response === 0) { })
setConfig(config)
.then()
.catch((e) => {
log('保存设置失败', e.stack)
})
}
else {
}
})
.catch((err) => {
log('保存设置询问弹窗错误', err)
})
}) })
ipcMain.on(CHANNEL_LOG, (event, arg) => { ipcMain.on(CHANNEL_LOG, (_event, arg) => {
log(arg) log(arg)
}) })
async function postReceiveMsg(msgList: RawMessage[]) { async function start() {
const { debug, reportSelfMessage } = getConfigUtil().getConfig() log('process pid', process.pid)
for (let message of msgList) {
// 过滤启动之前的消息
// log('收到新消息', message);
if (parseInt(message.msgTime) < startTime / 1000) {
continue
}
// log("收到新消息", message.msgId, message.msgSeq)
// if (message.senderUin !== selfInfo.uin){
message.msgShortId = await dbUtil.addMsg(message)
// }
OB11Constructor.message(message)
.then((msg) => {
if (!debug && msg.message.length === 0) {
return
}
const isSelfMsg = msg.user_id.toString() === 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)
}
})
OB11Constructor.PrivateEvent(message).then((privateEvent) => {
log(message)
if (privateEvent) {
// log("post private event", privateEvent);
postOb11Event(privateEvent)
}
})
// OB11Constructor.FriendAddEvent(message).then((friendAddEvent) => {
// log(message)
// if (friendAddEvent) {
// // log("post friend add event", friendAddEvent);
// postOb11Event(friendAddEvent)
// }
// })
}
}
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[] = [] // 避免重复上报
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
log('message update', message.msgId, message)
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
continue
}
recallMsgIds.push(message.msgId)
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then()
message.msgShortId = oriMessage.msgShortId
OB11Constructor.RecallEvent(message).then((recallEvent) => {
if (recallEvent) {
log('post recall event', recallEvent)
postOb11Event(recallEvent)
}
})
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
dbUtil.updateMsg(message).then()
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
const { reportSelfMessage } = getConfigUtil().getConfig()
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord])
} catch (e: any) {
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)
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
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: 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())
}
}
}
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() const config = getConfigUtil().getConfig()
if (!config.enableLLOB) { if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot') log('LLOneBot 开关设置为关闭不启动LLOneBot')
return return
} }
llonebotError.otherError = '' if (!existsSync(TEMP_DIR)) {
startTime = Date.now() await mkdir(TEMP_DIR)
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession: getSession()! })
dbUtil.init(uin)
log('start activate group member info')
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) { const dbDir = path.join(DATA_DIR, 'database')
ob11WebsocketServer.start(config.ob11.wsPort) if (!existsSync(dbDir)) {
await mkdir(dbDir)
} }
if (config.ob11.enableWsReverse) { const ctx = new Context()
ob11ReverseWebsockets.start() ctx.plugin(Log, {
} enable: config.log!,
if (config.ob11.enableHttpHeart) { filename: logFileName
httpHeart.start() })
} ctx.plugin(NTQQFileApi)
ctx.plugin(NTQQFileCacheApi)
log('LLOneBot start') ctx.plugin(NTQQFriendApi)
ctx.plugin(NTQQGroupApi)
ctx.plugin(NTQQMsgApi)
ctx.plugin(NTQQUserApi)
ctx.plugin(NTQQWebApi)
ctx.plugin(NTQQWindowApi)
ctx.plugin(Core, config)
ctx.plugin(OneBot11Adapter, {
...config.ob11,
heartInterval: config.heartInterval,
token: config.token!,
debug: config.debug!,
reportSelfMessage: config.reportSelfMessage!,
msgCacheExpire: config.msgCacheExpire!,
musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
ffmpeg: config.ffmpeg,
})
ctx.plugin(Database)
ctx.plugin(SQLiteDriver, {
path: path.join(dbDir, `${selfInfo.uin}.db`)
})
ctx.plugin(Store)
ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config)
})
} }
const init = async () => { const buildVersion = getBuildVersion()
const current = getSelfInfo()
if (!current.uin) { const intervalId = setInterval(() => {
setSelfInfo({ const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin, uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid, uid: globalThis.authData?.uid,
nick: current.uin, online: true
}) })
if (self.uin && (buildVersion >= 27187 || getSession())) {
clearInterval(intervalId)
start()
} }
//log('self info', selfInfo, globalThis.authData) }, 600)
if (current.uin) {
start(current.uid, current.uin)
}
else {
setTimeout(init, 1000)
}
}
init()
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (getSelfUid()) { if (![2, 4, 6].includes(window.id)) {
return return
} }
mainWindow = window if (window.id === 2) {
log('window create', window.webContents.getURL().toString()) mainWindow = window
}
//log('window create', window.webContents.getURL().toString())
try { try {
hookNTQQApiCall(window) hookNTQQApiCall(window, window.id !== 2)
hookNTQQApiReceive(window) hookNTQQApiReceive(window, window.id !== 2)
} catch (e: any) { } catch (e) {
log('LLOneBot hook error: ', e.toString()) log('LLOneBot hook error: ', String(e))
} }
} }
try { try {
onLoad() onLoad()
} catch (e: any) { } catch (e) {
console.log(e.toString()) console.log(e)
} }
// 这两个函数都是可选的 // 这两个函数都是可选的

View File

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

126
src/main/store.ts Normal file
View File

@@ -0,0 +1,126 @@
import { Peer } from '@/ntqqapi/types'
import { createHash } from 'node:crypto'
import { LimitedHashTable } from '@/common/utils/table'
import { FileCacheV2 } from '@/common/types'
import { Context, Service } from 'cordis'
declare module 'cordis' {
interface Context {
store: Store
}
interface Tables {
message: {
shortId: number
msgId: string
chatType: number
peerUid: string
}
file_v2: FileCacheV2
}
}
interface MsgInfo {
msgId: string
peer: Peer
}
export default class Store extends Service {
static inject = ['database', 'model']
private cache: LimitedHashTable<string, number>
constructor(protected ctx: Context) {
super(ctx, 'store', true)
this.cache = new LimitedHashTable<string, number>(1000)
this.initDatabase()
}
private async initDatabase() {
this.ctx.model.extend('message', {
shortId: 'integer(10)',
chatType: 'unsigned',
msgId: 'string(24)',
peerUid: 'string(24)'
}, {
primary: 'shortId'
})
this.ctx.model.extend('file_v2', {
fileName: 'string',
fileSize: 'string',
fileUuid: 'string(128)',
msgId: 'string(24)',
msgTime: 'unsigned(10)',
peerUid: 'string(24)',
chatType: 'unsigned',
elementId: 'string(24)',
elementType: 'unsigned',
}, {
primary: 'fileUuid',
indexes: ['fileName']
})
}
createMsgShortId(peer: Peer, msgId: string): number {
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(cacheKey).digest()
hash[0] &= 0x7f //设置第一个bit为0 保证shortId为正数
const shortId = hash.readInt32BE()
this.cache.set(cacheKey, shortId)
this.ctx.database.upsert('message', [{
msgId,
shortId,
chatType: peer.chatType,
peerUid: peer.peerUid
}], 'shortId').then()
return shortId
}
async getMsgInfoByShortId(shortId: number): Promise<MsgInfo | undefined> {
const data = this.cache.getKey(shortId)
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|')
return {
msgId,
peer: {
chatType: +chatTypeStr,
peerUid,
guildId: ''
}
}
}
const items = await this.ctx.database.get('message', { shortId })
if (items?.length) {
const { msgId, chatType, peerUid } = items[0]
return {
msgId,
peer: {
chatType,
peerUid,
guildId: ''
}
}
}
}
async getShortIdByMsgId(msgId: string): Promise<number | undefined> {
return (await this.ctx.database.get('message', { msgId }))[0]?.shortId
}
getShortIdByMsgInfo(peer: Peer, msgId: string) {
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
return this.cache.getValue(cacheKey)
}
addFileCache(data: FileCacheV2) {
return this.ctx.database.upsert('file_v2', [data], 'fileUuid')
}
getFileCacheByName(fileName: string) {
return this.ctx.database.get('file_v2', { fileName }, {
sort: { msgTime: 'desc' }
})
}
getFileCacheById(fileUuid: string) {
return this.ctx.database.get('file_v2', { fileUuid })
}
}

View File

@@ -1,10 +1,10 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { import {
CacheFileList, CacheFileList,
CacheFileListItem, CacheFileListItem,
CacheFileType, CacheFileType,
CacheScanResult, CacheScanResult,
ChatCacheList,
ChatCacheListItemBasic, ChatCacheListItemBasic,
ChatType, ChatType,
ElementType, ElementType,
@@ -13,87 +13,96 @@ import {
PicElement, PicElement,
} from '../types' } from '../types'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import { existsSync } from 'node:fs'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { log, TEMP_DIR } from '@/common/utils' import { RkeyManager } from '@/ntqqapi/helper/rkey'
import { rkeyManager } from '@/ntqqapi/api/rkey'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg' import { OnRichMediaDownloadCompleteParams, Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type' import { fileTypeFromFile } from 'file-type'
import fsPromise from 'node:fs/promises' import { copyFile, stat, unlink } from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask' import { Time } from 'cosmokit'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners' import { Service, Context } from 'cordis'
import { TEMP_DIR } from '@/common/globalVars'
export class NTQQFileApi { declare module 'cordis' {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string> { interface Context {
const session = getSession() ntFileApi: NTQQFileApi
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer, ntFileCacheApi: NTQQFileCacheApi
msgId, }
elementId, }
0,
{ downSourceType: 1, triggerType: 1 }))?.urlResult?.domainUrl[0]?.url! export class NTQQFileApi extends Service {
private rkeyManager: RkeyManager
constructor(protected ctx: Context) {
super(ctx, 'ntFileApi', true)
this.rkeyManager = new RkeyManager(ctx, 'https://llob.linyuchen.net/rkey')
} }
static async getFileType(filePath: string) { async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
const session = getSession()
if (session) {
return (await session.getRichMediaService().getVideoPlayUrlV2(
peer,
msgId,
elementId,
0,
{ downSourceType: 1, triggerType: 1 }
)).urlResult.domainUrl[0]?.url
} else {
const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
peer,
msgId,
elemId: elementId,
videoCodecFormat: 0,
exParams: {
downSourceType: 1,
triggerType: 1
},
}, null])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
}
return data.urlResult.domainUrl[0]?.url
}
}
async getFileType(filePath: string) {
return fileTypeFromFile(filePath) return fileTypeFromFile(filePath)
} }
static async copyFile(filePath: string, destPath: string) {
return await callNTQQApi<string>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_COPY,
args: [
{
fromPath: filePath,
toPath: destPath,
},
],
})
}
static async getFileSize(filePath: string) {
return await callNTQQApi<number>({
className: NTQQApiClass.FS_API,
methodName: NTQQApiMethod.FILE_SIZE,
args: [filePath],
})
}
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) { async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) {
const fileMd5 = await calculateFileMD5(filePath) const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext || '' let fileName = path.basename(filePath)
if (ext) { if (!fileName.includes('.')) {
ext = '.' + ext const ext = (await this.getFileType(filePath))?.ext
fileName += ext ? '.' + ext : ''
} }
let fileName = `${path.basename(filePath)}` const mediaPath = await invoke(NTMethod.MEDIA_FILE_PATH, [{
if (fileName.indexOf('.') === -1) { path_info: {
fileName += ext md5HexStr: fileMd5,
} fileName: fileName,
const session = getSession() elementType: elementType,
const mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({ elementSubType,
md5HexStr: fileMd5, thumbSize: 0,
fileName: fileName, needCreate: true,
elementType: elementType, downloadType: 1,
elementSubType, file_uuid: '',
thumbSize: 0, },
needCreate: true, }])
downloadType: 1, await copyFile(filePath, mediaPath)
file_uuid: '' const fileSize = (await stat(filePath)).size
})
await fsPromise.copyFile(filePath, mediaPath!)
const fileSize = (await fsPromise.stat(filePath)).size
return { return {
md5: fileMd5, md5: fileMd5,
fileName, fileName,
path: mediaPath!, path: mediaPath,
fileSize, fileSize,
ext
} }
} }
static async downloadMedia( async downloadMedia(
msgId: string, msgId: string,
chatType: ChatType, chatType: ChatType,
peerUid: string, peerUid: string,
@@ -104,57 +113,41 @@ export class NTQQFileApi {
force = false force = false
) { ) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && existsSync(sourcePath)) {
if (force) { if (force) {
try { try {
await fsPromise.unlink(sourcePath) await unlink(sourcePath)
} catch (e) { } catch { }
//
}
} else { } else {
return sourcePath return sourcePath
} }
} }
const data = await NTEventDispatch.CallNormalEvent< const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>(
( 'nodeIKernelMsgService/downloadRichMedia',
params: { [
fileModelId: string, {
downloadSourceType: number, getReq: {
triggerType: number, fileModelId: '0',
msgId: string, downloadSourceType: 0,
chatType: ChatType, triggerType: 1,
peerUid: string, msgId: msgId,
elementId: string, chatType: chatType,
thumbSize: number, peerUid: peerUid,
downloadType: number, elementId: elementId,
filePath: string thumbSize: 0,
}) => Promise<unknown>, downloadType: 1,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void filePath: thumbPath,
>( },
'NodeIKernelMsgService/downloadRichMedia', },
'NodeIKernelMsgListener/onRichMediaDownloadComplete', null,
1, ],
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{ {
fileModelId: '0', cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
downloadSourceType: 0, cmdCB: payload => payload.notifyInfo.msgId === msgId,
triggerType: 1, timeout
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
} }
) )
let filePath = data[1].filePath let filePath = data.notifyInfo.filePath
if (filePath.startsWith('\\')) { if (filePath.startsWith('\\')) {
const downloadPath = TEMP_DIR const downloadPath = TEMP_DIR
filePath = path.join(downloadPath, filePath) filePath = path.join(downloadPath, filePath)
@@ -163,35 +156,40 @@ export class NTQQFileApi {
return filePath return filePath
} }
static async getImageSize(filePath: string) { async getImageSize(filePath: string) {
return await callNTQQApi<{ width: number; height: number }>({ return await invoke<{
className: NTQQApiClass.FS_API, width: number
methodName: NTQQApiMethod.IMAGE_SIZE, height: number
args: [filePath], type: string
}) }>(
NTMethod.IMAGE_SIZE,
[filePath],
{
className: NTClass.FS_API,
}
)
} }
static async getImageUrl(element: PicElement) { async getImageUrl(element: PicElement) {
if (!element) { if (!element) {
return '' return ''
} }
const url: string = element.originImageUrl! // 没有域名 const url: string = element.originImageUrl! // 没有域名
const md5HexStr = element.md5HexStr const md5HexStr = element.md5HexStr
const fileMd5 = element.md5HexStr const fileMd5 = element.md5HexStr
const fileUuid = element.fileUuid
if (url) { if (url) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接 const parsedUrl = new URL(IMAGE_HTTP_HOST + url) //临时解析拼接
const imageAppid = UrlParse.searchParams.get('appid') const imageAppid = parsedUrl.searchParams.get('appid')
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid) const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) { if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey') let rkey = parsedUrl.searchParams.get('rkey')
if (UrlRkey) { if (rkey) {
return IMAGE_HTTP_HOST_NT + url return IMAGE_HTTP_HOST_NT + url
} }
const rkeyData = await rkeyManager.getRkey() const rkeyData = await this.rkeyManager.getRkey()
UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey
return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}` return IMAGE_HTTP_HOST_NT + url + rkey
} else { } else {
// 老的图片url不需要rkey // 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url return IMAGE_HTTP_HOST + url
@@ -200,134 +198,58 @@ export class NTQQFileApi {
// 没有url需要自己拼接 // 没有url需要自己拼接
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0` return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 || md5HexStr)!.toUpperCase()}/0`
} }
log('图片url获取失败', element) this.ctx.logger.error('图片url获取失败', element)
return '' return ''
} }
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi extends Service {
static async setCacheSilentScan(isSilent: boolean = true) { constructor(protected ctx: Context) {
return await callNTQQApi<GeneralCallResult>({ super(ctx, 'ntFileCacheApi', true)
methodName: NTQQApiMethod.CACHE_SET_SILENCE,
args: [
{
isSilent,
},
null,
],
})
} }
static getCacheSessionPathList() { async setCacheSilentScan(isSilent: boolean = true) {
return callNTQQApi< return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }, null])
}
getCacheSessionPathList() {
return invoke<
{ {
key: string key: string
value: string value: string
}[] }[]
>({ >(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API })
className: NTQQApiClass.OS_API,
methodName: NTQQApiMethod.CACHE_PATH_SESSION,
})
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { scanCache() {
return callNTQQApi<any>({ invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { classNameIsRegister: true })
// TODO: 目前还不知道真正的返回值是什么 return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [null, null], { timeout: 300 * Time.second })
methodName: NTQQApiMethod.CACHE_CLEAR,
args: [
{
keys: cacheKeys,
},
null,
],
})
} }
static addCacheScannedPaths(pathMap: object = {}) { getHotUpdateCachePath() {
return callNTQQApi<GeneralCallResult>({ return invoke<string>(NTMethod.CACHE_PATH_HOT_UPDATE, [], { className: NTClass.HOTUPDATE_API })
methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH,
args: [
{
pathMap: { ...pathMap },
},
null,
],
})
} }
static scanCache() { getDesktopTmpPath() {
callNTQQApi<GeneralCallResult>({ return invoke<string>(NTMethod.CACHE_PATH_DESKTOP_TEMP, [], { className: NTClass.BUSINESS_API })
methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then()
return callNTQQApi<CacheScanResult>({
methodName: NTQQApiMethod.CACHE_SCAN,
args: [null, null],
timeoutSecond: 300,
})
} }
static getHotUpdateCachePath() { getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
return callNTQQApi<string>({
className: NTQQApiClass.HOTUPDATE_API,
methodName: NTQQApiMethod.CACHE_PATH_HOT_UPDATE,
})
}
static getDesktopTmpPath() {
return callNTQQApi<string>({
className: NTQQApiClass.BUSINESS_API,
methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP,
})
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
callNTQQApi<ChatCacheList>({
methodName: NTQQApiMethod.CACHE_CHAT_GET,
args: [
{
chatType: type,
pageSize,
order: 1,
pageIndex,
},
null,
],
})
.then((list) => res(list))
.catch((e) => rej(e))
})
}
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 invoke<CacheFileList>(NTMethod.CACHE_FILE_GET, [{
methodName: NTQQApiMethod.CACHE_FILE_GET, fileType: fileType,
args: [ restart: true,
{ pageSize: pageSize,
fileType: fileType, order: 1,
restart: true, lastRecord: _lastRecord,
pageSize: pageSize, }, null])
order: 1,
lastRecord: _lastRecord,
},
null,
],
})
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await callNTQQApi<GeneralCallResult>({ return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{
methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, chats,
args: [ fileKeys,
{ }, null])
chats,
fileKeys,
},
null,
],
})
} }
} }

View File

@@ -1,125 +1,197 @@
import { Friend, FriendV2 } from '../types' import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType, NodeIKernelProfileService } from '../services' import { BuddyListReqType } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask' import { Dict, pick } from 'cosmokit'
import { CacheClassFuncAsyncExtend } from '@/common/utils/helper' import { Service, Context } from 'cordis'
import { LimitedHashTable } from '@/common/utils/table'
export class NTQQFriendApi { declare module 'cordis' {
static async getFriends(forced = false) { interface Context {
const data = await callNTQQApi<{ ntFriendApi: NTQQFriendApi
}
}
export class NTQQFriendApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntFriendApi', true)
}
/** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends() {
const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
categroyName: string categroyName: string
categroyMbCount: number categroyMbCount: number
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>({ }>(
methodName: NTQQApiMethod.FRIENDS, 'getBuddyList',
args: [{ force_update: forced }, undefined], [],
cbCmd: ReceiveCmdS.FRIENDS, {
afterFirstCmd: false, className: NTClass.NODE_STORE_API,
}) cbCmd: ReceiveCmdS.FRIENDS,
// log('获取好友列表', data) afterFirstCmd: false,
let _friends: Friend[] = [] }
for (const fData of data.data) { )
_friends.push(...fData.buddyList) const _friends: Friend[] = []
for (const item of data.data) {
_friends.push(...item.buddyList)
} }
return _friends return _friends
} }
static async likeFriend(uid: string, count = 1) { async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.LIKE_FRIEND, if (session) {
args: [ return session.getBuddyService().approvalFriendRequest({
{ friendUid,
doLikeUserInfo: { reqTime,
friendUid: uid, accept
sourceId: 71, })
doLikeCount: count, } else {
doLikeTollCount: 0, return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
}, approvalInfo: {
friendUid,
reqTime,
accept,
}, },
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[]> { async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = []
const session = getSession() const session = getSession()
const buddyService = session?.getBuddyService() if (session) {
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) const uids: string[] = []
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!) const buddyService = session.getBuddyService()
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
) const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
return Array.from(data.values()) return Array.from(data.values())
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
const uids = data.buddyCategory.flatMap(item => item.buddyUids)
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
}
} }
@CacheClassFuncAsyncExtend(3600 * 1000, 'getBuddyIdMap', () => true) /** uid => uin */
static async getBuddyIdMapCache(refresh = false): Promise<LimitedHashTable<string, string>> { async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
return await NTQQFriendApi.getBuddyIdMap(refresh) const retMap: Map<string, string> = new Map()
}
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 session = getSession()
const buddyService = session?.getBuddyService() if (session) {
const buddyListV2 = refresh ? await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL) const uids: string[] = []
uids.push(...buddyListV2?.data.flatMap(item => item.buddyUids)!) const buddyService = session.getBuddyService()
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>( const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
); const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
data.forEach((value, key) => { for (const [, item] of data) {
retMap.set(value.uin!, value.uid!) if (retMap.size > 5000) {
}) break
//console.log('getBuddyIdMap', retMap.getValue) }
retMap.set(item.uid!, item.uin!)
}
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
for (const item of Object.values(data.userSimpleInfos)) {
if (retMap.size > 5000) {
break
}
retMap.set(item.uid!, item.uin!)
}
}
return retMap return retMap
} }
static async getBuddyV2ExWithCate(refresh = false) { async getBuddyV2ExWithCate(refresh = false) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const session = getSession() const session = getSession()
const buddyService = session?.getBuddyService() if (session) {
const buddyListV2 = refresh ? (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data : (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data const uids: string[] = []
uids.push( const categoryMap: Map<string, Dict> = new Map()
...buddyListV2?.flatMap(item => { const buddyService = session.getBuddyService()
item.buddyUids.forEach(uid => { const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName }) uids.push(
...buddyListV2.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName })
})
return item.buddyUids
}))
const data = await session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map()
for (const item of data.buddyCategory) {
category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName']))
}
return Object.values(data.userSimpleInfos)
.filter(v => v.baseInfo && category.get(v.baseInfo.categoryId)?.buddyUids.includes(v.uid!))
.map(value => {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: category.get(value.baseInfo.categoryId)?.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> { async isBuddy(uid: string): Promise<boolean> {
const session = getSession() const session = getSession()
return session?.getBuddyService().isBuddy(uid)! if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
}
}
async getBuddyRecommendContact(uin: string) {
const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }, null])
return ret.arkMsg
}
async setBuddyRemark(uid: string, remark: string) {
return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{
remarkParams: { uid, remark }
}, null])
} }
} }

View File

@@ -1,167 +1,226 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes } from '../types' import {
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' Group,
import { NTQQWindowApi, NTQQWindows } from './window' GroupMember,
GroupMemberRole,
GroupNotifies,
GroupRequestOperateTypes,
GetFileListParam,
OnGroupFileInfoUpdateParams,
PublishGroupBulletinReq
} from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window'
import { getSession } from '../wrapper' import { getSession } from '../wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { NodeIKernelGroupService } from '../services'
import { NodeIKernelGroupListener } from '../listeners' import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc'
export class NTQQGroupApi { declare module 'cordis' {
static async activateMemberListChange() { interface Context {
return await callNTQQApi<GeneralCallResult>({ ntGroupApi: NTQQGroupApi
methodName: NTQQApiMethod.ACTIVATE_MEMBER_LIST_CHANGE, }
classNameIsRegister: true, }
args: [],
}) export class NTQQGroupApi extends Service {
static inject = ['ntWindowApi']
public groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
constructor(protected ctx: Context) {
super(ctx, 'ntGroupApi', true)
} }
static async activateMemberInfoChange() { async getGroups(): Promise<Group[]> {
return await callNTQQApi<GeneralCallResult>({ const result = await invoke<{
methodName: NTQQApiMethod.ACTIVATE_MEMBER_INFO_CHANGE, updateType: number
classNameIsRegister: true, groupList: Group[]
args: [], }>(
}) 'getGroupList',
[],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.GROUPS_STORE,
afterFirstCmd: false,
}
)
return result.groupList
} }
static async getGroupAllInfo(groupCode: string, source: number = 4) { async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
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 session = getSession()
const groupService = session?.getGroupService() let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
const sceneId = groupService?.createMemberListScene(groupQQ, 'groupMemberList_MainWindow') if (session) {
const result = await groupService?.getNextMemberList(sceneId!, undefined, num) const groupService = session.getGroupService()
if (result?.errCode !== 0) { const sceneId = groupService.createMemberListScene(groupCode, 'groupMemberList_MainWindow')
throw ('获取群成员列表出错,' + result?.errMsg) result = await groupService.getNextMemberList(sceneId, undefined, num)
} else {
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
result = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }, null])
}
if (result.errCode !== 0) {
throw ('获取群成员列表出错,' + result.errMsg)
} }
return result.result.infos return result.result.infos
} }
static async getGroupMembersInfo(groupCode: string, uids: string[], forceUpdate: boolean = false) { async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
return await callNTQQApi<GeneralCallResult>({ const groupCodeStr = groupCode.toString()
methodName: NTQQApiMethod.GROUP_MEMBERS_INFO, const memberUinOrUidStr = memberUinOrUid.toString()
args: [ if (!this.groupMembers.has(groupCodeStr)) {
{ try {
forceUpdate, // 更新群成员列表
groupCode, this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr))
uids }
}, catch (e) {
null, return null
], }
}) }
let members = this.groupMembers.get(groupCodeStr)!
const getMember = () => {
let member: GroupMember | undefined = undefined
if (isNumeric(memberUinOrUidStr)) {
member = Array.from(members.values()).find(member => member.uin === memberUinOrUidStr)
} else {
member = members.get(memberUinOrUidStr)
}
return member
}
let member = getMember()
if (!member) {
this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr))
members = this.groupMembers.get(groupCodeStr)!
member = getMember()
}
return member
} }
static async getGroupNotifies() { async getGroupIgnoreNotifies() {
// 获取管理员变更 await this.getSingleScreenNotifies(14)
// 加群通知,退出通知,需要管理员权限 return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
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, NTQQWindows.GroupNotifyFilterWindow,
[], [],
ReceiveCmdS.GROUP_NOTIFY, ReceiveCmdS.GROUP_NOTIFY,
) )
} }
static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) { async getSingleScreenNotifies(num: number) {
invoke(ReceiveCmdS.GROUP_NOTIFY, [], { classNameIsRegister: true })
return (await invoke<GroupNotifies>(
'nodeIKernelGroupService/getSingleScreenNotifies',
[{ doubt: false, startSeq: '', number: num }, null],
{
cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false,
}
)).notifies
}
async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|') const flagitem = flag.split('|')
const groupCode = flagitem[0] const groupCode = flagitem[0]
const seq = flagitem[1] const seq = flagitem[1]
const type = parseInt(flagitem[2]) const type = parseInt(flagitem[2])
const session = getSession() const session = getSession()
return session?.getGroupService().operateSysNotify( if (session) {
false, return session.getGroupService().operateSysNotify(false, {
{ operateType, // 2 拒绝
'operateType': operateType, // 2 拒绝 targetMsg: {
'targetMsg': { seq, // 通知序列号
'seq': seq, // 通知序列号 type,
'type': type, groupCode,
'groupCode': groupCode, postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
} }
}) })
} else {
return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
doubt: false,
operateMsg: {
operateType,
targetMsg: {
seq,
type,
groupCode,
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
},
},
}, null])
}
} }
static async quitGroup(groupQQ: string) { async quitGroup(groupCode: string) {
const session = getSession() const session = getSession()
return session?.getGroupService().quitGroup(groupQQ) if (session) {
return session.getGroupService().quitGroup(groupCode)
} else {
return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }, null])
}
} }
static async kickMember( async kickMember(
groupQQ: string, groupCode: string,
kickUids: string[], kickUids: string[],
refuseForever = false, refuseForever = false,
kickReason = '', kickReason = '',
) { ) {
const session = getSession() const session = getSession()
return session?.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason) if (session) {
return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
} else {
return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
}
} }
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
// timeStamp为秒数, 0为解除禁言 // timeStamp为秒数, 0为解除禁言
const session = getSession() const session = getSession()
return session?.getGroupService().setMemberShutUp(groupQQ, memList) if (session) {
return session.getGroupService().setMemberShutUp(groupCode, memList)
} else {
return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
}
} }
static async banGroup(groupQQ: string, shutUp: boolean) { async banGroup(groupCode: string, shutUp: boolean) {
const session = getSession() const session = getSession()
return session?.getGroupService().setGroupShutUp(groupQQ, shutUp) if (session) {
return session.getGroupService().setGroupShutUp(groupCode, shutUp)
} else {
return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }, null])
}
} }
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
const session = getSession() const session = getSession()
return session?.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName) if (session) {
return session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName)
} else {
return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }, null])
}
} }
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) {
const session = getSession() const session = getSession()
return session?.getGroupService().modifyMemberRole(groupQQ, memberUid, role) if (session) {
return session.getGroupService().modifyMemberRole(groupCode, memberUid, role)
} else {
return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }, null])
}
} }
static async setGroupName(groupQQ: string, groupName: string) { async setGroupName(groupCode: string, groupName: string) {
const session = getSession() const session = getSession()
return session?.getGroupService().modifyGroupName(groupQQ, groupName, false) if (session) {
return session.getGroupService().modifyGroupName(groupCode, groupName, false)
} else {
return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }, null])
}
} }
static async getGroupAtAllRemainCount(groupCode: string) { async getGroupRemainAtTimes(groupCode: string) {
return await callNTQQApi< return await invoke<
GeneralCallResult & { GeneralCallResult & {
atInfo: { atInfo: {
canAtAll: boolean canAtAll: boolean
@@ -171,53 +230,109 @@ export class NTQQGroupApi {
canNotAtAllMsg: '' canNotAtAllMsg: ''
} }
} }
>({ >(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }, null])
methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT, }
args: [
async removeGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().removeGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
}
async addGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().addGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/addGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
}
async createGroupFileFolder(groupId: string, folderName: string) {
return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }, null])
}
async deleteGroupFileFolder(groupId: string, folderId: string) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null])
}
async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }, null])
}
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { classNameIsRegister: true })
const data = await invoke<{ fileInfo: OnGroupFileInfoUpdateParams }>(
'nodeIKernelRichMediaService/getGroupFileList',
[
{ {
groupCode, groupId,
fileListForm
}, },
null, null,
], ],
}) {
cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
afterFirstCmd: false,
cmdCB: (payload, result) => payload.fileInfo.reqId === result
}
)
return data.fileInfo.item
} }
static async getGroupRemainAtTimes(GroupCode: string) { async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
const session = getSession() const ntUserApi = this.ctx.get('ntUserApi')!
return session?.getGroupService().getGroupRemainAtTimes(GroupCode)! const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }, null])
} }
// 头衔不可用 async uploadGroupBulletinPic(groupCode: string, path: string) {
static async setGroupTitle(groupQQ: string, uid: string, title: string) { const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }, null])
} }
static publishGroupBulletin(groupQQ: string, title: string, content: string) { } async getGroupRecommendContact(groupCode: string) {
const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }, null])
static async removeGroupEssence(GroupCode: string, msgId: string) { return ret.arkJson
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) { async queryCachedEssenceMsg(groupCode: string, msgSeq = '0', msgRandom = '0') {
const session = getSession() return await invoke('nodeIKernelGroupService/queryCachedEssenceMsg', [{
// 代码没测过 key: {
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom groupCode,
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false) msgSeq: +msgSeq,
let param = { msgRandom: +msgRandom
groupCode: GroupCode, }
msgRandom: parseInt(MsgData?.msgList[0].msgRandom!), }, null])
msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
} }
} }

View File

@@ -1,246 +1,189 @@
import { callNTQQApi, GeneralCallResult, NTQQApiMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types'
import { getSelfNick, getSelfUid } from '../../common/data'
import { getBuildVersion } from '../../common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask' import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
export class NTQQMsgApi { declare module 'cordis' {
static async getTempChatInfo(chatType: ChatType2, peerUid: string) { interface Context {
const session = getSession() ntMsgApi: NTQQMsgApi
return session?.getMsgService().getTempChatInfo(chatType, peerUid)! }
}
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
}
export class NTQQMsgApi extends Service {
static inject = ['ntUserApi']
constructor(protected ctx: Context) {
super(ctx, 'ntMsgApi', true)
} }
static async prepareTempChat(toUserUid: string, GroupCode: string, nickname: string) { async getTempChatInfo(chatType: ChatType2, peerUid: string) {
//By Jadx/Ida Mlikiowa const session = getSession()
let TempGameSession = { if (session) {
nickname: '', return session.getMsgService().getTempChatInfo(chatType, peerUid)
gameAppId: '', } else {
selfTinyId: '', return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }, null])
peerRoleId: '',
peerOpenId: '',
} }
const session = getSession()
return session?.getMsgService().prepareTempChat({
chatType: ChatType2.KCHATTYPETEMPC2CFROMGROUP,
peerUid: toUserUid,
peerNickname: nickname,
fromGroupCode: GroupCode,
sig: '',
selfPhone: '',
selfUid: getSelfUid(),
gameSession: TempGameSession
})
} }
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString()
const session = getSession() const session = getSession()
return session?.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set) const emojiType = emojiId.length > 3 ? '2' : '1'
if (session) {
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiType, setEmoji)
} else {
return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }, null])
}
} }
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
const session = getSession() const session = getSession()
return session?.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)! if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else {
return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }, null])
}
} }
static async activateChat(peer: Peer) { async activateChat(peer: Peer) {
// await this.fetchRecentContact(); return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 20 }, null])
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null],
})
} }
static async activateChatAndGetHistory(peer: Peer) { async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact(); return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt: 20 }, null])
// 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) { async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null])
}
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
if (!peer) throw new Error('peer is not allowed') if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed') if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession() const session = getSession()
//Mlikiowa 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得 if (session) {
return await session?.getMsgService().getMsgsByMsgId(peer, msgIds)! return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }, null])
}
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) { async getMsgHistory(peer: Peer, msgId: string, cnt: number, isReverseOrder: boolean = false) {
const session = getSession() const session = getSession()
// 消息时间从旧到新 // 消息时间从旧到新
return session?.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)! if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, cnt, isReverseOrder)
} else {
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }, null])
}
} }
static async recallMsg(peer: Peer, msgIds: string[]) { async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession() const session = getSession()
return await session?.getMsgService().recallMsg({ if (session) {
chatType: peer.chatType, return session.getMsgService().recallMsg(peer, msgIds)
peerUid: peer.peerUid } else {
}, msgIds) return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }, null])
}
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
function generateMsgId() { const msgId = 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方法 利用数据返回正确得到对应消息
// 与之前 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()
}
peer.guildId = msgId peer.guildId = msgId
const data = await NTEventDispatch.CallNormalEvent< const data = await invoke<{ msgList: RawMessage[] }>(
(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>, 'nodeIKernelMsgService/sendMsg',
(msgList: RawMessage[]) => void [
>( {
'NodeIKernelMsgService/sendMsg', msgId: '0',
'NodeIKernelMsgListener/onMsgInfoListUpdate', peer,
1, msgElements,
timeout, msgAttributeInfos: new Map()
(msgRecords: RawMessage[]) => { },
for (let msgRecord of msgRecords) { null
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) { ],
return true {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
} }
} return false
return false },
}, timeout
'0',
peer,
msgElements,
new Map()
)
const retMsg = data[1].find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
} }
})
return retMsg!
}
static async sendMsgV2(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方法 利用数据返回正确得到对应消息
// 与之前 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 => { return data.msgList.find(msgRecord => msgRecord.guildId === msgId)
if (msgRecord.msgId === msgId) {
return true
}
})
return retMsg!
} }
static async getMsgUnique(chatType: number, time: string) { async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession() const session = getSession()
if (getBuildVersion() >= 26702) { if (session) {
return session?.getMsgService().generateMsgUniqueId(chatType, time)! return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} else {
return await invoke(NTMethod.FORWARD_MSG, [{
msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
}, null])
} }
return session?.getMsgService().getMsgUniqueId(time)!
} }
static async getServerTime() { async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const session = getSession() const senderShowName = await this.ctx.ntUserApi.getSelfNick(true)
return session?.getMSFService().getServerTime()!
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession()
return session?.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])!
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await getSelfNick()
const msgInfos = msgIds.map(id => { const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName } return { msgId: id, senderShowName }
}) })
const selfUid = getSelfUid() const selfUid = selfInfo.uid
let data = await NTEventDispatch.CallNormalEvent< const data = await invoke<{ msgList: RawMessage[] }>(
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>, 'nodeIKernelMsgService/multiForwardMsgWithComment',
(msgList: RawMessage[]) => void [
>( {
'NodeIKernelMsgService/multiForwardMsgWithComment', msgInfos,
'NodeIKernelMsgListener/onMsgInfoListUpdate', srcContact: srcPeer,
1, dstContact: destPeer,
5000, commentElements: [],
(msgRecords: RawMessage[]) => { msgAttributeInfos: new Map(),
for (let msgRecord of msgRecords) { },
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) { null,
return true ],
{
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
} }
} return false
return false },
}, }
msgInfos,
srcPeer,
destPeer,
[],
new Map()
) )
for (let msg of data[1]) { for (const msg of data.msgList) {
const arkElement = msg.elements.find(ele => ele.arkElement) const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) { if (!arkElement) {
continue continue
} }
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData) const forwardData = JSON.parse(arkElement.arkElement!.bytesData)
if (forwardData.app != 'com.tencent.multimsg') { if (forwardData.app != 'com.tencent.multimsg') {
continue continue
} }
@@ -251,23 +194,85 @@ export class NTQQMsgApi {
throw new Error('转发消息超时') throw new Error('转发消息超时')
} }
static async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) { async getMsgsBySeqAndCount(peer: Peer, msgSeq: string, count: number, desc: boolean, z: boolean) {
const session = getSession() const session = getSession()
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, { if (session) {
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa return await session.getMsgService().getMsgsBySeqAndCount(peer, msgSeq, count, desc, z)
filterMsgType: [], } else {
filterSendersUid: [], return await invoke('nodeIKernelMsgService/getMsgsBySeqAndCount', [{
filterMsgToTime: '0', peer,
filterMsgFromTime: '0', cnt: count,
isReverseOrder: false, msgSeq,
isIncludeCurrent: true, queryOrder: desc
pageLimit: 1, }, null])
}) }
return ret!
} }
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { async getSingleMsg(peer: Peer, msgSeq: string) {
const session = getSession() const session = getSession()
return await session?.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)! if (session) {
return await session.getMsgService().getSingleMsg(peer, msgSeq)
} else {
return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null])
}
}
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, null])
}
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid,
filterMsgToTime: filterMsgTime,
filterMsgFromTime: filterMsgTime,
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, null])
}
async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }, null])
}
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
return await invoke('nodeIKernelMsgService/getMsgEmojiLikesList', [{
peer,
msgSeq,
emojiId,
emojiType,
cnt: count
}, null])
}
async fetchFavEmojiList(count: number) {
return await invoke('nodeIKernelMsgService/fetchFavEmojiList', [{
resId: '',
count,
backwardFetch: true,
forceRefresh: true
}, null])
} }
} }

View File

@@ -1,312 +1,291 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke } from '../ntcall'
import { SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '../types' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import { ReceiveCmdS } from '../hook' import { getBuildVersion } from '@/common/utils'
import { friends, groupMembers, getSelfUin } from '@/common/data'
import { CacheClassFuncAsync, log, getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper' import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType } from '../services' import { UserDetailSource, ProfileBizType } from '../services'
import { NodeIKernelProfileListener } from '../listeners' import { Time } from 'cosmokit'
import { NTEventDispatch } from '@/common/utils/EventTask' import { Service, Context } from 'cordis'
import { NTQQFriendApi } from './friend' import { selfInfo } from '@/common/globalVars'
export class NTQQUserApi { declare module 'cordis' {
static async setQQAvatar(filePath: string) { interface Context {
return await callNTQQApi<GeneralCallResult>({ ntUserApi: NTQQUserApi
methodName: NTQQApiMethod.SET_QQ_AVATAR, }
args: [ }
{
path: filePath, export class NTQQUserApi extends Service {
}, static inject = ['ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context) {
super(ctx, 'ntUserApi', true)
}
async setQQAvatar(path: string) {
return await invoke(
'nodeIKernelProfileService/setHeader',
[
{ path },
null, null,
], ],
timeoutSecond: 10, // 10秒不一定够 {
}) timeout: 10 * Time.second, // 10秒不一定够
}
)
} }
static async getSelfInfo() { async fetchUserDetailInfo(uid: string) {
return await callNTQQApi<SelfInfo>({ const result = await invoke<{ info: UserDetailInfoListenerArg }>(
className: NTQQApiClass.GLOBAL_DATA, 'nodeIKernelProfileService/fetchUserDetailInfo',
methodName: NTQQApiMethod.SELF_INFO, [
timeoutSecond: 2, {
}) callFrom: 'BuddyProfileStore',
} uid: [uid],
source: UserDetailSource.KSERVER,
static async getUserInfo(uid: string) { bizList: [ProfileBizType.KALL]
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) => {
if (profile.uid === uid) {
return true
}
return false
}, },
'BuddyProfileStore', null
[ ],
uid {
], cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
UserDetailSource.KSERVER, afterFirstCmd: false,
[ cmdCB: payload => payload.info.uid === uid,
ProfileBizType.KALL }
] )
) const { info } = result
const RetUser: User = { const ret: User = {
...profile.simpleInfo.coreInfo, ...info.simpleInfo.coreInfo,
...profile.simpleInfo.status, ...info.simpleInfo.status,
...profile.simpleInfo.vasInfo, ...info.simpleInfo.vasInfo,
...profile.commonExt, ...info.commonExt,
...profile.simpleInfo.baseInfo, ...info.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel, qqLevel: info.commonExt?.qqLevel,
pendantId: '' pendantId: ''
} }
return RetUser return ret
} }
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { async getUserDetailInfo(uid: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid) return this.fetchUserDetailInfo(uid)
} }
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] const result = await invoke<{ info: User }>(
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'] 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
const [_retData, profile] = await NTEventDispatch.CallNormalEvent [
<EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile: User) => {
if (profile.uid === uid) {
return true
}
return false
},
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', uid,
bizList: [0]
}, },
null,
], ],
}) {
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false,
cmdCB: (payload) => payload.info.uid === uid,
}
)
return result.info
} }
static async getQzoneCookies() { async getCookies(domain: string) {
const uin = getSelfUin() const clientKeyData = await this.forceFetchClientKey()
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + (await this.getClientKey()).clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + uin + '%2Finfocenter&keyindex=19%27' if (clientKeyData?.result !== 0) {
let cookies: { [key: string]: string } = {}
try {
cookies = await RequestUtil.HttpsGetCookies(requestUrl)
} catch (e: any) {
log('获取QZone Cookies失败', e)
cookies = {}
}
return cookies
}
static async getSkey(): Promise<string> {
const clientKeyData = await this.getClientKey()
if (clientKeyData.result !== 0) {
throw new Error('获取clientKey失败') throw new Error('获取clientKey失败')
} }
const url = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + getSelfUin() const uin = selfInfo.uin
+ '&clientkey=' + clientKeyData.clientKey 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'
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex const cookies: { [key: string]: string } = await RequestUtil.HttpsGetCookies(requestUrl)
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 return cookies
} }
static genBkn(sKey: string) { async getPSkey(domains: string[]) {
sKey = sKey || '' return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null])
let hash = 5381 }
for (let i = 0; i < sKey.length; i++) { async like(uid: string, count = 1) {
const code = sKey.charCodeAt(i) const session = getSession()
hash = hash + (hash << 5) + code if (session) {
return session.getProfileLikeService().setBuddyProfileLike({
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})
} else {
return await invoke(
'nodeIKernelProfileLikeService/setBuddyProfileLike',
[
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null,
],
)
} }
return (hash & 0x7fffffff).toString()
} }
static async getPSkey(domains: string[]): Promise<Map<string, string>> { async getUidByUinV1(uin: string) {
const session = getSession()
const res = await session?.getTipOffService().getPskey(domains, true)
if (res?.result !== 0) {
throw new Error(`获取Pskey失败: ${res?.errMsg}`)
}
return res.domainPskeyMap
}
static async getClientKey() {
const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')!
}
static async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> {
const session = getSession()
return session?.getProfileLikeService().setBuddyProfileLike({
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})!
}
static async getUidByUinV1(Uin: string) {
const session = getSession() const session = getSession()
// 通用转换开始尝试 // 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin) let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
// Uid 好友转
if (!uid) { if (!uid) {
friends.forEach((t) => { for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
if (t.uin == Uin) { for (const member of membersList.values()) {
uid = t.uid if (member.uin === uin) {
} uid = member.uid
}) break
}
//Uid 群友列表转
if (!uid) {
for (let groupMembersList of groupMembers.values()) {
for (let GroupMember of groupMembersList.values()) {
if (GroupMember.uin == Uin) {
uid = GroupMember.uid
} }
} }
if (uid) break
} }
} }
if (!uid) { if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三 const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (unveifyUid.indexOf('*') == -1) { if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid uid = unveifyUid
} }
} }
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid
}
return uid return uid
} }
static async getUidByUinV2(Uin: string) { async getUidByUinV2(uin: string) {
const session = getSession() const session = getSession()
let uid = (await session?.getProfileService().getUidByUin('FriendsServiceImpl', [Uin]))?.get(Uin) if (session) {
if (uid) return uid let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin)
uid = (await session?.getGroupService().getUidByUins([Uin]))?.uids.get(Uin) if (uid) return uid
if (uid) return uid uid = (await session.getProfileService().getUidByUin('FriendsServiceImpl', [uin])).get(uin)
uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin) if (uid) return uid
if (uid) return uid uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin)
console.log((await NTQQFriendApi.getBuddyIdMapCache(true))) if (uid) return uid
uid = (await NTQQFriendApi.getBuddyIdMapCache(true)).getValue(Uin)//从Buddy缓存获取Uid } else {
if (uid) return uid let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin)
uid = (await NTQQFriendApi.getBuddyIdMap(true)).getValue(Uin) if (uid) return uid
if (uid) return uid uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(Uin)).detail.uid//从QQ Native 特殊转换 if (uid) return uid
if (unveifyUid.indexOf('*') == -1) uid = unveifyUid uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
//if (uid) return uid if (uid) return uid
return uid }
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
} }
static async getUidByUin(Uin: string) { async getUidByUin(uin: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUidByUinV2(Uin) return this.getUidByUinV2(uin)
} }
return await NTQQUserApi.getUidByUinV1(Uin) return this.getUidByUinV1(uin)
} }
static async getUserDetailInfoByUinV2(Uin: string) { async getUserDetailInfoByUinV2(uin: string) {
return await NTEventDispatch.CallNoListenerEvent return await invoke<UserDetailInfoByUinV2>(
<(Uin: string) => Promise<UserDetailInfoByUinV2>>( 'nodeIKernelProfileService/getUserDetailInfoByUin',
'NodeIKernelProfileService/getUserDetailInfoByUin', [
5000, { uin },
Uin null,
) ],
} )
static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUin>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
)
} }
static async getUinByUidV1(Uid: string) { async getUserDetailInfoByUin(uin: string) {
const ret = await NTEventDispatch.CallNoListenerEvent return await invoke<UserDetailInfoByUin>(
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>( 'nodeIKernelProfileService/getUserDetailInfoByUin',
'NodeIKernelUixConvertService/getUin', [
5000, { uin },
[Uid] null,
) ],
let uin = ret.uinInfo.get(Uid) )
}
async getUinByUidV1(uid: string) {
const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])
let uin = ret.uinInfo.get(uid)
if (!uin) { if (!uin) {
//从Buddy缓存获取Uin uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
friends.forEach((t) => {
if (t.uid == Uid) {
uin = t.uin
}
})
}
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换
} }
return uin return uin
} }
static async getUinByUidV2(Uid: string) { async getUinByUidV2(uid: string) {
const session = getSession() const session = getSession()
let uin = (await session?.getProfileService().getUinByUid('FriendsServiceImpl', [Uid]))?.get(Uid) if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
if (uin) return uin
uin = (await session.getProfileService().getUinByUid('FriendsServiceImpl', [uid])).get(uid)
if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
} else {
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin if (uin) return uin
uin = (await session?.getGroupService().getUinByUids([Uid]))?.uins.get(Uid) uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
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 return uin
} }
static async getUinByUid(Uid: string) { async getUinByUid(uid: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return await NTQQUserApi.getUinByUidV2(Uid) return this.getUinByUidV2(uid)
} }
return await NTQQUserApi.getUinByUidV1(Uid) return this.getUinByUidV1(uid)
} }
@CacheClassFuncAsync(3600 * 1000, 'ClientKey') async forceFetchClientKey() {
static async forceFetchClientKey() {
const session = getSession() const session = getSession()
return await session?.getTicketService().forceFetchClientKey('')! if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ domain: '' }, null])
}
}
async getSelfNick(refresh = false) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const userInfo = await this.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
Object.assign(selfInfo, { nick: userInfo.nick })
return userInfo.nick
}
}
return selfInfo.nick
}
async setSelfStatus(status: number, extStatus: number, batteryStatus: number) {
return await invoke('nodeIKernelMsgService/setStatus', [{
statusReq: {
status,
extStatus,
batteryStatus,
}
}, null])
}
async getProfileLike(uid: string) {
return await invoke('nodeIKernelProfileLikeService/getBuddyProfileLike', [{
req: {
friendUids: [uid],
basic: 1,
vote: 1,
favorite: 0,
userProfile: 1,
type: 2,
start: 0,
limit: 20,
}
}, null])
} }
} }

View File

@@ -1,8 +1,12 @@
import { getSelfUin } from '@/common/data'
import { log } from '@/common/utils/log'
import { NTQQUserApi } from './user'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { CacheClassFuncAsync } from '@/common/utils/helper' import { Service, Context } from 'cordis'
import { Dict } from 'cosmokit'
declare module 'cordis' {
interface Context {
ntWebApi: NTQQWebApi
}
}
export enum WebHonorType { export enum WebHonorType {
ALL = 'all', ALL = 'all',
@@ -37,7 +41,7 @@ interface WebApiGroupMemberRet {
em: string em: string
cache: number cache: number
adm_num: number adm_num: number
levelname: any levelname: unknown
mems: WebApiGroupMember[] mems: WebApiGroupMember[]
count: number count: number
svr_time: number svr_time: number
@@ -46,321 +50,182 @@ interface WebApiGroupMemberRet {
extmode: number extmode: number
} }
export interface WebApiGroupNoticeFeed { export class NTQQWebApi extends Service {
u: number//发送者 static inject = ['ntUserApi']
fid: string//fid
pubt: number//时间
msg: {
text: string
text_face: string
title: string,
pics?: {
id: string,
w: string,
h: string
}[]
}
type: number
fn: number
cn: number
vn: number
settings: {
is_show_edit_card: number
remind_ts: number
tip_window_type: number
confirm_required: number
}
read_num: number
is_read: number
is_all_confirm: number
}
export interface WebApiGroupNoticeRet { constructor(protected ctx: Context) {
ec: number super(ctx, 'ntWebApi', true)
em: string
ltsm: number
srv_code: number
read_only: number
role: number
feeds: WebApiGroupNoticeFeed[]
group: {
group_id: number
class_ext: number
}
sta: number,
gln: number
tst: number,
ui: any
server_time: number
svrt: number
ad: number
}
interface GroupEssenceMsg {
group_code: string
msg_seq: number
msg_random: number
sender_uin: string
sender_nick: string
sender_time: number
add_digest_uin: string
add_digest_nick: string
add_digest_time: number
msg_content: any[]
can_be_removed: true
}
export interface GroupEssenceMsgRet {
retcode: number
retmsg: string
data: {
msg_list: GroupEssenceMsg[]
is_end: boolean
group_role: number
config_page_url: string
}
}
export class WebApi {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | 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') async getGroupMembers(groupCode: string): Promise<WebApiGroupMember[]> {
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>()
//logDebug('webapi 获取群成员', GroupCode) const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
let MemberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>() const cookieStr = this.cookieToString(cookieObject)
try { const retList: Promise<WebApiGroupMemberRet>[] = []
const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com') const params = new URLSearchParams({
const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ') st: '0',
const Bkn = WebApi.genBkn(CookiesObject.skey) end: '40',
const retList: Promise<WebApiGroupMemberRet>[] = [] sort: '1',
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 }); gc: groupCode,
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) { bkn: this.genBkn(cookieObject.skey)
return [] })
} else { const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
for (const key in fastRet.mems) { if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
MemberData.push(fastRet.mems[key]) return []
} } else {
for (const member of fastRet.mems) {
memberData.push(member)
} }
//初始化获取PageNum }
const PageNum = Math.ceil(fastRet.count / 40) const pageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求 //遍历批量请求
for (let i = 2; i <= PageNum; i++) { 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 }); params.set('st', String((i - 1) * 40))
retList.push(ret) params.set('end', String(i * 40))
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
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 member of ret.mems) {
for (let i = 1; i <= PageNum; i++) { memberData.push(member)
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
} }
return memberData
} }
static async getGrouptNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> { genBkn(sKey: string) {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com']))['qun.qq.com'] sKey = sKey || ''
const _Skey = await NTQQUserApi.getSkey() let hash = 5381
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++) { for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i); const code = sKey.charCodeAt(i)
hash = hash + (hash << 5) + code; hash = hash + (hash << 5) + code
} }
return (hash & 0x7FFFFFFF).toString()
return (hash & 0x7FFFFFFF).toString();
} }
//实现未缓存 考虑2h缓存 //实现未缓存 考虑2h缓存
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) { async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) { const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString(); const url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString()
let res = ''; let resJson
let resJson;
try { try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue }); const res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr })
const match = res.match(/window\.__INITIAL_STATE__=(.*?);/); const match = res.match(/window\.__INITIAL_STATE__=(.*?);/)
if (match) { if (match) {
resJson = JSON.parse(match[1].trim()); resJson = JSON.parse(match[1].trim())
} }
if (Internal_type === 1) { if (Internal_type === 1) {
return resJson?.talkativeList; return resJson?.talkativeList
} else { } else {
return resJson?.actorList; return resJson?.actorList
} }
} catch (e) { } catch (e) {
log('获取当前群荣耀失败', url, e); this.ctx.logger.error('获取当前群荣耀失败', url, e)
} }
return undefined; return undefined
} }
let HonorInfo: any = { group_id: groupCode }; const honorInfo: Dict = { group_id: groupCode }
const CookieValue = (await NTQQUserApi.getCookies('qun.qq.com')).cookies; const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = this.cookieToString(cookieObject)
if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) { if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 1); const RetInternal = await getDataInternal(groupCode, 1)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取龙王信息失败'); throw new Error('获取龙王信息失败')
} }
HonorInfo.current_talkative = { honorInfo.current_talkative = {
user_id: RetInternal[0]?.uin, user_id: RetInternal[0]?.uin,
avatar: RetInternal[0]?.avatar, avatar: RetInternal[0]?.avatar,
nickname: RetInternal[0]?.name, nickname: RetInternal[0]?.name,
day_count: 0, day_count: 0,
description: RetInternal[0]?.desc description: RetInternal[0]?.desc
} }
HonorInfo.talkative_list = []; honorInfo.talkative_list = [];
for (const talkative_ele of RetInternal) { for (const talkative_ele of RetInternal) {
HonorInfo.talkative_list.push({ honorInfo.talkative_list.push({
user_id: talkative_ele?.uin, user_id: talkative_ele?.uin,
avatar: talkative_ele?.avatar, avatar: talkative_ele?.avatar,
description: talkative_ele?.desc, description: talkative_ele?.desc,
day_count: 0, day_count: 0,
nickname: talkative_ele?.name nickname: talkative_ele?.name
}); })
} }
} catch (e) { } catch (e) {
log(e); this.ctx.logger.error(e)
} }
} }
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 2); const RetInternal = await getDataInternal(groupCode, 2)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取群聊之火失败'); throw new Error('获取群聊之火失败')
} }
HonorInfo.performer_list = []; honorInfo.performer_list = []
for (const performer_ele of RetInternal) { for (const performer_ele of RetInternal) {
HonorInfo.performer_list.push({ honorInfo.performer_list.push({
user_id: performer_ele?.uin, user_id: performer_ele?.uin,
nickname: performer_ele?.name, nickname: performer_ele?.name,
avatar: performer_ele?.avatar, avatar: performer_ele?.avatar,
description: performer_ele?.desc description: performer_ele?.desc
}); })
} }
} catch (e) { } catch (e) {
log(e); this.ctx.logger.error(e)
} }
} }
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) { if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 3); const RetInternal = await getDataInternal(groupCode, 3)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取群聊炽焰失败'); throw new Error('获取群聊炽焰失败')
} }
HonorInfo.legend_list = []; honorInfo.legend_list = []
for (const legend_ele of RetInternal) { for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({ honorInfo.legend_list.push({
user_id: legend_ele?.uin, user_id: legend_ele?.uin,
nickname: legend_ele?.name, nickname: legend_ele?.name,
avatar: legend_ele?.avatar, avatar: legend_ele?.avatar,
desc: legend_ele?.description desc: legend_ele?.description
}); })
} }
} catch (e) { } catch (e) {
log('获取群聊炽焰失败', e); this.ctx.logger.error('获取群聊炽焰失败', e)
} }
} }
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try { try {
let RetInternal = await getDataInternal(groupCode, 6); const RetInternal = await getDataInternal(groupCode, 6)
if (!RetInternal) { if (!RetInternal) {
throw new Error('获取快乐源泉失败'); throw new Error('获取快乐源泉失败')
} }
HonorInfo.emotion_list = []; honorInfo.emotion_list = []
for (const emotion_ele of RetInternal) { for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({ honorInfo.emotion_list.push({
user_id: emotion_ele?.uin, user_id: emotion_ele?.uin,
nickname: emotion_ele?.name, nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar, avatar: emotion_ele?.avatar,
desc: emotion_ele?.description desc: emotion_ele?.description
}); })
} }
} catch (e) { } catch (e) {
log('获取快乐源泉失败', e); this.ctx.logger.error('获取快乐源泉失败', e)
} }
} }
//冒尖小春笋好像已经被tx扬了 //冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) { if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
HonorInfo.strong_newbie_list = []; honorInfo.strong_newbie_list = []
} }
return HonorInfo; return honorInfo
}
private cookieToString(cookieObject: Dict) {
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
} }
} }

View File

@@ -1,42 +1,56 @@
import { callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { ReceiveCmd } from '../hook' import { GeneralCallResult } from '../services'
import { ReceiveCmdS } from '../hook'
import { BrowserWindow } from 'electron' import { BrowserWindow } from 'electron'
import { Service, Context } from 'cordis'
declare module 'cordis' {
interface Context {
ntWindowApi: NTQQWindowApi
}
}
export interface NTQQWindow { export interface NTQQWindow {
windowName: string windowName: string
windowUrlHash: string windowUrlHash: string
} }
export class NTQQWindows { export namespace NTQQWindows {
static GroupHomeWorkWindow: NTQQWindow = { export const GroupHomeWorkWindow: NTQQWindow = {
windowName: 'GroupHomeWorkWindow', windowName: 'GroupHomeWorkWindow',
windowUrlHash: '#/group-home-work', windowUrlHash: '#/group-home-work',
} }
static GroupNotifyFilterWindow: NTQQWindow = { export const GroupNotifyFilterWindow: NTQQWindow = {
windowName: 'GroupNotifyFilterWindow', windowName: 'GroupNotifyFilterWindow',
windowUrlHash: '#/group-notify-filter', windowUrlHash: '#/group-notify-filter',
} }
static GroupEssenceWindow: NTQQWindow = { export const GroupEssenceWindow: NTQQWindow = {
windowName: 'GroupEssenceWindow', windowName: 'GroupEssenceWindow',
windowUrlHash: '#/group-essence', windowUrlHash: '#/group-essence',
} }
} }
export class NTQQWindowApi { export class NTQQWindowApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntWindowApi', true)
}
// 打开窗口并获取对应的下发事件 // 打开窗口并获取对应的下发事件
static async openWindow<R = GeneralCallResult>( async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: any[], args: unknown[],
cbCmd: ReceiveCmd | null = null, cbCmd: ReceiveCmdS | undefined,
autoCloseSeconds: number = 2, autoCloseSeconds: number = 2,
) { ) {
const result = await callNTQQApi<R>({ const result = await invoke<R>(
className: NTQQApiClass.WINDOW_API, NTMethod.OPEN_EXTRA_WINDOW,
methodName: NTQQApiMethod.OPEN_EXTRA_WINDOW, [ntQQWindow.windowName, ...args],
cbCmd, {
afterFirstCmd: false, className: NTClass.WINDOW_API,
args: [ntQQWindow.windowName, ...args], cbCmd,
}) afterFirstCmd: false,
}
)
setTimeout(() => { setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) { for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL()) // log("close window", w.webContents.getURL())

241
src/ntqqapi/core.ts Normal file
View File

@@ -0,0 +1,241 @@
import fs from 'node:fs'
import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { Config as LLOBConfig } from '../common/types'
import { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall'
import {
RawMessage,
GroupNotify,
FriendRequestNotify,
FriendRequest,
GroupMember,
CategoryFriend,
SimpleInfo,
User,
ChatType
} from './types'
import { selfInfo } from '../common/globalVars'
import { version } from '../version'
import { invoke } from './ntcall'
declare module 'cordis' {
interface Context {
app: Core
}
interface Events {
'nt/message-created': (input: RawMessage[]) => void
'nt/message-deleted': (input: RawMessage) => void
'nt/message-sent': (input: RawMessage) => void
'nt/group-notify': (input: GroupNotify[]) => void
'nt/friend-request': (input: FriendRequest[]) => void
'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
'nt/system-message-created': (input: Uint8Array) => void
}
}
class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true)
}
public start() {
llonebotError.otherError = ''
this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => {
Object.assign(this.config, input)
})
}
private registerListener() {
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
let friendList: User[] = []
if ('userSimpleInfos' in payload) {
friendList = Object.values((payload as unknown as V2data).userSimpleInfos).map((v: SimpleInfo) => {
return {
...v.coreInfo,
}
})
} else {
for (const fData of payload.data) {
friendList.push(...fData.buddyList)
}
}
this.ctx.logger.info('好友列表变动', friendList.length)
for (const friend of friendList) {
this.ctx.ntMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend })
}
})
// 自动清理新消息文件
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], (payload) => {
if (!this.config.autoDeleteFile) {
return
}
for (const message of payload.msgList) {
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...(msgElement.picElement?.thumbPath ?? []).values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath = [...(msgElement.videoElement?.thumbPath ?? []).values()]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
for (const path of pathList) {
if (path) {
fs.unlink(path, () => {
this.ctx.logger.info('删除文件成功', path)
})
}
}
}, this.config.autoDeleteFileSecond! * 1000)
}
}
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
Object.assign(selfInfo, { online: info.info.status !== 20 })
})
const 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 contact of recentContact.changedList) {
if (activatedPeerUids.includes(contact.id)) continue
activatedPeerUids.push(contact.id)
const peer = { peerUid: contact.id, chatType: contact.chatType }
if (contact.chatType === ChatType.temp) {
this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => {
this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
const lastTempMsg = msgList.at(-1)
if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) {
this.ctx.parallel('nt/message-created', [lastTempMsg!])
}
})
})
}
else {
this.ctx.ntMsgApi.activateChat(peer)
}
}
}
})
registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
this.ctx.logger.info('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else if (!(await this.ctx.ntFriendApi.isBuddy(peerUid))) {
chatType = ChatType.temp
}
const peer = { peerUid, chatType }
await this.ctx.sleep(1000)
this.ctx.ntMsgApi.activateChat(peer).then((r) => {
this.ctx.logger.info('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
this.ctx.parallel('nt/group-member-info-updated', { groupCode, members })
})
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
this.ctx.parallel('nt/message-created', payload.msgList)
})
const sentMsgIds = new Map<string, boolean>()
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => {
for (const msg of payload.msgList) {
if (msg.recallTime !== '0' && !recallMsgIds.includes(msg.msgId)) {
recallMsgIds.push(msg.msgId)
this.ctx.parallel('nt/message-deleted', msg)
} else if (sentMsgIds.get(msg.msgId)) {
sentMsgIds.delete(msg.msgId)
this.ctx.parallel('nt/message-sent', msg)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
if (!this.config.reportSelfMessage) {
return
}
sentMsgIds.set(payload.msgRecord.msgId, true)
})
const groupNotifyFlags: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
let notifies: GroupNotify[]
try {
notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) {
return
}
const list = notifies.filter(v => {
const flag = v.group.groupCode + '|' + v.seq + '|' + v.type
if (groupNotifyFlags.includes(flag)) {
return false
}
groupNotifyFlags.push(flag)
return true
})
this.ctx.parallel('nt/group-notify', list)
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs)
})
invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { classNameIsRegister: true })
registerReceiveHook<{
msgBuf: number[]
}>('nodeIKernelMsgListener/onRecvSysMsg', payload => {
this.ctx.parallel('nt/system-message-created', Uint8Array.from(payload.msgBuf))
})
}
}
namespace Core {
export interface Config extends LLOBConfig {
}
}
export default Core

View File

@@ -1,3 +1,5 @@
import ffmpeg from 'fluent-ffmpeg'
import faceConfig from './helper/face_config.json'
import { import {
AtType, AtType,
ElementType, ElementType,
@@ -13,24 +15,17 @@ import {
SendTextElement, SendTextElement,
SendVideoElement, SendVideoElement,
} from './types' } from './types'
import { promises as fs } from 'node:fs' import { stat, writeFile, copyFile, unlink } from 'node:fs/promises'
import ffmpeg from 'fluent-ffmpeg' import { calculateFileMD5 } from '../common/utils/file'
import { NTQQFileApi } from './api/file'
import { calculateFileMD5, isGIF } from '../common/utils/file'
import { log } from '../common/utils/log'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
import { isNull } from '../common/utils' import { Context } from 'cordis'
import faceConfig from './face_config.json' import { isNullable } from 'cosmokit'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName //export const mFaceCache = new Map<string, string>() // emojiId -> faceName
export class SendMsgElementConstructor { export namespace SendElementEntities {
static poke(groupCode: string, uin: string) { export function text(content: string): SendTextElement {
return null
}
static text(content: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', elementId: '',
@@ -44,7 +39,7 @@ export class SendMsgElementConstructor {
} }
} }
static at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement { export function at(atUid: string, atNtUid: string, atType: AtType, display: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', elementId: '',
@@ -58,7 +53,7 @@ export class SendMsgElementConstructor {
} }
} }
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { export function reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return { return {
elementType: ElementType.REPLY, elementType: ElementType.REPLY,
elementId: '', elementId: '',
@@ -71,16 +66,12 @@ export class SendMsgElementConstructor {
} }
} }
static async pic(picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise<SendPicElement> { export async function pic(ctx: Context, picPath: string, summary = '', subType: 0 | 1 = 0, isFlashPic?: boolean): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(picPath, ElementType.PIC, subType)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常,大小为 0'
} }
const maxMB = 30; const imageSize = await ctx.ntFileApi.getImageSize(picPath)
if (fileSize > 1024 * 1024 * 30) {
throw `图片过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
}
const imageSize = await NTQQFileApi.getImageSize(picPath)
const picElement = { const picElement = {
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize.toString(), fileSize: fileSize.toString(),
@@ -89,14 +80,15 @@ export class SendMsgElementConstructor {
fileName: fileName, fileName: fileName,
sourcePath: path, sourcePath: path,
original: true, original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg, picType: imageSize.type === 'gif' ? PicType.gif : PicType.jpg,
picSubType: subType, picSubType: subType,
fileUuid: '', fileUuid: '',
fileSubId: '', fileSubId: '',
thumbFileSize: 0, thumbFileSize: 0,
summary, summary,
isFlashPic,
} }
log('图片信息', picElement) ctx.logger.info('图片信息', picElement)
return { return {
elementType: ElementType.PIC, elementType: ElementType.PIC,
elementId: '', elementId: '',
@@ -104,34 +96,35 @@ export class SendMsgElementConstructor {
} }
} }
static async file(filePath: string, fileName: string = '', folderId: string = ''): Promise<SendFileElement> { export async function file(ctx: Context, filePath: string, fileName: string, folderId = ''): Promise<SendFileElement> {
const { fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE) const fileSize = (await stat(filePath)).size.toString()
if (fileSize === 0) { if (fileSize === '0') {
throw '文件异常,大小为 0' ctx.logger.warn(`文件${fileName}异常,大小为 0`)
throw new Error('文件异常,大小为 0')
} }
const element: SendFileElement = { const element: SendFileElement = {
elementType: ElementType.FILE, elementType: ElementType.FILE,
elementId: '', elementId: '',
fileElement: { fileElement: {
fileName: fileName || _fileName, fileName,
folderId: folderId, folderId,
filePath: path!, filePath,
fileSize: fileSize.toString(), fileSize,
}, },
} }
return element return element
} }
static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> { export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> {
try { try {
await fs.stat(filePath) await stat(filePath)
} catch (e) { } catch (e) {
throw `文件${filePath}异常,不存在` throw `文件${filePath}异常,不存在`
} }
log('复制视频到QQ目录', filePath) ctx.logger.info('复制视频到QQ目录', filePath)
let { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO) const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.VIDEO)
log('复制视频到QQ目录完成', path) ctx.logger.info('复制视频到QQ目录完成', path)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
@@ -152,21 +145,21 @@ export class SendMsgElementConstructor {
filePath, filePath,
} }
try { try {
videoInfo = await getVideoInfo(path) videoInfo = await getVideoInfo(ctx, path)
log('视频信息', videoInfo) ctx.logger.info('视频信息', videoInfo)
} catch (e) { } catch (e) {
log('获取视频信息失败', e) ctx.logger.info('获取视频信息失败', e)
} }
const createThumb = new Promise<string>((resolve, reject) => { const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png` const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumbDir, thumbFileName) const thumbPath = pathLib.join(thumbDir, thumbFileName)
log('开始生成视频缩略图', filePath) ctx.logger.info('开始生成视频缩略图', filePath)
let completed = false let completed = false
function useDefaultThumb() { function useDefaultThumb() {
if (completed) return if (completed) return
log('获取视频封面失败,使用默认封面') ctx.logger.info('获取视频封面失败,使用默认封面')
fs.writeFile(thumbPath, defaultVideoThumb) writeFile(thumbPath, defaultVideoThumb)
.then(() => { .then(() => {
resolve(thumbPath) resolve(thumbPath)
}) })
@@ -175,9 +168,9 @@ export class SendMsgElementConstructor {
setTimeout(useDefaultThumb, 5000) setTimeout(useDefaultThumb, 5000)
ffmpeg(filePath) ffmpeg(filePath)
.on('error', (err) => { .on('error', () => {
if (diyThumbPath) { if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath) copyFile(diyThumbPath, thumbPath)
.then(() => { .then(() => {
completed = true completed = true
resolve(thumbPath) resolve(thumbPath)
@@ -194,19 +187,19 @@ export class SendMsgElementConstructor {
size: videoInfo.width + 'x' + videoInfo.height, size: videoInfo.width + 'x' + videoInfo.height,
}) })
.on('end', () => { .on('end', () => {
log('生成视频缩略图', thumbPath) ctx.logger.info('生成视频缩略图', thumbPath)
completed = true completed = true
resolve(thumbPath) resolve(thumbPath)
}) })
}) })
let thumbPath = new Map() const thumbPath = new Map()
const _thumbPath = await createThumb const _thumbPath = await createThumb
log('生成视频缩略图', _thumbPath) ctx.logger.info('生成视频缩略图', _thumbPath)
const thumbSize = (await fs.stat(_thumbPath)).size const thumbSize = (await stat(_thumbPath)).size
// log("生成缩略图", _thumbPath) // log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
let element: SendVideoElement = { const element: SendVideoElement = {
elementType: ElementType.VIDEO, elementType: ElementType.VIDEO,
elementId: '', elementId: '',
videoElement: { videoElement: {
@@ -232,22 +225,22 @@ export class SendMsgElementConstructor {
// sourceVideoCodecFormat: 2 // sourceVideoCodecFormat: 2
}, },
} }
log('videoElement', element) ctx.logger.info('videoElement', element)
return element return element
} }
static async ptt(pttPath: string): Promise<SendPttElement> { export async function ptt(ctx: Context, pttPath: string): Promise<SendPttElement> {
const { converted, path: silkPath, duration } = await encodeSilk(pttPath) const { converted, path: silkPath, duration } = await encodeSilk(ctx, pttPath)
if (!silkPath) { if (!silkPath) {
throw '语音转换失败, 请检查语音文件是否正常' throw '语音转换失败, 请检查语音文件是否正常'
} }
// log("生成语音", silkPath, duration); // log("生成语音", silkPath, duration);
const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT) const { md5, fileName, path, fileSize } = await ctx.ntFileApi.uploadFile(silkPath, ElementType.PTT)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw '文件异常大小为0'
} }
if (converted) { if (converted) {
fs.unlink(silkPath).then() unlink(silkPath)
} }
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,
@@ -271,7 +264,7 @@ export class SendMsgElementConstructor {
} }
} }
static face(faceId: number): SendFaceElement { export function face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji const emojiFaces = faceConfig.emoji
@@ -300,23 +293,24 @@ export class SendMsgElementConstructor {
} }
} }
static mface(emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement { export function mface(emojiPackageId: number, emojiId: string, key: string, summary?: string): SendMarketFaceElement {
return { return {
elementType: ElementType.MFACE, elementType: ElementType.MFACE,
marketFaceElement: { marketFaceElement: {
imageWidth: 300,
imageHeight: 300,
emojiPackageId, emojiPackageId,
emojiId, emojiId,
key, key,
faceName: faceName || mFaceCache.get(emojiId) || '[商城表情]', faceName: summary || '[商城表情]',
}, },
} }
} }
static dice(resultId: number | null): SendFaceElement { export function dice(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果 // 实际测试并不能控制结果
// 随机1到6 // 随机1到6
if (isNull(resultId)) resultId = Math.floor(Math.random() * 6) + 1 if (isNullable(resultId)) resultId = Math.floor(Math.random() * 6) + 1
return { return {
elementType: ElementType.FACE, elementType: ElementType.FACE,
elementId: '', elementId: '',
@@ -328,7 +322,7 @@ export class SendMsgElementConstructor {
stickerId: '33', stickerId: '33',
sourceType: 1, sourceType: 1,
stickerType: 2, stickerType: 2,
resultId: resultId?.toString(), resultId: resultId.toString(),
surpriseId: '', surpriseId: '',
// "randomType": 1, // "randomType": 1,
}, },
@@ -336,9 +330,9 @@ export class SendMsgElementConstructor {
} }
// 猜拳(石头剪刀布)表情 // 猜拳(石头剪刀布)表情
static rps(resultId: number | null): SendFaceElement { export function rps(resultId?: string | number): SendFaceElement {
// 实际测试并不能控制结果 // 实际测试并不能控制结果
if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1 if (isNullable(resultId)) resultId = Math.floor(Math.random() * 3) + 1
return { return {
elementType: ElementType.FACE, elementType: ElementType.FACE,
elementId: '', elementId: '',
@@ -350,14 +344,14 @@ export class SendMsgElementConstructor {
stickerId: '34', stickerId: '34',
sourceType: 1, sourceType: 1,
stickerType: 2, stickerType: 2,
resultId: resultId?.toString(), resultId: resultId.toString(),
surpriseId: '', surpriseId: '',
// "randomType": 1, // "randomType": 1,
}, },
} }
} }
static ark(data: string): SendArkElement { export function ark(data: string): SendArkElement {
return { return {
elementType: ElementType.ARK, elementType: ElementType.ARK,
elementId: '', elementId: '',
@@ -368,4 +362,16 @@ export class SendMsgElementConstructor {
}, },
} }
} }
export function shake(): SendFaceElement {
return {
elementType: ElementType.FACE,
elementId: '',
faceElement: {
faceIndex: 1,
faceType: 5,
pokeType: 1,
},
}
}
} }

View File

@@ -1,6 +1,4 @@
//远端rkey获取 import { Context } from 'cordis'
import { log } from '@/common/utils'
interface ServerRkeyData { interface ServerRkeyData {
group_rkey: string group_rkey: string
@@ -8,15 +6,15 @@ interface ServerRkeyData {
expired_time: number expired_time: number
} }
class RkeyManager { export class RkeyManager {
serverUrl: string = '' private serverUrl: string = ''
private rkeyData: ServerRkeyData = { private rkeyData: ServerRkeyData = {
group_rkey: '', group_rkey: '',
private_rkey: '', private_rkey: '',
expired_time: 0 expired_time: 0
} }
constructor(serverUrl: string) { constructor(protected ctx: Context, serverUrl: string) {
this.serverUrl = serverUrl this.serverUrl = serverUrl
} }
@@ -25,7 +23,7 @@ class RkeyManager {
try { try {
await this.refreshRkey() await this.refreshRkey()
} catch (e) { } catch (e) {
log('获取rkey失败', e) this.ctx.logger.error('获取rkey失败', e)
} }
} }
return this.rkeyData return this.rkeyData
@@ -37,7 +35,7 @@ class RkeyManager {
return now > this.rkeyData.expired_time return now > this.rkeyData.expired_time
} }
async refreshRkey(): Promise<any> { async refreshRkey() {
//刷新rkey //刷新rkey
this.rkeyData = await this.fetchServerRkey() this.rkeyData = await this.fetchServerRkey()
} }
@@ -60,5 +58,3 @@ class RkeyManager {
}) })
} }
} }
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey')

View File

@@ -1,170 +1,115 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTQQApiClass, NTQQApiMethod } from './ntcall' import { NTClass, NTMethod } from './ntcall'
import { NTQQMsgApi } from './api/msg'
import { CategoryFriend, ChatType, Group, GroupMember, GroupMemberRole, RawMessage } from './types'
import {
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 'fs'
import { dbUtil } from '@/common/db'
import { NTQQGroupApi } from './api/group'
import { log } from '@/common/utils' import { log } from '@/common/utils'
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'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Dict } from 'cosmokit'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export const hookApiCallbacks: Record<string, (res: any) => void> = {}
export let ReceiveCmdS = { export enum 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] type NTReturnData = [
{
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
0: {
type: 'request' type: 'request'
eventName: NTQQApiClass eventName: NTClass
callbackId?: string callbackId?: string
} },
1: { {
cmdName: ReceiveCmd cmdName: ReceiveCmdS
cmdType: 'event' cmdType: 'event'
payload: PayloadType payload: unknown
}[] }[]
} ]
let receiveHooks: Array<{ const logHook = false
method: ReceiveCmd[]
const receiveHooks: Array<{
method: ReceiveCmdS[]
hookFunc: (payload: any) => void | Promise<void> hookFunc: (payload: any) => void | Promise<void>
id: string id: string
}> = [] }> = []
let callHooks: Array<{ const callHooks: Array<{
method: NTQQApiMethod[] method: NTMethod[]
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow) { export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) {
const originalSend = window.webContents.send window.webContents.send = new Proxy(window.webContents.send, {
const patchSend = (channel: string, ...args: NTQQApiReturnData) => { apply(target, thisArg, args: [channel: string, ...args: NTReturnData]) {
// console.log("hookNTQQApiReceive", channel, args)
let isLogger = false
try {
isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi')
} catch (e) { }
if (!isLogger) {
try { try {
HOOK_LOG && log(`received ntqq api message: ${channel}`, args) if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) {
} catch (e) { log('received ntqq api message', args)
log('hook log error', e, args) }
} } catch { }
} if (!onlyLog) {
try { if (args[2] instanceof Array) {
if (args?.[1] instanceof Array) { for (const receiveData of args[2]) {
for (let receiveData of args?.[1]) { const ntMethodName = receiveData.cmdName
const ntQQApiMethodName = receiveData.cmdName for (const hook of receiveHooks) {
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) if (hook.method.includes(ntMethodName)) {
for (let hook of receiveHooks) { Promise.resolve(hook.hookFunc(receiveData.payload))
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[1]?.callbackId) {
if (args[0]?.callbackId) { const callbackId = args[1].callbackId
// log("hookApiCallback", hookApiCallbacks, args) if (hookApiCallbacks[callbackId]) {
const callbackId = args[0].callbackId Promise.resolve(hookApiCallbacks[callbackId](args[2]))
if (hookApiCallbacks[callbackId]) { delete hookApiCallbacks[callbackId]
// log("callback found") }
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1])
}).then()
delete hookApiCallbacks[callbackId]
} }
} }
} catch (e: any) { return target.apply(thisArg, args)
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, onlyLog: boolean) {
// 监听调用NTQQApi const webContents = window.webContents as Dict
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); const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
let isLogger = false
try {
isLogger = args[3][0].eventName.startsWith('ns-LoggerApi')
} catch (e) { }
if (!isLogger) { if (!isLogger) {
try { try {
HOOK_LOG && log('call NTQQ api', thisArg, args) logHook && 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 {
let _ = hook.hookFunc(callParams)
if (hook.hookFunc.constructor.name === 'AsyncFunction') {
(_ as Promise<void>).then()
}
} catch (e) {
log('hook call error', e, _args)
}
}).then()
}
})
} catch (e) { } } catch (e) { }
if (!onlyLog) {
try {
const _args: unknown[] = args[3][1]
const cmdName = _args[0] as NTMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
Promise.resolve(hook.hookFunc(callParams))
}
})
} catch { }
}
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
}, },
@@ -175,20 +120,17 @@ export function hookNTQQApiCall(window: BrowserWindow) {
webContents._events['-ipc-message'] = proxyIpcMsg webContents._events['-ipc-message'] = proxyIpcMsg
} }
const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke'] /*const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
// console.log(args); //HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], { args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) { apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs) sendtarget.apply(sendthisArg, sendargs)
}, },
}) })
let ret = target.apply(thisArg, args) const ret = target.apply(thisArg, args)
try { //HOOK_LOG && log('call NTQQ invoke api return', ret)
HOOK_LOG && log('call NTQQ invoke api return', ret)
} catch (e) { }
return ret return ret
}, },
}) })
@@ -196,11 +138,11 @@ export function hookNTQQApiCall(window: BrowserWindow) {
webContents._events['-ipc-invoke'][0] = proxyIpcInvoke webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
} else { } else {
webContents._events['-ipc-invoke'] = proxyIpcInvoke webContents._events['-ipc-invoke'] = proxyIpcInvoke
} }*/
} }
export function registerReceiveHook<PayloadType>( export function registerReceiveHook<PayloadType>(
method: ReceiveCmd | ReceiveCmd[], method: string | string[],
hookFunc: (payload: PayloadType) => void, hookFunc: (payload: PayloadType) => void,
): string { ): string {
const id = randomUUID() const id = randomUUID()
@@ -208,7 +150,7 @@ export function registerReceiveHook<PayloadType>(
method = [method] method = [method]
} }
receiveHooks.push({ receiveHooks.push({
method, method: method as ReceiveCmdS[],
hookFunc, hookFunc,
id, id,
}) })
@@ -216,7 +158,7 @@ export function registerReceiveHook<PayloadType>(
} }
export function registerCallHook( export function registerCallHook(
method: NTQQApiMethod | NTQQApiMethod[], method: NTMethod | NTMethod[],
hookFunc: (callParams: unknown[]) => void | Promise<void>, hookFunc: (callParams: unknown[]) => void | Promise<void>,
): void { ): void {
if (!Array.isArray(method)) { if (!Array.isArray(method)) {
@@ -232,280 +174,3 @@ 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[] = []
async function updateGroups(_groups: Group[], needUpdate: boolean = true) {
for (let group of _groups) {
log('update group', group)
if (group.privilegeFlag === 0) {
deleteGroup(group.groupCode)
continue
}
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[] }) {
try {
const newGroupList = payload.groupList
for (const group of newGroupList) {
let existGroup = groups.find((g) => g.groupCode == group.groupCode)
if (existGroup) {
if (existGroup.memberCount > group.memberCount) {
log(`群(${group.groupCode})成员数量减少${existGroup.memberCount} -> ${group.memberCount}`)
const oldMembers = existGroup.members
await sleep(200) // 如果请求QQ API的速度过快通常无法正确拉取到最新的群信息因此这里人为引入一个延时
const newMembers = await NTQQGroupApi.getGroupMembers(group.groupCode)
group.members = Array.from(newMembers.values())
const newMembersSet = new Set<string>() // 建立索引降低时间复杂度
for (const member of newMembers) {
newMembersSet.add(member[1].uin)
}
// 判断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) {
deleteGroup(group.groupCode)
}
}
}
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
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
// log("群成员信息变动", groupCode, members)
for (const member of members) {
const existMember = await getGroupMember(groupCode, member.uin)
if (existMember) {
if (member.cardName != existMember.cardName) {
log('群成员名片变动', `${groupCode}: ${existMember.uin}`, existMember.cardName, '->', member.cardName)
postOb11Event(
new OB11GroupCardEvent(parseInt(groupCode), parseInt(member.uin), member.cardName, existMember.cardName),
)
} 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);
// if (existGroup) {
// log("对比群成员", existGroup.members, members)
// for (const member of members) {
// const existMember = existGroup.members.find(m => m.uin == member.uin);
// if (existMember) {
// log("对比群名片", existMember.cardName, member.cardName)
// if (existMember.cardName != member.cardName) {
// postOB11Event(new OB11GroupCardEvent(parseInt(existGroup.groupCode), parseInt(member.uin), member.cardName, existMember.cardName));
// }
// Object.assign(existMember, member);
// }
// }
// }
})
// 好友列表变动
registerReceiveHook<{
data: CategoryFriend[]
}>(ReceiveCmdS.FRIENDS, (payload) => {
for (const fData of payload.data) {
const _friends = fData.buddyList
for (let friend of _friends) {
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()
if (!autoDeleteFile) {
return
}
for (const message of payload.msgList) {
// log("收到新消息push到历史记录", message.msgId)
// dbUtil.addMsg(message).then()
// 清理文件
for (const msgElement of message.elements) {
setTimeout(() => {
const picPath = msgElement.picElement?.sourcePath
const picThumbPath = [...msgElement.picElement?.thumbPath.values()]
const pttPath = msgElement.pttElement?.filePath
const filePath = msgElement.fileElement?.filePath
const videoPath = msgElement.videoElement?.filePath
const videoThumbPath: string[] = [...msgElement.videoElement.thumbPath?.values()!]
const pathList = [picPath, ...picThumbPath, pttPath, filePath, videoPath, ...videoThumbPath]
if (msgElement.picElement) {
pathList.push(...Object.values(msgElement.picElement.thumbPath))
}
// log("需要清理的文件", pathList);
for (const path of pathList) {
if (path) {
fs.unlink(picPath, () => {
log('删除文件成功', path)
})
}
}
}, getConfigUtil().getConfig().autoDeleteFileSecond! * 1000)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, ({ msgRecord }) => {
const message = msgRecord
dbUtil.addMsg(message).then()
})
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

@@ -1,240 +0,0 @@
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

@@ -1,514 +0,0 @@
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

@@ -1,44 +0,0 @@
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

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

View File

@@ -1,10 +1,23 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log' import { log } from '../common/utils/legacyLog'
import { HOOK_LOG } from '../common/config'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import {
GeneralCallResult,
NodeIKernelBuddyService,
NodeIKernelProfileService,
NodeIKernelGroupService,
NodeIKernelProfileLikeService,
NodeIKernelMsgService,
NodeIKernelMSFService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelSearchService,
} from './services'
export enum NTQQApiClass { export enum NTClass {
NT_API = 'ns-ntApi', NT_API = 'ns-ntApi',
FS_API = 'ns-FsApi', FS_API = 'ns-FsApi',
OS_API = 'ns-OsApi', OS_API = 'ns-OsApi',
@@ -15,212 +28,156 @@ export enum NTQQApiClass {
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',
NODE_STORE_API = 'ns-NodeStoreApi'
} }
export enum NTQQApiMethod { export enum NTMethod {
TEST = 'NodeIKernelTipOffService/getPskey',
RECENT_CONTACT = 'nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact',
ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息 ACTIVE_CHAT_PREVIEW = 'nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回最新预览消息
ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息 ACTIVE_CHAT_HISTORY = 'nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat', // 激活聊天窗口,有时候必须这样才能收到消息, 并返回历史消息
HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf', HISTORY_MSG = 'nodeIKernelMsgService/getMsgsIncludeSelf',
GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg', GET_MULTI_MSG = 'nodeIKernelMsgService/getMultiMsg',
DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid', DELETE_ACTIVE_CHAT = 'nodeIKernelMsgService/deleteActiveChatByUid',
ENTER_OR_EXIT_AIO = 'nodeIKernelMsgService/enterOrExitAio', MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
LIKE_FRIEND = 'nodeIKernelProfileLikeService/setBuddyProfileLike',
SELF_INFO = 'fetchAuthData', SELF_INFO = 'fetchAuthData',
FRIENDS = 'nodeIKernelBuddyService/getBuddyList',
GROUPS = 'nodeIKernelGroupService/getGroupList',
GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
GROUP_MEMBERS_INFO = 'nodeIKernelGroupService/getMemberInfo',
USER_INFO = 'nodeIKernelProfileService/getUserSimpleInfo',
USER_DETAIL_INFO = 'nodeIKernelProfileService/getUserDetailInfo',
USER_DETAIL_INFO_WITH_BIZ_INFO = 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
FILE_TYPE = 'getFileType', FILE_TYPE = 'getFileType',
FILE_MD5 = 'getFileMd5', FILE_MD5 = 'getFileMd5',
FILE_COPY = 'copyFile', FILE_COPY = 'copyFile',
IMAGE_SIZE = 'getImageSizeFromPath', IMAGE_SIZE = 'getImageSizeFromPath',
FILE_SIZE = 'getFileSize', FILE_SIZE = 'getFileSize',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild', CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
OPEN_EXTRA_WINDOW = 'openExternalWindow',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg', GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
SEND_MSG = 'nodeIKernelMsgService/sendMsg', GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
DOWNLOAD_MEDIA = 'nodeIKernelMsgService/downloadRichMedia',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
MULTI_FORWARD_MSG = 'nodeIKernelMsgService/multiForwardMsgWithComment', // 合并转发
GET_GROUP_NOTICE = 'nodeIKernelGroupService/getSingleScreenNotifies',
HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify', HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
QUIT_GROUP = 'nodeIKernelGroupService/quitGroup', QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes', GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
KICK_MEMBER = 'nodeIKernelGroupService/kickMember', KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp', MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp', MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp',
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName', SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName',
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole', SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole',
PUBLISH_GROUP_BULLETIN = 'nodeIKernelGroupService/publishGroupBulletinBulletin',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName', SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
SET_GROUP_TITLE = 'nodeIKernelGroupService/modifyMemberSpecialTitle',
ACTIVATE_MEMBER_LIST_CHANGE = 'nodeIKernelGroupListener/onMemberListChange', HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
ACTIVATE_MEMBER_INFO_CHANGE = 'nodeIKernelGroupListener/onMemberInfoChange',
GET_MSG_BOX_INFO = 'nodeIKernelMsgService/getABatchOfContactMsgBoxInfo',
GET_GROUP_ALL_INFO = 'nodeIKernelGroupService/getGroupAllInfo',
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan', CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths', 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_SCAN = 'nodeIKernelStorageCleanService/scanCache',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys', CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo', CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo', CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo', 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 { export enum NTChannel {
IPC_UP_1 = 'IPC_UP_1',
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_4 = 'IPC_UP_4'
} }
interface NTQQApiParams { interface NTService {
methodName: NTQQApiMethod | string nodeIKernelBuddyService: NodeIKernelBuddyService
className?: NTQQApiClass nodeIKernelProfileService: NodeIKernelProfileService
channel?: NTQQApiChannel nodeIKernelGroupService: NodeIKernelGroupService
nodeIKernelProfileLikeService: NodeIKernelProfileLikeService
nodeIKernelMsgService: NodeIKernelMsgService
nodeIKernelMSFService: NodeIKernelMSFService
nodeIKernelUixConvertService: NodeIKernelUixConvertService
nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelSearchService: NodeIKernelSearchService
}
interface InvokeOptions<ReturnType> {
className?: NTClass
channel?: NTChannel
classNameIsRegister?: boolean classNameIsRegister?: boolean
args?: unknown[] cbCmd?: string | string[]
cbCmd?: ReceiveCmd | ReceiveCmd[] | null cmdCB?: (payload: ReturnType, result: unknown) => boolean
cmdCB?: (payload: any) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeoutSecond?: number timeout?: number
} }
export function callNTQQApi<ReturnType>(params: NTQQApiParams) { export function invoke<
let { R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>,
className, S extends keyof NTService = any,
methodName, M extends keyof NTService[S] & string = any
channel, >(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
args, const className = options.className ?? NTClass.NT_API
cbCmd, const channel = options.channel ?? NTChannel.IPC_UP_2
timeoutSecond: timeout, const timeout = options.timeout ?? 5000
classNameIsRegister, const afterFirstCmd = options.afterFirstCmd ?? true
cmdCB, let eventName = className + '-' + channel[channel.length - 1]
afterFirstCmd, if (options.classNameIsRegister) {
} = params eventName += '-register'
className = className ?? NTQQApiClass.NT_API }
channel = channel ?? NTQQApiChannel.IPC_UP_2 return new Promise<R>((resolve, reject) => {
args = args ?? [] const apiArgs = [method, ...args]
timeout = timeout ?? 5 const callbackId = randomUUID()
afterFirstCmd = afterFirstCmd ?? true const timeoutId = setTimeout(() => {
const uuid = randomUUID() log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
HOOK_LOG && log('callNTQQApi', channel, className, methodName, args, uuid) reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
return new Promise((resolve: (data: ReturnType) => void, reject) => { }, timeout)
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000 if (!options.cbCmd) {
let success = false
let eventName = className + '-' + channel[channel.length - 1]
if (classNameIsRegister) {
eventName += '-register'
}
const apiArgs = [methodName, ...args]
if (!cbCmd) {
// QQ后端会返回结果并且可以根据uuid识别 // QQ后端会返回结果并且可以根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => { hookApiCallbacks[callbackId] = res => {
success = true clearTimeout(timeoutId)
resolve(r) resolve(res)
} }
} }
else { else {
let result: unknown
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
const secondCallback = () => { const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB); if (options.cmdCB) {
if (!!cmdCB) { if (options.cmdCB(payload, result)) {
if (cmdCB(payload)) {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true clearTimeout(timeoutId)
resolve(payload) resolve(payload)
} }
} }
else { else {
removeReceiveHook(hookId) removeReceiveHook(hookId)
success = true clearTimeout(timeoutId)
resolve(payload) resolve(payload)
} }
}) })
} }
!afterFirstCmd && secondCallback() !afterFirstCmd && secondCallback()
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { hookApiCallbacks[callbackId] = (res: GeneralCallResult) => {
log(`${methodName} callback`, result) result = res
if (result?.result == 0 || result === undefined) { if (res?.result === 0 || ['undefined', 'number'].includes(typeof res)) {
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
success = true log('ntqq api call failed,', method, res)
reject(`ntqq api call failed, ${result.errMsg}`) clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`)
} }
} }
} }
setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName)
if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
}
}, _timeout)
ipcMain.emit( ipcMain.emit(
channel, channel,
{ {
sender: { sender: {
send: (..._args: unknown[]) => { send: () => {
}, },
}, },
}, },
{ type: 'request', callbackId: uuid, eventName }, { type: 'request', callbackId, eventName },
apiArgs, apiArgs,
) )
}) })
} }
export interface GeneralCallResult {
result: number // 0: success
errMsg: string
}
export class NTQQApi {
static async call(className: NTQQApiClass, cmdName: string, args: any[]) {
return await callNTQQApi<GeneralCallResult>({
className,
methodName: cmdName,
args: [...args],
})
}
static async fetchUnitedCommendConfig() {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG,
args: [
{
groups: ['100243'],
},
],
})
}
}

469
src/ntqqapi/proto/compiled.d.ts vendored Normal file
View File

@@ -0,0 +1,469 @@
import * as $protobuf from "protobufjs";
import Long = require("long");
/** Namespace SysMsg. */
export namespace SysMsg {
/** Properties of a SystemMessage. */
interface ISystemMessage {
/** SystemMessage header */
header?: (SysMsg.ISystemMessageHeader[]|null);
/** SystemMessage msgSpec */
msgSpec?: (SysMsg.ISystemMessageMsgSpec[]|null);
/** SystemMessage bodyWrapper */
bodyWrapper?: (SysMsg.ISystemMessageBodyWrapper|null);
}
/** Represents a SystemMessage. */
class SystemMessage implements ISystemMessage {
/**
* Constructs a new SystemMessage.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessage);
/** SystemMessage header. */
public header: SysMsg.ISystemMessageHeader[];
/** SystemMessage msgSpec. */
public msgSpec: SysMsg.ISystemMessageMsgSpec[];
/** SystemMessage bodyWrapper. */
public bodyWrapper?: (SysMsg.ISystemMessageBodyWrapper|null);
/**
* Decodes a SystemMessage message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessage
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.SystemMessage;
/**
* Decodes a SystemMessage message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessage
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.SystemMessage;
/**
* Gets the default type url for SystemMessage
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a SystemMessageHeader. */
interface ISystemMessageHeader {
/** SystemMessageHeader peerNumber */
peerNumber?: (number|null);
/** SystemMessageHeader peerString */
peerString?: (string|null);
/** SystemMessageHeader uin */
uin?: (number|null);
/** SystemMessageHeader uid */
uid?: (string|null);
}
/** Represents a SystemMessageHeader. */
class SystemMessageHeader implements ISystemMessageHeader {
/**
* Constructs a new SystemMessageHeader.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessageHeader);
/** SystemMessageHeader peerNumber. */
public peerNumber: number;
/** SystemMessageHeader peerString. */
public peerString: string;
/** SystemMessageHeader uin. */
public uin: number;
/** SystemMessageHeader uid. */
public uid?: (string|null);
/**
* Decodes a SystemMessageHeader message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessageHeader
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.SystemMessageHeader;
/**
* Decodes a SystemMessageHeader message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessageHeader
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.SystemMessageHeader;
/**
* Gets the default type url for SystemMessageHeader
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a SystemMessageMsgSpec. */
interface ISystemMessageMsgSpec {
/** SystemMessageMsgSpec msgType */
msgType?: (number|null);
/** SystemMessageMsgSpec subType */
subType?: (number|null);
/** SystemMessageMsgSpec subSubType */
subSubType?: (number|null);
/** SystemMessageMsgSpec msgSeq */
msgSeq?: (number|null);
/** SystemMessageMsgSpec time */
time?: (number|null);
/** SystemMessageMsgSpec other */
other?: (number|null);
}
/** Represents a SystemMessageMsgSpec. */
class SystemMessageMsgSpec implements ISystemMessageMsgSpec {
/**
* Constructs a new SystemMessageMsgSpec.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessageMsgSpec);
/** SystemMessageMsgSpec msgType. */
public msgType: number;
/** SystemMessageMsgSpec subType. */
public subType: number;
/** SystemMessageMsgSpec subSubType. */
public subSubType: number;
/** SystemMessageMsgSpec msgSeq. */
public msgSeq: number;
/** SystemMessageMsgSpec time. */
public time: number;
/** SystemMessageMsgSpec other. */
public other: number;
/**
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessageMsgSpec
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.SystemMessageMsgSpec;
/**
* Decodes a SystemMessageMsgSpec message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessageMsgSpec
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.SystemMessageMsgSpec;
/**
* Gets the default type url for SystemMessageMsgSpec
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a SystemMessageBodyWrapper. */
interface ISystemMessageBodyWrapper {
/** SystemMessageBodyWrapper body */
body?: (Uint8Array|null);
}
/** Represents a SystemMessageBodyWrapper. */
class SystemMessageBodyWrapper implements ISystemMessageBodyWrapper {
/**
* Constructs a new SystemMessageBodyWrapper.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ISystemMessageBodyWrapper);
/** SystemMessageBodyWrapper body. */
public body: Uint8Array;
/**
* Decodes a SystemMessageBodyWrapper message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns SystemMessageBodyWrapper
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.SystemMessageBodyWrapper;
/**
* Decodes a SystemMessageBodyWrapper message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns SystemMessageBodyWrapper
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.SystemMessageBodyWrapper;
/**
* Gets the default type url for SystemMessageBodyWrapper
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a LikeDetail. */
interface ILikeDetail {
/** LikeDetail txt */
txt?: (string|null);
/** LikeDetail uin */
uin?: (number|null);
/** LikeDetail nickname */
nickname?: (string|null);
}
/** Represents a LikeDetail. */
class LikeDetail implements ILikeDetail {
/**
* Constructs a new LikeDetail.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ILikeDetail);
/** LikeDetail txt. */
public txt: string;
/** LikeDetail uin. */
public uin: number;
/** LikeDetail nickname. */
public nickname: string;
/**
* Decodes a LikeDetail message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns LikeDetail
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.LikeDetail;
/**
* Decodes a LikeDetail message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns LikeDetail
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.LikeDetail;
/**
* Gets the default type url for LikeDetail
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a LikeMsg. */
interface ILikeMsg {
/** LikeMsg count */
count?: (number|null);
/** LikeMsg time */
time?: (number|null);
/** LikeMsg detail */
detail?: (SysMsg.ILikeDetail|null);
}
/** Represents a LikeMsg. */
class LikeMsg implements ILikeMsg {
/**
* Constructs a new LikeMsg.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.ILikeMsg);
/** LikeMsg count. */
public count: number;
/** LikeMsg time. */
public time: number;
/** LikeMsg detail. */
public detail?: (SysMsg.ILikeDetail|null);
/**
* Decodes a LikeMsg message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns LikeMsg
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.LikeMsg;
/**
* Decodes a LikeMsg message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns LikeMsg
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.LikeMsg;
/**
* Gets the default type url for LikeMsg
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a ProfileLikeSubTip. */
interface IProfileLikeSubTip {
/** ProfileLikeSubTip msg */
msg?: (SysMsg.ILikeMsg|null);
}
/** Represents a ProfileLikeSubTip. */
class ProfileLikeSubTip implements IProfileLikeSubTip {
/**
* Constructs a new ProfileLikeSubTip.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.IProfileLikeSubTip);
/** ProfileLikeSubTip msg. */
public msg?: (SysMsg.ILikeMsg|null);
/**
* Decodes a ProfileLikeSubTip message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns ProfileLikeSubTip
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.ProfileLikeSubTip;
/**
* Decodes a ProfileLikeSubTip message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns ProfileLikeSubTip
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.ProfileLikeSubTip;
/**
* Gets the default type url for ProfileLikeSubTip
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
/** Properties of a ProfileLikeTip. */
interface IProfileLikeTip {
/** ProfileLikeTip msgType */
msgType?: (number|null);
/** ProfileLikeTip subType */
subType?: (number|null);
/** ProfileLikeTip content */
content?: (SysMsg.IProfileLikeSubTip|null);
}
/** Represents a ProfileLikeTip. */
class ProfileLikeTip implements IProfileLikeTip {
/**
* Constructs a new ProfileLikeTip.
* @param [properties] Properties to set
*/
constructor(properties?: SysMsg.IProfileLikeTip);
/** ProfileLikeTip msgType. */
public msgType: number;
/** ProfileLikeTip subType. */
public subType: number;
/** ProfileLikeTip content. */
public content?: (SysMsg.IProfileLikeSubTip|null);
/**
* Decodes a ProfileLikeTip message from the specified reader or buffer.
* @param reader Reader or buffer to decode from
* @param [length] Message length if known beforehand
* @returns ProfileLikeTip
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): SysMsg.ProfileLikeTip;
/**
* Decodes a ProfileLikeTip message from the specified reader or buffer, length delimited.
* @param reader Reader or buffer to decode from
* @returns ProfileLikeTip
* @throws {Error} If the payload is not a reader or valid buffer
* @throws {$protobuf.util.ProtocolError} If required fields are missing
*/
public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): SysMsg.ProfileLikeTip;
/**
* Gets the default type url for ProfileLikeTip
* @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com")
* @returns The default type url
*/
public static getTypeUrl(typeUrlPrefix?: string): string;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
syntax = "proto3";
package SysMsg;
message LikeDetail {
string txt = 1;
uint32 uin = 3;
string nickname = 5;
}
message LikeMsg {
uint32 count = 1;
uint32 time = 2;
LikeDetail detail = 3;
}
message ProfileLikeSubTip {
LikeMsg msg = 14;
}
message ProfileLikeTip {
uint32 msgType = 1;
uint32 subType = 2;
ProfileLikeSubTip content = 203;
}

View File

@@ -0,0 +1,30 @@
syntax = "proto3";
package SysMsg;
message SystemMessage {
repeated SystemMessageHeader header = 1;
repeated SystemMessageMsgSpec msgSpec = 2;
SystemMessageBodyWrapper bodyWrapper = 3;
}
message SystemMessageHeader {
uint32 peerNumber = 1;
string peerString = 2;
uint32 uin = 5;
optional string uid = 6;
}
message SystemMessageMsgSpec {
uint32 msgType = 1;
uint32 subType = 2;
uint32 subSubType = 3;
uint32 msgSeq = 5;
uint32 time = 6;
//uint64 msgId = 12;
uint32 other = 13;
}
message SystemMessageBodyWrapper {
bytes body = 2;
// Find the first [08], or ignore the first 7 bytes?
}

View File

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

View File

@@ -1,249 +1,275 @@
import { NodeIKernelGroupListener } from '@/ntqqapi/listeners'
import { import {
GroupExtParam, GroupExtParam,
GroupMember, GroupMember,
GroupMemberRole, GroupMemberRole,
GroupNotifyTypes, GroupNotifyType,
GroupRequestOperateTypes, GroupRequestOperateTypes,
} from '@/ntqqapi/types' } from '@/ntqqapi/types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
import { Dict } from 'cosmokit'
//高版本的接口不应该随意使用 使用应该严格进行pr审核 同时部分ipc中未出现的接口不要过于依赖 应该做好数据兜底
export interface NodeIKernelGroupService { export interface NodeIKernelGroupService {
getMemberCommonInfo(Req: { 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>
isEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
queryCachedEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<{
items: {
groupCode: string
msgSeq: number
msgRandom: number
msgSenderUin: string
msgSenderNick: string
opType: number
opUin: string
opNick: string
opTime: number
grayTipSeq: string
}[]
}>
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: Dict,
finish: true,
hasRobot: false
}
}>
setHeader(uid: string, path: string): unknown
addKernelGroupListener(listener: unknown): number
removeKernelGroupListener(listenerId: unknown): void
createMemberListScene(groupCode: string, scene: string): string
destroyMemberListScene(sceneId: string): void
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number
errMsg: string
result: {
ids: {
uid: string
index: number
}[]
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: GroupNotifyType,
groupCode: string, groupCode: string,
startUin: string, postscript: string
identifyFlag: string, }
uinList: string[], }): Promise<void>
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<{ setTop(groupCode: string, isTop: boolean): void
errCode: number,
errMsg: string,
uins: Map<string, string>
}>
getUidByUins(uins: string[]): Promise<{ getGroupBulletin(groupCode: string): unknown
errCode: number,
errMsg: string,
uids: Map<string, string>
}>
//26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
//26702(其实更早 但是我不知道) deleteGroupBulletin(groupCode: string, seq: string): void
getGroupLatestEssenceList(groupCode: string): Promise<unknown>
//26702(其实更早 但是我不知道) publishGroupBulletin(groupCode: string, pskey: string, data: unknown): Promise<GeneralCallResult>
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 publishInstructionForNewcomers(groupCode: string, arg: unknown): void
addKernelGroupListener(listener: NodeIKernelGroupListener): number uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<{
errCode: number
errMsg: string
picInfo?: {
id: string
width: number
height: number
}
}>
removeKernelGroupListener(listenerId: unknown): void downloadGroupBulletinRichMedia(groupCode: string): unknown
createMemberListScene(groupCode: string, scene: string): string getGroupBulletinList(groupCode: string): unknown
destroyMemberListScene(SceneId: string): void getGroupStatisticInfo(groupCode: string): unknown
//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 getGroupRemainAtTimes(groupCode: string): number
monitorMemberList(): unknown getJoinGroupNoVerifyFlag(groupCode: string): unknown
searchMember(sceneId: string, keywords: string[]): unknown getGroupArkInviteState(groupCode: string): unknown
getMemberInfo(group_id: string, uids: string[], forceFetch: boolean): Promise<GeneralCallResult> reqToJoinGroup(groupCode: string, arg: unknown): void
//getMemberInfo [ '56729xxxx', [ 'u_4Nj08cwW5Hxxxxx' ], true ]
kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void> setGroupShutUp(groupCode: string, shutUp: boolean): void
modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void getGroupShutUpMemberList(groupCode: string): unknown[]
modifyMemberCardName(groupCode: string, uid: string, cardName: string): void setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>
getTransferableMemberInfo(groupCode: string): unknown//获取整个群的 getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>
transferGroup(uid: string): void getJoinGroupLink(groupCode: string): unknown
getGroupList(force: boolean): Promise<GeneralCallResult> modifyGroupExtInfo(groupCode: string, arg: unknown): void
getGroupExtList(force: boolean): Promise<GeneralCallResult> //需要提前判断是否存在 高版本新增
addGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>
getGroupDetailInfo(groupCode: string): unknown //需要提前判断是否存在 高版本新增
removeGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>
getMemberExtInfo(param: GroupExtParam): Promise<unknown>//req isNull(): boolean
}
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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -16,9 +16,9 @@ export enum ProfileBizType {
} }
export interface NodeIKernelProfileService { export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string,string>>//uin->uid getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string, string>>//uin->uid
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string,string>> getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string, string>>
// { // {
// coreInfo: CoreInfo, // coreInfo: CoreInfo,
@@ -33,7 +33,7 @@ export interface NodeIKernelProfileService {
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown> fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>
addKernelProfileListener(listener: any): number addKernelProfileListener(listener: unknown): number
removeKernelProfileListener(listenerId: number): void removeKernelProfileListener(listenerId: number): void
@@ -64,7 +64,7 @@ export interface NodeIKernelProfileService {
modifySelfProfile(...args: unknown[]): Promise<unknown> modifySelfProfile(...args: unknown[]): Promise<unknown>
modifyDesktopMiniProfile(param: any): Promise<GeneralCallResult> modifyDesktopMiniProfile(param: unknown): Promise<GeneralCallResult>
setNickName(NickName: string): Promise<unknown> setNickName(NickName: string): Promise<unknown>
@@ -74,7 +74,7 @@ export interface NodeIKernelProfileService {
setGander(...args: unknown[]): Promise<unknown> setGander(...args: unknown[]): Promise<unknown>
setHeader(arg: string): Promise<unknown> setHeader(arg: string): Promise<GeneralCallResult>
setRecommendImgFlag(...args: unknown[]): Promise<unknown> setRecommendImgFlag(...args: unknown[]): Promise<unknown>
@@ -82,9 +82,9 @@ export interface NodeIKernelProfileService {
getUserDetailInfo(uid: string): Promise<unknown> getUserDetailInfo(uid: string): Promise<unknown>
getUserDetailInfoWithBizInfo(uid: string, Biz: any[]): Promise<GeneralCallResult> getUserDetailInfoWithBizInfo(uid: string, Biz: unknown[]): Promise<GeneralCallResult>
getUserDetailInfoByUin(uin: string): Promise<any> getUserDetailInfoByUin(uin: string): Promise<unknown>
getZplanAvatarInfos(args: string[]): Promise<unknown> getZplanAvatarInfos(args: string[]): Promise<unknown>
@@ -99,7 +99,7 @@ export interface NodeIKernelProfileService {
getProfileQzonePicInfo(uid: string, type: number, force: boolean): Promise<unknown> getProfileQzonePicInfo(uid: string, type: number, force: boolean): Promise<unknown>
//profileService.getCoreInfo("UserRemarkServiceImpl::getStrangerRemarkByUid", arrayList) //profileService.getCoreInfo("UserRemarkServiceImpl::getStrangerRemarkByUid", arrayList)
getCoreInfo(name: string, arg: any[]): unknown getCoreInfo(name: string, arg: unknown[]): unknown
//m429253e12.getOtherFlag("FriendListInfoCache_getKernelDataAndPutCache", new ArrayList<>()) //m429253e12.getOtherFlag("FriendListInfoCache_getKernelDataAndPutCache", new ArrayList<>())
isNull(): boolean isNull(): boolean

View File

@@ -2,269 +2,265 @@ import { GetFileListParam, MessageElement, Peer } from '../types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
export enum UrlFileDownloadType { export enum UrlFileDownloadType {
KUNKNOWN, KUNKNOWN,
KURLFILEDOWNLOADPRIVILEGEICON, KURLFILEDOWNLOADPRIVILEGEICON,
KURLFILEDOWNLOADPHOTOWALL, KURLFILEDOWNLOADPHOTOWALL,
KURLFILEDOWNLOADQZONE, KURLFILEDOWNLOADQZONE,
KURLFILEDOWNLOADCOMMON, KURLFILEDOWNLOADCOMMON,
KURLFILEDOWNLOADINSTALLAPP KURLFILEDOWNLOADINSTALLAPP
} }
export enum RMBizTypeEnum { export enum RMBizTypeEnum {
KUNKNOWN, KUNKNOWN,
KC2CFILE, KC2CFILE,
KGROUPFILE, KGROUPFILE,
KC2CPIC, KC2CPIC,
KGROUPPIC, KGROUPPIC,
KDISCPIC, KDISCPIC,
KC2CVIDEO, KC2CVIDEO,
KGROUPVIDEO, KGROUPVIDEO,
KC2CPTT, KC2CPTT,
KGROUPPTT, KGROUPPTT,
KFEEDCOMMENTPIC, KFEEDCOMMENTPIC,
KGUILDFILE, KGUILDFILE,
KGUILDPIC, KGUILDPIC,
KGUILDPTT, KGUILDPTT,
KGUILDVIDEO KGUILDVIDEO
} }
export interface CommonFileInfo { export interface CommonFileInfo {
bizType: number bizType: number
chatType: number chatType: number
elemId: string elemId: string
favId: string favId: string
fileModelId: string fileModelId: string
fileName: string fileName: string
fileSize: string fileSize: string
md5: string md5: string
md510m: string md510m: string
msgId: string msgId: string
msgTime: string msgTime: string
parent: string parent: string
peerUid: string peerUid: string
picThumbPath: Array<string> picThumbPath: Array<string>
sha: string sha: string
sha3: string sha3: string
subId: string subId: string
uuid: string uuid: string
} }
export interface NodeIKernelRichMediaService { export interface NodeIKernelRichMediaService {
//getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb) //getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb)
// public enum VideoCodecFormatType { // public enum VideoCodecFormatType {
// KCODECFORMATH264, // KCODECFORMATH264,
// KCODECFORMATH265, // KCODECFORMATH265,
// KCODECFORMATH266, // KCODECFORMATH266,
// KCODECFORMATAV1 // KCODECFORMATAV1
// } // }
// public enum VideoRequestWay { // public enum VideoRequestWay {
// KUNKNOW, // KUNKNOW,
// KHAND, // KHAND,
// KAUTO // KAUTO
// } // }
getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise<unknown> getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise<unknown>
//exParams (RMReqExParams) //exParams (RMReqExParams)
// this.downSourceType = i2 // this.downSourceType = i2
// this.triggerType = i3 // this.triggerType = i3
//peer, msgId, elemId, videoCodecFormat, exParams //peer, msgId, elemId, videoCodecFormat, exParams
// 1 0 频道在用 // 1 0 频道在用
// 1 1 // 1 1
// 0 2 // 0 2
// public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007 // public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007
// public static final int KDOWNSOURCETYPEAIOINNER = 1 // public static final int KDOWNSOURCETYPEAIOINNER = 1
// public static final int KDOWNSOURCETYPEBIGSCREEN = 2 // public static final int KDOWNSOURCETYPEBIGSCREEN = 2
// public static final int KDOWNSOURCETYPEHISTORY = 3 // public static final int KDOWNSOURCETYPEHISTORY = 3
// public static final int KDOWNSOURCETYPEUNKNOWN = 0 // public static final int KDOWNSOURCETYPEUNKNOWN = 0
// public static final int KTRIGGERTYPEAUTO = 1 // public static final int KTRIGGERTYPEAUTO = 1
// public static final int KTRIGGERTYPEMANUAL = 0 // public static final int KTRIGGERTYPEMANUAL = 0
getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & { getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & {
urlResult: { urlResult: {
v4IpUrl: [], v4IpUrl: [],
v6IpUrl: [], v6IpUrl: [],
domainUrl: Array<{ domainUrl: Array<{
url: string, url: string,
isHttps: boolean, isHttps: boolean,
httpsDomain: string httpsDomain: string
}>, }>,
videoCodecFormat: number 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 getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown
isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): 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
uploadRMFileWithoutMsg(arg: { getVideoPlayUrlInVisit(arg: {
bizType: RMBizTypeEnum, downloadType: number,
filePath: string, thumbSize: number,
peerUid: string, msgId: string,
transferId: string msgRandom: string,
useNTV2: 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: unknown, groupItem: Array<unknown> } }>
downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: 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> }): Promise<unknown>
searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
isNull(): boolean deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & {
transGroupFileResult: {
result: unknown
successFileIdList: Array<unknown>
failFileIdList: Array<unknown>
}
}>
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: unknown[]): unknown// needs 1 arguments
removeKernelSearchListener(...args: unknown[]): unknown// needs 1 arguments
searchStranger(...args: unknown[]): unknown// needs 3 arguments
searchGroup(...args: unknown[]): unknown// needs 1 arguments
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown
cancelSearchLocalInfo(...args: unknown[]): unknown// needs 3 arguments
searchBuddyChatInfo(...args: unknown[]): unknown// needs 2 arguments
searchMoreBuddyChatInfo(...args: unknown[]): unknown// needs 1 arguments
cancelSearchBuddyChatInfo(...args: unknown[]): unknown// needs 3 arguments
searchContact(...args: unknown[]): unknown// needs 2 arguments
searchMoreContact(...args: unknown[]): unknown// needs 1 arguments
cancelSearchContact(...args: unknown[]): unknown// needs 3 arguments
searchGroupChatInfo(...args: unknown[]): unknown// needs 3 arguments
resetSearchGroupChatInfoSortType(...args: unknown[]): unknown// needs 3 arguments
resetSearchGroupChatInfoFilterMembers(...args: unknown[]): unknown// needs 3 arguments
searchMoreGroupChatInfo(...args: unknown[]): unknown// needs 1 arguments
cancelSearchGroupChatInfo(...args: unknown[]): unknown// needs 3 arguments
searchChatsWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchMoreChatsWithKeywords(...args: unknown[]): unknown// needs 1 arguments
cancelSearchChatsWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchChatMsgs(...args: unknown[]): unknown// needs 2 arguments
searchMoreChatMsgs(...args: unknown[]): unknown// needs 1 arguments
cancelSearchChatMsgs(...args: unknown[]): unknown// needs 3 arguments
searchMsgWithKeywords(...args: unknown[]): unknown// needs 2 arguments
searchMoreMsgWithKeywords(...args: unknown[]): unknown// needs 1 arguments
cancelSearchMsgWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchFileWithKeywords(keywords: string[], source: number): Promise<string>// needs 2 arguments
searchMoreFileWithKeywords(...args: unknown[]): unknown// needs 1 arguments
cancelSearchFileWithKeywords(...args: unknown[]): unknown// needs 3 arguments
searchAtMeChats(...args: unknown[]): unknown// needs 3 arguments
searchMoreAtMeChats(...args: unknown[]): unknown// needs 1 arguments
cancelSearchAtMeChats(...args: unknown[]): unknown// needs 3 arguments
searchChatAtMeMsgs(...args: unknown[]): unknown// needs 1 arguments
searchMoreChatAtMeMsgs(...args: unknown[]): unknown// needs 1 arguments
cancelSearchChatAtMeMsgs(...args: unknown[]): 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: unknown[]): unknown// needs 1 arguments
searchCache(...args: unknown[]): unknown// needs 3 arguments
clearSearchCache(...args: unknown[]): unknown// needs 1 arguments
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
export * from './common'
export * from './NodeIKernelBuddyService' export * from './NodeIKernelBuddyService'
export * from './NodeIKernelProfileService' export * from './NodeIKernelProfileService'
export * from './NodeIKernelGroupService' export * from './NodeIKernelGroupService'
@@ -7,4 +8,5 @@ export * from './NodeIKernelMSFService'
export * from './NodeIKernelUixConvertService' export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService' export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelSearchService'

View File

@@ -36,6 +36,7 @@ export interface Group {
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q" memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
} }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string
} }
export enum GroupMemberRole { export enum GroupMemberRole {
@@ -45,7 +46,7 @@ export enum GroupMemberRole {
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle: string memberSpecialTitle?: string
avatarPath: string avatarPath: string
cardName: string cardName: string
cardType: number cardType: number
@@ -60,4 +61,20 @@ export interface GroupMember {
isRobot: boolean isRobot: boolean
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean
joinTime: number
lastSpeakTime: number
memberLevel: number
}
export interface PublishGroupBulletinReq {
text: string
picInfo?: {
id: string
width: number
height: number
}
oldFeedsId: ''
pinned: number
confirmRequired: number
} }

View File

@@ -6,6 +6,7 @@ export interface GetFileListParam {
startIndex: number startIndex: number
sortOrder: number sortOrder: number
showOnlinedocFolder: number showOnlinedocFolder: number
folderId?: string
} }
export enum ElementType { export enum ElementType {
@@ -102,7 +103,7 @@ export interface SendPicElement {
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY elementType: ElementType.REPLY
elementId: '' elementId: ''
replyElement: ReplyElement replyElement: Partial<ReplyElement>
} }
export interface SendFaceElement { export interface SendFaceElement {
@@ -129,6 +130,12 @@ export interface ReplyElement {
replayMsgId: string replayMsgId: string
senderUin: string senderUin: string
senderUinStr: string senderUinStr: string
sourceMsgIdInRecords: string
senderUid: string
senderUidStr: string
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replyMsgTime: string
} }
export interface FileElement { export interface FileElement {
@@ -275,21 +282,35 @@ export interface PicElement {
thumbPath: Map<number, string> thumbPath: Map<number, string>
picWidth: number picWidth: number
picHeight: number picHeight: number
fileSize: number fileSize: string
fileName: string fileName: string
fileUuid: string fileUuid: string
md5HexStr?: string md5HexStr?: string
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
RECALL = 1, REVOKE = 1,
INVITE_NEW_MEMBER = 12, PROCLAMATION = 2,
MEMBER_NEW_TITLE = 17, EMOJIREPLY = 3,
GROUP = 4,
BUDDY = 5,
FEED = 6,
ESSENCE = 7,
GROUPNOTIFY = 8,
BUDDYNOTIFY = 9,
FILE = 10,
FEEDCHANNELMSG = 11,
XMLMSG = 12,
LOCALMSG = 13,
BLOCK = 14,
AIOOP = 15,
WALLET = 16,
JSON = 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
@@ -299,14 +320,14 @@ export interface GrayTipElement {
isSelfOperate?: boolean isSelfOperate?: boolean
wording: string // 自定义的撤回提示语 wording: string // 自定义的撤回提示语
} }
aioOpGrayTipElement: TipAioOpGrayTipElement aioOpGrayTipElement?: TipAioOpGrayTipElement
groupElement: TipGroupElement groupElement?: TipGroupElement
xmlElement: { xmlElement?: {
templId: string templId: string
content: string content: string
} }
jsonGrayTipElement: { jsonGrayTipElement?: {
busiId: number busiId: string
jsonStr: string jsonStr: string
} }
} }
@@ -328,6 +349,7 @@ export interface FaceElement {
resultId?: string resultId?: string
surpriseId?: string surpriseId?: string
randomType?: number randomType?: number
pokeType?: number
} }
export interface MarketFaceElement { export interface MarketFaceElement {
@@ -335,6 +357,8 @@ export interface MarketFaceElement {
faceName?: string faceName?: string
emojiId: string emojiId: string
key: string key: string
imageWidth?: number
imageHeight?: number
} }
export interface VideoElement { export interface VideoElement {
@@ -350,7 +374,7 @@ export interface VideoElement {
thumbHeight?: number thumbHeight?: number
busiType?: 0 // 未知 busiType?: 0 // 未知
subBusiType?: 0 // 未知 subBusiType?: 0 // 未知
thumbPath?: Map<number, any> thumbPath?: Map<number, string>
transferStatus?: 0 // 未知 transferStatus?: 0 // 未知
progress?: 0 // 下载进度? progress?: 0 // 下载进度?
invalidState?: 0 // 未知 invalidState?: 0 // 未知
@@ -459,7 +483,7 @@ export interface RawMessage {
msgSeq: string msgSeq: string
msgRandom: string msgRandom: string
senderUid: string senderUid: string
senderUin?: string // 发送者QQ号 senderUin: string // 发送者QQ号
peerUid: string // 群号 或者 QQ uid peerUid: string // 群号 或者 QQ uid
peerUin: string // 群号 或者 发送者QQ号 peerUin: string // 群号 或者 发送者QQ号
guildId: string guildId: string
@@ -469,34 +493,7 @@ export interface RawMessage {
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[] records: RawMessage[]
elements: { elements: MessageElement[]
elementId: string
elementType: ElementType
replyElement: {
sourceMsgIdInRecords: string
senderUid: string // 原消息发送者QQ号
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
}
textElement: {
atType: AtType
atUid: string // QQ号
content: string
atNtUid: string // uid号
}
picElement: PicElement
pttElement: PttElement
arkElement: ArkElement
grayTipElement: GrayTipElement
faceElement: FaceElement
videoElement: VideoElement
fileElement: FileElement
marketFaceElement: MarketFaceElement
inlineKeyboardElement: InlineKeyboardElement
markdownElement: MarkdownElement
multiForwardMsgElement: MultiForwardMsgElement
}[]
} }
export interface Peer { export interface Peer {
@@ -511,7 +508,7 @@ export interface MessageElement {
extBufForUI: string //"0x" extBufForUI: string //"0x"
textElement?: TextElement textElement?: TextElement
faceElement?: FaceElement faceElement?: FaceElement
marketFaceElement?: MarkdownElement marketFaceElement?: MarketFaceElement
replyElement?: ReplyElement replyElement?: ReplyElement
picElement?: PicElement picElement?: PicElement
pttElement?: PttElement pttElement?: PttElement
@@ -519,22 +516,96 @@ export interface MessageElement {
grayTipElement?: GrayTipElement grayTipElement?: GrayTipElement
arkElement?: ArkElement arkElement?: ArkElement
fileElement?: FileElement fileElement?: FileElement
liveGiftElement?: null liveGiftElement?: unknown
markdownElement?: MarkdownElement markdownElement?: MarkdownElement
structLongMsgElement?: any structLongMsgElement?: unknown
multiForwardMsgElement?: MultiForwardMsgElement multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: any giphyElement?: unknown
walletElement?: null walletElement?: unknown
inlineKeyboardElement?: InlineKeyboardElement inlineKeyboardElement?: InlineKeyboardElement
textGiftElement?: null //???? textGiftElement?: unknown //????
calendarElement?: any calendarElement?: unknown
yoloGameResultElement?: any yoloGameResultElement?: unknown
avRecordElement?: any avRecordElement?: unknown
structMsgElement?: null structMsgElement?: unknown
faceBubbleElement?: any faceBubbleElement?: unknown
shareLocationElement?: any shareLocationElement?: unknown
tofuRecordElement?: any tofuRecordElement?: unknown
taskTopMsgElement?: any taskTopMsgElement?: unknown
recommendedMsgElement?: any recommendedMsgElement?: unknown
actionBarElement?: any actionBarElement?: unknown
} }
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
fileSrvErrCode: string
clientMsg: string
businessId: number
userTotalSpacePerDay: unknown
userUsedSpacePerDay: unknown
}
export interface OnGroupFileInfoUpdateParams {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: {
peerId: string
type: number
folderInfo?: {
folderId: string
parentFolderId: string
folderName: string
createTime: number
modifyTime: number
createUin: string
creatorName: string
totalFileCount: number
modifyUin: string
modifyName: string
usedSpace: string
}
fileInfo?: {
fileModelId: string
fileId: string
fileName: string
fileSize: string
busId: number
uploadedSize: string
uploadTime: number
deadTime: number
modifyTime: number
downloadTimes: number
sha: string
sha3: string
md5: string
uploaderLocalPath: string
uploaderName: string
uploaderUin: string
parentFolderId: string
localPath: string
transStatus: number
transType: number
elementId: string
isFolder: boolean
}
}[]
allFileCount: number
nextIndex: number
reqId: number
}

View File

@@ -1,13 +1,19 @@
export enum GroupNotifyTypes { export enum GroupNotifyType {
INVITE_ME = 1, INVITED_BY_MEMBER = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群 REFUSE_INVITED,
JOIN_REQUEST_BY_INVITED = 5, // 有人邀请了别人入群 REFUSED_BY_ADMINI_STRATOR,
JOIN_REQUEST = 7, AGREED_TOJOIN_DIRECT, // 有人接受了邀请入群
ADMIN_SET = 8, INVITED_NEED_ADMINI_STRATOR_PASS, // 有人邀请了别人入群
KICK_MEMBER = 9, AGREED_TO_JOIN_BY_ADMINI_STRATOR,
MEMBER_EXIT = 11, // 主动退出 REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS,
ADMIN_UNSET = 12, // 我被取消管理员 SET_ADMIN,
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员 KICK_MEMBER_NOTIFY_ADMIN,
KICK_MEMBER_NOTIFY_KICKED,
MEMBER_LEAVE_NOTIFY_ADMIN, // 主动退出
CANCEL_ADMIN_NOTIFY_CANCELED, // 我被取消管理员
CANCEL_ADMIN_NOTIFY_ADMIN, // 其他人取消管理员
TRANSFER_GROUP_NOTIFY_OLDOWNER,
TRANSFER_GROUP_NOTIFY_ADMIN
} }
export interface GroupNotifies { export interface GroupNotifies {
@@ -17,17 +23,18 @@ export interface GroupNotifies {
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
IGNORE = 0, KINIT, // 初始化
WAIT_HANDLE = 1, KUNHANDLE, // 未处理
APPROVE = 2, KAGREED, // 同意
REJECT = 3, KREFUSED, // 拒绝
KIGNORED // 忽略
} }
export interface GroupNotify { export interface GroupNotify {
time: number // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify time: number // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string // 唯一标识符转成数字再除以1000应该就是时间戳 seq: string // 唯一标识符转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes type: GroupNotifyType
status: GroupNotifyStatus // 0是已忽略1是未处理2是已同意 status: GroupNotifyStatus
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 } // 操作者

View File

@@ -78,9 +78,12 @@ export interface Friend extends User {
export interface CategoryFriend { export interface CategoryFriend {
categoryId: number categoryId: number
categorySortId: number
categroyName: string categroyName: string
categroyMbCount: number categroyMbCount: number
buddyList: User[] onlineCount: number
buddyList: User[] // V1
buddyUids: string[]
} }
export interface CoreInfo { export interface CoreInfo {
@@ -121,7 +124,7 @@ interface VideoInfo {
interface ExtOnlineBusinessInfo { interface ExtOnlineBusinessInfo {
buf: string buf: string
customStatus: any customStatus: unknown
videoBizInfo: VideoBizInfo videoBizInfo: VideoBizInfo
videoInfo: VideoInfo videoInfo: VideoInfo
} }
@@ -139,7 +142,7 @@ interface UserStatus {
termType: number termType: number
netType: number netType: number
iconType: number iconType: number
customStatus: any customStatus: unknown
setTime: string setTime: string
specialFlag: number specialFlag: number
abiFlag: number abiFlag: number
@@ -153,8 +156,8 @@ interface UserStatus {
interface PrivilegeIcon { interface PrivilegeIcon {
jumpUrl: string jumpUrl: string
openIconList: any[] openIconList: unknown[]
closeIconList: any[] closeIconList: unknown[]
} }
interface VasInfo { interface VasInfo {
@@ -177,7 +180,7 @@ interface VasInfo {
fontEffect: number fontEffect: number
newLoverDiamondFlag: number newLoverDiamondFlag: number
extendNameplateId: number extendNameplateId: number
diyNameplateIDs: any[] diyNameplateIDs: unknown[]
vipStartFlag: number vipStartFlag: number
vipDataFlag: number vipDataFlag: number
gameNameplateId: string gameNameplateId: string
@@ -197,8 +200,8 @@ export interface SimpleInfo {
status: UserStatus | null status: UserStatus | null
vasInfo: VasInfo | null vasInfo: VasInfo | null
relationFlags: RelationFlags | null relationFlags: RelationFlags | null
otherFlags: any | null otherFlags: unknown | null
intimate: any | null intimate: unknown | null
} }
interface RelationFlags { interface RelationFlags {
@@ -238,7 +241,7 @@ interface CommonExt {
address: string address: string
regTime: number regTime: number
interest: string interest: string
labels: any[] labels: string[]
qqLevel: QQLevel qqLevel: QQLevel
} }
@@ -320,12 +323,12 @@ export interface UserDetailInfoByUin {
regTime: number regTime: number
interest: string interest: string
termType: number termType: number
labels: any[] labels: unknown[]
qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number } qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number }
isHideQQLevel: number isHideQQLevel: number
privilegeIcon: { jumpUrl: string, openIconList: any[], closeIconList: any[] } privilegeIcon: { jumpUrl: string, openIconList: unknown[], closeIconList: unknown[] }
isHidePrivilegeIcon: number isHidePrivilegeIcon: number
photoWall: { picList: any[] } photoWall: { picList: unknown[] }
vipFlag: boolean vipFlag: boolean
yearVipFlag: boolean yearVipFlag: boolean
svipFlag: boolean svipFlag: boolean
@@ -340,4 +343,4 @@ export interface UserDetailInfoByUin {
pendantId: string pendantId: string
vipNameColorId: string vipNameColorId: string
} }
} }

View File

@@ -8,13 +8,14 @@ import {
NodeIKernelUixConvertService, NodeIKernelUixConvertService,
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelTicketService, NodeIKernelTicketService,
NodeIKernelTipOffService NodeIKernelTipOffService,
NodeIKernelSearchService
} from './services' } from './services'
import os from 'node:os' import { constants } from 'node:os'
import { Dict } from 'cosmokit'
const Process = require('node:process') const Process = require('node:process')
export interface NodeIQQNTWrapperSession { export interface NodeIQQNTWrapperSession {
[key: string]: any
getBuddyService(): NodeIKernelBuddyService getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService getProfileService(): NodeIKernelProfileService
@@ -25,68 +26,31 @@ export interface NodeIQQNTWrapperSession {
getRichMediaService(): NodeIKernelRichMediaService getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService getTipOffService(): NodeIKernelTipOffService
getSearchService(): NodeIKernelSearchService
} }
export interface WrapperApi { export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession 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 = {} 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.dlopenOrig = Process.dlopen
Process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) { Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags) const dlopenRet = this.dlopenOrig(module, filename, flags)
for (let export_name in module.exports) { for (const export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], { module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args, _newTarget) => { construct: (target, args) => {
const ret = new target(...args) const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret return ret
} }
}) })
if (constructor.includes(export_name)) {
wrapperConstructor[export_name] = module.exports[export_name]
}
} }
return dlopenRet return dlopenRet
} }
export function getSession() { export function getSession() {
return wrapperApi['NodeIQQNTWrapperSession'] return wrapperApi['NodeIQQNTWrapperSession']
} }

View File

@@ -1,49 +1,51 @@
import { ActionName, BaseCheckResult } from './types' import { ActionName } from './types'
import { OB11Response } from './OB11Response' import { OB11Response } from './OB11Response'
import { OB11Return } from '../types' import { OB11Return } from '../types'
import { Context, Schema } from 'cordis'
import { log } from '../../common/utils/log' import type Adapter from '../adapter'
abstract class BaseAction<PayloadType, ReturnDataType> { abstract class BaseAction<PayloadType, ReturnDataType> {
abstract actionName: ActionName abstract actionName: ActionName
protected ctx: Context
payloadSchema?: Schema<PayloadType>
protected async check(payload: PayloadType): Promise<BaseCheckResult> { constructor(protected adapter: Adapter) {
return { this.ctx = adapter.ctx
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) let params: PayloadType
if (!result.valid) { try {
return OB11Response.error(result.message, 400) params = this.payloadSchema ? new this.payloadSchema(payload) : payload
} catch (e) {
return OB11Response.error((e as Error).message, 400)
} }
try { try {
const resData = await this._handle(payload) const resData = await this._handle(params)
return OB11Response.ok(resData) return OB11Response.ok(resData)
} catch (e: any) { } catch (e) {
log('发生错误', e) this.ctx.logger.error('发生错误', e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200) return OB11Response.error((e as Error)?.toString() || (e as Error)?.stack?.toString() || '未知错误,可能操作超时', 200)
} }
} }
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> { public async websocketHandle(payload: PayloadType, echo: unknown): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload) let params: PayloadType
if (!result.valid) { try {
return OB11Response.error(result.message, 1400) params = this.payloadSchema ? new this.payloadSchema(payload) : payload
} catch (e) {
return OB11Response.error((e as Error).message, 1400)
} }
try { try {
const resData = await this._handle(payload) const resData = await this._handle(params)
return OB11Response.ok(resData, echo) return OB11Response.ok(resData, echo)
} catch (e: any) { } catch (e) {
log('发生错误', e) this.ctx.logger.error('发生错误', e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo) return OB11Response.error((e as Error)?.stack?.toString() || String(e), 1200, echo)
} }
} }
protected async _handle(payload: PayloadType): Promise<ReturnDataType> { protected abstract _handle(payload: PayloadType): Promise<ReturnDataType>
throw `pleas override ${this.actionName} _handle`
}
} }
export default BaseAction export { BaseAction, Schema }

View File

@@ -1,6 +1,5 @@
import { OB11Return } from '../types' import { OB11Return } from '../types'
import { isNullable } from 'cosmokit'
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> {
@@ -10,21 +9,21 @@ export class OB11Response {
data: data, data: data,
message: message, message: message,
wording: message, wording: message,
echo: null, echo: undefined,
} }
} }
static ok<T>(data: T, echo: any = null) { static ok<T>(data: T, echo?: unknown) {
let res = OB11Response.res<T>(data, 'ok', 0) const res = OB11Response.res<T>(data, 'ok', 0)
if (!isNull(echo)) { if (!isNullable(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?: unknown) {
let res = OB11Response.res(null, 'failed', retcode, err) const res = OB11Response.res(null, 'failed', retcode, err)
if (!isNull(echo)) { if (!isNullable(echo)) {
res.echo = echo res.echo = echo
} }
return res return res

View File

@@ -1,12 +1,7 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import fs from 'fs/promises' import { readFile } from 'node:fs/promises'
import { dbUtil } from '@/common/db'
import { getConfigUtil } from '@/common/config'
import { checkFileReceived, log, sleep, uri2local } from '@/common/utils'
import { NTQQFileApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { FileElement, RawMessage, VideoElement } from '@/ntqqapi/types' import { Peer, ElementType } from '@/ntqqapi/types'
import { FileCache } from '@/common/types'
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名或者fileUuid file: string // 文件名或者fileUuid
@@ -21,89 +16,74 @@ export interface GetFileResponse {
} }
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
private getElement(msg: RawMessage, elementId: string): VideoElement | FileElement { payloadSchema = Schema.object({
let element = msg.elements.find((e) => e.elementId === elementId) file: Schema.string().required()
if (!element) { })
throw new Error('element not found')
}
return element.fileElement
}
private async download(cache: FileCache, file: string) {
log('需要调用 NTQQ 下载文件api')
if (cache.msgId) {
let msg = await dbUtil.getMsgByLongId(cache.msgId)
if (msg) {
log('找到了文件 msg', msg)
let element = this.getElement(msg, cache.elementId)
log('找到了文件 element', element)
// 构建下载函数
await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, cache.elementId, '', '')
// 等待文件下载完成
msg = await dbUtil.getMsgByLongId(cache.msgId)
log('下载完成后的msg', msg)
cache.filePath = this.getElement(msg!, cache.elementId).filePath
await checkFileReceived(cache.filePath, 10 * 1000)
dbUtil.addFileCache(file, cache).then()
}
}
}
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let cache = await dbUtil.getFileCache(payload.file) const { enableLocalFile2Url } = this.adapter.config
if (!cache) {
throw new Error('file not found') let fileCache = await this.ctx.store.getFileCacheById(payload.file)
if (!fileCache?.length) {
fileCache = await this.ctx.store.getFileCacheByName(payload.file)
} }
const { autoDeleteFile, enableLocalFile2Url, autoDeleteFileSecond } = getConfigUtil().getConfig()
if (cache.downloadFunc) { if (fileCache?.length) {
await cache.downloadFunc() const downloadPath = await this.ctx.ntFileApi.downloadMedia(
} fileCache[0].msgId,
try { fileCache[0].chatType,
await fs.access(cache.filePath, fs.constants.F_OK) fileCache[0].peerUid,
} catch (e) { fileCache[0].elementId,
// log("file not found", e) '',
if (cache.url) { ''
const downloadResult = await uri2local(cache.url) )
if (downloadResult.success) { const res: GetFileResponse = {
cache.filePath = downloadResult.path file: downloadPath,
dbUtil.addFileCache(payload.file, cache).then() url: downloadPath,
} else { file_size: fileCache[0].fileSize,
await this.download(cache, payload.file) file_name: fileCache[0].fileName,
}
} else {
// 没有url的可能是私聊文件或者群文件需要自己下载
await this.download(cache, payload.file)
} }
} const peer: Peer = {
let res: GetFileResponse = { chatType: fileCache[0].chatType,
file: cache.filePath, peerUid: fileCache[0].peerUid,
url: cache.url, guildId: ''
file_size: cache.fileSize, }
file_name: cache.fileName, if (fileCache[0].elementType === ElementType.PIC) {
} const msgList = await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (enableLocalFile2Url) { if (msgList.msgList.length === 0) {
if (!cache.url) { 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 this.ctx.ntFileApi.getImageUrl(findEle.picElement!)
} else if (fileCache[0].elementType === ElementType.VIDEO) {
res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
}
if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {
try { try {
res.base64 = await fs.readFile(cache.filePath, 'base64') res.base64 = await readFile(downloadPath, 'base64')
} catch (e) { } catch (e) {
throw new Error('文件下载失败. ' + e) throw new Error('文件下载失败. ' + e)
} }
} }
//不手动删除?文件持久化了
return res
} }
// if (autoDeleteFile) { throw new Error('file not found')
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// }
return res
} }
} }
export default class GetFile extends GetFileBase { export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile actionName = ActionName.GetFile
payloadSchema = Schema.object({
file: Schema.string(),
file_id: Schema.string().required()
})
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) {
throw new Error('file_id 不能为空')
}
payload.file = payload.file_id payload.file = payload.file_id
return super._handle(payload) return super._handle(payload)
} }

View File

@@ -1,9 +1,9 @@
import path from 'node:path'
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 { decodeSilk } from '@/common/utils/audio'
import { getConfigUtil } from '@/common/config' import { Schema } from '../BaseAction'
import path from 'node:path' import { stat, readFile } from 'node:fs/promises'
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'
@@ -11,14 +11,18 @@ interface Payload extends GetFilePayload {
export default class GetRecord extends GetFileBase { export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord actionName = ActionName.GetRecord
payloadSchema = Schema.object({
file: Schema.string().required(),
out_format: Schema.string().default('mp3')
})
protected async _handle(payload: Payload): Promise<GetFileResponse> { protected async _handle(payload: Payload): Promise<GetFileResponse> {
let res = await super._handle(payload) const res = await super._handle(payload)
res.file = await decodeSilk(res.file!, payload.out_format) res.file = await decodeSilk(this.ctx, res.file!, payload.out_format)
res.file_name = path.basename(res.file) res.file_name = path.basename(res.file)
res.file_size = fs.statSync(res.file).size.toString() res.file_size = (await stat(res.file)).size.toString()
if (getConfigUtil().getConfig().enableLocalFile2Url){ if (this.adapter.config.enableLocalFile2Url) {
res.base64 = fs.readFileSync(res.file, 'base64') res.base64 = await readFile(res.file, 'base64')
} }
return res return res
} }

View File

@@ -0,0 +1,21 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
name: string
parent_id?: '/'
}
export class CreateGroupFileFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_CreateGroupFileFolder
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
name: Schema.string().required(),
})
async _handle(payload: Payload) {
await this.ctx.ntGroupApi.createGroupFileFolder(payload.group_id.toString(), payload.name)
return null
}
}

View File

@@ -1,24 +1,24 @@
import { BaseAction, Schema } from '../BaseAction'
import BaseAction from '../BaseAction'; import { ActionName } from '../types'
import { ActionName } from '../types';
import { NTQQGroupApi } from '../../../ntqqapi/api/group'
import { dbUtil } from '@/common/db';
interface Payload { interface Payload {
message_id: number | string; message_id: number | string
} }
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> { export class DelEssenceMsg extends BaseAction<Payload, unknown> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg; actionName = ActionName.GoCQHTTP_DelEssenceMsg
payloadSchema = Schema.object({
message_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload): Promise<any> { protected async _handle(payload: Payload) {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) { if (!msg) {
throw new Error('msg not found'); throw new Error('msg not found')
} }
return await NTQQGroupApi.removeGroupEssence( return await this.ctx.ntGroupApi.removeGroupEssence(
msg.peerUid, msg.peer.peerUid,
msg.msgId msg.msgId,
); )
} }
} }

View File

@@ -0,0 +1,22 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: string | number
file_id: string
busid: number
}
export class DelGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFile
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_id: Schema.string().required(),
busid: Schema.number().default(102)
})
async _handle(payload: Payload) {
await this.ctx.ntGroupApi.deleteGroupFile(payload.group_id.toString(), [payload.file_id], [payload.busid])
return null
}
}

View File

@@ -0,0 +1,20 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
folder_id: string
}
export class DelGroupFolder extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFolder
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
folder_id: Schema.string().required()
})
async _handle(payload: Payload) {
await this.ctx.ntGroupApi.deleteGroupFileFolder(payload.group_id.toString(), payload.folder_id)
return null
}
}

View File

@@ -1,9 +1,12 @@
import BaseAction from '../BaseAction'
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 { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { calculateFileMD5, fetchFile } from '@/common/utils'
import { TEMP_DIR } from '@/common/globalVars'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Dict } from 'cosmokit'
interface Payload { interface Payload {
thread_count?: number thread_count?: number
@@ -17,20 +20,25 @@ interface FileResponse {
file: string file: string
} }
export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileResponse> { export class DownloadFile extends BaseAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile actionName = ActionName.GoCQHTTP_DownloadFile
payloadSchema = Schema.object({
url: String,
base64: String,
headers: Schema.union([String, Schema.array(String)])
})
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 || randomUUID() 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 res = await fetchFile(payload.url, headers)
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary') await fsPromise.writeFile(filePath, res.data)
} else { } else {
throw new Error('不存在任何文件, 无法下载') throw new Error('不存在任何文件, 无法下载')
} }
@@ -38,8 +46,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
if (isRandomName) { if (isRandomName) {
// 默认实现要名称未填写时文件名为文件 md5 // 默认实现要名称未填写时文件名为文件 md5
const md5 = await calculateFileMD5(filePath) const md5 = await calculateFileMD5(filePath)
const newPath = joinPath(TEMP_DIR, md5) const newPath = path.join(TEMP_DIR, md5)
fs.renameSync(filePath, newPath) await fsPromise.rename(filePath, newPath)
return { file: newPath } return { file: newPath }
} }
return { file: filePath } return { file: filePath }
@@ -49,7 +57,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
} }
getHeaders(headersIn?: string | string[]): Record<string, string> { getHeaders(headersIn?: string | string[]): Record<string, string> {
const headers = {} const headers: Dict = {}
if (typeof headersIn == 'string') { if (typeof headersIn == 'string') {
headersIn = headersIn.split('[\\r\\n]') headersIn = headersIn.split('[\\r\\n]')
} }

View File

@@ -1,9 +1,8 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types' import { OB11ForwardMessage } from '../../types'
import { NTQQMsgApi } from '@/ntqqapi/api' import { OB11Entities } from '../../entities'
import { dbUtil } from '../../../common/db'
import { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { filterNullable } from '@/common/utils/misc'
interface Payload { interface Payload {
message_id: string // long msg idgocq message_id: string // long msg idgocq
@@ -11,41 +10,49 @@ interface Payload {
} }
interface Response { interface Response {
messages: (OB11Message & { content: OB11MessageData })[] messages: OB11ForwardMessage[]
} }
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, any> { export class GetForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetForwardMsg actionName = ActionName.GoCQHTTP_GetForwardMsg
protected async _handle(payload: Payload): Promise<any> { payloadSchema = Schema.object({
const message_id = payload.id || payload.message_id message_id: String,
if (!message_id) { id: String
})
protected async _handle(payload: Payload) {
const msgId = payload.id || payload.message_id
if (!msgId) {
throw Error('message_id不能为空') throw Error('message_id不能为空')
} }
const rootMsg = await dbUtil.getMsgByLongId(message_id) const rootMsgId = await this.ctx.store.getShortIdByMsgId(msgId)
const rootMsg = await this.ctx.store.getMsgInfoByShortId(rootMsgId || +msgId)
if (!rootMsg) { if (!rootMsg) {
throw Error('msg not found') throw Error('msg not found')
} }
let data = await NTQQMsgApi.getMultiMsg( const data = await this.ctx.ntMsgApi.getMultiMsg(rootMsg.peer, rootMsg.msgId, rootMsg.msgId)
{ chatType: rootMsg.chatType, peerUid: rootMsg.peerUid }, if (data?.result !== 0) {
rootMsg.msgId, throw Error('找不到相关的聊天记录' + data?.errMsg)
rootMsg.msgId,
)
if (data.result !== 0) {
throw Error('找不到相关的聊天记录' + data.errMsg)
} }
let msgList = data.msgList const msgList = data.msgList
let messages = await Promise.all( const messages = await Promise.all(
msgList.map(async (msg) => { msgList.map(async (msg) => {
let resMsg = await OB11Constructor.message(msg) const resMsg = await OB11Entities.message(this.ctx, msg)
resMsg.message_id = (await dbUtil.addMsg(msg))! if (!resMsg) return
resMsg.message_id = this.ctx.store.createMsgShortId({
chatType: msg.chatType,
peerUid: msg.peerUid,
}, msg.msgId)
return resMsg return resMsg
}), })
) )
messages.map(v => { const forwardMessages = filterNullable(messages)
const msg = v as Partial<OB11ForwardMessage> .map(v => {
msg.content = msg.message const msg = v as Partial<OB11ForwardMessage>
delete msg.message msg.content = msg.message
}) delete msg.message
return { messages } return msg as OB11ForwardMessage
})
return { messages: forwardMessages }
} }
} }

View File

@@ -0,0 +1,28 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Response {
can_at_all: boolean
remain_at_all_count_for_group: number
remain_at_all_count_for_uin: number
}
export class GetGroupAtAllRemain extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupRemainAtTimes(payload.group_id.toString())
return {
can_at_all: data.atInfo.canAtAll,
remain_at_all_count_for_group: data.atInfo.RemainAtAllCountForGroup,
remain_at_all_count_for_uin: data.atInfo.RemainAtAllCountForUin
}
}
}

View File

@@ -0,0 +1,54 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types'
interface Payload {
group_id: string | number
folder_id: string
file_count: string | number
}
interface Response {
files: OB11GroupFile[]
folders: OB11GroupFileFolder[]
}
export class GetGroupFilesByFolder extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
folder_id: Schema.string().required(),
file_count: Schema.union([Number, String]).default(50)
})
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId: payload.folder_id
})
return {
files: data.filter(item => item.fileInfo)
.map(item => {
const file = item.fileInfo!
return {
group_id: +item.peerId,
file_id: file.fileId,
file_name: file.fileName,
busid: file.busId,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,
download_times: file.downloadTimes,
uploader: +file.uploaderUin,
uploader_name: file.uploaderName
}
}),
folders: []
}
}
}

View File

@@ -1,45 +1,51 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } 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 { dbUtil } from '../../../common/db' import { OB11Entities } from '../../entities'
import { NTQQMsgApi } from '../../../ntqqapi/api/msg' import { RawMessage } from '@/ntqqapi/types'
import { OB11Constructor } from '../../constructor' import { filterNullable } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number group_id: number | string
message_seq: number message_seq?: number | string
count: number count: number | string
reverseOrder: boolean
} }
interface Response { interface Response {
messages: OB11Message[] messages: OB11Message[]
} }
export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Response> { export class GetGroupMsgHistory extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory actionName = ActionName.GoCQHTTP_GetGroupMsgHistory
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
message_seq: Schema.union([Number, String]),
count: Schema.union([Number, String]).default(20),
reverseOrder: Schema.boolean().default(false),
})
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, reverseOrder } = payload
if (!group) { const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() }
throw `${payload.group_id}不存在` let msgList: RawMessage[] | undefined
// 包含 message_seq 0
if (!payload.message_seq) {
msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
} else {
const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
} }
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0' if (!msgList?.length) throw new Error('未找到消息')
// log("startMsgId", startMsgId) if (reverseOrder) msgList.reverse()
let msgList = (
await NTQQMsgApi.getMsgHistory(
{ chatType: ChatType.group, peerUid: group.groupCode },
startMsgId,
parseInt(payload.count?.toString()) || 20,
)
).msgList
await Promise.all( await Promise.all(
msgList.map(async (msg) => { msgList.map(async msg => {
msg.msgShortId = await dbUtil.addMsg(msg) msg.msgShortId = this.ctx.store.createMsgShortId({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
}), })
) )
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg))) const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Entities.message(this.ctx, msg)))
return { messages: ob11MsgList } return { messages: filterNullable(ob11MsgList) }
} }
} }

View File

@@ -0,0 +1,63 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
interface Payload {
group_id: string | number
file_count: string | number
}
interface Response {
files: OB11GroupFile[]
folders: OB11GroupFileFolder[]
}
export class GetGroupRootFiles extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupRootFiles
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_count: Schema.union([Number, String]).default(50),
})
async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1,
fileCount: +payload.file_count,
startIndex: 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
return {
files: data.filter(item => item.fileInfo)
.map(item => {
const file = item.fileInfo!
return {
group_id: +item.peerId,
file_id: file.fileId,
file_name: file.fileName,
busid: file.busId,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,
download_times: file.downloadTimes,
uploader: +file.uploaderUin,
uploader_name: file.uploaderName
}
}),
folders: data.filter(item => item.folderInfo)
.map(item => {
const folder = item.folderInfo!
return {
group_id: +item.peerId,
folder_id: folder.folderId,
folder_name: folder.folderName,
create_time: folder.createTime,
creator: +folder.createUin,
creator_name: folder.creatorName,
total_file_count: folder.totalFileCount
}
})
}
}
}

View File

@@ -0,0 +1,59 @@
import { BaseAction } from '../BaseAction'
import { GroupNotifyStatus } from '@/ntqqapi/types'
import { ActionName } from '../types'
interface Response {
invited_requests: {
request_id: number
invitor_uin: number
invitor_nick: string
group_id: number
group_name: string
checked: boolean
actor: number
}[]
join_requests: {
request_id: number
requester_uin: number
requester_nick: string
message: string
group_id: number
group_name: string
checked: boolean
actor: number
}[]
}
export class GetGroupSystemMsg extends BaseAction<void, Response> {
actionName = ActionName.GoCQHTTP_GetGroupSystemMsg
async _handle() {
const singleScreenNotifies = await this.ctx.ntGroupApi.getSingleScreenNotifies(10)
const data: Response = { invited_requests: [], join_requests: [] }
for (const notify of singleScreenNotifies) {
if (notify.type == 1) {
data.invited_requests.push({
request_id: +notify.seq,
invitor_uin: Number(await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)),
invitor_nick: notify.user1.nickName,
group_id: +notify.group.groupCode,
group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.KUNHANDLE,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
})
} else if (notify.type == 7) {
data.join_requests.push({
request_id: +notify.seq,
requester_uin: Number(await this.ctx.ntUserApi.getUinByUid(notify.user1.uid)),
requester_nick: notify.user1.nickName,
message: notify.postscript,
group_id: +notify.group.groupCode,
group_name: notify.group.groupName,
checked: notify.status !== GroupNotifyStatus.KUNHANDLE,
actor: notify.user2?.uid ? Number(await this.ctx.ntUserApi.getUinByUid(notify.user2.uid)) : 0
})
}
}
return data
}
}

View File

@@ -1,24 +1,26 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { OB11User } from '../../types' import { OB11User } from '../../types'
import { OB11Constructor } from '../../constructor' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQUserApi } from '../../../ntqqapi/api/user' import { getBuildVersion } from '@/common/utils'
import { getBuildVersion } from '@/common/utils/QQBasicInfo'
import { OB11UserSex } from '../../types' import { OB11UserSex } from '../../types'
import { calcQQLevel } from '@/common/utils/qqlevel' import { calcQQLevel } from '@/common/utils/misc'
interface Payload { interface Payload {
user_id: number | string user_id: number | string
} }
export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11User> { export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo actionName = ActionName.GoCQHTTP_GetStrangerInfo
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload): Promise<OB11User> { protected async _handle(payload: Payload): Promise<OB11User> {
if (!(getBuildVersion() >= 26702)) { if (!(getBuildVersion() >= 26702)) {
const user_id = payload.user_id.toString() const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id) const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUin(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))! const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) { if (!uid || uid.indexOf('*') != -1) {
const ret = { const ret = {
...extendData, ...extendData,
@@ -33,12 +35,12 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
} }
return ret return ret
} }
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) } const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data) return OB11Entities.stranger(data)
} else { } else {
const user_id = payload.user_id.toString() const user_id = payload.user_id.toString()
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id) const extendData = await this.ctx.ntUserApi.getUserDetailInfoByUinV2(user_id)
const uid = (await NTQQUserApi.getUidByUin(user_id))! const uid = (await this.ctx.ntUserApi.getUidByUin(user_id))!
if (!uid || uid.indexOf('*') != -1) { if (!uid || uid.indexOf('*') != -1) {
const ret = { const ret = {
...extendData, ...extendData,
@@ -52,8 +54,8 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
} }
return ret return ret
} }
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) } const data = { ...extendData, ...(await this.ctx.ntUserApi.getUserDetailInfo(uid)) }
return OB11Constructor.stranger(data) return OB11Entities.stranger(data)
} }
} }
} }

View File

@@ -0,0 +1,22 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
message_id: number | string
}
export class MarkMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_MarkMsgAsRead
payloadSchema = Schema.object({
message_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
if (!msg) {
throw new Error('msg not found')
}
await this.ctx.ntMsgApi.setMsgRead(msg.peer)
return null
}
}

View File

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

View File

@@ -1,20 +1,181 @@
import SendMsg, { convertMessage2List } from '../msg/SendMsg' import { unlink } from 'node:fs/promises'
import { OB11PostSendMsg } from '../../types' import { OB11MessageNode } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { BaseAction, Schema } from '../BaseAction'
import { Peer } from '@/ntqqapi/types/msg'
import { ChatType, ElementType, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { selfInfo } from '@/common/globalVars'
import { convertMessage2List, createSendElements, sendMsg, createPeer, CreatePeerMode } from '../../helper/createMessage'
export class GoCQHTTPSendForwardMsg extends SendMsg { interface Payload {
user_id?: string | number
group_id?: string | number
messages: OB11MessageNode[]
message_type?: 'group' | 'private'
}
interface Response {
message_id: number
forward_id?: string
}
export class SendForwardMsg extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_SendForwardMsg actionName = ActionName.GoCQHTTP_SendForwardMsg
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]),
group_id: Schema.union([Number, String]),
messages: Schema.array(Schema.any()).required(),
message_type: Schema.union(['group', 'private'])
})
protected async check(payload: OB11PostSendMsg) { protected async _handle(payload: Payload) {
if (payload.messages) payload.message = convertMessage2List(payload.messages) let contextMode = CreatePeerMode.Normal
return super.check(payload) if (payload.message_type === 'group') {
contextMode = CreatePeerMode.Group
} else if (payload.message_type === 'private') {
contextMode = CreatePeerMode.Private
}
const peer = await createPeer(this.ctx, payload, contextMode)
const returnMsg = await this.handleForwardNode(peer, payload.messages)
return { message_id: returnMsg.msgShortId! }
}
private async cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
this.ctx.logger.info('克隆的目标消息', msg)
const sendElements: SendMessageElement[] = []
for (const ele of msg.elements) {
sendElements.push(ele as SendMessageElement)
}
if (sendElements.length === 0) {
this.ctx.logger.warn('需要clone的消息无法解析将会忽略掉', msg)
}
this.ctx.logger.info('克隆消息', sendElements)
try {
const peer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid
}
const nodeMsg = await this.ctx.ntMsgApi.sendMsg(peer, sendElements)
await this.ctx.sleep(300)
return nodeMsg
} catch (e) {
this.ctx.logger.warn(e, '克隆转发消息失败,将忽略本条消息', msg)
}
}
// 返回一个合并转发的消息id
private async handleForwardNode(destPeer: Peer, messageNodes: OB11MessageNode[]) {
const selfPeer = {
chatType: ChatType.friend,
peerUid: selfInfo.uid,
}
const nodeMsgIds: { msgId: string, peer: Peer }[] = []
// 先判断一遍是不是id和自定义混用
for (const messageNode of messageNodes) {
// 一个node表示一个人的消息
const nodeId = messageNode.data.id
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
const nodeMsg = await this.ctx.store.getMsgInfoByShortId(+nodeId)
if (!nodeMsg) {
this.ctx.logger.warn('转发消息失败,未找到消息', nodeId)
continue
}
nodeMsgIds.push(nodeMsg)
}
else {
// 自定义的消息
// 提取消息段发给自己生成消息id
try {
const { sendElements, deleteAfterSentFiles } = await createSendElements(
this.ctx,
convertMessage2List(messageNode.data.content),
destPeer
)
this.ctx.logger.info('开始生成转发节点', sendElements)
const sendElementsSplit: SendMessageElement[][] = []
let splitIndex = 0
for (const ele of sendElements) {
if (!sendElementsSplit[splitIndex]) {
sendElementsSplit[splitIndex] = []
}
if (ele.elementType === ElementType.FILE || ele.elementType === ElementType.VIDEO) {
if (sendElementsSplit[splitIndex].length > 0) {
splitIndex++
}
sendElementsSplit[splitIndex] = [ele]
splitIndex++
}
else {
sendElementsSplit[splitIndex].push(ele)
}
}
this.ctx.logger.info('分割后的转发节点', sendElementsSplit)
for (const eles of sendElementsSplit) {
const nodeMsg = await sendMsg(this.ctx, selfPeer, eles, [])
if (!nodeMsg) {
this.ctx.logger.warn('转发节点生成失败', eles)
continue
}
nodeMsgIds.push({ msgId: nodeMsg.msgId, peer: selfPeer })
await this.ctx.sleep(300)
}
deleteAfterSentFiles.map(path => unlink(path))
} catch (e) {
this.ctx.logger.error('生成转发消息节点失败', e)
}
}
}
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: RawMessage[] = []
let srcPeer: Peer
let needSendSelf = false
for (const { msgId, peer } of nodeMsgIds) {
const nodeMsg = (await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [msgId])).msgList[0]
srcPeer ??= { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true
}
nodeMsgArray.push(nodeMsg)
}
let retMsgIds: string[] = []
if (needSendSelf) {
for (const msg of nodeMsgArray) {
if (msg.peerUid === selfPeer.peerUid) {
retMsgIds.push(msg.msgId)
continue
}
const clonedMsg = await this.cloneMsg(msg)
if (clonedMsg) retMsgIds.push(clonedMsg.msgId)
}
} else {
retMsgIds = nodeMsgArray.map(msg => msg.msgId)
}
if (retMsgIds.length === 0) {
throw Error('转发消息失败,节点为空')
}
const returnMsg = await this.ctx.ntMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds)
returnMsg.msgShortId = this.ctx.store.createMsgShortId(destPeer, returnMsg.msgId)
return returnMsg
} }
} }
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendForwardMsg { export class SendPrivateForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg
protected _handle(payload: Payload) {
payload.message_type = 'private'
return super._handle(payload)
}
} }
export class GoCQHTTPSendGroupForwardMsg extends GoCQHTTPSendForwardMsg { export class SendGroupForwardMsg extends SendForwardMsg {
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg actionName = ActionName.GoCQHTTP_SendGroupForwardMsg
protected _handle(payload: Payload) {
payload.message_type = 'group'
return super._handle(payload)
}
} }

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