Compare commits

...

213 Commits

Author SHA1 Message Date
idranme
41822eb052 Merge pull request #460 from LLOneBot/dev
release: 4.0.1
2024-10-08 20:46:09 +08:00
idranme
b5578d6278 chore: bump versions 2024-10-08 20:43:56 +08:00
idranme
fecb4c4655 feat(onebot): delete_friend API 2024-10-08 20:40:02 +08:00
idranme
c82b849ead fix 2024-10-08 20:07:12 +08:00
idranme
0bc6e23343 Merge pull request #459 from LLOneBot/dev
release: 4.0.0
2024-10-07 20:26:59 +08:00
idranme
8e9523602b chore: v4.0.0 2024-10-07 20:23:54 +08:00
idranme
48588817fb chore 2024-10-07 19:10:38 +08:00
idranme
4cd9adde1d feat: satori protocol 2024-10-06 10:37:06 +08:00
idranme
8c0cc8beba refactor 2024-10-06 10:28:52 +08:00
idranme
9ec09c6eee Merge pull request #457 from LLOneBot/dev
release: 3.34.1
2024-10-03 15:18:47 +08:00
idranme
4d816b498a chore: v3.34.1 2024-10-03 15:17:57 +08:00
idranme
464efe819d fix 2024-10-03 15:16:48 +08:00
idranme
0876e4645f Merge pull request #456 from LLOneBot/dev
release: 3.34.0
2024-10-01 21:32:24 +08:00
idranme
a2f9128623 chore: v3.34.0 2024-10-01 21:25:19 +08:00
idranme
e313b2b3e6 feat 2024-10-01 21:16:39 +08:00
idranme
a7d86f8fe0 refactor 2024-10-01 21:09:27 +08:00
idranme
496d56f297 feat 2024-09-30 00:49:58 +08:00
idranme
ed2f554d4e refactor 2024-09-28 22:00:05 +08:00
idranme
36d990e328 Merge pull request #452 from LLOneBot/dev
release: 3.33.10
2024-09-28 14:40:11 +08:00
idranme
0ceef4d4c0 chore: v3.33.10 2024-09-28 14:37:44 +08:00
idranme
35bf4f001b feat: _get_group_notice API 2024-09-28 14:35:06 +08:00
idranme
544682fe41 fix 2024-09-28 12:54:02 +08:00
idranme
3da49fbfba optimize 2024-09-27 18:37:47 +08:00
idranme
d5875c9e5b Merge pull request #451 from LLOneBot/dev
release: 3.33.9
2024-09-27 16:53:44 +08:00
idranme
7895644156 chore: v3.33.9 2024-09-27 16:51:49 +08:00
idranme
f092626ede fix 2024-09-27 16:22:50 +08:00
idranme
a58fb31f8e Merge pull request #448 from LLOneBot/dev
release: 3.33.8
2024-09-26 12:57:16 +08:00
idranme
fe85e277f1 chore: v3.33.8 2024-09-26 12:54:30 +08:00
idranme
5217638b46 feat 2024-09-26 01:52:47 +08:00
idranme
f68b707e1c optimize 2024-09-25 22:34:59 +08:00
idranme
c24ce6ec65 adjustment of get_friends_with_category API returns 2024-09-25 22:04:52 +08:00
idranme
f9270c38cf Merge pull request #444 from LLOneBot/dev
release: 3.33.7
2024-09-25 14:59:34 +08:00
idranme
fd478cdaed chore: v3.33.7 2024-09-25 14:55:05 +08:00
idranme
517b233496 fix 2024-09-25 14:52:04 +08:00
idranme
1045c94a91 feat: get_group_file_url API 2024-09-25 12:13:28 +08:00
idranme
032ac85c04 refactor 2024-09-24 19:59:07 +08:00
idranme
1e35ffd7e6 optimize 2024-09-24 14:18:44 +08:00
idranme
e5ab6134cd Merge pull request #441 from LLOneBot/dev
release: 3.33.6
2024-09-23 23:43:50 +08:00
idranme
a95ae44614 chore: v3.33.6 2024-09-23 23:36:10 +08:00
idranme
3dc9940ac9 feat 2024-09-23 23:34:52 +08:00
idranme
277e418cf3 refactor 2024-09-23 22:10:12 +08:00
idranme
24f09d485e Merge pull request #438 from LLOneBot/dev
release: 3.33.5
2024-09-22 21:31:55 +08:00
idranme
3394823719 chore: v3.33.5 2024-09-22 21:26:37 +08:00
idranme
afa06f0760 fix 2024-09-22 21:18:59 +08:00
idranme
4f9e465fb2 optimize 2024-09-22 20:37:20 +08:00
idranme
f400d43b8a Merge pull request #436 from LLOneBot/dev
release: 3.33.4
2024-09-21 23:29:47 +08:00
idranme
fb2f1a8917 chore: v3.33.4 2024-09-21 23:29:05 +08:00
idranme
c849b9bea2 fix: get_forward_msg API 2024-09-21 23:28:27 +08:00
idranme
1c6364d98f Merge pull request #435 from LLOneBot/dev
release: 3.33.3
2024-09-21 21:52:54 +08:00
idranme
8a268c3968 chore: v3.33.3 2024-09-21 21:39:30 +08:00
idranme
806798bd48 refactor 2024-09-21 21:32:40 +08:00
idranme
0c456e2160 optimize 2024-09-21 20:19:26 +08:00
idranme
08e7e471d6 fix: delete_group_file API 2024-09-21 20:19:10 +08:00
idranme
13299c4631 chore: improve code quality 2024-09-21 09:21:08 +08:00
idranme
390e20c2ef optimize 2024-09-21 03:30:01 +08:00
idranme
d8433e22d2 chore: improve code quality 2024-09-21 01:06:49 +08:00
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
222 changed files with 10293 additions and 9540 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: 20 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,4 +1,4 @@
MIT Without Public Sicial Media Promotion License MIT Without Public Social Media Promotion License
Copyright (c) 2024 LLOneBot Copyright (c) 2024 LLOneBot

View File

@@ -1,9 +1,9 @@
# LLOneBot # LLOneBot
LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发 LiteLoaderQQNT 插件,实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发
> [!CAUTION]\ > [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本插件存在相关性的信息** > 请不要在 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>
@@ -31,10 +27,10 @@ TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
- [NapCatQQ](https://github.com/NapNeko/NapCatQQ) - [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,6 +1,7 @@
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',
@@ -12,7 +13,7 @@ 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',
@@ -30,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: [
@@ -39,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 和 Satori 协议,用于 QQ 机器人开发",
"version": "3.30.5", "version": "4.0.1",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -7,39 +7,44 @@
"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.5.0", "@minatojs/driver-sqlite": "^4.6.0",
"compressing": "^1.10.1", "@satorijs/element": "^3.1.7",
"cordis": "^3.18.0", "@satorijs/protocol": "^1.4.2",
"compare-versions": "^6.1.1",
"cordis": "^3.18.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cosmokit": "^1.6.2", "cosmokit": "^1.6.3",
"express": "^4.19.2", "express": "^5.0.0",
"fast-xml-parser": "^4.4.1", "fast-xml-parser": "^4.5.0",
"file-type": "^19.4.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"minato": "^3.5.1", "minato": "^3.6.0",
"protobufjs": "^7.4.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ts-case-convert": "^2.1.0",
"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": "^5.0.0",
"@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": "^31.4.0", "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.2", "typescript": "^5.6.2",
"vite": "^5.4.8",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
}, },
"packageManager": "yarn@4.4.1" "packageManager": "yarn@4.5.0"
} }

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 和 Satori 协议,用于 QQ 机器人开发',
version, version,
icon: './icon.webp', icon: './icon.webp',
authors: [ authors: [

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,9 +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, SatoriConfig } from './types'
import { DATA_DIR } from './utils' import { selfInfo, DATA_DIR } from './globalVars'
import { mergeNewProperties } from './utils/misc'
export class ConfigUtil { export class ConfigUtil {
private readonly configPath: string private readonly configPath: string
@@ -22,7 +21,8 @@ export class ConfigUtil {
} }
reloadConfig(): Config { reloadConfig(): Config {
let ob11Default: OB11Config = { const ob11Default: OB11Config = {
enable: true,
httpPort: 3000, httpPort: 3000,
httpHosts: [], httpHosts: [],
httpSecret: '', httpSecret: '',
@@ -34,10 +34,16 @@ export class ConfigUtil {
enableWsReverse: false, enableWsReverse: false,
messagePostFormat: 'array', messagePostFormat: 'array',
enableHttpHeart: false, enableHttpHeart: false,
enableQOAutoQuote: false listenLocalhost: false
} }
let defaultConfig: Config = { const satoriDefault: SatoriConfig = {
enableLLOB: true, enable: true,
port: 5600,
listen: '0.0.0.0',
token: ''
}
const defaultConfig: Config = {
satori: satoriDefault,
ob11: ob11Default, ob11: ob11Default,
heartInterval: 60000, heartInterval: 60000,
token: '', token: '',
@@ -67,7 +73,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
} }
@@ -79,21 +84,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,107 +0,0 @@
import {
type GroupMember,
type SelfInfo,
} from '../ntqqapi/types'
import { type LLOneBotError } from './types'
import { NTQQGroupApi } from '../ntqqapi/api/group'
import { isNumeric } from './utils/helper'
import { NTQQUserApi } from '../ntqqapi/api'
import { RawMessage } from '../ntqqapi/types'
import { getConfigUtil } from './config'
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 getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString()
const memberUinOrUidStr = memberUinOrUid.toString()
let members = groupMembers.get(groupCodeStr)
if (!members) {
try {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
// 更新群成员列表
groupMembers.set(groupCodeStr, members)
}
catch (e) {
return null
}
}
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) {
members = await NTQQGroupApi.getGroupMembers(groupCodeStr)
groupMembers.set(groupCodeStr, members)
member = getMember()
}
return member
}
const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true,
}
export async function getSelfNick(force = false): Promise<string> {
if ((!selfInfo.nick || force) && selfInfo.uid) {
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']
}
const messages: Map<string, RawMessage> = new Map()
/** 缓存近期消息内容 */
export async function addMsgCache(msg: RawMessage) {
const expire = getConfigUtil().getConfig().msgCacheExpire! * 1000
if (expire === 0) {
return
}
const id = msg.msgId
messages.set(id, msg)
setTimeout(() => {
messages.delete(id)
}, expire)
}
/** 获取近期消息内容 */
export function getMsgCache(msgId: string) {
return messages.get(msgId)
}

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,4 +1,5 @@
export interface OB11Config { export interface OB11Config {
enable: boolean
httpPort: number httpPort: number
httpHosts: string[] httpHosts: string[]
httpSecret?: string httpSecret?: string
@@ -10,19 +11,26 @@ 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 SatoriConfig {
result: boolean enable: boolean
version: string listen: string
port: number
token: string
} }
export interface Config { export interface Config {
enableLLOB: boolean satori: SatoriConfig
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
@@ -34,6 +42,19 @@ export interface Config {
ignoreBeforeLoginMsg?: boolean ignoreBeforeLoginMsg?: boolean
/** 单位为秒 */ /** 单位为秒 */
msgCacheExpire?: number msgCacheExpire?: number
/** @deprecated */
http?: string
/** @deprecated */
hosts?: string[]
/** @deprecated */
wsPort?: string
/** @deprecated */
enableLLOB?: boolean
}
export interface CheckVersion {
result: boolean
version: string
} }
export interface LLOneBotError { export interface LLOneBotError {

View File

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

View File

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

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,22 +93,22 @@ 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',

View File

@@ -1,17 +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 { TEMP_DIR } from './index' import { TEMP_DIR } from '../globalVars'
import { randomUUID, createHash } from 'node:crypto' import { randomUUID, createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { Context } from 'cordis'
export function isGIF(path: string) {
const buffer = Buffer.alloc(4)
const fd = fs.openSync(path, 'r')
fs.readSync(fd, buffer, 0, 4, 0)
fs.closeSync(fd)
return buffer.toString() === 'GIF8'
}
// 定义一个异步函数来检查文件是否存在 // 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> { export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
@@ -56,34 +49,6 @@ export function calculateFileMD5(filePath: string): Promise<string> {
}) })
} }
export interface HttpDownloadOptions {
url: string
headers?: Record<string, string> | string
}
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 enum FileUriType { export enum FileUriType {
Unknown = 0, Unknown = 0,
FileURL = 1, FileURL = 1,
@@ -117,17 +82,27 @@ interface FetchFileRes {
url: string url: string
} }
async function fetchFile(url: string): Promise<FetchFileRes> { export async function fetchFile(url: string, headersInit?: Record<string, string>): Promise<FetchFileRes> {
const headers: Record<string, string> = { 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', '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 'Host': new URL(url).hostname,
} ...headersInit
const raw = await fetch(url, { headers }).catch((err) => { })
let raw = await fetch(url, { headers }).catch((err) => {
if (err.cause) { if (err.cause) {
throw err.cause throw err.cause
} }
throw err 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}`) if (!raw.ok) throw new Error(`statusText: ${raw.statusText}`)
return { return {
data: Buffer.from(await raw.arrayBuffer()), data: Buffer.from(await raw.arrayBuffer()),
@@ -143,7 +118,7 @@ type Uri2LocalRes = {
isLocal: boolean isLocal: boolean
} }
export async function uri2local(uri: string, filename?: string): Promise<Uri2LocalRes> { export async function uri2local(ctx: Context, uri: string, needExt?: boolean): Promise<Uri2LocalRes> {
const { type } = checkUriType(uri) const { type } = checkUriType(uri)
if (type === FileUriType.FileURL) { if (type === FileUriType.FileURL) {
@@ -161,25 +136,38 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
try { try {
const res = await fetchFile(uri) const res = await fetchFile(uri)
const match = res.url.match(/.+\/([^/?]*)(?=\?)?/) const match = res.url.match(/.+\/([^/?]*)(?=\?)?/)
let filename: string
if (match?.[1]) { if (match?.[1]) {
filename ??= match[1].replace(/[/\\:*?"<>|]/g, '_') filename = match[1].replace(/[/\\:*?"<>|]/g, '_')
} else { } else {
filename ??= randomUUID() filename = randomUUID()
} }
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, res.data) await fsPromise.writeFile(filePath, res.data)
if (needExt && !path.extname(filePath)) {
const ext = (await ctx.ntFileApi.getFileType(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: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} catch (e: any) { } catch (e) {
const errMsg = `${uri}下载失败,` + e.toString() const errMsg = `${uri} 下载失败, ${(e as Error).message}`
return { success: false, errMsg, fileName: '', path: '', isLocal: false } return { success: false, errMsg, fileName: '', path: '', isLocal: false }
} }
} }
if (type === FileUriType.OneBotBase64) { if (type === FileUriType.OneBotBase64) {
filename ??= randomUUID() let filename = randomUUID()
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
const base64 = uri.replace(/^base64:\/\//, '') const base64 = uri.replace(/^base64:\/\//, '')
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await ctx.ntFileApi.getFileType(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: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} }
@@ -187,36 +175,19 @@ export async function uri2local(uri: string, filename?: string): Promise<Uri2Loc
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri) const capture = /^data:([\w/.+-]+);base64,(.*)$/.exec(uri)
if (capture) { if (capture) {
filename ??= randomUUID() let filename = randomUUID()
const [, _type, base64] = capture const [, _type, base64] = capture
const filePath = path.join(TEMP_DIR, filename) let filePath = path.join(TEMP_DIR, filename)
await fsPromise.writeFile(filePath, base64, 'base64') await fsPromise.writeFile(filePath, base64, 'base64')
if (needExt) {
const ext = (await ctx.ntFileApi.getFileType(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: true, errMsg: '', fileName: filename, path: filePath, isLocal: false }
} }
} }
return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false } return { success: false, errMsg: '未知文件类型', fileName: '', path: '', isLocal: false }
} }
export async function copyFolder(sourcePath: string, destPath: string) {
try {
const entries = await fsPromise.readdir(sourcePath, { withFileTypes: true })
await fsPromise.mkdir(destPath, { recursive: true })
for (let entry of entries) {
const srcPath = path.join(sourcePath, entry.name)
const dstPath = path.join(destPath, entry.name)
if (entry.isDirectory()) {
await copyFolder(srcPath, dstPath)
} else {
try {
await fsPromise.copyFile(srcPath, dstPath)
} catch (error) {
console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`)
// 这里可以决定是否要继续复制其他文件
}
}
}
} catch (error) {
console.error('复制文件夹时出错:', error)
}
}

View File

@@ -1,169 +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
}
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/common/utils/helper.ts#L14
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr)
const low = BigInt(lowStr)
const highHex = high.toString(16).padStart(16, '0')
const lowHex = low.toString(16).padStart(16, '0')
const combinedHex = highHex + lowHex
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`
return uuid
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '')
const high = BigInt('0x' + hex.substring(0, 16))
const low = BigInt('0x' + hex.substring(16))
return { high: high.toString(), low: low.toString() }
}
}

View File

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

View File

@@ -0,0 +1,29 @@
import fs from 'fs'
import path from 'node:path'
import { getConfigUtil } from '../config'
import { LOG_DIR } from '../globalVars'
import { inspect } from 'node:util'
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 += inspect(msgItem, {
depth: 10,
compact: true,
breakLength: Infinity,
maxArrayLength: 220
}) + ' '
} else {
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, () => {})
}

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

@@ -0,0 +1,47 @@
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]
return +globalThis.LiteLoader.package.qqnt.buildVersion
}
/** 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 */
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>[]
}
export function parseBool(value: string) {
if (['', 'true', '1'].includes(value)) {
return true
}
return false
}

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

@@ -16,12 +16,11 @@ export class LimitedHashTable<K, V> {
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) { while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value const oldestKey = this.keyToValue.keys().next().value!
this.valueToKey.delete(this.keyToValue.get(oldestKey)!) this.valueToKey.delete(this.keyToValue.get(oldestKey)!)
this.keyToValue.delete(oldestKey) this.keyToValue.delete(oldestKey)
} }
@@ -56,7 +55,7 @@ export class LimitedHashTable<K, V> {
} }
//获取最近刚写入的几个值 //获取最近刚写入的几个值
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

View File

@@ -1,97 +1,73 @@
import path from 'node:path'
import { writeFile } from 'node:fs/promises'
import { version } from '../../version' import { version } from '../../version'
import * as path from 'node:path' import { log, fetchFile } from '.'
import * as fs from 'node:fs' import { TEMP_DIR } from '../globalVars'
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from '.' import { compare } from 'compare-versions'
import compressing from 'compressing'
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 latestVersion = await getRemoteVersion()
const latestVersion = latestVersionText.split('.') log('LLOneBot latest version', latestVersion)
log('llonebot last version', latestVersion) if (latestVersion === '') {
const currentVersion: string[] = version.split('.') return { result: false, version: latestVersion }
log('llonebot current version', currentVersion)
for (let k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) {
log('')
return { result: true, version: latestVersionText }
} else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) {
break
} }
if (compare(latestVersion, version, '>')) {
return { result: true, version: latestVersion }
} }
return { result: false, version: version } return { result: false, version: version }
} }
export async function upgradeLLOneBot() { export async function upgradeLLOneBot(): Promise<boolean> {
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
// 多镜像下载 // 多镜像下载
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 return globalThis.LiteLoader.api.plugin.install(filePath)
break
} catch (e) { } catch (e) {
log('llonebot upgrade error', e) log('llonebot upgrade error', e)
} }
} }
if (!downloadSuccess) {
log('llonebot upgrade error', 'download failed')
return false
}
const temp_ver_dir = path.join(TEMP_DIR, 'LLOneBot' + latestVersion)
let uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => {
compressing.zip
.uncompress(filePath, temp_ver_dir)
.then(() => {
resolve(true)
})
.catch((reason: any) => {
log('llonebot upgrade failed, ', reason)
if (reason?.errno == -4082) {
resolve(true)
}
resolve(false)
})
})
}
const uncompressedResult = await uncompressedPromise()
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR)
return uncompressedResult
} }
return false return false
} }
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('llob/config-updated', input => {
enable = input.log!
})
}
}

View File

@@ -1,9 +1,13 @@
// 运行在 Electron 主进程 下的插件入口
import { BrowserWindow, dialog, ipcMain } from 'electron'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import Log from './log'
import { Config } from '../common/types' import Core from '../ntqqapi/core'
import OneBot11Adapter from '../onebot11/adapter'
import SatoriAdapter from '../satori/adapter'
import Database from 'minato'
import SQLiteDriver from '@minatojs/driver-sqlite'
import Store from './store'
import { BrowserWindow, dialog, ipcMain } from 'electron'
import { Config as LLOBConfig } from '../common/types'
import { import {
CHANNEL_CHECK_VERSION, CHANNEL_CHECK_VERSION,
CHANNEL_ERROR, CHANNEL_ERROR,
@@ -12,55 +16,55 @@ 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 { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook'
import { DATA_DIR, getBuildVersion, TEMP_DIR } from '../common/utils'
import {
llonebotError,
setSelfInfo,
getSelfInfo,
getSelfUid,
getSelfUin,
addMsgCache
} from '../common/data'
import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook, startHook } from '../ntqqapi/hook'
import { OB11Constructor } from '../onebot11/constructor'
import {
FriendRequestNotify,
GroupNotify,
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 { MessageUnique } from '../common/utils/MessageUnique'
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 { Context } from 'cordis'
import '../ntqqapi/wrapper' import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars'
import { NTEventDispatch } from '../common/utils/EventTask' import { log, logFileName } from '../common/utils/legacyLog'
import { wrapperConstructor, getSession } from '../ntqqapi/wrapper' import {
import { Peer } from '../ntqqapi/types' NTQQFileApi,
NTQQFileCacheApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQMsgApi,
NTQQUserApi,
NTQQWebApi,
NTQQWindowApi
} from '../ntqqapi/api'
import { mkdir } from 'node:fs/promises'
import { existsSync, mkdirSync } from 'node:fs'
declare module 'cordis' {
interface Events {
'llob/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({
@@ -73,11 +77,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]);
// getConfigUtil().setConfig(config);
}
resolve('') resolve('')
}
}) })
.catch((err) => { .catch((err) => {
reject(err) reject(err)
@@ -90,30 +92,30 @@ 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) => {
ipcMain.handle(CHANNEL_SET_CONFIG, (_event, ask: boolean, config: LLOBConfig) => {
return new Promise<boolean>(resolve => {
if (!ask) { if (!ask) {
setConfig(config) getConfigUtil().setConfig(config)
.then() log('配置已更新', config)
.catch((e) => { checkFfmpeg(config.ffmpeg).then()
log('保存设置失败', e.stack) resolve(true)
})
return return
} }
dialog dialog
@@ -127,327 +129,113 @@ function onLoad() {
}) })
.then((result) => { .then((result) => {
if (result.response === 0) { if (result.response === 0) {
setConfig(config) getConfigUtil().setConfig(config)
.then() log('配置已更新', config)
.catch((e) => { checkFfmpeg(config.ffmpeg).then()
log('保存设置失败', e.stack) resolve(true)
})
}
else {
} }
}) })
.catch((err) => { .catch((err) => {
log('保存设置询问弹窗错误', err) log('保存设置询问弹窗错误', err)
resolve(false)
})
}) })
}) })
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()
for (let message of msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < startTime / 1000) {
continue
}
// log("收到新消息", message.msgId, message.msgSeq)
const peer: Peer = {
chatType: message.chatType,
peerUid: message.peerUid
}
message.msgShortId = MessageUnique.createMsg(peer, message.msgId)
addMsgCache(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)
}
})
}
}
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) {
if (message.recallTime != '0') {
if (recallMsgIds.includes(message.msgId)) {
continue
}
recallMsgIds.push(message.msgId)
const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId)
if (!oriMessageId) {
continue
}
OB11Constructor.RecallEvent(message, oriMessageId).then((recallEvent) => {
if (recallEvent) {
//log('post recall event', recallEvent)
postOb11Event(recallEvent)
}
})
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
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())
}
})
const processedGroupNotify: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notifies: GroupNotify[]
try {
notifies = (await NTQQGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) {
// log("获取群通知详情失败", e);
return
}
for (const notify of notifies) {
try {
notify.time = Date.now()
const notifyTime = parseInt(notify.seq) / 1000
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
if (notifyTime < startTime || processedGroupNotify.includes(flag)) {
continue
}
processedGroupNotify.push(flag)
if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log('有成员退出通知', notify)
const member1Uin = (await NTQQUserApi.getUinByUid(notify.user1.uid))!
let operatorId = member1Uin
let subType: GroupDecreaseSubType = 'leave'
if (notify.user2.uid) {
// 是被踢的
const member2Uin = await NTQQUserApi.getUinByUid(notify.user2.uid)
if (member2Uin) {
operatorId = member2Uin
}
subType = 'kick'
}
const groupDecreaseEvent = new OB11GroupDecreaseEvent(
parseInt(notify.group.groupCode),
parseInt(member1Uin),
parseInt(operatorId),
subType,
)
postOb11Event(groupDecreaseEvent, true)
}
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
}
if (+req.reqTime < startTime / 1000) {
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('process pid', process.pid) log('process pid', process.pid)
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
if (!config.enableLLOB) { if (!existsSync(TEMP_DIR)) {
llonebotError.otherError = 'LLOneBot 未启动' await mkdir(TEMP_DIR)
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
} }
if (!fs.existsSync(TEMP_DIR)) { const dbDir = path.join(DATA_DIR, 'database')
fs.mkdirSync(TEMP_DIR, { recursive: true }) if (!existsSync(dbDir)) {
await mkdir(dbDir)
} }
llonebotError.otherError = '' const ctx = new Context()
startTime = Date.now() ctx.plugin(Log, {
const WrapperSession = getSession() enable: config.log!,
if (WrapperSession) { filename: logFileName
NTEventDispatch.init({ ListenerMap: wrapperConstructor, WrapperSession }) })
} ctx.plugin(NTQQFileApi)
MessageUnique.init(uin) ctx.plugin(NTQQFileCacheApi)
ctx.plugin(NTQQFriendApi)
//log('start activate group member info') ctx.plugin(NTQQGroupApi)
// 下面两个会导致CPU占用过高QQ卡死 ctx.plugin(NTQQMsgApi)
// NTQQGroupApi.activateMemberInfoChange().then().catch(log) ctx.plugin(NTQQUserApi)
// NTQQGroupApi.activateMemberListChange().then().catch(log) ctx.plugin(NTQQWebApi)
startReceiveHook().then() ctx.plugin(NTQQWindowApi)
ctx.plugin(Core, config)
if (config.ob11.enableHttp) { ctx.plugin(Database)
ob11HTTPServer.start(config.ob11.httpPort) ctx.plugin(SQLiteDriver, {
} path: path.join(dbDir, `${selfInfo.uin}.db`)
if (config.ob11.enableWs) { })
ob11WebsocketServer.start(config.ob11.wsPort) ctx.plugin(Store, {
} msgCacheExpire: config.msgCacheExpire! * 1000
if (config.ob11.enableWsReverse) { })
ob11ReverseWebsockets.start() if (config.ob11.enable) {
} ctx.plugin(OneBot11Adapter, {
if (config.ob11.enableHttpHeart) { ...config.ob11,
httpHeart.start() heartInterval: config.heartInterval,
} token: config.token!,
debug: config.debug!,
log('LLOneBot start') reportSelfMessage: config.reportSelfMessage!,
} musicSignUrl: config.musicSignUrl,
enableLocalFile2Url: config.enableLocalFile2Url!,
const buildVersion = getBuildVersion() ffmpeg: config.ffmpeg,
const intervalId = setInterval(() => {
const current = getSelfInfo()
if (!current.uin) {
setSelfInfo({
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
nick: current.uin,
}) })
} }
if (current.uin && (buildVersion >= 27187 || getSession())) { if (config.satori.enable) {
ctx.plugin(SatoriAdapter, {
...config.satori,
ffmpeg: config.ffmpeg,
})
}
ctx.start()
llonebotError.otherError = ''
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llob/config-updated', config)
})
}
const intervalId = setInterval(() => {
const self = Object.assign(selfInfo, {
uin: globalThis.authData?.uin,
uid: globalThis.authData?.uid,
online: true
})
if (self.uin) {
clearInterval(intervalId) clearInterval(intervalId)
start(current.uid, current.uin) start()
} }
}, 600) }, 600)
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (window.id !== 2) { if (![2, 4, 6].includes(window.id)) {
return return
} }
if (window.id === 2) {
mainWindow = window mainWindow = window
log('window create', window.webContents.getURL().toString()) }
//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()
}

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

@@ -0,0 +1,154 @@
import { Peer, RawMessage } 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
}
class Store extends Service {
static inject = ['database', 'model']
private cache: LimitedHashTable<string, number>
private messages: Map<string, RawMessage>
constructor(protected ctx: Context, public config: Store.Config) {
super(ctx, 'store', true)
this.cache = new LimitedHashTable(1000)
this.messages = new Map()
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 {
// OneBot 11 要求 message_id 为 int32
const cacheKey = `${msgId}|${peer.chatType}|${peer.peerUid}`
const hash = createHash('md5').update(cacheKey).digest()
hash[0] &= 0x7f //保证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 })
}
async addMsgCache(msg: RawMessage) {
const expire = this.config.msgCacheExpire
if (expire === 0) {
return
}
const id = msg.msgId
this.messages.set(id, msg)
setTimeout(() => {
this.messages.delete(id)
}, expire)
}
getMsgCache(msgId: string) {
return this.messages.get(msgId)
}
}
namespace Store {
export interface Config {
/** 单位为毫秒 */
msgCacheExpire: number
}
}
export default Store

View File

@@ -5,7 +5,6 @@ import {
CacheFileListItem, CacheFileListItem,
CacheFileType, CacheFileType,
CacheScanResult, CacheScanResult,
ChatCacheList,
ChatCacheListItemBasic, ChatCacheListItemBasic,
ChatType, ChatType,
ElementType, ElementType,
@@ -14,63 +13,65 @@ 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/helper/rkey' import { RichMediaDownloadCompleteNotify, Peer } from '@/ntqqapi/types/msg'
import { getSession } from '@/ntqqapi/wrapper'
import { Peer } from '@/ntqqapi/types/msg'
import { calculateFileMD5 } from '@/common/utils/file' import { calculateFileMD5 } from '@/common/utils/file'
import { fileTypeFromFile } from 'file-type' import { copyFile, stat, unlink } from 'node:fs/promises'
import fsPromise from 'node:fs/promises'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { OnRichMediaDownloadCompleteParams } from '@/ntqqapi/listeners'
import { Time } from 'cosmokit' import { Time } from 'cosmokit'
import { Service, Context } from 'cordis'
export class NTQQFileApi { declare module 'cordis' {
/** 27187 TODO */ interface Context {
static async getVideoUrl(peer: Peer, msgId: string, elementId: string) { ntFileApi: NTQQFileApi
const session = getSession() ntFileCacheApi: NTQQFileCacheApi
return (await session?.getRichMediaService().getVideoPlayUrlV2(peer, }
}
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')
}
async getVideoUrl(peer: Peer, msgId: string, elementId: string): Promise<string | undefined> {
const data = await invoke('nodeIKernelRichMediaService/getVideoPlayUrlV2', [{
peer,
msgId, msgId,
elementId, elemId: elementId,
0, videoCodecFormat: 0,
{ downSourceType: 1, triggerType: 1 }))?.urlResult.domainUrl[0].url params: {
downSourceType: 1,
triggerType: 1
}
}])
if (data.result !== 0) {
this.ctx.logger.warn('getVideoUrl', data)
}
return data.urlResult.domainUrl[0]?.url
} }
static async getFileType(filePath: string) { async getFileType(filePath: string) {
return fileTypeFromFile(filePath) return await invoke<{
} ext: string
mime: string
// 上传文件到QQ的文件夹 }>(NTMethod.FILE_TYPE, [filePath], {
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType = 0) { className: NTClass.FS_API
const fileMd5 = await calculateFileMD5(filePath)
let ext = (await NTQQFileApi.getFileType(filePath))?.ext || ''
if (ext) {
ext = '.' + ext
}
let fileName = `${path.basename(filePath)}`
if (fileName.indexOf('.') === -1) {
fileName += ext
}
const session = getSession()
let mediaPath: string
if (session) {
mediaPath = session?.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName: fileName,
elementType: elementType,
elementSubType,
thumbSize: 0,
needCreate: true,
downloadType: 1,
file_uuid: ''
}) })
} else { }
mediaPath = await invoke<string>({
methodName: NTMethod.MEDIA_FILE_PATH, /** 上传文件到 QQ 的文件夹 */
args: [ async uploadFile(filePath: string, elementType = ElementType.Pic, elementSubType = 0) {
{ const fileMd5 = await calculateFileMD5(filePath)
let fileName = path.basename(filePath)
if (!fileName.includes('.')) {
const ext = (await this.getFileType(filePath))?.ext
fileName += ext ? '.' + ext : ''
}
const mediaPath = await invoke(NTMethod.MEDIA_FILE_PATH, [{
path_info: { path_info: {
md5HexStr: fileMd5, md5HexStr: fileMd5,
fileName: fileName, fileName: fileName,
@@ -81,88 +82,40 @@ export class NTQQFileApi {
downloadType: 1, downloadType: 1,
file_uuid: '', file_uuid: '',
}, },
}, }])
], await copyFile(filePath, mediaPath)
}) 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,
elementId: string, elementId: string,
thumbPath: string, thumbPath = '',
sourcePath: string, sourcePath = '',
timeout = 1000 * 60 * 2, timeout = 1000 * 60 * 2,
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 { } } catch { }
} else { } else {
return sourcePath return sourcePath
} }
} }
let filePath: string const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
if (NTEventDispatch.initialised) { 'nodeIKernelMsgService/downloadRichMedia',
const data = await NTEventDispatch.CallNormalEvent< [{
(
params: {
fileModelId: string,
downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) {
return true
}
return false
},
{
fileModelId: '0',
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
chatType: chatType,
peerUid: peerUid,
elementId: elementId,
thumbSize: 0,
downloadType: 1,
filePath: thumbPath
}
)
filePath = data[1].filePath
} else {
const data = await invoke<{ notifyInfo: OnRichMediaDownloadCompleteParams }>({
methodName: NTMethod.DOWNLOAD_MEDIA,
args: [
{
getReq: { getReq: {
fileModelId: '0', fileModelId: '0',
downloadSourceType: 0, downloadSourceType: 0,
@@ -175,32 +128,31 @@ export class NTQQFileApi {
downloadType: 1, downloadType: 1,
filePath: thumbPath, filePath: thumbPath,
}, },
}, }],
null, {
],
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.msgId === msgId, cmdCB: payload => payload.notifyInfo.msgId === msgId,
timeout timeout
})
filePath = data.notifyInfo.filePath
} }
if (filePath.startsWith('\\')) { )
const downloadPath = TEMP_DIR return data.notifyInfo.filePath
filePath = path.join(downloadPath, filePath)
// 下载路径是下载文件夹的相对路径
}
return filePath
} }
static async getImageSize(filePath: string) { async getImageSize(filePath: string) {
return await invoke<{ width: number; height: number }>({ return await invoke<{
width: number
height: number
type: string
}>(
NTMethod.IMAGE_SIZE,
[filePath],
{
className: NTClass.FS_API, className: NTClass.FS_API,
methodName: NTMethod.IMAGE_SIZE, }
args: [filePath], )
})
} }
static async getImageUrl(element: PicElement) { async getImageUrl(element: PicElement) {
if (!element) { if (!element) {
return '' return ''
} }
@@ -209,17 +161,17 @@ export class NTQQFileApi {
const fileMd5 = element.md5HexStr const fileMd5 = element.md5HexStr
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 isNTPic = imageAppid && ['1406', '1407'].includes(imageAppid)
if (isNewPic) { if (isNTPic) {
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
@@ -228,134 +180,74 @@ 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 ''
} }
async downloadFileForModelId(peer: Peer, fileModelId: string, timeout = 2 * Time.minute) {
const data = await invoke<{ notifyInfo: RichMediaDownloadCompleteNotify }>(
'nodeIKernelRichMediaService/downloadFileForModelId',
[{
peer,
fileModelIdList: [fileModelId],
save_path: ''
}],
{
cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: payload => payload.notifyInfo.fileModelId === fileModelId,
timeout,
afterFirstCmd: false
}
)
return data.notifyInfo.filePath
}
} }
export class NTQQFileCacheApi { export class NTQQFileCacheApi extends Service {
static async setCacheSilentScan(isSilent: boolean = true) { constructor(protected ctx: Context) {
return await invoke<GeneralCallResult>({ super(ctx, 'ntFileCacheApi', true)
methodName: NTMethod.CACHE_SET_SILENCE,
args: [
{
isSilent,
},
null,
],
})
} }
static getCacheSessionPathList() { async setCacheSilentScan(isSilent: boolean = true) {
return invoke< return await invoke<GeneralCallResult>(NTMethod.CACHE_SET_SILENCE, [{ isSilent }])
{ }
getCacheSessionPathList() {
return invoke<Array<{
key: string key: string
value: string value: string
}[] }>>(NTMethod.CACHE_PATH_SESSION, [], { className: NTClass.OS_API })
>({
className: NTClass.OS_API,
methodName: NTMethod.CACHE_PATH_SESSION,
})
} }
static clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) { scanCache() {
return invoke<any>({ invoke<GeneralCallResult>(ReceiveCmdS.CACHE_SCAN_FINISH, [], { registerEvent: true })
// TODO: 目前还不知道真正的返回值是什么 return invoke<CacheScanResult>(NTMethod.CACHE_SCAN, [], { timeout: 300 * Time.second })
methodName: NTMethod.CACHE_CLEAR,
args: [
{
keys: cacheKeys,
},
null,
],
})
} }
static addCacheScannedPaths(pathMap: object = {}) { getHotUpdateCachePath() {
return invoke<GeneralCallResult>({ return invoke<string>(NTMethod.CACHE_PATH_HOT_UPDATE, [], { className: NTClass.HOTUPDATE_API })
methodName: NTMethod.CACHE_ADD_SCANNED_PATH,
args: [
{
pathMap: { ...pathMap },
},
null,
],
})
} }
static scanCache() { getDesktopTmpPath() {
invoke<GeneralCallResult>({ return invoke<string>(NTMethod.CACHE_PATH_DESKTOP_TEMP, [], { className: NTClass.BUSINESS_API })
methodName: ReceiveCmdS.CACHE_SCAN_FINISH,
classNameIsRegister: true,
}).then()
return invoke<CacheScanResult>({
methodName: NTMethod.CACHE_SCAN,
args: [null, null],
timeout: 300 * Time.second,
})
} }
static getHotUpdateCachePath() { getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
return invoke<string>({
className: NTClass.HOTUPDATE_API,
methodName: NTMethod.CACHE_PATH_HOT_UPDATE,
})
}
static getDesktopTmpPath() {
return invoke<string>({
className: NTClass.BUSINESS_API,
methodName: NTMethod.CACHE_PATH_DESKTOP_TEMP,
})
}
static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return new Promise<ChatCacheList>((res, rej) => {
invoke<ChatCacheList>({
methodName: NTMethod.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 invoke<CacheFileList>({ return invoke<CacheFileList>(NTMethod.CACHE_FILE_GET, [{
methodName: NTMethod.CACHE_FILE_GET,
args: [
{
fileType: fileType, fileType: fileType,
restart: true, restart: true,
pageSize: pageSize, pageSize: pageSize,
order: 1, order: 1,
lastRecord: _lastRecord, lastRecord: _lastRecord,
}, }])
null,
],
})
} }
static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return await invoke<GeneralCallResult>({ return await invoke<GeneralCallResult>(NTMethod.CACHE_CHAT_CLEAR, [{
methodName: NTMethod.CACHE_CHAT_CLEAR,
args: [
{
chats, chats,
fileKeys, fileKeys,
}, }])
null,
],
})
} }
} }

View File

@@ -1,15 +1,21 @@
import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types' import { Friend, SimpleInfo, CategoryFriend } from '../types'
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { invoke, NTMethod, NTClass } from '../ntcall' import { invoke, NTMethod, NTClass } from '../ntcall'
import { getSession } from '@/ntqqapi/wrapper' import { Service, Context } from 'cordis'
import { BuddyListReqType, NodeIKernelProfileService } from '../services'
import { NTEventDispatch } from '@/common/utils/EventTask' declare module 'cordis' {
import { LimitedHashTable } from '@/common/utils/table' interface Context {
import { pick } from 'cosmokit' ntFriendApi: NTQQFriendApi
}
}
export class NTQQFriendApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntFriendApi', true)
}
export class NTQQFriendApi {
/** 大于或等于 26702 应使用 getBuddyV2 */ /** 大于或等于 26702 应使用 getBuddyV2 */
static async getFriends(forced = false) { async getFriends() {
const data = await invoke<{ const data = await invoke<{
data: { data: {
categoryId: number categoryId: number
@@ -17,11 +23,10 @@ export class NTQQFriendApi {
categroyMbCount: number categroyMbCount: number
buddyList: Friend[] buddyList: Friend[]
}[] }[]
}>({ }>('getBuddyList', [], {
className: NTClass.NODE_STORE_API, className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false, afterFirstCmd: false
}) })
const _friends: Friend[] = [] const _friends: Friend[] = []
for (const item of data.data) { for (const item of data.data) {
@@ -30,158 +35,95 @@ export class NTQQFriendApi {
return _friends return _friends
} }
static async handleFriendRequest(flag: string, accept: boolean) { async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
const data = flag.split('|') return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
if (data.length < 2) {
return
}
const friendUid = data[0]
const reqTime = data[1]
const session = getSession()
if (session) {
return session.getBuddyService().approvalFriendRequest({
friendUid,
reqTime,
accept
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_FRIEND_REQUEST,
args: [
{
approvalInfo: { approvalInfo: {
friendUid, friendUid,
reqTime, reqTime,
accept, accept,
}, },
}, }])
],
})
}
} }
static async getBuddyV2(refresh = false): Promise<FriendV2[]> { async getBuddyV2(refresh = false): Promise<SimpleInfo[]> {
const session = getSession()
if (session) {
const uids: string[] = []
const buddyService = session.getBuddyService()
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data.values())
} else {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
}>({ }>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API, className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false, afterFirstCmd: false,
})
const categoryUids: Map<number, string[]> = new Map()
for (const item of data.buddyCategory) {
categoryUids.set(item.categoryId, item.buddyUids)
}
return Object.values(data.userSimpleInfos).filter(v => v.baseInfo && categoryUids.get(v.baseInfo.categoryId)?.includes(v.uid!))
} }
)
const uids = data.buddyCategory.flatMap(item => item.buddyUids)
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
} }
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> { /** uid => uin */
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000) async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const session = getSession() const retMap: Map<string, string> = new Map()
if (session) {
const uids: string[] = []
const buddyService = session?.getBuddyService()
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids))
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
data.forEach((value, key) => {
retMap.set(value.uin!, value.uid!)
})
} else {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
}>({ }>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API, className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false, afterFirstCmd: false,
}) }
)
for (const item of Object.values(data.userSimpleInfos)) { for (const item of Object.values(data.userSimpleInfos)) {
retMap.set(item.uin!, item.uid!) if (retMap.size > 5000) {
break
} }
retMap.set(item.uid!, item.uin!)
} }
return retMap return retMap
} }
static async getBuddyV2ExWithCate(refresh = false) { async getBuddyV2WithCate(refresh = false) {
const session = getSession()
if (session) {
const uids: string[] = []
const categoryMap: Map<string, any> = new Map()
const buddyService = session.getBuddyService()
const buddyListV2 = (await buddyService?.getBuddyListV2('0', BuddyListReqType.KNOMAL))?.data
uids.push(
...buddyListV2?.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categroyName: item.categroyName })
})
return item.buddyUids
})!)
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
} else {
const data = await invoke<{ const data = await invoke<{
buddyCategory: CategoryFriend[] buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo> userSimpleInfos: Record<string, SimpleInfo>
}>({ }>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API, className: NTClass.NODE_STORE_API,
methodName: 'getBuddyList',
args: [refresh],
cbCmd: ReceiveCmdS.FRIENDS, cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false, 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 data
} }
static async isBuddy(uid: string): Promise<boolean> { async isBuddy(uid: string): Promise<boolean> {
const session = getSession() return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }])
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke<boolean>({
methodName: 'nodeIKernelBuddyService/isBuddy',
args: [
{ uid },
null,
],
})
} }
async getBuddyRecommendContact(uin: string) {
const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }])
return ret.arkMsg
}
async setBuddyRemark(uid: string, remark: string) {
return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{
remarkParams: { uid, remark }
}])
}
async delBuddy(friendUid: string) {
return await invoke('nodeIKernelBuddyService/delBuddy', [{
delInfo: {
friendUid,
tempBlock: false,
tempBothDel: true
}
}])
} }
} }

View File

@@ -1,145 +1,112 @@
import { ReceiveCmdS } from '../hook' import { ReceiveCmdS } from '../hook'
import { Group, GroupMember, GroupMemberRole, GroupNotifies, GroupRequestOperateTypes, GroupNotify } from '../types' import {
Group,
GroupMember,
GroupMemberRole,
GroupNotifies,
GroupRequestOperateTypes,
GetFileListParam,
PublishGroupBulletinReq,
GroupAllInfo,
GroupFileInfo,
GroupBulletinListResult
} from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { NTQQWindowApi, NTQQWindows } from './window' import { NTQQWindows } from './window'
import { getSession } from '../wrapper' import { Service, Context } from 'cordis'
import { NTEventDispatch } from '@/common/utils/EventTask'
import { NodeIKernelGroupListener } from '../listeners'
import { NodeIKernelGroupService } from '../services'
export class NTQQGroupApi { declare module 'cordis' {
static async getGroups(forced = false): Promise<Group[]> { interface Context {
if (NTEventDispatch.initialised) { ntGroupApi: NTQQGroupApi
type ListenerType = NodeIKernelGroupListener['onGroupListUpdate'] }
const [, , groupList] = await NTEventDispatch.CallNormalEvent }
<(force: boolean) => Promise<any>, ListenerType>
( export class NTQQGroupApi extends Service {
'NodeIKernelGroupService/getGroupList', static inject = ['ntWindowApi']
'NodeIKernelGroupListener/onGroupListUpdate',
1, constructor(protected ctx: Context) {
5000, super(ctx, 'ntGroupApi', true)
() => true, }
forced
) async getGroups(): Promise<Group[]> {
return groupList
} else {
const result = await invoke<{ const result = await invoke<{
updateType: number updateType: number
groupList: Group[] groupList: Group[]
}>({ }>(
'getGroupList',
[],
{
className: NTClass.NODE_STORE_API, className: NTClass.NODE_STORE_API,
methodName: 'getGroupList',
cbCmd: ReceiveCmdS.GROUPS_STORE, cbCmd: ReceiveCmdS.GROUPS_STORE,
afterFirstCmd: false, afterFirstCmd: false,
}) }
)
return result.groupList return result.groupList
} }
async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
const data = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }])
if (data.errCode !== 0) {
throw new Error('获取群成员列表出错,' + data.errMsg)
}
return data.result.infos
} }
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { async getGroupMember(groupCode: string, uid: string, forceUpdate = false) {
const session = getSession() invoke('nodeIKernelGroupListener/onMemberInfoChange', [], {
let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>> registerEvent: true
if (session) {
const groupService = session.getGroupService()
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow')
result = await groupService.getNextMemberList(sceneId, undefined, num)
} else {
const sceneId = await invoke<string>({
methodName: NTMethod.GROUP_MEMBER_SCENE,
args: [
{
groupCode: groupQQ,
scene: 'groupMemberList_MainWindow',
},
],
}) })
result = await invoke<
ReturnType<NodeIKernelGroupService['getNextMemberList']> const data = await invoke<{
>({ groupCode: string
methodName: NTMethod.GROUP_MEMBERS, members: Map<string, GroupMember>
args: [ }>(
'nodeIKernelGroupService/getMemberInfo',
[{
groupCode,
uids: [uid],
forceUpdate
}],
{ {
sceneId, cbCmd: 'nodeIKernelGroupListener/onMemberInfoChange',
num, afterFirstCmd: false,
}, cmdCB: payload => payload.members.has(uid),
null, timeout: 2000
],
})
} }
if (result.errCode !== 0) { )
throw ('获取群成员列表出错,' + result.errMsg) return data.members.get(uid)!
}
return result.result.infos
} }
static async getGroupIgnoreNotifies() { async getGroupIgnoreNotifies() {
await NTQQGroupApi.getSingleScreenNotifies(14) await this.getSingleScreenNotifies(14)
return await NTQQWindowApi.openWindow<GeneralCallResult & GroupNotifies>( return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
NTQQWindows.GroupNotifyFilterWindow, NTQQWindows.GroupNotifyFilterWindow,
[], [],
ReceiveCmdS.GROUP_NOTIFY, ReceiveCmdS.GROUP_NOTIFY,
) )
} }
static async getSingleScreenNotifies(num: number) { async getSingleScreenNotifies(number: number, startSeq = '') {
if (NTEventDispatch.initialised) { invoke(ReceiveCmdS.GROUP_NOTIFY, [], { registerEvent: true })
const [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
<(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void> return (await invoke<GroupNotifies>(
( 'nodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupService/getSingleScreenNotifies', [{ doubt: false, startSeq, number }],
'NodeIKernelGroupListener/onGroupSingleScreenNotifies', {
1,
5000,
() => true,
false,
'',
num,
)
return notifies
} else {
invoke({
methodName: ReceiveCmdS.GROUP_NOTIFY,
classNameIsRegister: true,
})
return (await invoke<GroupNotifies>({
methodName: NTMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmdS.GROUP_NOTIFY, cbCmd: ReceiveCmdS.GROUP_NOTIFY,
afterFirstCmd: false, afterFirstCmd: false,
args: [{ doubt: false, startSeq: '', number: num }, null],
})).notifies
} }
)).notifies
} }
/** 27187 TODO */ async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
static async delGroupFile(groupCode: string, files: string[]) {
const session = getSession()
return session?.getRichMediaService().deleteGroupFile(groupCode, [102], files)
}
static 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() return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
if (session) {
return session.getGroupService().operateSysNotify(
false,
{
'operateType': operateType, // 2 拒绝
'targetMsg': {
'seq': seq, // 通知序列号
'type': type,
'groupCode': groupCode,
'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}
})
} else {
return await invoke({
methodName: NTMethod.HANDLE_GROUP_REQUEST,
args: [
{
doubt: false, doubt: false,
operateMsg: { operateMsg: {
operateType, operateType,
@@ -150,190 +117,183 @@ export class NTQQGroupApi {
postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格 postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
}, },
}, },
}, }])
null,
],
})
}
} }
static async quitGroup(groupQQ: string) { async quitGroup(groupCode: string) {
const session = getSession() return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }])
if (session) {
return session.getGroupService().quitGroup(groupQQ)
} else {
return await invoke({
methodName: NTMethod.QUIT_GROUP,
args: [{ groupCode: groupQQ }, null],
})
}
} }
static async kickMember( async kickMember(groupCode: string, kickUids: string[], refuseForever = false, kickReason = '') {
groupQQ: string, return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
kickUids: string[], }
refuseForever = false,
kickReason = '', /** timeStamp为秒数, 0为解除禁言 */
) { async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
const session = getSession() return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
if (session) { }
return session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason)
} else { async banGroup(groupCode: string, shutUp: boolean) {
return await invoke({ return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }])
methodName: NTMethod.KICK_MEMBER, }
args: [
async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }])
}
async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) {
return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }])
}
async setGroupName(groupCode: string, groupName: string) {
return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }])
}
async getGroupRemainAtTimes(groupCode: string) {
return await invoke(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }])
}
async removeGroupEssence(groupCode: string, msgId: string) {
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)
}
}])
}
async addGroupEssence(groupCode: string, msgId: string) {
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)
}
}])
}
async createGroupFileFolder(groupId: string, folderName: string) {
return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }])
}
async deleteGroupFileFolder(groupId: string, folderId: string) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }])
}
async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }])
}
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { registerEvent: true })
const data = await invoke<{ fileInfo: GroupFileInfo }>(
'nodeIKernelRichMediaService/getGroupFileList',
[{
groupId,
fileListForm
}],
{ {
groupCode: groupQQ, cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
kickUids, afterFirstCmd: false,
refuseForever, cmdCB: (payload, result) => payload.fileInfo.reqId === result
kickReason,
},
],
})
} }
)
return data.fileInfo
} }
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
// timeStamp为秒数, 0为解除禁言 const ntUserApi = this.ctx.get('ntUserApi')!
const session = getSession() const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
if (session) { return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }])
return session.getGroupService().setMemberShutUp(groupQQ, memList)
} else {
return await invoke({
methodName: NTMethod.MUTE_MEMBER,
args: [
{
groupCode: groupQQ,
memList,
},
],
})
}
} }
static async banGroup(groupQQ: string, shutUp: boolean) { async uploadGroupBulletinPic(groupCode: string, path: string) {
const session = getSession() const ntUserApi = this.ctx.get('ntUserApi')!
if (session) { const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return session.getGroupService().setGroupShutUp(groupQQ, shutUp) return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }])
} else {
return await invoke({
methodName: NTMethod.MUTE_GROUP,
args: [
{
groupCode: groupQQ,
shutUp,
},
null,
],
})
}
} }
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { async getGroupRecommendContact(groupCode: string) {
const session = getSession() const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }])
if (session) { return ret.arkJson
return session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName)
} else {
return await invoke({
methodName: NTMethod.SET_MEMBER_CARD,
args: [
{
groupCode: groupQQ,
uid: memberUid,
cardName,
},
null,
],
})
}
} }
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { async queryCachedEssenceMsg(groupCode: string, msgSeq = '0', msgRandom = '0') {
const session = getSession() return await invoke('nodeIKernelGroupService/queryCachedEssenceMsg', [{
if (session) { key: {
return session.getGroupService().modifyMemberRole(groupQQ, memberUid, role)
} else {
return await invoke({
methodName: NTMethod.SET_MEMBER_ROLE,
args: [
{
groupCode: groupQQ,
uid: memberUid,
role,
},
null,
],
})
}
}
static async setGroupName(groupQQ: string, groupName: string) {
const session = getSession()
if (session) {
return session.getGroupService().modifyGroupName(groupQQ, groupName, false)
} else {
return await invoke({
methodName: NTMethod.SET_GROUP_NAME,
args: [
{
groupCode: groupQQ,
groupName,
},
null,
],
})
}
}
static async getGroupAtAllRemainCount(groupCode: string) {
return await invoke<
GeneralCallResult & {
atInfo: {
canAtAll: boolean
RemainAtAllCountForUin: number
RemainAtAllCountForGroup: number
atTimesMsg: string
canNotAtAllMsg: ''
}
}
>({
methodName: NTMethod.GROUP_AT_ALL_REMAIN_COUNT,
args: [
{
groupCode, groupCode,
}, msgSeq: +msgSeq,
null, msgRandom: +msgRandom
], }
}])
}
async getGroupHonorList(groupCode: string) {
// 还缺点东西
return await invoke('nodeIKernelGroupService/getGroupHonorList', [{
req: {
groupCode: [+groupCode]
}
}])
}
async getGroupAllInfo(groupCode: string) {
invoke('nodeIKernelGroupListener/onGroupAllInfoChange', [], {
registerEvent: true
}) })
return await invoke<{ groupAll: GroupAllInfo }>(
'nodeIKernelGroupService/getGroupAllInfo',
[{
groupCode,
source: 4
}],
{
cbCmd: 'nodeIKernelGroupListener/onGroupAllInfoChange',
afterFirstCmd: false,
cmdCB: payload => payload.groupAll.groupCode === groupCode
}
)
} }
/** 27187 TODO */ async getGroupBulletinList(groupCode: string) {
static async removeGroupEssence(GroupCode: string, msgId: string) { invoke('nodeIKernelGroupListener/onGetGroupBulletinListResult', [], {
const session = getSession() registerEvent: true
// 代码没测过 })
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false) const ntUserApi = this.ctx.get('ntUserApi')!
let param = { const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
groupCode: GroupCode, return await invoke<{
msgRandom: parseInt(MsgData?.msgList[0].msgRandom!), groupCode: string
msgSeq: parseInt(MsgData?.msgList[0].msgSeq!) context: string
result: GroupBulletinListResult
}>(
'nodeIKernelGroupService/getGroupBulletinList',
[{
groupCode,
psKey,
context: '',
req: {
startIndex: -1,
num: 20,
needInstructionsForJoinGroup: 1,
needPublisherInfo: 1
} }
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数 }],
return session?.getGroupService().removeGroupEssence(param) {
cbCmd: 'nodeIKernelGroupListener/onGetGroupBulletinListResult',
cmdCB: payload => payload.groupCode === groupCode,
afterFirstCmd: false
}
)
} }
/** 27187 TODO */ async setGroupAvatar(groupCode: string, path: string) {
static async addGroupEssence(GroupCode: string, msgId: string) { return await invoke('nodeIKernelGroupService/setHeader', [{ path, groupCode }])
const session = getSession()
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await session?.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false)
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData?.msgList[0].msgRandom!),
msgSeq: parseInt(MsgData?.msgList[0].msgSeq!)
}
// GetMsgByShoretID(ShoretID) -> MsgService.getMsgs(Peer,MsgId,1,false) -> 组出参数
return session?.getGroupService().addGroupEssence(param)
} }
} }

View File

@@ -1,372 +1,266 @@
import { invoke, NTMethod } from '../ntcall' import { invoke, NTMethod } from '../ntcall'
import { GeneralCallResult, TmpChatInfoApi } from '../services' import { RawMessage, SendMessageElement, Peer, ChatType } from '../types'
import { RawMessage, SendMessageElement, Peer, ChatType2 } from '../types' import { Service, Context } from 'cordis'
import { getSelfNick, getSelfUid } from '../../common/data' import { selfInfo } from '@/common/globalVars'
import { getSession } from '@/ntqqapi/wrapper'
import { NTEventDispatch } from '@/common/utils/EventTask'
function generateMsgId() { declare module 'cordis' {
const timestamp = Math.floor(Date.now() / 1000) interface Context {
const random = Math.floor(Math.random() * Math.pow(2, 32)) ntMsgApi: NTQQMsgApi
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 { export class NTQQMsgApi extends Service {
static async getTempChatInfo(chatType: ChatType2, peerUid: string) { static inject = ['ntUserApi']
const session = getSession()
if (session) { constructor(protected ctx: Context) {
return session.getMsgService().getTempChatInfo(chatType, peerUid) super(ctx, 'ntMsgApi', true)
} else {
return await invoke<TmpChatInfoApi>({
methodName: 'nodeIKernelMsgService/getTempChatInfo',
args: [
{
chatType,
peerUid,
},
null,
],
})
}
} }
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { async getTempChatInfo(chatType: ChatType, peerUid: string) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览 return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }])
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid }
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean) {
// nt_qq/global/nt_data/Emoji/emoji-resource/sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq/global/nt_data/Emoji/emoji-resource/face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString() const emojiType = emojiId.length > 3 ? '2' : '1'
const session = getSession() return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }])
if (session) {
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set)
} else {
return await invoke({
methodName: NTMethod.EMOJI_LIKE,
args: [
{
peer,
msgSeq,
emojiId,
emojiType: emojiId.length > 3 ? '2' : '1',
setEmoji: set,
},
null,
],
})
}
} }
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
const session = getSession() return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }])
if (session) {
return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.GET_MULTI_MSG,
args: [
{
peer,
rootMsgId,
parentMsgId,
},
null,
],
})
}
} }
static async activateChat(peer: Peer) { async activateChat(peer: Peer) {
return await invoke<GeneralCallResult>({ return await invoke(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }])
methodName: NTMethod.ACTIVE_CHAT_PREVIEW,
args: [{ peer, cnt: 20 }, null],
})
} }
static async activateChatAndGetHistory(peer: Peer) { async activateChatAndGetHistory(peer: Peer, cnt: number) {
return await invoke<GeneralCallResult>({ // 消息从旧到新
methodName: NTMethod.ACTIVE_CHAT_HISTORY, return await invoke(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt, msgId: '0', queryOrder: true }])
// 参数似乎不是这样
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 }])
}
async getMsgsByMsgId(peer: Peer, msgIds: string[]) {
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() return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }])
if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsByMsgId',
args: [
{
peer,
msgIds,
},
null,
],
})
}
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) { async getMsgHistory(peer: Peer, msgId: string, cnt: number, queryOrder = false) {
const session = getSession() // 默认情况下消息时间从旧到新
// 消息时间从旧到新 return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder }])
if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder)
} else {
return await invoke<GeneralCallResult & { msgList: RawMessage[] }>({
methodName: NTMethod.HISTORY_MSG,
args: [
{
peer,
msgId,
cnt: count,
queryOrder: isReverseOrder,
},
null,
],
})
}
} }
static async recallMsg(peer: Peer, msgIds: string[]) { async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession() return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }])
if (session) {
return session.getMsgService().recallMsg({
chatType: peer.chatType,
peerUid: peer.peerUid
}, msgIds)
} else {
return await invoke({
methodName: NTMethod.RECALL_MSG,
args: [
{
peer,
msgIds,
},
null,
],
})
}
} }
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) { async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
const msgId = generateMsgId() const uniqueId = await this.generateMsgUniqueId(peer.chatType)
peer.guildId = msgId peer.guildId = uniqueId
let msgList: RawMessage[] const data = await invoke<{ msgList: RawMessage[] }>(
if (NTEventDispatch.initialised) { 'nodeIKernelMsgService/sendMsg',
const 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 (const msgRecord of msgRecords) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
'0',
peer,
msgElements,
new Map()
)
msgList = data[1]
} else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/sendMsg',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
}
}
return false
},
args: [
{
msgId: '0', msgId: '0',
peer, peer,
msgElements, msgElements,
msgAttributeInfos: new Map() msgAttributeInfos: new Map()
}, }],
null
],
timeout
})
msgList = data.msgList
}
const retMsg = msgList.find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true
}
})
return retMsg!
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const session = getSession()
if (session) {
return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} else {
return await invoke<GeneralCallResult>({
methodName: NTMethod.FORWARD_MSG,
args: [
{ {
msgIds: msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
},
null,
],
})
}
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await getSelfNick()
const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
})
const selfUid = getSelfUid()
let msgList: RawMessage[]
if (NTEventDispatch.initialised) {
const data = await NTEventDispatch.CallNormalEvent<
(msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/multiForwardMsgWithComment',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
},
msgInfos,
srcPeer,
destPeer,
[],
new Map()
)
msgList = data[1]
} else {
const data = await invoke<{ msgList: RawMessage[] }>({
methodName: 'nodeIKernelMsgService/multiForwardMsgWithComment',
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate', cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: payload => { cmdCB: payload => {
for (const msgRecord of payload.msgList) { for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) { if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
return true return true
} }
} }
return false return false
}, },
args: [ timeout
}
)
delete peer.guildId
return data.msgList.find(msgRecord => msgRecord.guildId === uniqueId)
}
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
const uniqueId = await this.generateMsgUniqueId(destPeer.chatType)
destPeer.guildId = uniqueId
const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/forwardMsg',
[{
msgIds,
srcContact: srcPeer,
dstContacts: [destPeer],
commentElements: [],
msgAttributeInfos: new Map(),
}],
{ {
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === uniqueId && msgRecord.sendStatus === 2) {
return true
}
}
return false
}
}
)
delete destPeer.guildId
return data.msgList.filter(msgRecord => msgRecord.guildId === uniqueId)
}
async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const senderShowName = await this.ctx.ntUserApi.getSelfNick(true)
const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName }
})
const selfUid = selfInfo.uid
const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/multiForwardMsgWithComment',
[{
msgInfos, msgInfos,
srcContact: srcPeer, srcContact: srcPeer,
dstContact: destPeer, dstContact: destPeer,
commentElements: [], commentElements: [],
msgAttributeInfos: new Map(), msgAttributeInfos: new Map(),
}, }],
null, {
], cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
}) afterFirstCmd: false,
msgList = data.msgList cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid === destPeer.peerUid && msgRecord.senderUid === selfUid) {
return true
} }
for (const msg of msgList) { }
return false
}
}
)
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
} }
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) { if (msg.peerUid === destPeer.peerUid && msg.senderUid === selfUid) {
return msg return msg
} }
} }
throw new Error('转发消息超时') throw new Error('转发消息超时')
} }
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { async getSingleMsg(peer: Peer, msgSeq: string) {
const session = getSession() return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }])
if (session) {
return await session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z)
} else {
return await invoke<GeneralCallResult & {
msgList: RawMessage[]
}>({
methodName: 'nodeIKernelMsgService/getMsgsBySeqAndCount',
args: [
{
peer,
cnt: count,
msgSeq: seq,
queryOrder: desc
},
null,
],
})
}
} }
/** 27187 TODO */ async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
static async getLastestMsgByUids(peer: Peer, count = 20, isReverseOrder = false) { return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
const session = getSession() msgId: '0',
const ret = await session?.getMsgService().queryMsgsWithFilterEx('0', '0', '0', { msgTime: '0',
msgSeq,
params: {
chatInfo: peer, chatInfo: peer,
filterMsgType: [], filterMsgType: [],
filterSendersUid: [], filterSendersUid: [],
filterMsgToTime: '0', filterMsgToTime: '0',
filterMsgFromTime: '0', filterMsgFromTime: '0',
isReverseOrder: isReverseOrder, //此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息 isReverseOrder: true,
isIncludeCurrent: true, isIncludeCurrent: true,
pageLimit: count, pageLimit: 1,
}) }
return ret }])
} }
static async getSingleMsg(peer: Peer, seq: string) { async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) {
const session = getSession() return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
if (session) { msgId: '0',
return await session.getMsgService().getSingleMsg(peer, seq) msgTime: '0',
} else { msgSeq,
return await invoke<GeneralCallResult & { params: {
msgList: RawMessage[] chatInfo: peer,
}>({ filterMsgType: [],
methodName: 'nodeIKernelMsgService/getSingleMsg', filterSendersUid,
args: [ filterMsgToTime: filterMsgTime,
{ filterMsgFromTime: filterMsgTime,
peer, isReverseOrder: true,
msgSeq: seq, isIncludeCurrent: true,
}, pageLimit: 1,
null,
],
})
} }
}])
}
async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }])
}
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
return await invoke('nodeIKernelMsgService/getMsgEmojiLikesList', [{
peer,
msgSeq,
emojiId,
emojiType,
cnt: count
}])
}
async fetchFavEmojiList(count: number) {
return await invoke('nodeIKernelMsgService/fetchFavEmojiList', [{
resId: '',
count,
backwardFetch: true,
forceRefresh: true
}])
}
async generateMsgUniqueId(chatType: number) {
const time = await this.getServerTime()
const uniqueId = await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType, time }])
if (typeof uniqueId === 'string') {
return uniqueId
} else {
const random = Math.trunc(Math.random() * 100)
return `${Date.now()}${random}`
}
}
async queryMsgsById(chatType: ChatType, msgId: string) {
const msgTime = this.getMsgTimeFromId(msgId)
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId,
msgTime: '0',
msgSeq: '0',
params: {
chatInfo: {
peerUid: '',
chatType
},
filterMsgToTime: msgTime,
filterMsgFromTime: msgTime,
isIncludeCurrent: true,
pageLimit: 1,
}
}])
}
getMsgTimeFromId(msgId: string) {
// 小概率相差1毫秒
return String(BigInt(msgId) >> 32n)
}
async getServerTime() {
return await invoke('nodeIKernelMSFService/getServerTime', [])
} }
} }

View File

@@ -1,67 +1,50 @@
import { invoke, NTMethod } from '../ntcall' import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfo, UserDetailSource, ProfileBizType, SimpleInfo } from '../types'
import { GeneralCallResult } from '../services' import { invoke } from '../ntcall'
import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types' import { getBuildVersion } from '@/common/utils'
import { groupMembers, getSelfUin } from '@/common/data'
import { CacheClassFuncAsync, getBuildVersion } from '@/common/utils'
import { getSession } from '@/ntqqapi/wrapper'
import { RequestUtil } from '@/common/utils/request' import { RequestUtil } from '@/common/utils/request'
import { NodeIKernelProfileService, UserDetailSource, ProfileBizType, forceFetchClientKeyRetType } from '../services' import { isNullable, pick, Time } from 'cosmokit'
import { NodeIKernelProfileListener } from '../listeners' import { Service, Context } from 'cordis'
import { NTEventDispatch } from '@/common/utils/EventTask' import { selfInfo } from '@/common/globalVars'
import { NTQQFriendApi } from './friend'
import { Time } from 'cosmokit'
export class NTQQUserApi { declare module 'cordis' {
static async setQQAvatar(filePath: string) { interface Context {
return await invoke<GeneralCallResult>({ ntUserApi: NTQQUserApi
methodName: NTMethod.SET_QQ_AVATAR, }
args: [ }
{
path: filePath, export class NTQQUserApi extends Service {
}, static inject = ['ntFriendApi', 'ntGroupApi']
null,
], constructor(protected ctx: Context) {
timeout: 10 * Time.second, // 10秒不一定够 super(ctx, 'ntUserApi', true)
})
} }
static async fetchUserDetailInfo(uid: string) { async setSelfAvatar(path: string) {
let info: UserDetailInfoListenerArg return await invoke(
if (NTEventDispatch.initialised) { 'nodeIKernelProfileService/setHeader',
type EventService = NodeIKernelProfileService['fetchUserDetailInfo'] [{ path }],
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged']
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
1,
5000,
(profile) => profile.uid === uid,
'BuddyProfileStore',
[uid],
UserDetailSource.KSERVER,
[ProfileBizType.KALL]
)
info = profile
} else {
const result = await invoke<{ info: UserDetailInfoListenerArg }>({
methodName: 'nodeIKernelProfileService/fetchUserDetailInfo',
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
args: [
{ {
timeout: 10 * Time.second // 10秒不一定够
}
)
}
async fetchUserDetailInfo(uid: string) {
const result = await invoke<{ info: UserDetailInfo }>(
'nodeIKernelProfileService/fetchUserDetailInfo',
[{
callFrom: 'BuddyProfileStore', callFrom: 'BuddyProfileStore',
uid: [uid], uid: [uid],
source: UserDetailSource.KSERVER, source: UserDetailSource.KSERVER,
bizList: [ProfileBizType.KALL] bizList: [ProfileBizType.KALL]
}, }],
null {
], cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
}) afterFirstCmd: false,
info = result.info cmdCB: payload => payload.info.uid === uid,
} }
)
const { info } = result
const ret: User = { const ret: User = {
...info.simpleInfo.coreInfo, ...info.simpleInfo.coreInfo,
...info.simpleInfo.status, ...info.simpleInfo.status,
@@ -74,281 +57,230 @@ export class NTQQUserApi {
return ret return ret
} }
static async getUserDetailInfo(uid: string, getLevel = false, withBizInfo = true) { async getUserDetailInfo(uid: string) {
if (getBuildVersion() >= 26702) { if (getBuildVersion() >= 26702) {
return NTQQUserApi.fetchUserDetailInfo(uid) return this.fetchUserDetailInfo(uid)
} }
if (NTEventDispatch.initialised) { const result = await invoke<{ info: User }>(
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'] 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'] [{
const [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile) => profile.uid === uid,
uid, uid,
[0] bizList: [0]
) }],
return profile {
} else {
const result = await invoke<{ info: User }>({
methodName: 'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged', cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false, afterFirstCmd: false,
cmdCB: (payload) => payload.info.uid === uid, cmdCB: (payload) => payload.info.uid === uid,
args: [ }
{ )
uid,
bizList: [0]
},
null,
],
})
return result.info return result.info
} }
}
static async getSkey(): Promise<string> { async getCookies(domain: string) {
const clientKeyData = await NTQQUserApi.forceFetchClientKey() const clientKeyData = await this.forceFetchClientKey()
if (clientKeyData?.result !== 0) { 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
+ '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=' + clientKeyData.keyIndex
return (await RequestUtil.HttpsGetCookies(url))?.skey
}
@CacheClassFuncAsync(1800 * 1000)
static async getCookies(domain: string) {
const clientKeyData = await NTQQUserApi.forceFetchClientKey()
if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败')
}
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 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) 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 }])
let hash = 5381
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i)
hash = hash + (hash << 5) + code
} }
return (hash & 0x7fffffff).toString() async like(uid: string, count = 1) {
} return await invoke(
'nodeIKernelProfileLikeService/setBuddyProfileLike',
static async like(uid: string, count = 1) { [{
const session = getSession()
if (session) {
return session.getProfileLikeService().setBuddyProfileLike({
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
})
} else {
return await invoke<GeneralCallResult & { succCounts: number }>({
methodName: 'nodeIKernelProfileLikeService/setBuddyProfileLike',
args: [
{
doLikeUserInfo: { doLikeUserInfo: {
friendUid: uid, friendUid: uid,
sourceId: 71, sourceId: 71,
doLikeCount: count, doLikeCount: count,
doLikeTollCount: 0 doLikeTollCount: 0
} }
}, }]
null, )
],
})
}
} }
static async getUidByUinV1(Uin: string) { async getUidByUinV1(uin: string, groupCode?: string) {
const session = getSession() let uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([Uin]))?.uidInfo.get(Uin)
//Uid 群友列表转
if (!uid) { if (!uid) {
for (let groupMembersList of groupMembers.values()) { const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
for (let GroupMember of groupMembersList.values()) { if (unveifyUid.indexOf('*') === -1) {
if (GroupMember.uin == Uin) {
uid = GroupMember.uid
}
}
}
}
if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
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
}
if (!uid && groupCode) {
const members = await this.ctx.ntGroupApi.getGroupMembers(groupCode)
uid = Array.from(members.values()).find(e => e.uin === uin)?.uid
}
return uid return uid
} }
static async getUidByUinV2(uin: string) { async getUidByUinV2(uin: string) {
const session = getSession() let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uinList: [uin] }])).uids.get(uin)
if (session) {
let 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 invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid if (uid) return uid
uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin) uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid if (uid) return uid
} else { const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid
let uid = (await invoke<{ uids: Map<string, string> }>({ //if (!unveifyUid.includes('*')) return unveifyUid
methodName: 'nodeIKernelGroupService/getUidByUins', return unveifyUid
args: [
{ uin: [uin] },
null,
],
})).uids.get(uin)
if (uid) return uid
uid = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUidByUin',
args: [
{
callFrom: 'FriendsServiceImpl',
uin: [uin],
},
null,
],
})).get(uin)
if (uid) return uid
uid = (await invoke<{ uidInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUid',
args: [
{ uin: [uin] },
null,
],
})).uidInfo.get(uin)
if (uid) return uid
}
const unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
} }
static async getUidByUin(Uin: string) { async getUidByUin(uin: string, groupCode?: 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, groupCode)
} }
static async getUserDetailInfoByUinV2(uin: string) { async getUserDetailInfoByUinV2(uin: string) {
if (NTEventDispatch.initialised) { return await invoke<UserDetailInfoByUinV2>(
return await NTEventDispatch.CallNoListenerEvent 'nodeIKernelProfileService/getUserDetailInfoByUin',
<(Uin: string) => Promise<UserDetailInfoByUinV2>>( [{ uin }]
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
uin
)
} else {
return await invoke<UserDetailInfoByUinV2>({
methodName: 'nodeIKernelProfileService/getUserDetailInfoByUin',
args: [
{ 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', [{ uin }]
5000,
[Uid]
) )
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) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin //从QQ Native 转换 uin = (await this.getUserDetailInfo(uid)).uin
} }
return uin return uin
} }
static async getUinByUidV2(uid: string) { async getUinByUidV2(uid: string) {
const session = getSession() let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uidList: [uid] }])).uins.get(uid)
if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
if (uin) return uin if (uin) return uin
uin = (await session.getProfileService().getUinByUid('FriendsServiceImpl', [uid])).get(uid) uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid) uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin if (uin) return uin
uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin
return uin return uin
} else {
let uin = (await invoke<{ uins: Map<string, string> }>({
methodName: 'nodeIKernelGroupService/getUinByUids',
args: [
{ uid: [uid] },
null,
],
})).uins.get(uid)
if (uin) return uin
uin = (await invoke<Map<string, string>>({
methodName: 'nodeIKernelProfileService/getUinByUid',
args: [
{
callFrom: 'FriendsServiceImpl',
uid: [uid],
},
null,
],
})).get(uid)
if (uin) return uin
uin = (await invoke<{ uinInfo: Map<string, string> }>({
methodName: 'nodeIKernelUixConvertService/getUin',
args: [
{ uid: [uid] },
null,
],
})).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(uid)
if (uin) return uin
uin = (await NTQQUserApi.getUserDetailInfo(uid)).uin //从QQ Native 转换
} }
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)
} }
static async forceFetchClientKey() { async forceFetchClientKey() {
const session = getSession() return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ url: '' }])
if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke<forceFetchClientKeyRetType>({
methodName: 'nodeIKernelTicketService/forceFetchClientKey',
args: [{
domain: ''
}, null],
})
} }
async getSelfNick(refresh = true) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const data = await this.getUserSimpleInfo(selfInfo.uid)
selfInfo.nick = data.nick
}
return selfInfo.nick
}
async setSelfStatus(status: number, extStatus: number, batteryStatus: number) {
return await invoke('nodeIKernelMsgService/setStatus', [{
statusReq: {
status,
extStatus,
batteryStatus,
}
}])
}
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,
}
}])
}
async getUserSimpleInfoV2(uid: string, force = true) {
const data = await invoke<{ profiles: Record<string, SimpleInfo> }>(
'nodeIKernelProfileService/getUserSimpleInfo',
[{
uids: [uid],
force
}],
{
cbCmd: 'onProfileSimpleChanged',
afterFirstCmd: false,
cmdCB: payload => !isNullable(payload.profiles[uid]),
}
)
return data.profiles[uid].coreInfo
}
async getUserSimpleInfo(uid: string, force = true) {
if (getBuildVersion() >= 26702) {
return this.getUserSimpleInfoV2(uid, force)
}
const data = await invoke<{ profiles: Map<string, User> }>(
'nodeIKernelProfileService/getUserSimpleInfo',
[{
uids: [uid],
force
}],
{
cbCmd: 'nodeIKernelProfileListener/onProfileSimpleChanged',
afterFirstCmd: false,
cmdCB: payload => payload.profiles.has(uid),
}
)
const profile = data.profiles.get(uid)!
return pick(profile, ['nick', 'remark', 'uid', 'uin'])
}
async getCoreAndBaseInfo(uids: string[]) {
return await invoke(
'nodeIKernelProfileService/getCoreAndBaseInfo',
[{
uids,
callFrom: 'nodeStore'
}]
)
}
async getRobotUinRange() {
const data = await invoke(
'nodeIKernelRobotService/getRobotUinRange',
[{
req: {
justFetchMsgConfig: '1',
type: 1,
version: 0,
aioKeywordVersion: 0
}
}]
)
return data.response.robotUinRanges
} }
} }

View File

@@ -1,7 +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 { 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',
@@ -12,299 +17,140 @@ export enum WebHonorType {
EMOTION = 'emotion' EMOTION = 'emotion'
} }
export interface WebApiGroupMember { export class NTQQWebApi extends Service {
uin: number static inject = ['ntUserApi']
role: number
g: number
join_time: number
last_speak_time: number
lv: {
point: number
level: number
}
card: string
tags: string
flag: number
nick: string
qage: number
rm: number
}
interface WebApiGroupMemberRet { constructor(protected ctx: Context) {
ec: number super(ctx, 'ntWebApi', true)
errcode: number
em: string
cache: number
adm_num: number
levelname: any
mems: WebApiGroupMember[]
count: number
svr_time: number
max_count: number
search_count: number
extmode: number
}
export interface WebApiGroupNoticeFeed {
u: number//发送者
fid: string//fid
pubt: number//时间
msg: {
text: string
text_face: string
title: string,
pics?: {
id: string,
w: string,
h: string
}[]
}
type: number
fn: number
cn: number
vn: number
settings: {
is_show_edit_card: number
remind_ts: number
tip_window_type: number
confirm_required: number
}
read_num: number
is_read: number
is_all_confirm: number
}
export interface WebApiGroupNoticeRet {
ec: number
em: string
ltsm: number
srv_code: number
read_only: number
role: number
feeds: WebApiGroupNoticeFeed[]
group: {
group_id: number
class_ext: number
}
sta: number,
gln: number
tst: number,
ui: any
server_time: number
svrt: number
ad: number
}
interface GroupEssenceMsg {
group_code: string
msg_seq: number
msg_random: number
sender_uin: string
sender_nick: string
sender_time: number
add_digest_uin: string
add_digest_nick: string
add_digest_time: number
msg_content: any[]
can_be_removed: true
}
export interface GroupEssenceMsgRet {
retcode: number
retmsg: string
data: {
msg_list: GroupEssenceMsg[]
is_end: boolean
group_role: number
config_page_url: string
}
}
export class WebApi {
static async getGroupEssenceMsg(GroupCode: string, page_start: string): Promise<GroupEssenceMsgRet | 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
} }
static async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> { genBkn(sKey: string) {
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>() sKey = sKey || ''
const cookieObject = await NTQQUserApi.getCookies('qun.qq.com') let hash = 5381
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
const retList: Promise<WebApiGroupMemberRet>[] = []
const params = new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: WebApi.genBkn(cookieObject.skey)
})
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${params}`, 'POST', '', { 'Cookie': cookieStr })
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return []
} else {
for (const member of fastRet.mems) {
memberData.push(member)
}
}
const pageNum = Math.ceil(fastRet.count / 40)
//遍历批量请求
for (let i = 2; i <= pageNum; i++) {
params.set('st', String((i - 1) * 40))
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) {
memberData.push(member)
}
}
return memberData
}
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(); async getGroupHonorInfo(groupCode: string, getType: string) {
} const getDataInternal = async (groupCode: string, type: number) => {
const url = 'https://qun.qq.com/interactive/honorlist?gc=' + groupCode + '&type=' + type
//实现未缓存 考虑2h缓存 let resJson
static async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
async function getDataInternal(Internal_groupCode: string, Internal_type: number) {
let url = 'https://qun.qq.com/interactive/honorlist?gc=' + Internal_groupCode + '&type=' + Internal_type.toString();
let res = '';
let resJson;
try { try {
res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr }); 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 (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 cookieObject = await NTQQUserApi.getCookies('qun.qq.com') const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
const cookieStr = Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ') 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,46 +1,58 @@
import { invoke, NTClass, NTMethod } from '../ntcall' import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services' import { GeneralCallResult } from '../services'
import { ReceiveCmd } from '../hook' 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) {
static async openWindow<R = GeneralCallResult>( super(ctx, 'ntWindowApi', true)
}
/** 打开窗口并获取对应的下发事件 */
async openWindow<R = GeneralCallResult>(
ntQQWindow: NTQQWindow, ntQQWindow: NTQQWindow,
args: any[], args: unknown[],
cbCmd: ReceiveCmd | undefined, cbCmd: ReceiveCmdS | undefined,
autoCloseSeconds: number = 2, autoCloseSeconds: number = 2,
) { ) {
const result = await invoke<R>({ const result = await invoke<R>(
NTMethod.OPEN_EXTRA_WINDOW,
[ntQQWindow.windowName, ...args],
{
className: NTClass.WINDOW_API, className: NTClass.WINDOW_API,
methodName: NTMethod.OPEN_EXTRA_WINDOW,
cbCmd, cbCmd,
afterFirstCmd: false, afterFirstCmd: false,
args: [ntQQWindow.windowName, ...args], }
}) )
setTimeout(() => { setTimeout(() => {
for (const w of BrowserWindow.getAllWindows()) { for (const w of BrowserWindow.getAllWindows()) {
// log("close window", w.webContents.getURL())
if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) { if (w.webContents.getURL().indexOf(ntQQWindow.windowUrlHash) != -1) {
w.close() w.close()
} }

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

@@ -0,0 +1,267 @@
import { unlink } from 'node:fs/promises'
import { Service, Context } from 'cordis'
import { registerCallHook, registerReceiveHook, ReceiveCmdS } from './hook'
import { Config as LLOBConfig } from '../common/types'
import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall'
import {
RawMessage,
GroupNotify,
FriendRequestNotify,
FriendRequest,
GroupMember,
CategoryFriend,
SimpleInfo,
ChatType,
BuddyReqType,
GrayTipElementSubType
} from './types'
import { selfInfo, llonebotError } 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', 'store']
public startTime = 0
constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true)
}
public start() {
if (!this.config.ob11.enable && !this.config.satori.enable) {
llonebotError.otherError = 'LLOneBot 未启动'
this.ctx.logger.info('LLOneBot 开关设置为关闭,不启动 LLOneBot')
return
}
this.startTime = Date.now()
this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llob/config-updated', input => {
Object.assign(this.config, input)
})
}
private registerListener() {
registerReceiveHook<{
data?: CategoryFriend[]
userSimpleInfos?: Map<string, SimpleInfo> //V2
buddyCategory?: CategoryFriend[] //V2
}>(ReceiveCmdS.FRIENDS, (payload) => {
let uids: string[] = []
if (payload.buddyCategory) {
uids = payload.buddyCategory.flatMap(item => item.buddyUids)
} else if (payload.data) {
uids = payload.data.flatMap(item => item.buddyList.map(e => e.uid))
}
for (const uid of uids) {
this.ctx.ntMsgApi.activateChat({ peerUid: uid, chatType: ChatType.C2C })
}
this.ctx.logger.info('好友列表变动', uids.length)
})
// 自动清理新消息文件
registerReceiveHook<{ msgList: 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) {
unlink(path).then(() => 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.TempC2CFromGroup) {
this.ctx.ntMsgApi.activateChatAndGetHistory(peer, 2).then(res => {
for (const msg of res.msgList) {
if (Date.now() / 1000 - Number(msg.msgTime) > 3) {
continue
}
if (msg.senderUin && msg.senderUin !== '0') {
this.ctx.store.addMsgCache(msg)
}
this.ctx.parallel('nt/message-created', msg)
}
})
} 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.C2C
if (isNumeric(peerUid)) {
chatType = ChatType.Group
}
else if (!(await this.ctx.ntFriendApi.isBuddy(peerUid))) {
chatType = ChatType.TempC2CFromGroup
}
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 => {
for (const message of payload.msgList) {
// 过滤启动之前的消息
if (parseInt(message.msgTime) < this.startTime / 1000) {
continue
}
if (message.senderUin && message.senderUin !== '0') {
this.ctx.store.addMsgCache(message)
}
this.ctx.parallel('nt/message-created', message)
}
})
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' &&
msg.msgType === 5 &&
msg.subMsgType === 4 &&
msg.elements[0]?.grayTipElement?.subElementType === GrayTipElementSubType.Revoke &&
!recallMsgIds.includes(msg.msgId)
) {
recallMsgIds.shift()
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(payload.unreadCount)
} catch (e) {
return
}
for (const notify of notifies) {
const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type
const notifyTime = parseInt(notify.seq) / 1000
if (groupNotifyFlags.includes(flag) || notifyTime < this.startTime) {
continue
}
groupNotifyFlags.shift()
groupNotifyFlags.push(flag)
this.ctx.parallel('nt/group-notify', notify)
}
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
for (const req of payload.data.buddyReqs) {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.MeInitiatorWaitPeerConfirm)) {
continue
}
if (+req.reqTime < this.startTime / 1000) {
continue
}
this.ctx.parallel('nt/friend-request', req)
}
})
invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { registerEvent: 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,6 @@
import ffmpeg from 'fluent-ffmpeg'
import faceConfig from './helper/face_config.json'
import pathLib from 'node:path'
import { import {
AtType, AtType,
ElementType, ElementType,
@@ -13,30 +16,21 @@ 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 './helper/face_config.json' import { isNullable } from 'cosmokit'
export const mFaceCache = new Map<string, string>() // emojiId -> faceName export namespace SendElement {
export function text(content: string): SendTextElement {
export class SendMsgElementConstructor {
static poke(groupCode: string, uin: string) {
return null
}
static text(content: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.Text,
elementId: '', elementId: '',
textElement: { textElement: {
content, content,
atType: AtType.notAt, atType: AtType.Unknown,
atUid: '', atUid: '',
atTinyId: '', atTinyId: '',
atNtUid: '', atNtUid: '',
@@ -44,9 +38,9 @@ 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: '',
textElement: { textElement: {
content: display, content: display,
@@ -58,29 +52,25 @@ export class SendMsgElementConstructor {
} }
} }
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { export function reply(msgSeq: string, msgId: string, senderUin: string): SendReplyElement {
return { return {
elementType: ElementType.REPLY, elementType: ElementType.Reply,
elementId: '', elementId: '',
replyElement: { replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq replayMsgSeq: msgSeq,
replayMsgId: msgId, // raw.msgId replayMsgId: msgId,
senderUin: senderUin, senderUin: senderUin,
senderUinStr: senderUinStr, senderUinStr: senderUin,
}, },
} }
} }
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,49 +79,51 @@ 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.JPEG,
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: '',
picElement, picElement,
} }
} }
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'
} }
@@ -139,7 +131,6 @@ export class SendMsgElementConstructor {
if (fileSize > 1024 * 1024 * maxMB) { if (fileSize > 1024 * 1024 * maxMB) {
throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`
} }
const pathLib = require('path')
let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumbDir = pathLib.dirname(thumbDir) thumbDir = pathLib.dirname(thumbDir)
// log("thumb 目录", thumb) // log("thumb 目录", thumb)
@@ -152,21 +143,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 +166,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,20 +185,20 @@ 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: {
fileName: fileName || _fileName, fileName: fileName || _fileName,
@@ -219,45 +210,34 @@ export class SendMsgElementConstructor {
thumbSize, thumbSize,
thumbWidth: videoInfo.width, thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height, thumbHeight: videoInfo.height,
fileSize: '' + fileSize, fileSize: String(fileSize),
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// 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,
elementId: '', elementId: '',
pttElement: { pttElement: {
fileName: fileName, fileName: fileName,
filePath: path, filePath: path,
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, fileSize: String(fileSize),
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration, duration: duration,
formatType: 1, formatType: 1,
voiceType: 1, voiceType: 1,
@@ -271,22 +251,22 @@ export class SendMsgElementConstructor {
} }
} }
static face(faceId: number): SendFaceElement { export function face(faceId: number, faceType?: number): SendFaceElement {
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface const sysFaces = faceConfig.sysface
const emojiFaces = faceConfig.emoji const face = sysFaces.find(face => face.QSid === String(faceId))
const face = sysFaces.find((face) => face.QSid === faceId.toString()) if (!faceType) {
faceId = parseInt(faceId.toString()) if (faceId < 222) {
// let faceType = parseInt(faceId.toString().substring(0, 1)); faceType = 1
let faceType = 1 } else {
if (faceId >= 222) {
faceType = 2 faceType = 2
} }
if (face?.AniStickerType) { if (face?.AniStickerType) {
faceType = 3; faceType = 3
}
} }
return { return {
elementType: ElementType.FACE, elementType: ElementType.Face,
elementId: '', elementId: '',
faceElement: { faceElement: {
faceIndex: faceId, faceIndex: faceId,
@@ -300,35 +280,37 @@ 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.MarketFace,
elementId: '',
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: '',
faceElement: { faceElement: {
faceIndex: FaceIndex.dice, faceIndex: FaceIndex.Dice,
faceType: 3, faceType: 3,
faceText: '[骰子]', faceText: '[骰子]',
packId: '1', packId: '1',
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,11 +318,11 @@ 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: '',
faceElement: { faceElement: {
faceIndex: FaceIndex.RPS, faceIndex: FaceIndex.RPS,
@@ -350,16 +332,16 @@ 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: '',
arkElement: { arkElement: {
bytesData: data, bytesData: data,
@@ -368,4 +350,16 @@ export class SendMsgElementConstructor {
}, },
} }
} }
export function shake(): SendFaceElement {
return {
elementType: ElementType.Face,
elementId: '',
faceElement: {
faceIndex: 1,
faceType: 5,
pokeType: 1,
},
}
}
} }

View File

@@ -1,5 +1,15 @@
{ {
"sysface": [ "sysface": [
{
"QSid": "419",
"QDes": "/火车",
"IQLid": "419",
"AQLid": "419",
"EMCode": "10419",
"AniStickerType": 3,
"AniStickerPackId": "1",
"AniStickerId": "47"
},
{ {
"QSid": "392", "QSid": "392",
"QDes": "/龙年快乐", "QDes": "/龙年快乐",

View File

@@ -1,4 +1,4 @@
import { log } from '@/common/utils' import { Context } from 'cordis'
interface ServerRkeyData { interface ServerRkeyData {
group_rkey: string group_rkey: string
@@ -6,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
} }
@@ -23,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
@@ -31,32 +31,18 @@ class RkeyManager {
isExpired(): boolean { isExpired(): boolean {
const now = new Date().getTime() / 1000 const now = new Date().getTime() / 1000
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`)
return now > this.rkeyData.expired_time return now > this.rkeyData.expired_time
} }
async refreshRkey(): Promise<any> { async refreshRkey() {
//刷新rkey
this.rkeyData = await this.fetchServerRkey() this.rkeyData = await this.fetchServerRkey()
} }
async fetchServerRkey() { async fetchServerRkey(): Promise<ServerRkeyData> {
return new Promise<ServerRkeyData>((resolve, reject) => { const response = await fetch(this.serverUrl)
fetch(this.serverUrl)
.then(response => {
if (!response.ok) { if (!response.ok) {
return reject(response.statusText) // 请求失败,返回错误信息 throw new Error(response.statusText)
} }
return response.json() // 解析 JSON 格式的响应体 return response.json()
})
.then(data => {
resolve(data)
})
.catch(error => {
reject(error)
})
})
} }
} }
export const rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey')

View File

@@ -1,74 +1,50 @@
import type { BrowserWindow } from 'electron' import type { BrowserWindow } from 'electron'
import { NTClass, NTMethod } from './ntcall' import { NTClass, NTMethod } from './ntcall'
import { NTQQMsgApi, NTQQFriendApi } from './api'
import {
CategoryFriend,
ChatType,
GroupMember,
GroupMemberRole,
RawMessage,
SimpleInfo,
User
} from './types'
import {
getGroupMember,
setSelfInfo
} from '@/common/data'
import { postOb11Event } from '../onebot11/server/post-ob11-event'
import { getConfigUtil } from '@/common/config'
import fs from 'node:fs'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { MessageUnique } from '../common/utils/MessageUnique' import { Dict } from 'cosmokit'
import { isNumeric, sleep } from '@/common/utils'
import { OB11Constructor } from '../onebot11/constructor'
import { OB11GroupCardEvent } from '../onebot11/event/notice/OB11GroupCardEvent'
import { OB11GroupAdminNoticeEvent } from '../onebot11/event/notice/OB11GroupAdminNoticeEvent'
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {} export 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', }
} as const
export type ReceiveCmd = string type NTReturnData = [
{
interface NTQQApiReturnData<Payload = unknown> extends Array<any> {
0: {
type: 'request' type: 'request'
eventName: NTClass eventName: NTClass
callbackId?: string callbackId?: string
} },
1: { {
cmdName: ReceiveCmd cmdName: ReceiveCmdS
cmdType: 'event' cmdType: 'event'
payload: Payload payload: unknown
}[] }[]
} ]
const logHook = false const logHook = false
const receiveHooks: Array<{ const receiveHooks: Array<{
method: ReceiveCmd[] method: ReceiveCmdS[]
hookFunc: (payload: any) => void | Promise<void> hookFunc: (payload: any) => void | Promise<void>
id: string id: string
}> = [] }> = []
@@ -78,83 +54,61 @@ const callHooks: Array<{
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]) {
try { try {
const isLogger = args[0]?.eventName?.startsWith('ns-LoggerApi') if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) {
if (logHook && !isLogger) { log('received ntqq api message', args)
log(`received ntqq api message: ${channel}`, args)
} }
} catch { } } catch { }
if (args?.[1] instanceof Array) { if (!onlyLog) {
for (const receiveData of args?.[1]) { if (args[2] instanceof Array) {
const ntQQApiMethodName = receiveData.cmdName for (const receiveData of args[2]) {
// log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) const ntMethodName = receiveData.cmdName
for (const hook of receiveHooks) { for (const hook of receiveHooks) {
if (hook.method.includes(ntQQApiMethodName)) { if (hook.method.includes(ntMethodName)) {
new Promise((resolve, reject) => { Promise.resolve(hook.hookFunc(receiveData.payload))
try {
hook.hookFunc(receiveData.payload)
} catch (e: any) {
log('hook error', ntQQApiMethodName, e.stack.toString())
}
resolve(undefined)
}).then()
} }
} }
} }
} }
if (args[0]?.callbackId) { if (args[1]?.callbackId) {
// log("hookApiCallback", hookApiCallbacks, args) const callbackId = args[1].callbackId
const callbackId = args[0].callbackId
if (hookApiCallbacks[callbackId]) { if (hookApiCallbacks[callbackId]) {
// log("callback found") Promise.resolve(hookApiCallbacks[callbackId](args[2]))
new Promise((resolve, reject) => {
hookApiCallbacks[callbackId](args[1])
resolve(undefined)
}).then()
delete hookApiCallbacks[callbackId] delete hookApiCallbacks[callbackId]
} }
} }
originalSend.call(window.webContents, channel, ...args)
} }
window.webContents.send = patchSend return target.apply(thisArg, args)
},
})
} }
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 {
logHook && log('call NTQQ api', thisArg, args) logHook && log('call NTQQ api', args)
} catch (e) { } } catch (e) { }
if (!onlyLog) {
try { try {
const _args: unknown[] = args[3][1] const _args: unknown[] = args[3][1]
const cmdName: NTMethod = _args[0] as NTMethod const cmdName = _args[0] as NTMethod
const callParams = _args.slice(1) const callParams = _args.slice(1)
callHooks.forEach((hook) => { callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) { if (hook.method.includes(cmdName)) {
new Promise((resolve, reject) => { Promise.resolve(hook.hookFunc(callParams))
try {
hook.hookFunc(callParams)
} catch (e: any) {
log('hook call error', e, _args)
}
resolve(undefined)
}).then()
} }
}) })
} catch (e) { } } catch { }
}
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
}, },
@@ -165,20 +119,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
}, },
}) })
@@ -186,11 +137,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()
@@ -198,7 +149,7 @@ export function registerReceiveHook<PayloadType>(
method = [method] method = [method]
} }
receiveHooks.push({ receiveHooks.push({
method, method: method as ReceiveCmdS[],
hookFunc, hookFunc,
id, id,
}) })
@@ -222,180 +173,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)
} }
export async function startHook() {
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) => {
// log("onBuddyListChange", payload)
// let friendListV2: {userSimpleInfos: Map<string, SimpleInfo>} = []
type V2data = { userSimpleInfos: Map<string, SimpleInfo> }
let friendList: User[] = [];
if ((payload as any).userSimpleInfos) {
// friendListV2 = payload as any
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)
}
}
log('好友列表变动', friendList.length)
for (let friend of friendList) {
NTQQMsgApi.activateChat({ peerUid: friend.uid, chatType: ChatType.friend }).then()
}
})
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 { msgId, chatType, peerUid } = msgRecord
const peer = {
chatType,
peerUid
}
MessageUnique.createMsg(peer, msgId)
})
registerReceiveHook<{ info: { status: number } }>(ReceiveCmdS.SELF_STATUS, (info) => {
setSelfInfo({
online: info.info.status !== 20
})
})
let activatedPeerUids: string[] = []
registerReceiveHook<{
changedRecentContactLists: {
listType: number
sortedContactList: string[]
changedList: {
id: string // peerUid
chatType: ChatType
}[]
}[]
}>(ReceiveCmdS.RECENT_CONTACT, async (payload) => {
for (const recentContact of payload.changedRecentContactLists) {
for (const changedContact of recentContact.changedList) {
if (activatedPeerUids.includes(changedContact.id)) continue
activatedPeerUids.push(changedContact.id)
const peer = { peerUid: changedContact.id, chatType: changedContact.chatType }
if (changedContact.chatType === ChatType.temp) {
log('收到临时会话消息', peer)
NTQQMsgApi.activateChatAndGetHistory(peer).then(() => {
NTQQMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
let lastTempMsg = msgList.pop()
log('激活窗口之前的第一条临时会话消息:', lastTempMsg)
if (Date.now() / 1000 - parseInt(lastTempMsg?.msgTime!) < 5) {
OB11Constructor.message(lastTempMsg!).then((r) => postOb11Event(r))
}
})
})
}
else {
NTQQMsgApi.activateChat(peer).then()
}
}
}
})
registerCallHook(NTMethod.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 NTQQFriendApi.isBuddy(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,8 +1,21 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook' import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/log' import { log } from '../common/utils/legacyLog'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { GeneralCallResult } from './services' import {
GeneralCallResult,
NodeIKernelBuddyService,
NodeIKernelProfileService,
NodeIKernelGroupService,
NodeIKernelProfileLikeService,
NodeIKernelMsgService,
NodeIKernelMSFService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService,
NodeIKernelRobotService
} from './services'
export enum NTClass { export enum NTClass {
NT_API = 'ns-ntApi', NT_API = 'ns-ntApi',
@@ -19,161 +32,150 @@ export enum NTClass {
} }
export enum NTMethod { export enum NTMethod {
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',
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',
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',
} }
export enum NTChannel { 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 InvokeParams<ReturnType> { interface NTService {
methodName: string nodeIKernelBuddyService: NodeIKernelBuddyService
nodeIKernelProfileService: NodeIKernelProfileService
nodeIKernelGroupService: NodeIKernelGroupService
nodeIKernelProfileLikeService: NodeIKernelProfileLikeService
nodeIKernelMsgService: NodeIKernelMsgService
nodeIKernelMSFService: NodeIKernelMSFService
nodeIKernelUixConvertService: NodeIKernelUixConvertService
nodeIKernelRichMediaService: NodeIKernelRichMediaService
nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService
nodeIKernelRobotService: NodeIKernelRobotService
}
interface InvokeOptions<ReturnType> {
className?: NTClass className?: NTClass
channel?: NTChannel channel?: NTChannel
classNameIsRegister?: boolean registerEvent?: boolean
args?: unknown[]
cbCmd?: string | string[] cbCmd?: string | string[]
cmdCB?: (payload: ReturnType) => boolean cmdCB?: (payload: ReturnType, result: unknown) => boolean
afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
timeout?: number timeout?: number
} }
export function invoke<ReturnType>(params: InvokeParams<ReturnType>) { export function invoke<
const className = params.className ?? NTClass.NT_API R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>,
const channel = params.channel ?? NTChannel.IPC_UP_2 S extends keyof NTService = any,
const timeout = params.timeout ?? 5000 M extends keyof NTService[S] & string = any
const afterFirstCmd = params.afterFirstCmd ?? true >(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
const uuid = randomUUID() const className = options.className ?? NTClass.NT_API
const channel = options.channel ?? NTChannel.IPC_UP_2
const timeout = options.timeout ?? 5000
const afterFirstCmd = options.afterFirstCmd ?? true
let eventName = className + '-' + channel[channel.length - 1] let eventName = className + '-' + channel[channel.length - 1]
if (params.classNameIsRegister) { if (options.registerEvent) {
eventName += '-register' eventName += '-register'
} }
const apiArgs = [params.methodName, ...(params.args ?? [])] return new Promise<R>((resolve, reject) => {
//log('callNTQQApi', channel, eventName, apiArgs, uuid) const apiArgs = [method, ...args]
return new Promise((resolve: (data: ReturnType) => void, reject) => { const callbackId = randomUUID()
let success = false const timeoutId = setTimeout(() => {
if (!params.cbCmd) { log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
}, timeout)
if (!options.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>(params.cbCmd!, (payload) => { const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
// log(methodName, "second callback", cbCmd, payload, cmdCB); if (options.cmdCB) {
if (!!params.cmdCB) { if (options.cmdCB(payload, result)) {
if (params.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) => {
if (result?.result === 0 || result === undefined) { result = res
log(`${params.methodName} callback`, result) if (res?.result === 0 || ['undefined', 'number'].includes(typeof res)) {
afterFirstCmd && secondCallback() afterFirstCmd && secondCallback()
} }
else { else {
log('ntqq api call failed', result) log('ntqq api call failed,', method, args, res)
reject(`ntqq api call failed, ${result.errMsg}`) clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res?.errMsg}`)
} }
} }
} }
setTimeout(() => {
if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${params.methodName}`, apiArgs)
reject(`ntqq api timeout ${channel}, ${eventName}, ${params.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,
) )
}) })

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

View File

@@ -1,249 +1,128 @@
import { NodeIKernelGroupListener } from '@/ntqqapi/listeners'
import { import {
GroupExtParam,
GroupMember, GroupMember,
GroupMemberRole, GroupMemberRole,
GroupNotifyTypes, GroupNotifyType,
GroupRequestOperateTypes, GroupRequestOperateTypes,
} from '@/ntqqapi/types' } from '@/ntqqapi/types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
//高版本的接口不应该随意使用 使用应该严格进行pr审核 同时部分ipc中未出现的接口不要过于依赖 应该做好数据兜底
export interface NodeIKernelGroupService { export interface NodeIKernelGroupService {
getMemberCommonInfo(Req: { getGroupHonorList(req: { groupCode: number[] }): Promise<{
groupCode: string, errCode: number
startUin: string, errMsg: string
identifyFlag: string, groupMemberHonorList: {
uinList: string[], honorList: {
memberCommonFilter: { groupCode: string
memberUin: number, id: number[]
uinFlag: number, isGray: number
uinFlagExt: number, }[]
uinMobileFlag: number, cacheTs: number
shutUpTime: number, honorInfos: unknown[]
privilege: number, joinTime: 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<{ getUinByUids(uins: string[]): Promise<{
errCode: number, errCode: number
errMsg: string, errMsg: string
uins: Map<string, string> uins: Map<string, string>
}> }>
getUidByUins(uins: string[]): Promise<{ getUidByUins(uins: string[]): Promise<{
errCode: number, errCode: number
errMsg: string, errMsg: string
uids: Map<string, string> uids: Map<string, string>
}> }>
//26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>
//26702(其实更早 但是我不知道) queryCachedEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<{
getGroupLatestEssenceList(groupCode: string): Promise<unknown> items: {
groupCode: string
//26702(其实更早 但是我不知道) msgSeq: number
shareDigest(Req: { msgRandom: number
appId: string, msgSenderUin: string
appType: number, msgSenderNick: string
msgStyle: number, opType: number
recvUin: string, opUin: string
sendType: number, opNick: string
clientInfo: { opTime: number
platform: number grayTipSeq: string
}, }[]
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 setHeader(uid: string, path: string): unknown
addKernelGroupListener(listener: NodeIKernelGroupListener): number
removeKernelGroupListener(listenerId: unknown): void
createMemberListScene(groupCode: string, scene: string): string createMemberListScene(groupCode: string, scene: string): string
destroyMemberListScene(SceneId: string): void
//About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string}
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{ getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number, errMsg: string, errCode: number
result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean } 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> kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>
modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void
modifyMemberCardName(groupCode: string, uid: string, cardName: string): 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 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 quitGroup(groupCode: string): void
destroyGroup(groupCode: string): void getSingleScreenNotifies(force: boolean, startSeq: string, num: number): Promise<GeneralCallResult>
//获取单屏群通知列表
getSingleScreenNotifies(force: boolean, start_seq: string, num: number): Promise<GeneralCallResult>
clearGroupNotifies(groupCode: string): void
getGroupNotifiesUnreadCount(unknown: Boolean): Promise<GeneralCallResult>
clearGroupNotifiesUnreadCount(groupCode: string): void
operateSysNotify( operateSysNotify(
doubt: boolean, doubt: boolean,
operateMsg: { operateMsg: {
operateType: GroupRequestOperateTypes, // 2 拒绝 operateType: GroupRequestOperateTypes // 2 拒绝
targetMsg: { targetMsg: {
seq: string, // 通知序列号 seq: string // 通知序列号
type: GroupNotifyTypes, type: GroupNotifyType
groupCode: string, groupCode: string
postscript: string postscript: string
} }
}): Promise<void> }
): Promise<void>
setTop(groupCode: string, isTop: boolean): void publishGroupBulletin(groupCode: string, pskey: string, data: unknown): Promise<GeneralCallResult>
getGroupBulletin(groupCode: string): unknown uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<{
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 errCode: number
errMsg: string
picInfo?: { picInfo?: {
id: string, id: string
width: number, width: number
height: number height: number
} }
}> }>
downloadGroupBulletinRichMedia(groupCode: string): unknown getGroupRemainAtTimes(groupCode: string): Promise<GeneralCallResult & {
atInfo: {
getGroupBulletinList(groupCode: string): unknown canAtAll: boolean
RemainAtAllCountForUin: number
getGroupStatisticInfo(groupCode: string): unknown RemainAtAllCountForGroup: number
atTimesMsg: string
getGroupRemainAtTimes(groupCode: string): number canNotAtAllMsg: ''
}
getJoinGroupNoVerifyFlag(groupCode: string): unknown }>
getGroupArkInviteState(groupCode: string): unknown
reqToJoinGroup(groupCode: string, arg: unknown): void
setGroupShutUp(groupCode: string, shutUp: boolean): void setGroupShutUp(groupCode: string, shutUp: boolean): void
getGroupShutUpMemberList(groupCode: string): unknown[]
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void> setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>
getGroupRecommendContactArkJson(groupCode: string): unknown getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>
getJoinGroupLink(groupCode: string): unknown addGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
modifyGroupExtInfo(groupCode: string, arg: unknown): void removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
//需要提前判断是否存在 高版本新增 setHeader(args: unknown[]): Promise<GeneralCallResult>
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,744 +1,98 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types' import { ElementType, MessageElement, Peer, RawMessage, QueryMsgsParams, TmpChatInfoApi } from '@/ntqqapi/types'
import { NodeIKernelMsgListener } from '@/ntqqapi/listeners/NodeIKernelMsgListener'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
export interface QueryMsgsParams {
chatInfo: Peer,
filterMsgType: [],
filterSendersUid: string[],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi {
errMsg: string
result: number
tmpChatInfo?: TmpChatInfo
}
export interface TmpChatInfo {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
export interface NodeIKernelMsgService { export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string generateMsgUniqueId(chatType: number, time: string): string
addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number sendMsg(msgId: string, peer: Peer, msgElements: MessageElement[], map: Map<unknown, unknown>): Promise<GeneralCallResult>
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>): Promise<GeneralCallResult>
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult> recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>
addKernelMsgImportToolListener(arg: Object): unknown
removeKernelMsgListener(args: unknown): unknown
addKernelTempChatSigListener(...args: unknown[]): unknown
removeKernelTempChatSigListener(...args: unknown[]): unknown
setAutoReplyTextList(AutoReplyText: Array<unknown>, i2: number): unknown
getAutoReplyTextList(...args: unknown[]): unknown
getOnLineDev(): void
kickOffLine(DevInfo: Object): unknown
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult> setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
fetchStatusMgrInfo(): unknown forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult & {
detailErr: Map<unknown, unknown>
}>
fetchStatusUnitedConfigInfo(): unknown forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult>
getOnlineStatusSmallIconBasePath(): unknown
getOnlineStatusSmallIconFileNameByUrl(Url: string): unknown
downloadOnlineStatusSmallIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusBigIconBasePath(): unknown
downloadOnlineStatusBigIconByUrl(arg0: number, arg1: string): unknown
getOnlineStatusCommonPath(arg: string): unknown
getOnlineStatusCommonFileNameByUrl(Url: string): unknown
downloadOnlineStatusCommonByUrl(arg0: string, arg1: string): unknown
// this.tokenType = i2
// this.apnsToken = bArr
// this.voipToken = bArr2
// this.profileId = str
setToken(arg: Object): unknown
switchForeGround(): unknown
switchBackGround(arg: Object): unknown
//hex
setTokenForMqq(token: string): unknown
switchForeGroundForMqq(...args: unknown[]): unknown
switchBackGroundForMqq(...args: unknown[]): unknown
getMsgSetting(...args: unknown[]): unknown
setMsgSetting(...args: unknown[]): unknown
addSendMsg(...args: unknown[]): unknown
cancelSendMsg(...args: unknown[]): unknown
switchToOfflineSendMsg(peer: Peer, MsgId: string): unknown
reqToOfflineSendMsg(...args: unknown[]): unknown
refuseReceiveOnlineFileMsg(peer: Peer, MsgId: string): unknown
resendMsg(...args: unknown[]): unknown
recallMsg(...args: unknown[]): unknown
reeditRecallMsg(...args: unknown[]): unknown
//调用请检查除开commentElements其余参数不能为null
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
forwardMsgWithComment(...args: unknown[]): unknown
forwardSubMsgWithComment(...args: unknown[]): unknown
forwardRichMsgInVist(...args: unknown[]): unknown
forwardFile(...args: unknown[]): unknown
//Array<Msg>, Peer from, Peer to
multiForwardMsg(...args: unknown[]): unknown
multiForwardMsgWithComment(...args: unknown[]): unknown multiForwardMsgWithComment(...args: unknown[]): unknown
deleteRecallMsg(...args: unknown[]): unknown getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & { msgList: RawMessage[] }>
deleteRecallMsgForLocal(...args: unknown[]): unknown getAioFirstViewLatestMsgsAndAddActiveChat(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
addLocalGrayTipMsg(...args: unknown[]): unknown getMsgsIncludeSelfAndAddActiveChat(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
addLocalJsonGrayTipMsg(...args: unknown[]): unknown getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
addLocalJsonGrayTipMsgExt(...args: unknown[]): unknown
IsLocalJsonTipValid(...args: unknown[]): unknown
addLocalAVRecordMsg(...args: unknown[]): unknown
addLocalTofuRecordMsg(...args: unknown[]): unknown
addLocalRecordMsg(Peer: Peer, msgId: string, ele: MessageElement, attr: Array<any> | number, front: boolean): Promise<unknown>
deleteMsg(Peer: Peer, msgIds: Array<string>): Promise<any>
updateElementExtBufForUI(...args: unknown[]): unknown
updateMsgRecordExtPbBufForUI(...args: unknown[]): unknown
startMsgSync(...args: unknown[]): unknown
startGuildMsgSync(...args: unknown[]): unknown
isGuildChannelSync(...args: unknown[]): unknown
getMsgUniqueId(UniqueId: string): string
isMsgMatched(...args: unknown[]): unknown
getOnlineFileMsgs(...args: unknown[]): unknown
getAllOnlineFileMsgs(...args: unknown[]): unknown
getLatestDbMsgs(peer: Peer, cnt: number): Promise<unknown>
getLastMessageList(peer: Peer[]): Promise<unknown>
getAioFirstViewLatestMsgs(peer: Peer, num: number): unknown
//deprecated 从9.9.15-26702版本开始该接口已经废弃请使用getMsgsEx
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
// this.$peer = contact
// this.$msgTime = j2
// this.$clientSeq = j3
// this.$cnt = i2
getMsgsWithMsgTimeAndClientSeqForC2C(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsWithStatus(params: {
peer: Peer
msgId: string
msgTime: unknown
cnt: unknown
queryOrder: boolean
isIncludeSelf: boolean
appid: unknown
}): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqRange(peer: Peer, startSeq: string, endSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsByMsgId(peer: Peer, ids: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMsgsByMsgId(peer: Peer, ids: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getRecallMsgsByMsgId(peer: Peer, MsgId: string[]): Promise<unknown>
getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }> getSingleMsg(peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSourceOfReplyMsg(peer: Peer, MsgId: string, SourceSeq: string): unknown
getSourceOfReplyMsgV2(peer: Peer, RootMsgId: string, ReplyMsgId: string): unknown
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown
//cnt clientSeq?并不是吧
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown
queryMsgsWithFilter(...args: unknown[]): unknown
/**
* @deprecated 该函数已被标记为废弃,请使用新的替代方法。
* 使用过滤条件查询消息列表的版本2接口。
*
* 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
* 函数返回一个Promise解析为查询结果的未知类型对象。
*
* @param MsgId 消息ID用于特定消息的查询。
* @param MsgTime 消息时间,用于指定消息的时间范围。
* @param param 查询参数对象,包含详细的过滤条件和分页信息。
* @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
* @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
* @param param.filterSendersUid 需要过滤的发送者用户ID数组。
* @param param.filterMsgFromTime 查询消息的起始时间。
* @param param.filterMsgToTime 查询消息的结束时间。
* @param param.pageLimit 每页的消息数量限制。
* @param param.isReverseOrder 是否按时间顺序倒序返回消息。
* @param param.isIncludeCurrent 是否包含当前页码。
* @returns 返回一个Promise解析为查询结果的未知类型对象。
*/
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>
// this.chatType = i2
// this.peerUid = str
// this.chatInfo = new ChatInfo()
// this.filterMsgType = new ArrayList<>()
// this.filterSendersUid = new ArrayList<>()
// this.chatInfo = chatInfo
// this.filterMsgType = arrayList
// this.filterSendersUid = arrayList2
// this.filterMsgFromTime = j2
// this.filterMsgToTime = j3
// this.pageLimit = i2
// this.isReverseOrder = z
// this.isIncludeCurrent = z2
//queryMsgsWithFilterEx(0L, 0L, 0L, new QueryMsgsParams(new ChatInfo(2, str), new ArrayList(), new ArrayList(), 0L, 0L, 250, false, true))
queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<GeneralCallResult & { queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<GeneralCallResult & {
msgList: RawMessage[] msgList: RawMessage[]
}> }>
//queryMsgsWithFilterEx(this.$msgId, this.$msgTime, this.$msgSeq, this.$param)
queryFileMsgsDesktop(...args: unknown[]): unknown
setMsgRichInfoFlag(...args: unknown[]): unknown
queryPicOrVideoMsgs(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<unknown>
queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown
queryEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryTroopEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>
queryMsgsAndAbstractsWithFilter(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): unknown
setFocusOnGuild(...args: unknown[]): unknown
setFocusSession(...args: unknown[]): unknown
enableFilterUnreadInfoNotify(...args: unknown[]): unknown
enableFilterMsgAbstractNotify(...args: unknown[]): unknown
onScenesChangeForSilenceMode(...args: unknown[]): unknown
getContactUnreadCnt(...args: unknown[]): unknown
getUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntInfo(...args: unknown[]): unknown
getGuildUnreadCntTabInfo(...args: unknown[]): unknown
getAllGuildUnreadCntInfo(...args: unknown[]): unknown
getAllJoinGuildCnt(...args: unknown[]): unknown
getAllDirectSessionUnreadCntInfo(...args: unknown[]): unknown
getCategoryUnreadCntInfo(...args: unknown[]): unknown
getGuildFeedsUnreadCntInfo(...args: unknown[]): unknown
setUnVisibleChannelCntInfo(...args: unknown[]): unknown
setUnVisibleChannelTypeCntInfo(...args: unknown[]): unknown
setVisibleGuildCntInfo(...args: unknown[]): unknown
setMsgRead(peer: Peer): Promise<GeneralCallResult> setMsgRead(peer: Peer): Promise<GeneralCallResult>
setAllC2CAndGroupMsgRead(): Promise<unknown>
setGuildMsgRead(...args: unknown[]): unknown
setAllGuildMsgRead(...args: unknown[]): unknown
setMsgReadAndReport(...args: unknown[]): unknown
setSpecificMsgReadAndReport(...args: unknown[]): unknown
setLocalMsgRead(...args: unknown[]): unknown
setGroupGuildMsgRead(...args: unknown[]): unknown
getGuildGroupTransData(...args: unknown[]): unknown
setGroupGuildBubbleRead(...args: unknown[]): unknown
getGuildGroupBubble(...args: unknown[]): unknown
fetchGroupGuildUnread(...args: unknown[]): unknown
setGroupGuildFlag(...args: unknown[]): unknown
setGuildUDCFlag(...args: unknown[]): unknown
setGuildTabUserFlag(...args: unknown[]): unknown
setBuildMode(flag: number/*0 1 3*/): unknown
setConfigurationServiceData(...args: unknown[]): unknown
setMarkUnreadFlag(...args: unknown[]): unknown
getChannelEventFlow(...args: unknown[]): unknown
getMsgEventFlow(...args: unknown[]): unknown
getRichMediaFilePathForMobileQQSend(...args: unknown[]): unknown
getRichMediaFilePathForGuild(arg: { getRichMediaFilePathForGuild(arg: {
md5HexStr: string, md5HexStr: string
fileName: string, fileName: string
elementType: ElementType, elementType: ElementType
elementSubType: number, elementSubType: number
thumbSize: 0, thumbSize: 0
needCreate: true, needCreate: true
downloadType: 1, downloadType: 1
file_uuid: '' file_uuid: ''
}): string }): string
assembleMobileQQRichMediaFilePath(...args: unknown[]): unknown
getFileThumbSavePathForSend(...args: unknown[]): unknown
getFileThumbSavePath(...args: unknown[]): unknown
//猜测居多
translatePtt2Text(MsgId: string, Peer: {}, MsgElement: {}): unknown
setPttPlayedState(...args: unknown[]): unknown
// NodeIQQNTWrapperSession fetchFavEmojiList [
// "",
// 48,
// true,
// true
// ]
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & { fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: Array<{ emojiInfoList: {
uin: string, uin: string
emoId: number, emoId: number
emoPath: string, emoPath: string
isExist: boolean, isExist: boolean
resId: string, resId: string
url: string, url: string
md5: string, md5: string
emoOriginalPath: string, emoOriginalPath: string
thumbPath: string, thumbPath: string
RomaingType: string, RomaingType: string
isAPNG: false, isAPNG: false
isMarkFace: false, isMarkFace: false
eId: string, eId: string
epId: string, epId: string
ocrWord: string, ocrWord: string
modifyWord: string, modifyWord: string
exposeNum: number, exposeNum: number
clickNum: number, clickNum: number
desc: string desc: string
}[]
}> }>
}>
addFavEmoji(...args: unknown[]): unknown
fetchMarketEmoticonList(...args: unknown[]): unknown
fetchMarketEmoticonShowImage(...args: unknown[]): unknown
fetchMarketEmoticonAioImage(...args: unknown[]): unknown
fetchMarketEmotionJsonFile(...args: unknown[]): unknown
getMarketEmoticonPath(...args: unknown[]): unknown
getMarketEmoticonPathBySync(...args: unknown[]): unknown
fetchMarketEmoticonFaceImages(...args: unknown[]): unknown
fetchMarketEmoticonAuthDetail(...args: unknown[]): unknown
getFavMarketEmoticonInfo(...args: unknown[]): unknown
addRecentUsedFace(...args: unknown[]): unknown
getRecentUsedFaceList(...args: unknown[]): unknown
getMarketEmoticonEncryptKeys(...args: unknown[]): unknown
downloadEmojiPic(...args: unknown[]): unknown
deleteFavEmoji(...args: unknown[]): unknown
modifyFavEmojiDesc(...args: unknown[]): unknown
queryFavEmojiByDesc(...args: unknown[]): unknown
getHotPicInfoListSearchString(...args: unknown[]): unknown
getHotPicSearchResult(...args: unknown[]): unknown
getHotPicHotWords(...args: unknown[]): unknown
getHotPicJumpInfo(...args: unknown[]): unknown
getEmojiResourcePath(...args: unknown[]): unknown
JoinDragonGroupEmoji(JoinDragonGroupEmojiReq: any/*joinDragonGroupEmojiReq*/): unknown
getMsgAbstracts(...args: unknown[]): unknown
getMsgAbstract(...args: unknown[]): unknown
getMsgAbstractList(...args: unknown[]): unknown
getMsgAbstractListBySeqRange(...args: unknown[]): unknown
refreshMsgAbstracts(...args: unknown[]): unknown
refreshMsgAbstractsByGuildIds(...args: unknown[]): unknown
getRichMediaElement(...args: unknown[]): unknown
cancelGetRichMediaElement(...args: unknown[]): unknown
refuseGetRichMediaElement(...args: unknown[]): unknown
switchToOfflineGetRichMediaElement(...args: unknown[]): unknown
downloadRichMedia(...args: unknown[]): unknown downloadRichMedia(...args: unknown[]): unknown
getFirstUnreadMsgSeq(args: { setMsgEmojiLikes(...args: unknown[]): Promise<GeneralCallResult>
peerUid: string
guildId: string
}): unknown
getFirstUnreadCommonMsg(...args: unknown[]): unknown
getFirstUnreadAtmeMsg(...args: unknown[]): unknown
getFirstUnreadAtallMsg(...args: unknown[]): unknown
getNavigateInfo(...args: unknown[]): unknown
getChannelFreqLimitInfo(...args: unknown[]): unknown
getRecentUseEmojiList(...args: unknown[]): unknown
getRecentEmojiList(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{ getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number, result: number
errMsg: string, errMsg: string
emojiLikesList: emojiLikesList: {
Array<{ tinyId: string
tinyId: string, nickName: string
nickName: string,
headUrl: string headUrl: string
}>, }[]
cookie: string, cookie: string
isLastPage: boolean, isLastPage: boolean
isFirstPage: boolean isFirstPage: boolean
}> }>
setMsgEmojiLikesForRole(...args: unknown[]): unknown getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
clickInlineKeyboardButton(...args: unknown[]): unknown
setCurOnScreenMsg(...args: unknown[]): unknown
setCurOnScreenMsgForMsgEvent(...args: unknown[]): unknown
getMiscData(key: string): unknown
setMiscData(key: string, value: string): unknown
getBookmarkData(...args: unknown[]): unknown
setBookmarkData(...args: unknown[]): unknown
sendShowInputStatusReq(ChatType: number, EventType: number, toUid: string): Promise<unknown>
queryCalendar(...args: unknown[]): unknown
queryFirstMsgSeq(peer: Peer, ...args: unknown[]): unknown
queryRoamCalendar(...args: unknown[]): unknown
queryFirstRoamMsg(...args: unknown[]): unknown
fetchLongMsg(peer: Peer, msgId: string): unknown
fetchLongMsgWithCb(...args: unknown[]): unknown
setIsStopKernelFetchLongMsg(...args: unknown[]): unknown
insertGameResultAsMsgToDb(...args: unknown[]): unknown
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
setDraft(...args: unknown[]): unknown
getDraft(...args: unknown[]): unknown
deleteDraft(...args: unknown[]): unknown
getRecentHiddenSesionList(...args: unknown[]): unknown
setRecentHiddenSession(...args: unknown[]): unknown
delRecentHiddenSession(...args: unknown[]): unknown
getCurHiddenSession(...args: unknown[]): unknown
setCurHiddenSession(...args: unknown[]): unknown
setReplyDraft(...args: unknown[]): unknown
getReplyDraft(...args: unknown[]): unknown
deleteReplyDraft(...args: unknown[]): unknown
getFirstUnreadAtMsg(peer: Peer): unknown
clearMsgRecords(...args: unknown[]): unknown//设置已读后调用我觉得比较好 清理记录 现在别了
IsExistOldDb(...args: unknown[]): unknown
canImportOldDbMsg(...args: unknown[]): unknown
setPowerStatus(z: boolean): unknown
canProcessDataMigration(...args: unknown[]): unknown
importOldDbMsg(...args: unknown[]): unknown
stopImportOldDbMsgAndroid(...args: unknown[]): unknown
isMqqDataImportFinished(...args: unknown[]): unknown
getMqqDataImportTableNames(...args: unknown[]): unknown
getCurChatImportStatusByUin(...args: unknown[]): unknown
getDataImportUserLevel(): unknown
getMsgQRCode(...args: unknown[]): unknown
getGuestMsgAbstracts(...args: unknown[]): unknown
getGuestMsgByRange(...args: unknown[]): unknown
getGuestMsgAbstractByRange(...args: unknown[]): unknown
registerSysMsgNotification(...args: unknown[]): unknown
unregisterSysMsgNotification(...args: unknown[]): unknown
enterOrExitAio(...args: unknown[]): unknown
// this.peerUid = ""
// this.peerNickname = ""
// this.fromGroupCode = ""
// this.sig = new byte[0]
// this.selfUid = ""
// this.selfPhone = ""
// this.chatType = i2
// this.peerUid = str
// this.peerNickname = str2
// this.fromGroupCode = str3
// this.sig = bArr
// this.selfUid = str4
// this.selfPhone = str5
// this.gameSession = tempChatGameSession
prepareTempChat(args: unknown): unknown//主动临时消息 不做
sendSsoCmdReqByContend(cmd: string, param: string): Promise<unknown>
//chattype,uid->Promise<any>
getTempChatInfo(ChatType: number, Uid: string): Promise<TmpChatInfoApi>
setContactLocalTop(...args: unknown[]): unknown
switchAnonymousChat(...args: unknown[]): unknown
renameAnonyChatNick(...args: unknown[]): unknown
getAnonymousInfo(...args: unknown[]): unknown
updateAnonymousInfo(...args: unknown[]): unknown
sendSummonMsg(peer: Peer, MsgElement: unknown, MsgAttributeInfo: unknown): Promise<unknown>//频道的东西
outputGuildUnreadInfo(...args: unknown[]): unknown
checkMsgWithUrl(...args: unknown[]): unknown
checkTabListStatus(...args: unknown[]): unknown
getABatchOfContactMsgBoxInfo(...args: unknown[]): unknown
insertMsgToMsgBox(peer: Peer, msgId: string, arg: 2006): unknown
isHitEmojiKeyword(...args: unknown[]): unknown
getKeyWordRelatedEmoji(...args: unknown[]): unknown
recordEmoji(...args: unknown[]): unknown
fetchGetHitEmotionsByWord(args: Object): Promise<unknown>//表情推荐?
deleteAllRoamMsgs(...args: unknown[]): unknown//漫游消息?
packRedBag(...args: unknown[]): unknown
grabRedBag(...args: unknown[]): unknown
pullDetail(...args: unknown[]): unknown
selectPasswordRedBag(...args: unknown[]): unknown
pullRedBagPasswordList(...args: unknown[]): unknown
requestTianshuAdv(...args: unknown[]): unknown
tianshuReport(...args: unknown[]): unknown
tianshuMultiReport(...args: unknown[]): unknown
GetMsgSubType(a0: number, a1: number): unknown
setIKernelPublicAccountAdapter(...args: unknown[]): unknown
//tempChatGameSession有关
createUidFromTinyId(fromTinyId: string, toTinyId: string): unknown
dataMigrationGetDataAvaiableContactList(...args: unknown[]): unknown
dataMigrationGetMsgList(...args: unknown[]): unknown
dataMigrationStopOperation(...args: unknown[]): unknown
//新的希望
dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{
extensionData: string//"Hex"
extraData: string //""
chatType: number
chatUin: string
msgType: number
msgTime: string
msgSeq: string
msgRandom: string
}>, DataMigrationResourceInfo: {
extraData: string
filePath: string
fileSize: string
msgRandom: string
msgSeq: string
msgSubType: number
msgType: number
}): unknown
dataMigrationGetResourceLocalDestinyPath(...args: unknown[]): unknown
dataMigrationSetIOSPathPrefix(...args: unknown[]): unknown
getServiceAssistantSwitch(...args: unknown[]): unknown
setServiceAssistantSwitch(...args: unknown[]): unknown
setSubscribeFolderUsingSmallRedPoint(...args: unknown[]): unknown
clearGuildNoticeRedPoint(...args: unknown[]): unknown
clearFeedNoticeRedPoint(...args: unknown[]): unknown
clearFeedSquareRead(...args: unknown[]): unknown
IsC2CStyleChatType(...args: unknown[]): unknown
IsTempChatType(uin: number): unknown//猜的
getGuildInteractiveNotification(...args: unknown[]): unknown
getGuildNotificationAbstract(...args: unknown[]): unknown
setFocusOnBase(...args: unknown[]): unknown
queryArkInfo(...args: unknown[]): unknown
queryUserSecQuality(...args: unknown[]): unknown
getGuildMsgAbFlag(...args: unknown[]): unknown
getGroupMsgStorageTime(): unknown//这是嘛啊
getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi>
} }

View File

@@ -1,22 +1,25 @@
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 setBuddyProfileLike(...args: unknown[]): GeneralCallResult & { succCounts: number }
removeKernelProfileLikeListener(listener: unknown): void
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
isNull(): boolean
} }

View File

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

View File

@@ -1,195 +1,23 @@
import { GetFileListParam, MessageElement, Peer } from '../types' import { GetFileListParam, Peer } from '../types'
import { GeneralCallResult } from './common' import { GeneralCallResult } from './common'
export enum UrlFileDownloadType {
KUNKNOWN,
KURLFILEDOWNLOADPRIVILEGEICON,
KURLFILEDOWNLOADPHOTOWALL,
KURLFILEDOWNLOADQZONE,
KURLFILEDOWNLOADCOMMON,
KURLFILEDOWNLOADINSTALLAPP
}
export enum RMBizTypeEnum {
KUNKNOWN,
KC2CFILE,
KGROUPFILE,
KC2CPIC,
KGROUPPIC,
KDISCPIC,
KC2CVIDEO,
KGROUPVIDEO,
KC2CPTT,
KGROUPPTT,
KFEEDCOMMENTPIC,
KGUILDFILE,
KGUILDPIC,
KGUILDPTT,
KGUILDVIDEO
}
export interface CommonFileInfo {
bizType: number
chatType: number
elemId: string
favId: string
fileModelId: string
fileName: string
fileSize: string
md5: string
md510m: string
msgId: string
msgTime: string
parent: string
peerUid: string
picThumbPath: Array<string>
sha: string
sha3: string
subId: string
uuid: string
}
export interface NodeIKernelRichMediaService { export interface NodeIKernelRichMediaService {
//getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb)
// public enum VideoCodecFormatType {
// KCODECFORMATH264,
// KCODECFORMATH265,
// KCODECFORMATH266,
// KCODECFORMATAV1
// }
// public enum VideoRequestWay {
// KUNKNOW,
// KHAND,
// KAUTO
// }
getVideoPlayUrl(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, VideoRequestWay: number): Promise<unknown>
//exParams (RMReqExParams)
// this.downSourceType = i2
// this.triggerType = i3
//peer, msgId, elemId, videoCodecFormat, exParams
// 1 0 频道在用
// 1 1
// 0 2
// public static final int KCOMMONREDENVELOPEMSGTYPEINMSGBOX = 1007
// public static final int KDOWNSOURCETYPEAIOINNER = 1
// public static final int KDOWNSOURCETYPEBIGSCREEN = 2
// public static final int KDOWNSOURCETYPEHISTORY = 3
// public static final int KDOWNSOURCETYPEUNKNOWN = 0
// public static final int KTRIGGERTYPEAUTO = 1
// public static final int KTRIGGERTYPEMANUAL = 0
getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & { 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: {
url: string, url: string
isHttps: boolean, isHttps: boolean
httpsDomain: string httpsDomain: string
}>, }[]
videoCodecFormat: number videoCodecFormat: number
} }
}> }>
getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown deleteGroupFolder(groupCode: string, folderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>
// this.senderUid = "" createGroupFolder(groupCode: string, folderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: unknown, groupItem: unknown[] } }>
// 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 & { getGroupFileList(groupCode: string, params: GetFileListParam): Promise<GeneralCallResult & {
groupSpaceResult: { groupSpaceResult: {
@@ -202,69 +30,14 @@ export interface NodeIKernelRichMediaService {
} }
}> }>
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 moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
transGroupFile(arg1: unknown, arg2: unknown): unknown deleteGroupFile(groupCode: string, params: Array<number>, files: Array<string>): Promise<GeneralCallResult & {
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: { transGroupFileResult: {
result: any result: unknown
successFileIdList: Array<any> successFileIdList: Array<unknown>
failFileIdList: Array<any> 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,13 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelRobotService {
getRobotUinRange(req: unknown): Promise<GeneralCallResult & {
response: {
version: number
robotUinRanges: {
minUin: string
maxUin: string
}[]
}
}>
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,4 +9,4 @@ export * from './NodeIKernelUixConvertService'
export * from './NodeIKernelRichMediaService' export * from './NodeIKernelRichMediaService'
export * from './NodeIKernelTicketService' export * from './NodeIKernelTicketService'
export * from './NodeIKernelTipOffService' export * from './NodeIKernelTipOffService'
export * from './NodeIKernelSearchService' export * from './NodeIKernelRobotService'

View File

@@ -22,9 +22,9 @@ export interface Group {
hasModifyConfGroupName: boolean hasModifyConfGroupName: boolean
remarkName: string remarkName: string
hasMemo: boolean hasMemo: boolean
groupShutupExpireTime: string //"0", groupShutupExpireTime: string
personShutupExpireTime: string //"0", personShutupExpireTime: string
discussToGroupUin: string //"0", discussToGroupUin: string
discussToGroupMaxMsgSeq: number discussToGroupMaxMsgSeq: number
discussToGroupTime: number discussToGroupTime: number
groupFlagExt: number //1073938496, groupFlagExt: number //1073938496,
@@ -32,10 +32,11 @@ export interface Group {
groupCreditLevel: number //0, groupCreditLevel: number //0,
groupFlagExt3: number //0, groupFlagExt3: number //0,
groupOwnerId: { groupOwnerId: {
memberUin: string //"0", memberUin: string
memberUid: string //"u_fbf8N7aeuZEnUiJAbQ9R8Q" memberUid: string
} }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段 members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string
} }
export enum GroupMemberRole { export enum GroupMemberRole {
@@ -61,6 +62,113 @@ export interface GroupMember {
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean isChangeRole: boolean
joinTime: string joinTime: number
lastSpeakTime: string lastSpeakTime: number
memberLevel: number
}
export interface PublishGroupBulletinReq {
text: string
picInfo?: {
id: string
width: number
height: number
}
oldFeedsId: ''
pinned: number
confirmRequired: number
}
export interface GroupAllInfo {
groupCode: string
ownerUid: string
groupFlag: number
groupFlagExt: number
maxMemberNum: number
memberNum: number
groupOption: number
classExt: number
groupName: string
fingerMemo: string
groupQuestion: string
certType: number
shutUpAllTimestamp: number
shutUpMeTimestamp: number //解除禁言时间
groupTypeFlag: number
privilegeFlag: number
groupSecLevel: number
groupFlagExt3: number
isConfGroup: number
isModifyConfGroupFace: number
isModifyConfGroupName: number
noFigerOpenFlag: number
noCodeFingerOpenFlag: number
groupFlagExt4: number
groupMemo: string
cmdUinMsgSeq: number
cmdUinJoinTime: number
cmdUinUinFlag: number
cmdUinMsgMask: number
groupSecLevelInfo: number
cmdUinPrivilege: number
cmdUinFlagEx2: number
appealDeadline: number
remarkName: number
isTop: boolean
richFingerMemo: string
groupAnswer: string
joinGroupAuth: string
isAllowModifyConfGroupName: number
}
export interface GroupBulletinListResult {
groupCode: string
srvCode: number
readOnly: number
role: number
inst: unknown[]
feeds: {
uin: string
feedId: string
publishTime: string
msg: {
text: string
textFace: string
pics: {
id: string
width: number
height: number
}[]
title: string
}
type: number
fn: number
cn: number
vn: number
settings: {
isShowEditCard: number
remindTs: number
tipWindowType: number
confirmRequired: number
}
pinned: number
readNum: number
is_read: number
is_all_confirm: number
}[]
groupInfo: {
groupCode: string
classId: number
}
gln: number
tst: number
publisherInfos: {
uin: string
nick: string
avatar: string
}[]
server_time: string
svrt: string
nextIndex: number
jointime: string
} }

View File

@@ -1,124 +1,108 @@
import { GroupMemberRole } from './group' import { GroupMemberRole } from './group'
import { GeneralCallResult } from '../services'
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
}
export enum ElementType { export enum ElementType {
UNKNOWN = 0, Text = 1,
TEXT = 1, Pic = 2,
PIC = 2, File = 3,
FILE = 3, Ptt = 4,
PTT = 4, Video = 5,
VIDEO = 5, Face = 6,
FACE = 6, Reply = 7,
REPLY = 7, GrayTip = 8,
WALLET = 9, Ark = 10,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字 MarketFace = 11,
ARK = 10, LiveGift = 12,
MFACE = 11, StructLongMsg = 13,
LIVEGIFT = 12, Markdown = 14,
STRUCTLONGMSG = 13, Giphy = 15,
MARKDOWN = 14, MultiForward = 16,
GIPHY = 15, InlineKeyboard = 17,
MULTIFORWARD = 16, Calendar = 19,
INLINEKEYBOARD = 17, YoloGameResult = 20,
INTEXTGIFT = 18, AvRecord = 21,
CALENDAR = 19, TofuRecord = 23,
YOLOGAMERESULT = 20, FaceBubble = 27,
AVRECORD = 21, ShareLocation = 28,
FEED = 22, TaskTopMsg = 29,
TOFURECORD = 23, RecommendedMsg = 43,
ACEBUBBLE = 24, ActionBar = 44
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
} }
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT elementType: ElementType.Text
elementId: '' elementId: ''
textElement: TextElement textElement: TextElement
} }
export interface SendPttElement { export interface SendPttElement {
elementType: ElementType.PTT elementType: ElementType.Ptt
elementId: '' elementId: ''
pttElement: { pttElement: Partial<PttElement>
fileName: string
filePath: string
md5HexStr: string
fileSize: number
duration: number // 单位是秒
formatType: number
voiceType: number
voiceChangeType: number
canConvert2Text: boolean
waveAmplitudes: number[]
fileSubId: ''
playState: number
autoConvertText: number
}
}
export enum PicType {
gif = 2000,
jpg = 1000,
}
export enum PicSubType {
normal = 0, // 普通图片,大图
face = 1, // 表情包小图
} }
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC elementType: ElementType.Pic
elementId: '' elementId: ''
picElement: { picElement: Partial<PicElement>
md5HexStr: string
fileSize: number | string
picWidth: number
picHeight: number
fileName: string
sourcePath: string
original: boolean
picType: PicType
picSubType: PicSubType
fileUuid: string
fileSubId: string
thumbFileSize: number
summary: string
}
} }
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 {
elementType: ElementType.FACE elementType: ElementType.Face
elementId: '' elementId: ''
faceElement: FaceElement faceElement: FaceElement
} }
export interface SendMarketFaceElement { export interface SendMarketFaceElement {
elementType: ElementType.MFACE elementType: ElementType.MarketFace
elementId: ''
marketFaceElement: MarketFaceElement marketFaceElement: MarketFaceElement
} }
export interface SendFileElement {
elementType: ElementType.File
elementId: ''
fileElement: FileElement
}
export interface SendVideoElement {
elementType: ElementType.Video
elementId: ''
videoElement: VideoElement
}
export interface SendArkElement {
elementType: ElementType.Ark
elementId: ''
arkElement: ArkElement
}
export type SendMessageElement =
| SendTextElement
| SendPttElement
| SendPicElement
| SendReplyElement
| SendFaceElement
| SendMarketFaceElement
| SendFileElement
| SendVideoElement
| SendArkElement
export enum AtType {
Unknown,
All,
One,
}
export interface TextElement { export interface TextElement {
content: string content: string
atType: number atType: AtType
atUid: string atUid: string
atTinyId: string atTinyId: string
atNtUid: string atNtUid: string
@@ -129,6 +113,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 {
@@ -149,91 +139,6 @@ export interface FileElement {
fileBizId?: number fileBizId?: number
} }
export interface SendFileElement {
elementType: ElementType.FILE
elementId: ''
fileElement: FileElement
}
export interface SendVideoElement {
elementType: ElementType.VIDEO
elementId: ''
videoElement: VideoElement
}
export interface SendArkElement {
elementType: ElementType.ARK
elementId: ''
arkElement: ArkElement
}
export type SendMessageElement =
| SendTextElement
| SendPttElement
| SendPicElement
| SendReplyElement
| SendFaceElement
| SendMarketFaceElement
| SendFileElement
| SendVideoElement
| SendArkElement
export enum AtType {
notAt = 0,
atAll = 1,
atUser = 2,
}
export enum ChatType {
friend = 1,
group = 2,
temp = 100,
}
// 来自Android分析
export enum ChatType2 {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
}
export interface PttElement { export interface PttElement {
canConvert2Text: boolean canConvert2Text: boolean
duration: number // 秒数 duration: number // 秒数
@@ -244,7 +149,7 @@ export interface PttElement {
fileSize: string // "4261" fileSize: string // "4261"
fileSubId: string // "0" fileSubId: string // "0"
fileUuid: string // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV" fileUuid: string // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string // 1 formatType: number // 1
invalidState: number // 0 invalidState: number // 0
md5HexStr: string // "e4d09c784d5a2abcb2f9980bdc7acfe6" md5HexStr: string // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number // 0 playState: number // 0
@@ -255,6 +160,7 @@ export interface PttElement {
voiceChangeType: number // 0 voiceChangeType: number // 0
voiceType: number // 0 voiceType: number // 0
waveAmplitudes: number[] waveAmplitudes: number[]
autoConvertText: number
} }
export interface ArkElement { export interface ArkElement {
@@ -266,6 +172,16 @@ export interface ArkElement {
export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn' export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn'
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn' export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export enum PicType {
GIF = 2000,
JPEG = 1000,
}
export enum PicSubType {
Normal = 0, // 普通图片,大图
Face = 1, // 表情包小图
}
export interface PicElement { export interface PicElement {
picSubType: PicSubType picSubType: PicSubType
picType: PicType // 有这玩意儿吗 picType: PicType // 有这玩意儿吗
@@ -275,21 +191,89 @@ 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 interface TipAioOpGrayTipElement {
operateType: number
peerUid: string
fromGrpCodeOfTmpChat: string
}
export enum TipGroupElementType {
MemberIncrease = 1,
Kicked = 3, // 被移出群
Ban = 8,
}
export interface TipGroupElement {
type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
role: number
groupName: string // 暂时获取不到
memberUid: string
memberNick: string
memberRemark: string
adminUid: string
adminNick: string
adminRemark: string
createGroup: null
memberAdd?: {
showType: number
otherAdd?: {
uid: string
name: string
}
otherAddByOtherQRCode?: unknown
otherAddByYourQRCode?: unknown
youAddByOtherQRCode?: unknown
otherInviteOther?: unknown
otherInviteYou?: unknown
youInviteOther?: unknown
}
shutUp?: {
curTime: string
duration: string // 禁言时间,秒
admin: {
uid: string
card: string
name: string
role: GroupMemberRole
}
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
}
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,21 +283,22 @@ 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
templParam: Map<string, string>
members: Map<string, string> // uid -> remark
} }
jsonGrayTipElement: { jsonGrayTipElement?: {
busiId: number busiId: string
jsonStr: string jsonStr: string
} }
} }
export enum FaceIndex { export enum FaceIndex {
dice = 358, Dice = 358,
RPS = 359, // 石头剪刀布 RPS = 359, // 石头剪刀布
} }
@@ -328,6 +313,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 +321,12 @@ export interface MarketFaceElement {
faceName?: string faceName?: string
emojiId: string emojiId: string
key: string key: string
imageWidth?: number
imageHeight?: number
supportSize?: {
width: number
height: number
}[]
} }
export interface VideoElement { export interface VideoElement {
@@ -350,7 +342,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 // 未知
@@ -384,6 +376,7 @@ export interface InlineKeyboardElementRowButton {
enter: false enter: false
subscribeDataTemplateIds: [] subscribeDataTemplateIds: []
} }
export interface InlineKeyboardElement { export interface InlineKeyboardElement {
rows: [ rows: [
{ {
@@ -392,56 +385,9 @@ export interface InlineKeyboardElement {
] ]
} }
export interface TipAioOpGrayTipElement { export interface StructLongMsgElement {
// 这是什么提示来着? xmlContent: string
operateType: number resId: string
peerUid: string
fromGrpCodeOfTmpChat: string
}
export enum TipGroupElementType {
memberIncrease = 1,
kicked = 3, // 被移出群
ban = 8,
}
export interface TipGroupElement {
type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
role: 0 // 暂时不知
groupName: string // 暂时获取不到
memberUid: string
memberNick: string
memberRemark: string
adminUid: string
adminNick: string
adminRemark: string
createGroup: null
memberAdd?: {
showType: 1
otherAdd: null
otherAddByOtherQRCode: null
otherAddByYourQRCode: null
youAddByOtherQRCode: null
otherInviteOther: null
otherInviteYou: null
youInviteOther: null
}
shutUp?: {
curTime: string
duration: string // 禁言时间,秒
admin: {
uid: string
card: string
name: string
role: GroupMemberRole
}
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
} }
export interface MultiForwardMsgElement { export interface MultiForwardMsgElement {
@@ -450,52 +396,44 @@ export interface MultiForwardMsgElement {
fileName: string fileName: string
} }
export enum ChatType {
C2C = 1,
Group = 2,
TempC2CFromGroup = 100,
}
export interface RawMessage { export interface RawMessage {
msgId: string msgId: string
msgType: number msgType: number
subMsgType: number subMsgType: number
msgShortId?: number // 自己维护的消息id
msgTime: string // 时间戳,秒 msgTime: string // 时间戳,秒
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
sendNickName: string sendNickName: string
sendMemberName?: string // 发送者群名片 sendMemberName?: string // 发送者群名片
sendRemarkName?: string // 发送者好友备注
chatType: ChatType chatType: ChatType
sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string // 撤回时间, "0"是没有撤回 recallTime: string // 撤回时间, "0"是没有撤回
records: RawMessage[] records: RawMessage[]
elements: { elements: MessageElement[]
elementId: string peerName: string
elementType: ElementType multiTransInfo?: {
replyElement: { status: number
sourceMsgIdInRecords: string msgId: number
senderUid: string // 原消息发送者QQ号 friendFlag: number
sourceMsgIsIncPic: boolean // 原消息是否有图片 fromFaceUrl: string
sourceMsgText: string
replayMsgSeq: string // 源消息的msgSeq可以通过这个找到源消息的msgId
} }
textElement: { emojiLikesList: {
atType: AtType emojiId: string
atUid: string // QQ号 emojiType: string
content: string likesCnt: string
atNtUid: string // uid号 isClicked: boolean
}
picElement: PicElement
pttElement: PttElement
arkElement: ArkElement
grayTipElement: GrayTipElement
faceElement: FaceElement
videoElement: VideoElement
fileElement: FileElement
marketFaceElement: MarketFaceElement
inlineKeyboardElement: InlineKeyboardElement
markdownElement: MarkdownElement
multiForwardMsgElement: MultiForwardMsgElement
}[] }[]
} }
@@ -511,7 +449,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 +457,126 @@ 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?: StructLongMsgElement
multiForwardMsgElement?: MultiForwardMsgElement multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: any giphyElement?: unknown
walletElement?: null
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 RichMediaDownloadCompleteNotify {
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 GroupFileInfo {
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
}
export interface QueryMsgsParams {
chatInfo: Peer
filterMsgType: []
filterSendersUid: string[]
filterMsgFromTime: string
filterMsgToTime: string
pageLimit: number
isReverseOrder: boolean
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi extends GeneralCallResult {
tmpChatInfo?: {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
}
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
folderId?: string
} }

View File

@@ -1,13 +1,19 @@
export enum GroupNotifyTypes { export enum GroupNotifyType {
INVITE_ME = 1, InvitedByMember = 1,
INVITED_JOIN = 4, // 有人接受了邀请入群 RefuseInvited,
JOIN_REQUEST_BY_INVITED = 5, // 有人邀请了别人入群 RefusedByAdminiStrator,
JOIN_REQUEST = 7, AgreedTojoinDirect, // 有人接受了邀请入群
ADMIN_SET = 8, InvitedNeedAdminiStratorPass, // 有人邀请了别人入群
KICK_MEMBER = 9, AgreedToJoinByAdminiStrator,
MEMBER_EXIT = 11, // 主动退出 RequestJoinNeedAdminiStratorPass,
ADMIN_UNSET = 12, // 我被取消管理员 SetAdmin,
ADMIN_UNSET_OTHER = 13, // 其他人取消管理员 KickMemberNotifyAdmin,
KickMemberNotifyKicked,
MemberLeaveNotifyAdmin, // 主动退出
CancelAdminNotifyCanceled, // 我被取消管理员
CancelAdminNotifyAdmin, // 其他人取消管理员
TransferGroupNotifyOldowner,
TransferGroupNotifyAdmin
} }
export interface GroupNotifies { export interface GroupNotifies {
@@ -17,17 +23,18 @@ export interface GroupNotifies {
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
IGNORE = 0, Init, // 初始化
WAIT_HANDLE = 1, Unhandle, // 未处理
APPROVE = 2, Agreed, // 同意
REJECT = 3, Refused, // 拒绝
Ignored // 忽略
} }
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 } // 操作者
@@ -49,20 +56,8 @@ export enum GroupRequestOperateTypes {
} }
export enum BuddyReqType { export enum BuddyReqType {
KMEINITIATOR, MsgInfo = 12,
KPEERINITIATOR, MeInitiatorWaitPeerConfirm = 13,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
@@ -84,41 +79,3 @@ export interface FriendRequestNotify {
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
} }
export enum MemberExtSourceType {
DEFAULTTYPE = 0,
TITLETYPE = 1,
NEWGROUPTYPE = 2,
}
export interface GroupExtParam {
groupCode: string
seq: string
beginUin: string
dataTime: string
uinList: Array<string>
uinNum: string
groupType: string
richCardNameVer: string
sourceType: MemberExtSourceType
memberExtFilter: {
memberLevelInfoUin: number
memberLevelInfoPoint: number
memberLevelInfoActiveDay: number
memberLevelInfoLevel: number
memberLevelInfoName: number
levelName: number
dataTime: number
userShowFlag: number
sysShowFlag: number
timeToUpdate: number
nickName: number
specialTitle: number
levelNameNew: number
userShowFlagNew: number
msgNeedField: number
cmdUinFlagExt3Grocery: number
memberIcon: number
memberInfoSeq: number
}
}

View File

@@ -105,7 +105,7 @@ export interface BaseInfo {
phoneNum: string phoneNum: string
categoryId: number categoryId: number
richTime: number richTime: number
richBuffer: string richBuffer: Uint8Array
} }
interface MusicInfo { interface MusicInfo {
@@ -124,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
} }
@@ -142,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
@@ -156,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 {
@@ -180,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
@@ -200,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 {
@@ -221,11 +221,6 @@ interface RelationFlags {
isHidePrivilegeIcon: number isHidePrivilegeIcon: number
} }
export interface FriendV2 extends SimpleInfo {
categoryId?: number
categroyName?: string
}
interface CommonExt { interface CommonExt {
constellation: number constellation: number
shengXiao: number shengXiao: number
@@ -241,7 +236,7 @@ interface CommonExt {
address: string address: string
regTime: number regTime: number
interest: string interest: string
labels: any[] labels: string[]
qqLevel: QQLevel qqLevel: QQLevel
} }
@@ -255,7 +250,7 @@ interface PhotoWall {
picList: Pic[] picList: Pic[]
} }
export interface UserDetailInfoListenerArg { export interface UserDetailInfo {
uid: string uid: string
uin: string uin: string
simpleInfo: SimpleInfo simpleInfo: SimpleInfo
@@ -323,12 +318,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
@@ -344,3 +339,21 @@ export interface UserDetailInfoByUin {
vipNameColorId: string vipNameColorId: string
} }
} }
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}

View File

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

View File

@@ -1,49 +1,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> {
@@ -14,17 +13,17 @@ export class OB11Response {
} }
} }
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,11 +1,7 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import fsPromise from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { getConfigUtil } from '@/common/config'
import { NTQQFileApi, NTQQGroupApi, NTQQUserApi, NTQQFriendApi, NTQQMsgApi } from '@/ntqqapi/api'
import { ActionName } from '../types' import { ActionName } from '../types'
import { UUIDConverter } from '@/common/utils/helper' import { Peer, ElementType } from '@/ntqqapi/types'
import { Peer, ChatType, ElementType } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique'
export interface GetFilePayload { export interface GetFilePayload {
file: string // 文件名或者fileUuid file: string // 文件名或者fileUuid
@@ -20,17 +16,20 @@ export interface GetFileResponse {
} }
export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
// forked from https://github.com/NapNeko/NapCatQQ/blob/6f6b258f22d7563f15d84e7172c4d4cbb547f47e/src/onebot11/action/file/GetFile.ts#L44 payloadSchema = Schema.object({
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { file: Schema.string().required()
const { enableLocalFile2Url } = getConfigUtil().getConfig() })
let fileCache = await MessageUnique.getFileCacheById(String(payload.file)) protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
const { enableLocalFile2Url } = this.adapter.config
let fileCache = await this.ctx.store.getFileCacheById(payload.file)
if (!fileCache?.length) { if (!fileCache?.length) {
fileCache = await MessageUnique.getFileCacheByName(String(payload.file)) fileCache = await this.ctx.store.getFileCacheByName(payload.file)
} }
if (fileCache?.length) { if (fileCache?.length) {
const downloadPath = await NTQQFileApi.downloadMedia( const downloadPath = await this.ctx.ntFileApi.downloadMedia(
fileCache[0].msgId, fileCache[0].msgId,
fileCache[0].chatType, fileCache[0].chatType,
fileCache[0].peerUid, fileCache[0].peerUid,
@@ -49,8 +48,8 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
peerUid: fileCache[0].peerUid, peerUid: fileCache[0].peerUid,
guildId: '' guildId: ''
} }
if (fileCache[0].elementType === ElementType.PIC) { if (fileCache[0].elementType === ElementType.Pic) {
const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId]) const msgList = await this.ctx.ntMsgApi.getMsgsByMsgId(peer, [fileCache[0].msgId])
if (msgList.msgList.length === 0) { if (msgList.msgList.length === 0) {
throw new Error('msg not found') throw new Error('msg not found')
} }
@@ -59,13 +58,13 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
if (!findEle) { if (!findEle) {
throw new Error('element not found') throw new Error('element not found')
} }
res.url = await NTQQFileApi.getImageUrl(findEle.picElement) res.url = await this.ctx.ntFileApi.getImageUrl(findEle.picElement!)
} else if (fileCache[0].elementType === ElementType.VIDEO) { } else if (fileCache[0].elementType === ElementType.Video) {
res.url = await NTQQFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId) res.url = await this.ctx.ntFileApi.getVideoUrl(peer, fileCache[0].msgId, fileCache[0].elementId)
} }
if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) { if (enableLocalFile2Url && downloadPath && (res.file === res.url || res.url === undefined)) {
try { try {
res.base64 = await fsPromise.readFile(downloadPath, 'base64') res.base64 = await readFile(downloadPath, 'base64')
} catch (e) { } catch (e) {
throw new Error('文件下载失败. ' + e) throw new Error('文件下载失败. ' + e)
} }
@@ -79,11 +78,12 @@ export abstract class GetFileBase extends BaseAction<GetFilePayload, GetFileResp
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

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

View File

@@ -1,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,27 +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 { MessageUnique } from '@/common/utils/MessageUnique'
interface Payload { interface Payload {
message_id: number | string message_id: number | string
} }
export default class GoCQHTTPDelEssenceMsg extends BaseAction<Payload, any> { export class DeleteEssenceMsg 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) {
if (!payload.message_id) { const msg = await this.ctx.store.getMsgInfoByShortId(+payload.message_id)
throw Error('message_id不能为空')
}
const msg = await MessageUnique.getMsgIdAndPeerByShortId(+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.Peer.peerUid, msg.peer.peerUid,
msg.MsgId, msg.msgId,
) )
} }
} }

View File

@@ -1,17 +1,22 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types' import { ActionName } from '../types'
import { NTQQGroupApi } from '@/ntqqapi/api'
interface Payload { interface Payload {
group_id: string | number group_id: number | string
file_id: string file_id: string
busid?: 102 busid: number | string
} }
export class GoCQHTTPDelGroupFile extends BaseAction<Payload, void> { export class DelGroupFile extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DelGroupFile actionName = ActionName.GoCQHTTP_DelGroupFile
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_id: Schema.string().required(),
busid: Schema.union([Number, String]).default(102)
})
async _handle(payload: Payload) { async _handle(payload: Payload) {
await NTQQGroupApi.delGroupFile(payload.group_id.toString(), [payload.file_id]) 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

@@ -0,0 +1,21 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
user_id: number | string
}
export class DeleteFriend extends BaseAction<Payload, null> {
actionName = ActionName.GoCQHTTP_DeleteFriend
payloadSchema = Schema.object({
user_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const uin = payload.user_id.toString()
const uid = await this.ctx.ntUserApi.getUidByUin(uin)
if (!uid) throw new Error('无法获取用户信息')
await this.ctx.ntFriendApi.delBuddy(uid)
return null
}
}

View File

@@ -1,10 +1,12 @@
import BaseAction from '../BaseAction'
import { ActionName } from '../types'
import fs from 'fs' import fs from 'fs'
import fsPromise from 'fs/promises' import fsPromise from 'fs/promises'
import path from 'node:path' import path from 'node:path'
import { calculateFileMD5, httpDownload, TEMP_DIR } from '@/common/utils' 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
@@ -18,8 +20,13 @@ 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
@@ -30,8 +37,8 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
await fsPromise.writeFile(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)
const buffer = await httpDownload({ url: payload.url, headers: headers }) const res = await fetchFile(payload.url, headers)
await fsPromise.writeFile(filePath, buffer) await fsPromise.writeFile(filePath, res.data)
} else { } else {
throw new Error('不存在任何文件, 无法下载') throw new Error('不存在任何文件, 无法下载')
} }
@@ -50,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 { OB11Constructor } from '../../constructor'
import { ActionName } from '../types' import { ActionName } from '../types'
import { MessageUnique } from '@/common/utils/MessageUnique' 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,47 @@ interface Payload {
} }
interface Response { interface Response {
messages: (OB11Message & { content: OB11MessageData })[] messages: OB11ForwardMessage[]
} }
export class GoCQHTTGetForwardMsgAction extends BaseAction<Payload, Response> { 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({
message_id: Schema.string(),
id: Schema.string()
})
protected async _handle(payload: Payload) {
const msgId = payload.id || payload.message_id const msgId = payload.id || payload.message_id
if (!msgId) { if (!msgId) {
throw Error('message_id不能为空') throw Error('message_id不能为空')
} }
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId) const rootMsgId = await this.ctx.store.getShortIdByMsgId(msgId)
const rootMsg = await MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || +msgId) const rootMsg = await this.ctx.store.getMsgInfoByShortId(rootMsgId || +msgId)
if (!rootMsg) { if (!rootMsg) {
throw Error('msg not found') throw Error('msg not found')
} }
const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId) const data = await this.ctx.ntMsgApi.getMultiMsg(rootMsg.peer, rootMsg.msgId, rootMsg.msgId)
if (data?.result !== 0) { if (data?.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg) throw Error('找不到相关的聊天记录' + data?.errMsg)
} }
const msgList = data.msgList const messages: (OB11ForwardMessage | undefined)[] = await Promise.all(
const messages = await Promise.all( data.msgList.map(async (msg) => {
msgList.map(async (msg) => { const res = await OB11Entities.message(this.ctx, msg)
const resMsg = await OB11Constructor.message(msg) if (res) {
resMsg.message_id = MessageUnique.createMsg({ return {
chatType: msg.chatType, content: res.message,
peerUid: msg.peerUid, sender: {
}, msg.msgId)! nickname: res.sender.nickname,
return resMsg user_id: res.sender.user_id
}), },
) time: res.time,
messages.map(v => { message_format: res.message_format,
const msg = v as Partial<OB11ForwardMessage> message_type: res.message_type
msg.content = msg.message }
delete msg.message }
}) })
return { messages } )
return { messages: filterNullable(messages) }
} }
} }

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,50 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types'
interface Payload {
group_id: number | string
}
interface EssenceMsg {
sender_id: number
sender_nick: string
sender_time: number
operator_id: number
operator_nick: string
operator_time: number
message_id: number
}
export class GetEssenceMsgList extends BaseAction<Payload, EssenceMsg[]> {
actionName = ActionName.GoCQHTTP_GetEssenceMsgList
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const groupCode = payload.group_id.toString()
const peer = {
guildId: '',
chatType: ChatType.Group,
peerUid: groupCode
}
const essence = await this.ctx.ntGroupApi.queryCachedEssenceMsg(groupCode)
const data: EssenceMsg[] = []
for (const item of essence.items) {
const { msgList } = await this.ctx.ntMsgApi.queryMsgsWithFilterExBySeq(peer, String(item.msgSeq), '0')
const sourceMsg = msgList.find(e => e.msgRandom === String(item.msgRandom))
if (!sourceMsg) continue
data.push({
sender_id: +item.msgSenderUin,
sender_nick: item.msgSenderNick,
sender_time: +sourceMsg.msgTime,
operator_id: +item.opUin,
operator_nick: item.opNick,
operator_time: item.opTime,
message_id: this.ctx.store.createMsgShortId(peer, sourceMsg.msgId)
})
}
return data
}
}

View File

@@ -0,0 +1,82 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { pathToFileURL } from 'node:url'
import { ChatType } from '@/ntqqapi/types'
import { GroupFileInfo } from '@/ntqqapi/types'
export interface Payload {
group_id: number | string
file_id: string
busid?: number
}
export interface Response {
url: string
}
export class GetGroupFileUrl extends BaseAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupFileUrl
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required(),
file_id: Schema.string().required()
})
protected async _handle(payload: Payload) {
const file = await this.ctx.store.getFileCacheById(payload.file_id)
if (file.length > 0) {
const { msgId, chatType, peerUid, elementId } = file[0]
const path = await this.ctx.ntFileApi.downloadMedia(msgId, chatType, peerUid, elementId)
return {
url: pathToFileURL(path).href
}
} else {
const groupId = payload.group_id.toString()
const modelId = await this.search(groupId, payload.file_id)
if (modelId) {
const peer = {
chatType: ChatType.Group,
peerUid: groupId,
guildId: ''
}
const path = await this.ctx.ntFileApi.downloadFileForModelId(peer, modelId)
return {
url: pathToFileURL(path).href
}
}
throw new Error('file not found')
}
}
private async search(groupId: string, fileId: string, folderId?: string) {
let modelId: string | undefined
let nextIndex: number | undefined
const folders: GroupFileInfo['item'] = []
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId
})
const file = res.item.find(item => item.fileInfo?.fileId === fileId)
if (file) {
modelId = file.fileInfo?.fileModelId
break
}
folders.push(...res.item.filter(item => item.folderInfo?.totalFileCount))
nextIndex = res.nextIndex
}
if (!modelId) {
for (const item of folders) {
const res = await this.search(groupId, fileId, item.folderInfo?.folderId)
if (res) {
modelId = res
break
}
}
}
return modelId
}
}

View File

@@ -0,0 +1,62 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot11/types'
import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload {
group_id: number | string
folder_id: string
}
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()
})
async _handle(payload: Payload) {
const groupId = payload.group_id.toString()
const data: GroupFileInfo['item'] = []
let nextIndex: number | undefined
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
folderId: payload.folder_id
})
data.push(...res.item)
nextIndex = res.nextIndex
}
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,47 +1,45 @@
import BaseAction from '../BaseAction' import { BaseAction, Schema } from '../BaseAction'
import { OB11Message } from '../../types' import { OB11Message } from '../../types'
import { ActionName } from '../types' import { ActionName } from '../types'
import { ChatType } from '@/ntqqapi/types' import { ChatType } from '@/ntqqapi/types'
import { NTQQMsgApi } from '@/ntqqapi/api/msg' import { OB11Entities } from '../../entities'
import { OB11Constructor } from '../../constructor'
import { RawMessage } from '@/ntqqapi/types' import { RawMessage } from '@/ntqqapi/types'
import { MessageUnique } from '@/common/utils/MessageUnique' import { filterNullable, parseBool } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
message_seq?: number message_seq?: number | string
count?: number count: number | string
reverseOrder?: boolean 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.union([Boolean, Schema.transform(String, parseBool)]).default(false)
})
protected async _handle(payload: Payload): Promise<Response> { protected async _handle(payload: Payload): Promise<Response> {
const count = payload.count || 20 const { count, reverseOrder } = payload
const isReverseOrder = payload.reverseOrder || true const peer = { chatType: ChatType.Group, peerUid: payload.group_id.toString() }
const peer = { chatType: ChatType.group, peerUid: payload.group_id.toString() } let msgList: RawMessage[]
let msgList: RawMessage[] | undefined if (!payload.message_seq || payload.message_seq === '0') {
// 包含 message_seq 0 msgList = (await this.ctx.ntMsgApi.getAioFirstViewLatestMsgs(peer, +count)).msgList
if (!payload.message_seq) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count))?.msgList
} else { } else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId const startMsgId = (await this.ctx.store.getMsgInfoByShortId(+payload.message_seq))?.msgId
if (!startMsgId) throw `消息${payload.message_seq}不存在` if (!startMsgId) throw new Error(`消息${payload.message_seq}不存在`)
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList msgList = (await this.ctx.ntMsgApi.getMsgHistory(peer, startMsgId, +count)).msgList
} }
if (!msgList?.length) throw '未找到消息' if (!msgList?.length) throw new Error('未找到消息')
if (isReverseOrder) msgList.reverse() if (reverseOrder) msgList.reverse()
await Promise.all( const ob11MsgList = await Promise.all(msgList.map(msg => OB11Entities.message(this.ctx, msg)))
msgList.map(async msg => { return { messages: filterNullable(ob11MsgList) }
msg.msgShortId = MessageUnique.createMsg({ chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId)
})
)
const ob11MsgList = await Promise.all(msgList.map((msg) => OB11Constructor.message(msg)))
return { messages: ob11MsgList }
} }
} }

View File

@@ -0,0 +1,48 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
group_id: number | string
}
interface Notice {
sender_id: number
publish_time: number
message: {
text: string
images: {
height: string
width: string
id: string
}[]
}
}
export class GetGroupNotice extends BaseAction<Payload, Notice[]> {
actionName = ActionName.GoCQHTTP_GetGroupNotice
payloadSchema = Schema.object({
group_id: Schema.union([Number, String]).required()
})
protected async _handle(payload: Payload) {
const data = await this.ctx.ntGroupApi.getGroupBulletinList(payload.group_id.toString())
const result: Notice[] = []
for (const feed of data.result.feeds) {
result.push({
sender_id: +feed.uin,
publish_time: +feed.publishTime,
message: {
text: feed.msg.text,
images: feed.msg.pics.map(image => {
return {
height: String(image.height),
width: String(image.width),
id: image.id
}
})
}
})
}
return result
}
}

View File

@@ -0,0 +1,71 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
import { OB11GroupFile, OB11GroupFileFolder } from '../../types'
import { GroupFileInfo } from '@/ntqqapi/types'
interface Payload {
group_id: number | string
}
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()
})
async _handle(payload: Payload) {
const groupId = payload.group_id.toString()
const data: GroupFileInfo['item'] = []
let nextIndex: number | undefined
while (nextIndex !== 0) {
const res = await this.ctx.ntGroupApi.getGroupFileList(groupId, {
sortType: 1,
fileCount: 100,
startIndex: nextIndex ?? 0,
sortOrder: 2,
showOnlinedocFolder: 0,
})
data.push(...res.item)
nextIndex = res.nextIndex
}
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.Unhandle,
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.Unhandle,
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
} }
} }

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