Compare commits

...

496 Commits

Author SHA1 Message Date
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
idranme
67d3dfb3cf Merge pull request #367 from LLOneBot/dev
3.30.5
2024-08-25 23:09:44 +08:00
idranme
afe8392a1e chore: v3.30.5 2024-08-25 23:07:33 +08:00
idranme
c1f5c5cd58 fix 2024-08-25 20:00:13 +08:00
idranme
85001a40da Merge pull request #366 from LLOneBot/dev
3.30.4
2024-08-23 17:05:03 +08:00
idranme
867a05c85a chore: v3.30.4 2024-08-23 17:03:58 +08:00
idranme
d8a63f6561 fix 2024-08-23 17:02:31 +08:00
idranme
e9fb9d1b30 Update publish.yml 2024-08-23 16:08:59 +08:00
idranme
b4fc987537 Merge pull request #365 from LLOneBot/dev
3.30.3
2024-08-23 13:40:59 +08:00
idranme
d0ccf53d88 chore: v3.30.3 2024-08-23 13:39:26 +08:00
idranme
d5ca94569d fix 2024-08-23 13:32:58 +08:00
idranme
bf72685501 Merge pull request #363 from LLOneBot/dev
3.30.2
2024-08-23 00:30:48 +08:00
idranme
c07467b670 chore: v3.30.2 2024-08-23 00:08:52 +08:00
idranme
ea164fb048 fix: friend list 2024-08-22 23:47:15 +08:00
idranme
0c0ad9a616 Merge pull request #362 from LLOneBot/dev
3.30.1
2024-08-22 20:41:32 +08:00
idranme
7bb4808e2d chore: v3.30.1 2024-08-22 20:18:16 +08:00
idranme
3f7592d06d opt 2024-08-22 20:17:28 +08:00
idranme
2f341fcf43 fix 2024-08-22 18:16:08 +08:00
idranme
9c59e5903e Merge pull request #360 from LLOneBot/dev
3.30.0
2024-08-22 12:41:06 +08:00
idranme
339ba409ee chore: v3.30.0 2024-08-22 12:37:43 +08:00
idranme
099da66661 fix: poke event 2024-08-22 12:32:09 +08:00
idranme
adcde6e49e fix 2024-08-22 06:37:28 +08:00
idranme
b3b8f9cd72 fix 2024-08-22 06:23:35 +08:00
idranme
8b57ebd7de fix: adaptation 27187 2024-08-22 05:45:02 +08:00
idranme
1afaeb0396 fix: adaptation 27187 2024-08-22 03:34:42 +08:00
idranme
235a986253 fix: adaptation 27187 2024-08-22 02:48:01 +08:00
idranme
b16bea9548 fix: adaptation 27187 2024-08-22 02:01:44 +08:00
idranme
7897034d13 opt 2024-08-22 00:42:12 +08:00
idranme
eabe891838 opt 2024-08-21 23:36:35 +08:00
idranme
75d3fc27f0 chore: remove unused methods 2024-08-21 22:51:00 +08:00
idranme
111bb4dd88 fix: adaptation 27187 2024-08-21 22:14:52 +08:00
idranme
f8bf60a3a0 Merge pull request #357 from cnxysoft/dev
fix: Linux上报
2024-08-21 17:50:42 +08:00
Alen
7c22eb3376 fix: Linux上报 2024-08-21 17:42:33 +08:00
Alen
7e1f7ac7f5 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-21 10:56:00 +08:00
idranme
4ea02676f7 Merge pull request #354 from LLOneBot/dev
3.29.6
2024-08-21 00:29:41 +08:00
idranme
ddefb4c194 chore: v3.29.6 2024-08-21 00:27:47 +08:00
idranme
2792fa4776 fix 2024-08-21 00:14:15 +08:00
idranme
c37858e2f9 opt 2024-08-20 21:13:27 +08:00
idranme
59a11faa7f Merge pull request #352 from LLOneBot/dev
3.29.5
2024-08-19 17:40:30 +08:00
idranme
3b3795c946 chore: v3.29.5 2024-08-19 17:38:42 +08:00
idranme
ff18937828 fix 2024-08-19 17:29:58 +08:00
idranme
65d02d7f21 Merge pull request #351 from LLOneBot/main
merge
2024-08-19 12:59:10 +08:00
idranme
9cb8ba017e Merge pull request #350 from snsin09/nocache
ws修复必须no_cache参数
2024-08-19 12:55:27 +08:00
yota
1e579858b8 ws修复必须no_cache参数 2024-08-19 09:47:24 +08:00
idranme
db0c800851 Merge pull request #347 from LLOneBot/dev
3.29.4
2024-08-18 21:09:15 +08:00
idranme
e912911dd8 chore: v3.29.4 2024-08-18 21:04:30 +08:00
idranme
2245d0d3de fix 2024-08-18 20:58:26 +08:00
idranme
a56eac0251 Merge pull request #345 from LLOneBot/main
merge
2024-08-18 16:45:02 +08:00
linyuchen
8be0562c19 Merge pull request #344 from LLOneBot/linyuchen-patch-1
Fix: typo
2024-08-17 23:46:12 +08:00
linyuchen
f4c77f3e20 Fix: typo 2024-08-17 23:45:41 +08:00
linyuchen
508e6f2928 Merge pull request #342 from gfhdhytghd/patch-1
Update LICENSE
2024-08-17 16:20:50 +08:00
lin
9353cb0432 Update LICENSE
修改许可证以在法律层面上禁止宣传
2024-08-17 14:21:27 +08:00
idranme
816e07f47c Merge pull request #341 from LLOneBot/dev
3.29.3
2024-08-16 22:27:41 +08:00
idranme
46b1e8e67d chore: v3.29.3 2024-08-16 22:25:17 +08:00
idranme
8542594181 fix 2024-08-16 21:58:05 +08:00
idranme
0d7aa9bd2c fix 2024-08-16 21:28:43 +08:00
idranme
a47ee4c3e4 fix 2024-08-16 09:53:23 +08:00
idranme
0182803ae1 Merge pull request #339 from LLOneBot/dev
3.29.2
2024-08-15 11:14:35 +08:00
idranme
94c1aea6df chore: v3.29.2 2024-08-15 10:57:15 +08:00
idranme
d143dc043c fix 2024-08-15 10:31:51 +08:00
idranme
3f4b0b44cf feat: cache recalled message content 2024-08-14 23:04:15 +08:00
idranme
26fc0c68b2 Merge pull request #337 from LLOneBot/dev
3.29.1
2024-08-14 19:00:42 +08:00
idranme
c1d7aa7aed chore: v3.29.1 2024-08-14 18:59:27 +08:00
idranme
6aa44bdd79 fix: /get_image 2024-08-14 18:20:39 +08:00
idranme
77f3bfc5c5 Merge pull request #335 from LLOneBot/dev
3.29.0
2024-08-13 22:11:17 +08:00
idranme
2715552814 chore: v3.29.0 2024-08-13 22:08:36 +08:00
idranme
8ed0e6c1be fix 2024-08-13 21:59:13 +08:00
idranme
260a0be184 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-08-13 19:31:10 +08:00
idranme
6582ffe964 fix: msg 2024-08-13 19:29:22 +08:00
linyuchen
f8e231b8b8 chore: v3.28.7
fix: CPU占用过高
fix: 好友列表变动hook失败
2024-08-13 19:09:13 +08:00
Alen
4efcf5b520 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 19:56:48 +08:00
Alen
9ff6ff7cab Merge remote-tracking branch 'upstream/dev' into dev 2024-08-12 16:09:24 +08:00
idranme
a0f5cc0e36 Merge pull request #333 from LLOneBot/dev
Update README.md
2024-08-12 15:01:26 +08:00
idranme
277c2a9b67 Update README.md 2024-08-12 15:00:41 +08:00
idranme
874acdd7fe Merge pull request #331 from LLOneBot/dev
3.28.6
2024-08-12 00:03:25 +08:00
idranme
b2b996df9c chore: v3.28.6 2024-08-12 00:01:39 +08:00
idranme
4427774c2d fix: multiForwardMsg 2024-08-12 00:01:06 +08:00
idranme
41c04faa05 Merge pull request #330 from LLOneBot/dev
3.28.5
2024-08-11 20:01:29 +08:00
idranme
6ad4492f01 chore: v3.28.5 2024-08-11 20:00:47 +08:00
idranme
d52f16bc88 opt 2024-08-11 19:42:44 +08:00
idranme
2b0179acd1 opt 2024-08-11 18:10:27 +08:00
idranme
f540f324a1 Merge pull request #329 from LLOneBot/dev
3.28.4
2024-08-11 12:21:37 +08:00
idranme
128f40a51d chore: v3.28.4 2024-08-11 12:17:47 +08:00
idranme
c815e0ca6b sync 2024-08-11 12:16:53 +08:00
idranme
1da720e0a7 sync 2024-08-11 02:43:14 +08:00
idranme
1472c9c949 opt 2024-08-11 00:23:17 +08:00
idranme
4678253815 sync 2024-08-11 00:18:54 +08:00
idranme
e1176e18cd Merge pull request #328 from LLOneBot/dev
3.28.3
2024-08-10 23:19:09 +08:00
idranme
107f02f21f chore: 3.28.3 2024-08-10 23:17:38 +08:00
idranme
51f8db3a83 opt 2024-08-10 22:31:14 +08:00
idranme
25691a4124 sync 2024-08-10 22:09:35 +08:00
idranme
40f03e6401 sync 2024-08-10 21:34:28 +08:00
idranme
9f89094978 sync 2024-08-10 20:36:15 +08:00
idranme
04f837145c sync 2024-08-10 18:14:33 +08:00
idranme
6126920830 sync 2024-08-10 17:17:19 +08:00
idranme
5c219aa003 opt 2024-08-09 22:32:54 +08:00
Alen
594a421163 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-09 22:15:54 +08:00
idranme
ce5cf82339 Merge pull request #325 from LLOneBot/dev
3.28.2
2024-08-09 18:10:50 +08:00
idranme
6931277e33 chore: v3.28.2 2024-08-09 18:07:50 +08:00
idranme
be1b9c21c1 feat: support for at message segment specifying name 2024-08-09 18:02:52 +08:00
idranme
b02cd3af00 Create .editorconfig 2024-08-09 16:46:08 +08:00
idranme
22dcbac16f Merge pull request #324 from LLOneBot/dev
fix ci
2024-08-09 16:06:44 +08:00
idranme
44faedd6c0 fix ci 2024-08-09 16:05:51 +08:00
idranme
fb3b673e63 Merge pull request #323 from LLOneBot/dev
fix ci
2024-08-09 15:53:42 +08:00
idranme
4e377f86d1 fix ci 2024-08-09 15:53:04 +08:00
idranme
e8bd98020b Merge pull request #322 from LLOneBot/dev
v3.28.1
2024-08-09 15:49:29 +08:00
idranme
c520034934 chore: v3.28.1 2024-08-09 15:47:57 +08:00
idranme
5d5fd403b8 fix: filtering at segments when sending private chat messages 2024-08-09 15:44:18 +08:00
idranme
1fc02229df sync 2024-08-09 15:40:08 +08:00
idranme
6c8d3db3a4 opt 2024-08-09 14:26:30 +08:00
idranme
c5b69561af sync 2024-08-09 14:20:59 +08:00
idranme
b5bffff941 fix 2024-08-07 23:17:13 +08:00
idranme
1a2cdc8c0e opt 2024-08-07 22:08:47 +08:00
idranme
50ab62f103 opt: config 2024-08-07 21:39:26 +08:00
Alen
b748d84e8a Merge branch 'dev' of https://github.com/cnxysoft/LLOneBot into dev 2024-08-07 15:06:19 +08:00
Alen
e8d83d2958 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-07 15:06:11 +08:00
idranme
5005d83ce0 opt: audio encoding and decoding 2024-08-07 04:22:51 +08:00
idranme
d7e40e488c Update README.md
LLAPI 已删库
2024-08-06 22:31:39 +08:00
idranme
4958e22770 Update README.md 2024-08-06 22:28:49 +08:00
idranme
a5e3f94228 chore: deps 2024-08-06 22:26:21 +08:00
idranme
9e57b2c17e Update publish.yml 2024-08-06 14:51:17 +08:00
idranme
e1ff366e10 clean 2024-08-06 02:32:28 +08:00
idranme
6b03b01a24 Merge pull request #319 from LLOneBot/dev
chore: v3.28.0
2024-08-06 02:08:51 +08:00
idranme
18f01b7f21 chore: v3.28.0 2024-08-06 02:08:00 +08:00
idranme
897f691d6c make ts happy 2024-08-06 01:47:51 +08:00
idranme
a9902d9109 sync 2024-08-05 22:49:48 +08:00
Alen
cdb34ffe61 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 22:15:48 +08:00
idranme
5d78fdd6a4 fix 2024-08-05 22:07:04 +08:00
idranme
72eb013371 fix 2024-08-05 20:44:28 +08:00
idranme
808777c044 fix: import path 2024-08-05 19:18:15 +08:00
idranme
a2d1379866 sync 2024-08-05 19:09:41 +08:00
Alen
a45c56bd85 Merge remote-tracking branch 'upstream/dev' into dev 2024-08-05 10:09:12 +08:00
Alen
bb07ebd5d7 Merge branch 'main' into dev 2024-08-05 10:07:28 +08:00
idranme
c41a8556fa Change description 2024-08-05 00:23:41 +08:00
idranme
fa2df2a3cd opt 2024-08-04 23:11:59 +08:00
idranme
b28dd3a723 Update publish.yml 2024-08-04 22:44:20 +08:00
idranme
6ffa41e0d6 prioritise local versions 2024-08-04 22:14:07 +08:00
idranme
85df3794e8 optimise 2024-08-04 22:07:55 +08:00
idranme
4bee2ba062 reduce icon size 2024-08-04 20:35:31 +08:00
idranme
4bf992c4a9 chore: deps 2024-08-04 20:31:29 +08:00
idranme
898e856150 poke require >=25765 2024-08-04 20:22:07 +08:00
idranme
c86797afc8 chore: remove unused eslint 2024-08-04 19:54:32 +08:00
idranme
799593b788 chore: support yarn berry 2024-08-04 19:48:17 +08:00
idranme
74d9a083aa Update README.md 2024-08-04 19:28:13 +08:00
idranme
cae525429a Update README.md 2024-08-04 19:21:44 +08:00
idranme
cc0d1e2a9b Merge pull request #316 from idranme/uuid
refa: deps
2024-08-04 18:36:01 +08:00
idranme
34ecfcfa16 Merge branch 'dev' into uuid 2024-08-04 18:35:11 +08:00
idranme
79c5041216 Merge pull request #318 from LLOneBot/dev 2024-08-04 18:07:03 +08:00
idranme
8fb53260ab chore: v3.27.4 2024-08-04 10:05:03 +00:00
idranme
07d9ac823a Merge pull request #317 from LLOneBot/dev
chore: v3.27.4
2024-08-04 17:48:24 +08:00
idranme
b571ef434c chore 2024-08-02 20:50:34 +00:00
idranme
c1f4dcd6a6 chore 2024-08-02 20:40:50 +00:00
idranme
4c5befbe44 chore 2024-08-02 20:39:26 +00:00
linyuchen
296cd4d0a3 Merge pull request #315 from idranme/main
feat: at segment add name
2024-08-02 23:11:14 +08:00
linyuchen
e77a2ca34a Merge pull request #311 from cnxysoft/dev
BUG修复
2024-08-02 23:09:35 +08:00
idranme
f3af0d18bc refa: deps 2024-08-02 12:00:13 +00:00
idranme
406e3c7e6b opt 2024-08-02 10:49:30 +00:00
idranme
3f5ca8ebfa chore 2024-08-02 10:31:37 +00:00
idranme
6e8389e833 chore 2024-08-02 10:26:18 +00:00
idranme
71aedca4c6 feat: the name attribute of the at message segment 2024-08-02 10:23:48 +00:00
Alen
6410689549 BUG修复
尝试修复设精事件shortId和senderId
2024-08-01 21:56:44 +08:00
linyuchen
6d0e2269cc Merge pull request #304 from cnxysoft/dev
功能更新
2024-07-28 14:52:58 +08:00
linyuchen
2e28fc678c Merge branch 'dev' into dev 2024-07-28 14:52:17 +08:00
linyuchen
8204f4407f Merge pull request #300 from super1207/dev
Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev
2024-07-26 09:57:37 +08:00
Alen
9f1d4c4db2 功能修改
修改群管变更事件获取渠道,让所有群角色都能收到群管变更通知
2024-07-25 17:25:40 +08:00
Alen
8ba47635d3 功能更新
1.增加设精事件上报(目前上报的shortId经常出错,实际消息体却是正确的,待解决)
2.增加设精/取消设精api接口
3.poke事件增加raw信息上报
2024-07-25 01:02:48 +08:00
Alen
5fa2427c51 修改poke事件
新增poke事件支持上传raw信息
2024-07-24 19:04:07 +08:00
Alen
aa8739d016 Merge remote-tracking branch 'upstream/main' into dev 2024-07-24 11:48:55 +08:00
super1207
79f0329da7 Merge branch 'dev' of https://github.com/LLOneBot/LLOneBot into dev 2024-07-20 18:01:30 +08:00
super1207
7a33a36f44 add get_event api 2024-07-20 17:58:00 +08:00
linyuchen
808424d08e Merge branch 'main' into dev 2024-07-20 17:08:59 +08:00
linyuchen
d0967785de chore: v3.27.3 2024-07-20 16:58:03 +08:00
linyuchen
eccabb8189 Merge pull request #299 from Natsukage/main
fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
2024-07-20 15:25:27 +08:00
夏影
c9374ff515 fix: skip problematic name-value pairs in encodeCQCode to prevent undefined errors
Added logic to skip name-value pairs in encodeCQCode when value cannot be converted to string, preventing errors caused by undefined values. This ensures the function can handle such cases gracefully and continue processing other valid data.
2024-07-20 00:49:34 +08:00
Alen
92c4889924 Merge remote-tracking branch 'upstream/main' 2024-07-16 23:19:32 +08:00
linyuchen
f9454039a1 fix: old poke event 2024-07-16 21:52:15 +08:00
linyuchen
bc4511e175 chore: v3.27.2 2024-07-16 21:43:50 +08:00
linyuchen
f191103f99 Merge pull request #294 from cnxysoft/dev
修复戳一戳
2024-07-16 21:38:17 +08:00
linyuchen
408463f63b Merge branch 'dev' into dev 2024-07-16 21:21:50 +08:00
Alen
fb96c4272e 修复戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 21:01:19 +08:00
Alen
c6b302d5a8 修复好友戳一戳
取缔FriendAddEvent,并入Private Event处理
2024-07-16 20:27:44 +08:00
linyuchen
1dd468e2ff fix: #290 2024-07-13 16:25:00 +08:00
linyuchen
2a1aa8c649 feat: image subType 2024-07-13 14:26:23 +08:00
linyuchen
1633734e08 Merge branch 'dev' 2024-07-13 14:09:45 +08:00
linyuchen
dff92e6f27 chore: version 3.27.0
feat: support poke
feat: LLOneBot global switch
2024-07-13 14:09:03 +08:00
linyuchen
dba5e30d5d doc: plugin description 2024-07-10 13:48:05 +08:00
linyuchen
2d04ab2e72 fix: crychic crash 2024-07-10 13:47:44 +08:00
linyuchen
1a015ac8d3 Merge pull request #262 from LLOneBot/dev
get_record 支持 out_format 进行转码,和其他小修复
2024-06-21 17:39:53 +08:00
linyuchen
6390620ddd chore: version 3.26.7 2024-06-21 17:33:48 +08:00
linyuchen
0d19005dc3 refactor: remove duplicate import 2024-06-21 17:28:17 +08:00
linyuchen
c6479dd2c4 Merge remote-tracking branch 'origin/dev' into dev 2024-06-21 16:21:15 +08:00
linyuchen
8871331b7c 🐛 fix: ws echo #261 2024-06-21 16:20:59 +08:00
linyuchen
e01148b86a 🐛 fix: ws echo 2024-06-21 16:20:26 +08:00
linyuchen
2f87e3818e Merge pull request #260 from idranme/main
perf: audio
2024-06-21 10:36:29 +08:00
linyuchen
2c8a594c38 Merge branch 'dev' into main 2024-06-21 10:36:14 +08:00
idranme
1508dab7fe perf: audio 2024-06-18 19:15:56 +00:00
linyuchen
958b21e47e fix: wait get_file download complete 2024-06-17 17:41:23 +08:00
linyuchen
781c3311ae fix: get_file cache not found 2024-06-17 16:20:37 +08:00
linyuchen
52850d172e feat: decode silk 2024-06-17 16:05:38 +08:00
linyuchen
52a065542e chore: v3.26.6 2024-06-10 14:38:20 +08:00
linyuchen
fd10469685 feat: video url 2024-06-10 14:35:00 +08:00
linyuchen
a2ee75b113 refactor: sent msg status waiter 2024-06-09 15:27:33 +08:00
linyuchen
0f7f243b98 Merge pull request #250 from Bluefissure/reverse-ws-ua
feat: add ua to reverse websocket headers
2024-06-06 17:35:21 +08:00
Bluefissure
97d7996a50 fix: add version to ua 2024-06-06 08:53:37 +00:00
Bluefissure
b658d164f9 feat: add ua to reverse websocket headers 2024-06-06 08:48:18 +00:00
linyuchen
f150ae478b chore: v3.26.5 2024-06-01 20:19:05 +08:00
linyuchen
d1f68553f1 fix: 加载卡顿,群成员名片变动 2024-06-01 20:18:38 +08:00
linyuchen
f47f0800de Merge remote-tracking branch 'origin/main' 2024-05-29 16:56:08 +08:00
linyuchen
b7ddefc950 fix: QZone cookies 2024-05-29 16:38:22 +08:00
linyuchen
25b3325a44 fix: comment 2024-05-29 16:28:46 +08:00
linyuchen
c281b87bab merge main 2024-05-29 16:27:06 +08:00
linyuchen
c0946ddda2 chore: version 3.26.4 2024-05-29 16:26:04 +08:00
linyuchen
1128cf679c refactor: send file timeout 2024-05-29 16:25:42 +08:00
linyuchen
ff65a42350 Merge pull request #242 from LLOneBot/dev
feat: support qzone cookies
2024-05-29 16:24:32 +08:00
手瓜一十雪
c459587dcd refactor: get cookies 2024-05-29 12:03:35 +08:00
手瓜一十雪
6f8ea9677f feat: support qzone cookies 2024-05-28 17:14:24 +08:00
手瓜一十雪
38197527fa Merge branch 'main' into dev 2024-05-28 17:11:13 +08:00
手瓜一十雪
21b2bd2c8e feat: cookies 2024-05-28 17:11:07 +08:00
linyuchen
25158eee55 chore: version 3.26.3 2024-05-28 16:41:28 +08:00
linyuchen
1aa804f255 chore: version 3.26.3 2024-05-28 16:41:22 +08:00
linyuchen
fbe101339d fix: #237 2024-05-28 16:40:51 +08:00
linyuchen
a4aeb8171d fix: QQ package.json on macOS 2024-05-28 15:42:22 +08:00
linyuchen
27f98a459c fix: member info change on version 24108 2024-05-28 15:31:59 +08:00
linyuchen
e6b0eaa46d Merge pull request #235 from LLOneBot/dev
快速操作回复自动引用原消息开关
2024-05-24 17:14:54 +08:00
linyuchen
f336317a33 chore: version 3.26.2 2024-05-24 17:12:35 +08:00
linyuchen
17b44cc0fa refactor: #226 Quick operation reply automatically quotes the original message switch 2024-05-24 17:10:41 +08:00
linyuchen
debe3a8597 chore: version 3.26.1 2024-05-24 08:54:23 +08:00
linyuchen
f36c5e849f Merge pull request #234 from LLOneBot/dev
fix: #215 get_forward_msg params missing id(onebot11)
2024-05-24 08:52:34 +08:00
linyuchen
abbd6797c4 fix: #215 get_forward_msg params missing id(onebot11) 2024-05-24 08:50:22 +08:00
linyuchen
fdb7784a7d Merge pull request #233 from LLOneBot/dev
[Feature] OneBot11消息构造添加raw字段,单条转发消息接口返回message_id
2024-05-24 08:40:44 +08:00
linyuchen
92b49015b0 feat: Forward single msg return message_id 2024-05-24 08:36:42 +08:00
linyuchen
1765ffff7b style: format 2024-05-24 08:15:08 +08:00
linyuchen
3024316b5b feat: #232 /get_msg, /get_group_msg_history add raw message 2024-05-24 08:11:38 +08:00
linyuchen
9a0d89bfbf Update README.md 2024-05-19 07:52:12 +08:00
linyuchen
807ef3b700 Merge pull request #228 from LLOneBot/dev
feat: Quick operation reply auto quote original message
2024-05-18 16:53:37 +08:00
linyuchen
948f10d4e3 feat: Quick operation reply auto quote original message 2024-05-18 16:51:34 +08:00
linyuchen
0f99b5cb87 Merge pull request #227 from LLOneBot/dev
fix: Send msg timeout minimum
2024-05-18 16:36:30 +08:00
linyuchen
6413b0ff82 fix: Send msg timeout minimum 2024-05-18 16:34:12 +08:00
linyuchen
39713d8e11 Merge branch 'main' into dev 2024-05-18 16:31:22 +08:00
linyuchen
739a497af6 chore: v3.26.0 2024-05-18 13:16:45 +08:00
linyuchen
de2fe9b0aa Merge pull request #225 from LLOneBot/dev
Feature: #209,New API get_friends_with_category
2024-05-18 13:11:30 +08:00
linyuchen
44448895a0 feat: 209 2024-05-18 13:09:45 +08:00
linyuchen
cfd9097769 feat: 209 2024-05-18 13:08:44 +08:00
linyuchen
627042fd25 Merge pull request #224 from LLOneBot/dev
Fix: #219,发送视频图片进行文件大小判断,超时时间根据文件大小(512kb/s)动态调整
2024-05-18 12:53:42 +08:00
linyuchen
b51ce24d0c fix: #219 2024-05-18 12:50:11 +08:00
linyuchen
fc0881eccc Merge pull request #223 from LLOneBot/dev
fix: #218
2024-05-18 12:13:23 +08:00
linyuchen
6b8509d2b2 fix: #218 2024-05-18 12:12:16 +08:00
linyuchen
cf1d67a5cf Merge pull request #222 from LLOneBot/dev
Feature: websocket .handle_quick_operation
2024-05-18 11:47:56 +08:00
linyuchen
473ebd25b8 fix: promise catch 2024-05-18 11:46:51 +08:00
linyuchen
d4427cfff4 feat: .handle_quick_operation of websocket 2024-05-18 11:45:42 +08:00
linyuchen
9d2e9786cc chore: v3.25.0 2024-05-15 23:03:19 +08:00
linyuchen
9968f714c7 chore: v3.25.0 2024-05-15 23:03:04 +08:00
linyuchen
bd212c4bf3 remove debug 2024-05-15 22:45:13 +08:00
linyuchen
32c7f904db fix: Http download headers 2024-05-15 22:44:15 +08:00
linyuchen
2ef017282f feat: get_group_honor_info 2024-05-15 22:33:55 +08:00
手瓜一十雪
9672f67a23 feat: new Api GetGroupEssence&GetGroupHonorInfo 2024-05-15 21:12:10 +08:00
手瓜一十雪
6e5cfd827c feat: webapi 2024-05-15 21:05:33 +08:00
手瓜一十雪
5402bef4a9 Merge branch 'main' into dev 2024-05-15 20:56:52 +08:00
linyuchen
4194512cce fix: Get cookies miss uin 2024-05-15 19:47:11 +08:00
linyuchen
b3aad8b0d9 fix: Check pic fil name ext 2024-05-15 19:19:10 +08:00
linyuchen
1489c6df25 feat: New face 2024-05-15 18:47:38 +08:00
linyuchen
2e225045e6 feat: Get cookies support domain 2024-05-15 17:57:15 +08:00
手瓜一十雪
11ed06148c fix: checkVersion Mirror 2024-05-14 14:43:52 +08:00
linyuchen
a3fc018186 fix: Compatible with win7 2024-05-12 20:36:27 +08:00
linyuchen
9692bf6ec6 refactor: Rename native node module dirname 2024-05-11 14:56:01 +08:00
linyuchen
9b3916307a fix: All images are the first image in single msg
fix: remote rkey
2024-05-11 14:52:59 +08:00
linyuchen
fdf96b479c Merge branch 'main' into dev
# Conflicts:
#	src/ntqqapi/external/cpmodule.ts
#	src/ntqqapi/external/crychic/index.ts
#	src/ntqqapi/external/moehook/hook.ts
#	src/onebot11/action/msg/SendMsg.ts
#	tsconfig.json
2024-05-10 20:28:44 +08:00
linyuchen
25c7a6096d refactor: path alias
fix: moehook
2024-05-10 20:23:30 +08:00
student_2333
627955e7fd chore: format 2024-05-10 13:34:49 +08:00
student_2333
43e9b070a9 fix: try 2 fix cannot parse msg err 2024-05-10 13:33:48 +08:00
linyuchen
78bb36a2bb fix: Music sign return null then throw exception 2024-05-07 17:46:47 +08:00
linyuchen
58e6e3cbda fix: Music sign return null then throw exception 2024-05-07 17:39:44 +08:00
linyuchen
1da086ce0a chore: v3.24.2 2024-05-05 20:20:30 +08:00
linyuchen
e9d43a9449 fix: http download filename special character 2024-05-05 20:06:07 +08:00
linyuchen
ce31052661 refactor: OB11Message add message_seq filed 2024-05-05 19:42:48 +08:00
linyuchen
3fd9b0a183 fix: 表情回应兼容int类型的emoji_id 2024-05-05 13:07:07 +08:00
linyuchen
7e1dee8e07 fix: msg db cache missing shortId 2024-05-04 23:35:19 +08:00
linyuchen
f2854fdf00 fix: report self recall twice 2024-05-04 20:30:39 +08:00
linyuchen
1fad95a55b chore: Version 3.24.1 2024-05-04 11:34:41 +08:00
linyuchen
5342e1521c Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/ntqqapi/external/moehook/MoeHoo-linux-x64.node
2024-05-03 21:26:31 +08:00
linyuchen
c0bb7def20 fix: Get image rkey on Linux x64 2024-05-03 21:25:47 +08:00
student_2333
3c532526df chore: sync external files 2024-05-01 15:25:49 +08:00
student_2333
05c6cae86f fix: reference before define 2024-05-01 11:10:42 +08:00
linyuchen
24a49f035e fix: music params check 2024-05-01 02:14:20 +08:00
linyuchen
ec27d73605 fix: copy .node 2024-04-30 23:12:36 +08:00
linyuchen
59cd28a2fd feat: FriendAddNotice 2024-04-30 23:06:50 +08:00
linyuchen
bcb6b51241 feat: send mface with summary param 2024-04-30 19:45:59 +08:00
linyuchen
b00ca24fe3 feat: send mface 2024-04-30 19:42:34 +08:00
linyuchen
3a4cdc1e34 Merge branch 'main' of https://github.com/markyfsun/LLOneBot into mface 2024-04-30 19:35:43 +08:00
linyuchen
de4d901412 refactor: 获取rkey后进行检查rkey是否正确 2024-04-30 19:26:51 +08:00
linyuchen
297c495df9 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/ntqqapi/external/crychic/index.ts
#	src/ntqqapi/external/moehook/hook.ts
2024-04-30 18:58:25 +08:00
student_2333
b78bd235f9 fix 2024-04-30 14:13:40 +08:00
student_2333
23d32a1464 Merge branch 'main' into markyfsun/main 2024-04-30 14:07:18 +08:00
手瓜一十雪
25c3d51d69 Merge pull request #206 from LLOneBot/feat/music-card
feat: music card sign
2024-04-30 13:45:15 +08:00
student_2333
05091798f4 fix 2024-04-30 13:40:15 +08:00
student_2333
78c6050d61 refactor 2024-04-30 13:08:33 +08:00
student_2333
2abdcd23db fix 2024-04-30 13:07:51 +08:00
student_2333
1d7100a053 fix 2024-04-30 13:05:08 +08:00
student_2333
6ff49722d8 feat: music card sign 2024-04-30 12:50:38 +08:00
student_2333
9c6abd5167 Merge branch 'main' into markyfsun/main 2024-04-30 11:35:38 +08:00
student_2333
dc1e1ea21b style: reformat 2024-04-30 11:28:24 +08:00
student_2333
f38e544815 style: reformat 2024-04-30 11:24:33 +08:00
student_2333
bb0fcd8614 chore(dep) 2024-04-30 11:22:26 +08:00
linyuchen
710fa3f686 Update README.md thanks list 2024-04-29 19:30:00 +08:00
linyuchen
91089cdb9e refactor: import native .node 2024-04-29 16:25:27 +08:00
linyuchen
58f544862b refactor: private/group image rkey 2024-04-29 11:57:58 +08:00
linyuchen
09ab8cbe93 fix: private/group image rkey 2024-04-28 10:30:41 +08:00
linyuchen
4ce4f3d3a5 fix: image rkey 2024-04-28 09:26:55 +08:00
linyuchen
b5ab717634 优化发送语音或者不支持的消息类型错误提示 2024-04-28 09:19:14 +08:00
markyfsun
2e55924a19 feat: market face 2024-04-27 23:01:47 +08:00
linyuchen
fe3ac3060a Merge remote-tracking branch 'origin/main' 2024-04-26 01:27:10 +08:00
linyuchen
e7e06d655f optimize get file 2024-04-25 23:28:35 +08:00
linyuchen
dec531c567 fix: get image rkey 2024-04-25 23:27:39 +08:00
linyuchen
05f0985f7f feat: upload private file 2024-04-25 23:27:14 +08:00
linyuchen
ac852cc382 feat: msg emoji like 2024-04-25 23:26:46 +08:00
linyuchen
b7855e91f6 feat: msg emoji like 2024-04-25 23:25:38 +08:00
linyuchen
3ae2d2a1e6 feat: forward single msg 2024-04-25 23:24:58 +08:00
linyuchen
857625469f Merge pull request #199 from disymayufei/patch-2
向README.md中添加了一个警告信息
2024-04-20 17:18:52 +08:00
Disy
ca3f68a42a chore: Update caution message
添加了一个警告信息,希望可以起到警示作用,防止一些小白私自将仓库和插件信息广泛传播出去引发tx的警觉
2024-04-20 14:18:26 +08:00
手瓜一十雪
1d47f89011 Merge pull request #197 from jinyu2022/main
添加CORS允许跨源访问
2024-04-17 15:22:38 +08:00
堇羽
2c24e234c8 添加CORS允许跨源访问 2024-04-17 07:14:23 +00:00
linyuchen
5562a3251d feat: get cookies 2024-04-16 23:55:21 +08:00
linyuchen
019b590f36 refactor: auto escape cq code for send msg 2024-04-16 23:23:19 +08:00
linyuchen
c2b3316603 fix: send empty forward msg
fix: ignore post history msg before login
fix: quit group not sync to groups of data
feat: support post url params
feat: support port http heart
2024-04-16 23:16:25 +08:00
linyuchen
f8890b309b fix: face msg faceType 2024-04-11 18:57:58 +08:00
linyuchen
b5e578733f fix: quick reply friend msg 2024-04-11 18:17:02 +08:00
linyuchen
51602b987e fix: ws 没有上报群文件上传事件 2024-04-08 00:21:24 +08:00
linyuchen
b501af6e0e feat: 骰子魔法表情 & 猜拳魔法表情 2024-04-07 18:51:26 +08:00
linyuchen
81821e74d8 fix: 手动频繁切换聊天窗口时导致旧的窗口接收不到消息 2024-04-07 17:37:52 +08:00
207 changed files with 16355 additions and 14685 deletions

9
.editorconfig Normal file
View File

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

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,39 +1,43 @@
name: "publish" name: Publish
on: on:
push: push:
tags: tags:
- "v*" - 'v*'
jobs: jobs:
build-and-publish: build-and-publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: setup node - name: Setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 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
cp manifest.json ./dist/manifest.json cd ./dist/
cd ./dist/ zip -r ../LLOneBot.zip ./*
zip -r ../LLOneBot.zip ./*
- name: publish - name: Extract version from tag
uses: ncipollo/release-action@v1 id: get-version
with: run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
artifacts: "LLOneBot.zip"
draft: true - name: Release
token: ${{ secrets.RELEASE_TOKEN }} uses: ncipollo/release-action@v1
with:
artifacts: 'LLOneBot.zip'
draft: true
token: ${{ secrets.RELEASE_TOKEN }}
name: LLOneBot v${{ steps.get-version.outputs.VERSION }}

18
.gitignore vendored
View File

@@ -1,6 +1,16 @@
node_modules/ .yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
package-lock.json package-lock.json
dist/ yarn.lock
out/ node_modules
.idea/ dist
out
.DS_Store .DS_Store
.idea
.vscode

4
.prettierrc.yml Normal file
View File

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

1
.yarnrc.yml Normal file
View File

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

View File

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

View File

@@ -1,8 +1,11 @@
# LLOneBot
# LLOneBot API LiteLoaderQQNT 插件,实现 OneBot 11 协议,用于 QQ 机器人开发
LiteLoaderQQNT插件使你的NTQQ支持OneBot11协议进行QQ机器人开发
TG群<https://t.me/+nLZEnpne-pQ1OWFl> > [!CAUTION]\
> 请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论任何与本插件存在相关性的信息
TG 群:<https://t.me/+nLZEnpne-pQ1OWFl>
## 安装方法 ## 安装方法
@@ -10,43 +13,24 @@ TG群<https://t.me/+nLZEnpne-pQ1OWFl>
## 设置界面 ## 设置界面
<img src="./doc/image/setting.png" width="500px" alt="图片名称"/> <img src="./doc/image/setting.png" width="400px" alt="设置界面"/>
## HTTP 调用示例 ## 支持的 API
![](doc/image/example.jpg)
## 支持的 api 和功能详情
<https://llonebot.github.io/zh-CN/develop/api> <https://llonebot.github.io/zh-CN/develop/api>
## TODO
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录
- [x] 好友点赞api
- [x] 群管理功能,禁言、踢人,改群名片等
- [x] 视频消息
- [x] 文件消息
- [x] 群禁言事件上报
- [x] 优化加群成功事件上报
- [x] 清理缓存api
- [ ] 无头模式
- [ ] 框架对接文档
## onebot11文档
<https://11.onebot.dev/>
## Stargazers over time ## Stargazers over time
[![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot) [![Stargazers over time](https://starchart.cc/LLOneBot/LLOneBot.svg?variant=adaptive)](https://starchart.cc/LLOneBot/LLOneBot)
## 鸣谢 ## 鸣谢
* [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) - [NapCatQQ](https://github.com/NapNeko/NapCatQQ)
* [chronocat](https://github.com/chrononeko/chronocat/) - [LiteLoaderQQNT](https://liteloaderqqnt.github.io/guide/install.html)
* [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot) - [Chronocat](https://github.com/chrononeko/chronocat)
- [koishi-plugin-adapter-onebot](https://github.com/koishijs/koishi-plugin-adapter-onebot)
- [silk-wasm](https://github.com/idranme/silk-wasm)
## 友链 ## 友链
* [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core) 一款用C#实现的NTQQ纯协议跨平台QQ机器人框架
- [Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core): An Implementation of NTQQ Protocol

View File

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

BIN
icon.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
icon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

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

6782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,44 +2,47 @@
"name": "llonebot", "name": "llonebot",
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"description": "NTQQLiteLoaderOneBotApi", "description": "",
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"build": "electron-vite build", "build": "electron-vite build",
"build-mac": "npm run build && npm run deploy-mac", "build-mac": "npm run build && npm run deploy-mac",
"deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/LiteLoaderQQNT/plugins/LLOneBot/", "deploy-mac": "cp -r dist/* ~/Library/Containers/com.tencent.qq/Data/Documents/LiteLoaderQQNT/plugins/LLOneBot/",
"build-win": "npm run build && npm run deploy-win", "build-win": "npm run build && npm run deploy-win",
"deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %USERPROFILE%\\documents\\LiteLoaderQQNT\\plugins\\LLOneBot\\\"" "deploy-win": "cmd /c \"xcopy /C /S /Y dist\\* %LITELOADERQQNT_PROFILE%\\plugins\\LLOneBot\\\"",
"format": "prettier -cw .",
"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": "ISC", "license": "MIT",
"dependencies": { "dependencies": {
"compressing": "^1.10.0", "@minatojs/driver-sqlite": "^4.6.0",
"express": "^4.18.2", "compressing": "^1.10.1",
"file-type": "^19.0.0", "cordis": "^3.18.1",
"fluent-ffmpeg": "^2.1.2", "cors": "^2.8.5",
"level": "^8.0.1", "cosmokit": "^1.6.2",
"silk-wasm": "^3.3.4", "express": "^5.0.0",
"utf-8-validate": "^6.0.3", "fast-xml-parser": "^4.5.0",
"uuid": "^9.0.1", "file-type": "^19.5.0",
"ws": "^8.16.0" "fluent-ffmpeg": "^2.1.3",
"minato": "^3.6.0",
"protobufjs": "^7.4.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.20", "@types/cors": "^2.8.17",
"@types/fluent-ffmpeg": "^2.1.24", "@types/express": "^4.17.21",
"@types/node": "^20.11.24", "@types/fluent-ffmpeg": "^2.1.26",
"@types/uuid": "^9.0.8", "@types/node": "^20.14.15",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^6.4.0", "electron": "^31.4.0",
"electron": "^29.0.1", "electron-vite": "^2.3.0",
"electron-vite": "^2.0.0", "protobufjs-cli": "^1.1.3",
"eslint": "^8.0.1", "typescript": "^5.6.2",
"eslint-plugin-import": "^2.25.2", "vite": "^5.4.7",
"eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
"eslint-plugin-promise": "^6.0.0",
"ts-node": "^10.9.2",
"typescript": "*",
"vite": "^5.1.4",
"vite-plugin-cp": "^4.0.8" "vite-plugin-cp": "^4.0.8"
} },
"packageManager": "yarn@4.5.0"
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,104 +0,0 @@
import {
type Friend,
type FriendRequest,
type Group,
type GroupMember,
type SelfInfo
} from '../ntqqapi/types'
import {type FileCache, type LLOneBotError} from './types'
import {NTQQGroupApi} from "../ntqqapi/api/group";
import {log} from "./utils/log";
import {isNumeric} from "./utils/helper";
export const selfInfo: SelfInfo = {
uid: '',
uin: '',
nick: '',
online: true
}
export let groups: Group[] = []
export let friends: Friend[] = []
export let friendRequests: Map<number, FriendRequest> = new Map<number, FriendRequest>()
export const llonebotError: LLOneBotError = {
ffmpegError: '',
httpServerError: '',
wsServerError: '',
otherError: 'LLOnebot未能正常启动请检查日志查看错误'
}
export async function getFriend(uinOrUid: string): Promise<Friend | undefined> {
let filterKey = isNumeric(uinOrUid.toString()) ? "uin" : "uid"
let filterValue = uinOrUid
let friend = friends.find(friend => friend[filterKey] === filterValue.toString())
// if (!friend) {
// try {
// friends = (await NTQQApi.getFriends(true))
// friend = friends.find(friend => friend[filterKey] === filterValue.toString())
// } catch (e) {
// // log("刷新好友列表失败", e.stack.toString())
// }
// }
return friend
}
export async function getGroup(qq: string): Promise<Group | undefined> {
let group = groups.find(group => group.groupCode === qq.toString())
if (!group) {
try {
const _groups = await NTQQGroupApi.getGroups(true);
group = _groups.find(group => group.groupCode === qq.toString())
if (group) {
groups.push(group)
}
} catch (e) {
}
}
return group
}
export async function getGroupMember(groupQQ: string | number, memberUinOrUid: string | number) {
groupQQ = groupQQ.toString()
memberUinOrUid = memberUinOrUid.toString()
const group = await getGroup(groupQQ)
if (group) {
const filterKey = isNumeric(memberUinOrUid) ? "uin" : "uid"
const filterValue = memberUinOrUid
let filterFunc: (member: GroupMember) => boolean = member => member[filterKey] === filterValue
let member = group.members?.find(filterFunc)
if (!member) {
try {
const _members = await NTQQGroupApi.getGroupMembers(groupQQ)
if (_members.length > 0) {
group.members = _members
}
} catch (e) {
// log("刷新群成员列表失败", e.stack.toString())
}
member = group.members?.find(filterFunc)
}
return member
}
return null
}
export async function refreshGroupMembers(groupQQ: string) {
const group = groups.find(group => group.groupCode === groupQQ)
if (group) {
group.members = await NTQQGroupApi.getGroupMembers(groupQQ)
}
}
export const uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) {
for (const uid in uidMaps) {
if (uidMaps[uid] === uin) {
return uid
}
}
}
export let tempGroupCodeMap: Record<string, string> = {} // peerUid => 群号

View File

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

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,114 +0,0 @@
import express, {Express, Request, Response} from "express";
import http from "http";
import {log} from "../utils/log";
import {getConfigUtil} from "../config";
import {llonebotError} from "../data";
type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase {
name: string = "LLOneBot";
private readonly expressAPP: Express;
private server: http.Server = null;
constructor() {
this.expressAPP = express();
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) {
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
}
log("收到http请求", url, payload);
try {
res.send(await handler(res, payload))
} catch (e) {
this.handleFailed(res, payload, e.stack.toString())
}
});
}
protected listen(port: number) {
this.server = this.expressAPP.listen(port, "0.0.0.0", () => {
const info = `${this.name} started 0.0.0.0:${port}`
console.log(info);
log(info);
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,68 +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: any) {
return value === undefined || value === null;
}
/**
* 将字符串按最大长度分割并添加换行符
* @param str 原始字符串
* @param maxLength 每行的最大字符数
* @returns 处理后的字符串,超过长度的地方将会换行
*/
export function wrapText(str: string, maxLength: number): string {
// 初始化一个空字符串用于存放结果
let result: string = '';
// 循环遍历字符串每次步进maxLength个字符
for (let i = 0; i < str.length; i += maxLength) {
// 从i开始截取长度为maxLength的字符串段并添加到结果字符串
// 如果不是第一段,先添加一个换行符
if (i > 0) result += '\n';
result += str.substring(i, i + maxLength);
}
return result;
}

View File

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

View File

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

View File

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

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

@@ -0,0 +1,40 @@
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>[]
}

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

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

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

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

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

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

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

View File

@@ -1,98 +1,108 @@
import { version } from "../../version"; import path from 'node:path'
import * as path from "node:path"; import compressing from 'compressing'
import * as fs from "node:fs"; import { writeFile } from 'node:fs/promises'
import { copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR } from "."; import { version } from '../../version'
import compressing from "compressing"; import { copyFolder, log, fetchFile } from '.'
import { PLUGIN_DIR, TEMP_DIR } from '../globalVars'
const downloadMirrorHosts = ['https://ghp.ci/']
const downloadMirrorHosts = ["https://mirror.ghproxy.com/"]; const releasesMirrorHosts = ['https://kkgithub.com']
const checkVersionMirrorHosts = ["https://521github.com"];
export async function checkNewVersion() { export async function checkNewVersion() {
const latestVersionText = await getRemoteVersion(); const latestVersionText = await getRemoteVersion()
const latestVersion = latestVersionText.split("."); const latestVersion = latestVersionText.split('.')
log("llonebot last version", latestVersion); log('LLOneBot latest version', latestVersion)
const currentVersion: string[] = version.split("."); const currentVersion = version.split('.')
log("llonebot current version", currentVersion); //log('llonebot current version', currentVersion)
for (let k of [0, 1, 2]) { for (const k of [0, 1, 2]) {
if (parseInt(latestVersion[k]) > parseInt(currentVersion[k])) { const latest = parseInt(latestVersion[k])
log("") const current = parseInt(currentVersion[k])
return { result: true, version: latestVersionText }; if (latest > current) {
} log('')
else if (parseInt(latestVersion[k]) < parseInt(currentVersion[k])) { return { result: true, version: latestVersionText }
break; } else if (latest < current) {
} break
} }
return { result: false, version: version }; }
return { result: false, version: version }
} }
export async function upgradeLLOneBot() { export async function upgradeLLOneBot() {
const latestVersion = await getRemoteVersion(); const latestVersion = await getRemoteVersion()
if (latestVersion && latestVersion != "") { if (latestVersion && latestVersion != '') {
const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip"; const downloadUrl = `https://github.com/LLOneBot/LLOneBot/releases/download/v${latestVersion}/LLOneBot.zip`
const filePath = path.join(TEMP_DIR, "./update-" + latestVersion + ".zip"); const filePath = path.join(TEMP_DIR, './update-' + latestVersion + '.zip')
let downloadSuccess = false; let downloadSuccess = false
// 多镜像下载 // 多镜像下载
for (const mirrorGithub of downloadMirrorHosts) { for (const mirrorGithub of downloadMirrorHosts) {
try { try {
const buffer = await httpDownload(mirrorGithub + downloadUrl); const res = await fetchFile(mirrorGithub + downloadUrl)
fs.writeFileSync(filePath, buffer) await writeFile(filePath, res.data)
downloadSuccess = true; downloadSuccess = true
break; break
} catch (e) { } catch (e) {
log("llonebot upgrade error", e); log('llonebot upgrade error', e)
} }
}
if (!downloadSuccess) {
log("llonebot upgrade error", "download failed");
return false;
}
const temp_ver_dir = path.join(TEMP_DIR, "LLOneBot" + latestVersion);
let uncompressedPromise = async function () {
return new Promise<boolean>((resolve, reject) => {
compressing.zip.uncompress(filePath, temp_ver_dir).then(() => {
resolve(true);
}).catch((reason: any) => {
log("llonebot upgrade failed, ", reason);
if (reason?.errno == -4082) {
resolve(true);
}
resolve(false);
});
});
}
const uncompressedResult = await uncompressedPromise();
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR);
return uncompressedResult;
} }
return false; if (!downloadSuccess) {
log('llonebot upgrade error', 'download failed')
return false
}
const temp_ver_dir = path.join(TEMP_DIR, 'LLOneBot' + latestVersion)
const uncompressedPromise = async function () {
return new Promise<boolean>(resolve => {
compressing.zip
.uncompress(filePath, temp_ver_dir)
.then(() => {
resolve(true)
})
.catch(reason => {
log('llonebot upgrade failed, ', reason)
if (reason?.errno == -4082) {
resolve(true)
}
resolve(false)
})
})
}
const uncompressedResult = await uncompressedPromise()
// 复制文件
await copyFolder(temp_ver_dir, PLUGIN_DIR)
return uncompressedResult
}
return false
} }
export async function getRemoteVersion() { export async function getRemoteVersion() {
let Version = ""; 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;
}
} }
return Version; }
for (const mirror of downloadMirrorHosts) {
const version = await getRemoteVersionByDownloadMirror(mirror)
if (version) {
return version
}
}
return ''
} }
export async function getRemoteVersionByMirror(mirrorGithub: string) { export async function getRemoteVersionByDownloadMirror(mirrorGithub: string) {
let releasePage = "error"; try {
const source = 'https://raw.githubusercontent.com/LLOneBot/LLOneBot/main/src/version.ts'
try { const page = (await fetchFile(mirrorGithub + source)).data.toString()
releasePage = (await httpDownload(mirrorGithub + "/LLOneBot/LLOneBot/releases")).toString(); return page.match(/(\d+\.\d+\.\d+)/)?.[0]
// log("releasePage", releasePage); } catch (e) {
if (releasePage === "error") return ""; log(e?.toString())
return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0]; }
} catch { }
}
return ""; 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: any var authData: Dict | undefined
} var navigation: Dict | undefined
} }

View File

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

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

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

View File

@@ -1,479 +1,240 @@
// 运行在 Electron 主进程 下的插件入口 import path from 'node:path'
import Log from './log'
import {BrowserWindow, dialog, ipcMain} from 'electron'; import Core from '../ntqqapi/core'
import * as fs from 'node:fs'; import OneBot11Adapter from '../onebot11/adapter'
import {Config} from "../common/types"; 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,
CHANNEL_GET_CONFIG, CHANNEL_GET_CONFIG,
CHANNEL_LOG, CHANNEL_LOG,
CHANNEL_SELECT_FILE, CHANNEL_SELECT_FILE,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
CHANNEL_UPDATE, CHANNEL_UPDATE,
} from "../common/channels"; CHANNEL_SET_CONFIG_CONFIRMED
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; } from '../common/channels'
import {DATA_DIR} from "../common/utils"; import { getBuildVersion } from '../common/utils'
import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video'
import { getSession } from '../ntqqapi/wrapper'
import { Context } from 'cordis'
import { llonebotError, selfInfo, LOG_DIR, DATA_DIR, TEMP_DIR } from '../common/globalVars'
import { log, logFileName } from '../common/utils/legacyLog'
import { import {
friendRequests, NTQQFileApi,
getFriend, NTQQFileCacheApi,
getGroup, NTQQFriendApi,
getGroupMember, groups, NTQQGroupApi,
llonebotError, NTQQMsgApi,
refreshGroupMembers, NTQQUserApi,
selfInfo, NTQQWebApi,
uidMaps NTQQWindowApi
} from "../common/data"; } from '../ntqqapi/api'
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmdS, registerReceiveHook} from "../ntqqapi/hook"; import { mkdir } from 'node:fs/promises'
import {OB11Constructor} from "../onebot11/constructor"; import { existsSync, mkdirSync } from 'node:fs'
import { import Database from 'minato'
ChatType, import SQLiteDriver from '@minatojs/driver-sqlite'
FriendRequestNotify, import Store from './store'
GroupMemberRole,
GroupNotifies,
GroupNotifyTypes,
RawMessage
} from "../ntqqapi/types";
import {ob11HTTPServer} from "../onebot11/server/http";
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent";
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent";
import {postOB11Event} from "../onebot11/server/postOB11Event";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent";
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest";
import {OB11FriendRequestEvent} from "../onebot11/event/request/OB11FriendRequest";
import * as path from "node:path";
import {dbUtil} from "../common/db";
import {setConfig} from "./setConfig";
import {NTQQUserApi} from "../ntqqapi/api/user";
import {NTQQGroupApi} from "../ntqqapi/api/group";
import {crychic} from "../ntqqapi/external/crychic";
import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent";
import {checkNewVersion, upgradeLLOneBot} from "../common/utils/upgrade";
import {log} from "../common/utils/log";
import {getConfigUtil} from "../common/config";
import {checkFfmpeg} from "../common/utils/video";
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
let running = false; declare module 'cordis' {
interface Events {
'llonebot/config-updated': (input: LLOBConfig) => void
}
}
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null
// 加载插件时触发 // 加载插件时触发
function onLoad() { function onLoad() {
log("llonebot main onLoad"); if (!existsSync(DATA_DIR)) {
ipcMain.handle(CHANNEL_CHECK_VERSION, async (event, arg) => { mkdirSync(DATA_DIR, { recursive: true })
return checkNewVersion(); }
});
ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => { if (!existsSync(LOG_DIR)) {
return upgradeLLOneBot(); mkdirSync(LOG_DIR)
}); }
ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => {
const selectPath = new Promise<string>((resolve, reject) => { ipcMain.handle(CHANNEL_CHECK_VERSION, async () => {
dialog return checkNewVersion()
.showOpenDialog({ })
title: "请选择ffmpeg",
properties: ["openFile"], ipcMain.handle(CHANNEL_UPDATE, async () => {
buttonLabel: "确定", return upgradeLLOneBot()
}) })
.then((result) => {
log("选择文件", result); ipcMain.handle(CHANNEL_SELECT_FILE, async () => {
if (!result.canceled) { const selectPath = new Promise<string>((resolve, reject) => {
const _selectPath = path.join(result.filePaths[0]); dialog
resolve(_selectPath); .showOpenDialog({
// let config = getConfigUtil().getConfig() title: '请选择ffmpeg',
// config.ffmpeg = path.join(result.filePaths[0]); properties: ['openFile'],
// getConfigUtil().setConfig(config); buttonLabel: '确定',
} })
resolve("") .then((result) => {
}) log('选择文件', result)
.catch((err) => { if (!result.canceled) {
reject(err); const _selectPath = path.join(result.filePaths[0])
}); resolve(_selectPath)
} else {
resolve('')
}
})
.catch((err) => {
reject(err)
}) })
try {
return await selectPath;
} catch (e) {
log("选择文件出错", e)
return ""
}
}) })
if (!fs.existsSync(DATA_DIR)) { try {
fs.mkdirSync(DATA_DIR, {recursive: true}); return await selectPath
} catch (e) {
log('选择文件出错', e)
return ''
} }
ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { })
const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
llonebotError.ffmpegError = ffmpegOk ? "" : "没有找到ffmpeg,音频只能发送wav和silk,视频尺寸可能异常" ipcMain.handle(CHANNEL_ERROR, async () => {
let {httpServerError, wsServerError, otherError, ffmpegError} = llonebotError; const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg)
let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` llonebotError.ffmpegError = ffmpegOk ? '' : '没有找到 FFmpeg, 音频只能发送 WAV 和 SILK, 视频尺寸可能异常'
error = error.replace("\n\n", "\n") const { httpServerError, wsServerError, otherError, ffmpegError } = llonebotError
error = error.trim(); let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}`
log("查询llonebot错误信息", error); error = error.replace('\n\n', '\n')
return error; error = error.trim()
log('查询 LLOneBot 错误信息', error)
return error
})
ipcMain.handle(CHANNEL_GET_CONFIG, async () => {
const config = getConfigUtil().getConfig()
return config
})
ipcMain.handle(CHANNEL_SET_CONFIG, (_event, ask: boolean, config: LLOBConfig) => {
return new Promise<boolean>(resolve => {
if (!ask) {
getConfigUtil().setConfig(config)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
resolve(true)
return
}
dialog
.showMessageBox(mainWindow!, {
type: 'question',
buttons: ['确认', '取消'],
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认"
title: '确认保存',
message: '是否保存?',
detail: 'LLOneBot配置已更改是否保存',
})
.then((result) => {
if (result.response === 0) {
getConfigUtil().setConfig(config)
log('配置已更新', config)
checkFfmpeg(config.ffmpeg).then()
resolve(true)
}
})
.catch((err) => {
log('保存设置询问弹窗错误', err)
resolve(false)
})
}) })
ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { })
const config = getConfigUtil().getConfig()
return config; ipcMain.on(CHANNEL_LOG, (_event, arg) => {
log(arg)
})
async function start() {
log('process pid', process.pid)
const config = getConfigUtil().getConfig()
if (!config.enableLLOB) {
llonebotError.otherError = 'LLOneBot 未启动'
log('LLOneBot 开关设置为关闭不启动LLOneBot')
return
}
if (!existsSync(TEMP_DIR)) {
await mkdir(TEMP_DIR)
}
const dbDir = path.join(DATA_DIR, 'database')
if (!existsSync(dbDir)) {
await mkdir(dbDir)
}
const ctx = new Context()
ctx.plugin(Log, {
enable: config.log!,
filename: logFileName
}) })
ipcMain.on(CHANNEL_SET_CONFIG, (event, ask: boolean, config: Config) => { ctx.plugin(NTQQFileApi)
if (!ask) { ctx.plugin(NTQQFileCacheApi)
setConfig(config).then().catch(e => { ctx.plugin(NTQQFriendApi)
log("保存设置失败", e.stack) ctx.plugin(NTQQGroupApi)
}); ctx.plugin(NTQQMsgApi)
return ctx.plugin(NTQQUserApi)
} ctx.plugin(NTQQWebApi)
dialog.showMessageBox(mainWindow, { ctx.plugin(NTQQWindowApi)
type: 'question', ctx.plugin(Core, config)
buttons: ['确认', '取消'], ctx.plugin(OneBot11Adapter, {
defaultId: 0, // 默认选中的按钮0 代表第一个按钮,即 "确认" ...config.ob11,
title: '确认保存', heartInterval: config.heartInterval,
message: '是否保存?', token: config.token!,
detail: 'LLOneBot配置已更改是否保存' debug: config.debug!,
}).then(result => { reportSelfMessage: config.reportSelfMessage!,
if (result.response === 0) { msgCacheExpire: config.msgCacheExpire!,
setConfig(config).then().catch(e => { musicSignUrl: config.musicSignUrl,
log("保存设置失败", e.stack) enableLocalFile2Url: config.enableLocalFile2Url!,
}); ffmpeg: config.ffmpeg,
} else {
}
}).catch(err => {
log("保存设置询问弹窗错误", err);
});
}) })
ctx.plugin(Database)
ipcMain.on(CHANNEL_LOG, (event, arg) => { ctx.plugin(SQLiteDriver, {
log(arg); path: path.join(dbDir, `${selfInfo.uin}.db`)
}) })
ctx.plugin(Store)
ctx.start()
ipcMain.on(CHANNEL_SET_CONFIG_CONFIRMED, (event, config: LLOBConfig) => {
ctx.parallel('llonebot/config-updated', config)
})
}
async function postReceiveMsg(msgList: RawMessage[]) { const buildVersion = getBuildVersion()
const {debug, reportSelfMessage} = getConfigUtil().getConfig();
for (let message of msgList) {
// log("收到新消息", message.msgId, message.msgSeq) const intervalId = setInterval(() => {
// if (message.senderUin !== selfInfo.uin){ const self = Object.assign(selfInfo, {
message.msgShortId = await dbUtil.addMsg(message); uin: globalThis.authData?.uin,
// } uid: globalThis.authData?.uid,
online: true
OB11Constructor.message(message).then((msg) => { })
if (debug) { if (self.uin && (buildVersion >= 27187 || getSession())) {
msg.raw = message; clearInterval(intervalId)
} else { start()
if (msg.message.length === 0) {
return
}
}
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
if (isSelfMsg) {
msg.target_id = parseInt(message.peerUin);
}
postOB11Event(msg);
// log("post msg", msg)
}).catch(e => log("constructMessage error: ", e.stack.toString()));
OB11Constructor.GroupEvent(message).then(groupEvent => {
if (groupEvent) {
// log("post group event", groupEvent);
postOB11Event(groupEvent);
}
})
}
} }
}, 600)
async function startReceiveHook() {
if (getConfigUtil().getConfig().enablePoke) {
crychic.loadNode()
crychic.registerPokeHandler((id, isGroup) => {
log(`收到戳一戳消息了!是否群聊:${isGroup}id:${id}`)
let pokeEvent: OB11FriendPokeEvent | OB11GroupPokeEvent;
if (isGroup) {
pokeEvent = new OB11GroupPokeEvent(parseInt(id));
} else {
pokeEvent = new OB11FriendPokeEvent(parseInt(id));
}
postOB11Event(pokeEvent);
})
}
registerReceiveHook<{
msgList: Array<RawMessage>
}>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], async (payload) => {
try {
await postReceiveMsg(payload.msgList);
} catch (e) {
log("report message error: ", e.stack.toString());
}
})
registerReceiveHook<{ msgList: Array<RawMessage> }>([ReceiveCmdS.UPDATE_MSG], async (payload) => {
for (const message of payload.msgList) {
// log("message update", message.sendStatus, message.msgId, message.msgSeq)
if (message.recallTime != "0") { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断
// 撤回消息上报
const oriMessage = await dbUtil.getMsgByLongId(message.msgId)
if (!oriMessage) {
continue
}
oriMessage.recallTime = message.recallTime
dbUtil.updateMsg(oriMessage).then();
if (message.chatType == ChatType.friend) {
const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId);
postOB11Event(friendRecallEvent);
} else if (message.chatType == ChatType.group) {
let operatorId = message.senderUin
for (const element of message.elements) {
const operatorUid = element.grayTipElement?.revokeElement.operatorUid
const operator = await getGroupMember(message.peerUin, operatorUid)
operatorId = operator.uin
}
const groupRecallEvent = new OB11GroupRecallNoticeEvent(
parseInt(message.peerUin),
parseInt(message.senderUin),
parseInt(operatorId),
oriMessage.msgShortId
)
postOB11Event(groupRecallEvent);
}
// 不让入库覆盖原来消息,不然就获取不到撤回的消息内容了
continue
}
dbUtil.updateMsg(message).then();
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, async (payload) => {
const {reportSelfMessage} = getConfigUtil().getConfig();
if (!reportSelfMessage) {
return
}
// log("reportSelfMessage", payload)
try {
await postReceiveMsg([payload.msgRecord]);
} catch (e) {
log("report self message error: ", e.stack.toString());
}
})
registerReceiveHook<{
"doubt": boolean,
"oldestUnreadSeq": string,
"unreadCount": number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
// log("开始获取群通知详情")
let notify: GroupNotifies;
try {
notify = await NTQQGroupApi.getGroupNotifies();
} catch (e) {
// log("获取群通知详情失败", e);
return
}
const notifies = notify.notifies.slice(0, payload.unreadCount)
// log("获取群通知详情完成", notifies, payload);
for (const notify of notifies) {
try {
notify.time = Date.now();
// const notifyTime = parseInt(notify.seq) / 1000
// log(`加群通知时间${notifyTime}`, `LLOneBot启动时间${startTime}`);
// if (notifyTime < startTime) {
// continue;
// }
let existNotify = await dbUtil.getGroupNotify(notify.seq);
if (existNotify) {
continue
}
log("收到群通知", notify);
await dbUtil.addGroupNotify(notify);
// let member2: GroupMember;
// if (notify.user2.uid) {
// member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
// }
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) {
const member1 = await getGroupMember(notify.group.groupCode, notify.user1.uid);
log("有管理员变动通知");
refreshGroupMembers(notify.group.groupCode).then()
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode);
log("开始获取变动的管理员")
if (member1) {
log("变动管理员获取成功")
groupAdminNoticeEvent.user_id = parseInt(member1.uin);
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "unset" : "set";
// member1.role = notify.type == GroupNotifyTypes.ADMIN_SET ? GroupMemberRole.admin : GroupMemberRole.normal;
postOB11Event(groupAdminNoticeEvent, true);
} else {
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
}
} else if (notify.type == GroupNotifyTypes.MEMBER_EXIT || notify.type == GroupNotifyTypes.KICK_MEMBER) {
log("有成员退出通知", notify);
try {
const member1 = await NTQQUserApi.getUserDetailInfo(notify.user1.uid);
let operatorId = member1.uin;
let subType: GroupDecreaseSubType = "leave";
if (notify.user2.uid) {
// 是被踢的
const member2 = await getGroupMember(notify.group.groupCode, notify.user2.uid);
operatorId = member2.uin;
subType = "kick";
}
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin), parseInt(operatorId), subType)
postOB11Event(groupDecreaseEvent, true);
} catch (e) {
log("获取群通知的成员信息失败", notify, e.stack.toString())
}
} else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)) {
log("有加群请求");
let groupRequestEvent = new OB11GroupRequestEvent();
groupRequestEvent.group_id = parseInt(notify.group.groupCode);
let requestQQ = ""
try {
requestQQ = (await NTQQUserApi.getUserDetailInfo(notify.user1.uid)).uin;
} catch (e) {
log("获取加群人QQ号失败", e)
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0;
groupRequestEvent.sub_type = "add"
groupRequestEvent.comment = notify.postscript;
groupRequestEvent.flag = notify.seq;
postOB11Event(groupRequestEvent);
} else if (notify.type == GroupNotifyTypes.INVITE_ME) {
log("收到邀请我加群通知")
let groupInviteEvent = new OB11GroupRequestEvent();
groupInviteEvent.group_id = parseInt(notify.group.groupCode);
let user_id = (await getFriend(notify.user2.uid))?.uin
if (!user_id) {
user_id = (await NTQQUserApi.getUserDetailInfo(notify.user2.uid))?.uin
}
groupInviteEvent.user_id = parseInt(user_id);
groupInviteEvent.sub_type = "invite";
groupInviteEvent.flag = notify.seq;
postOB11Event(groupInviteEvent);
}
} catch (e) {
log("解析群通知失败", e.stack.toString());
}
}
} else if (payload.doubt) {
// 可能有群管理员变动
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, async (payload) => {
for (const req of payload.data.buddyReqs) {
let flag = req.friendUid + req.reqTime;
if (req.isUnread && (parseInt(req.reqTime) > startTime / 1000)) {
friendRequests[flag] = req;
log("有新的好友请求", req);
let friendRequestEvent = new OB11FriendRequestEvent();
try {
let requester = await NTQQUserApi.getUserDetailInfo(req.friendUid)
friendRequestEvent.user_id = parseInt(requester.uin);
} catch (e) {
log("获取加好友者QQ号失败", e);
}
friendRequestEvent.flag = flag;
friendRequestEvent.comment = req.extWords;
postOB11Event(friendRequestEvent);
}
}
})
}
let startTime = 0;
async function start() {
log("llonebot pid", process.pid)
llonebotError.otherError = "";
startTime = Date.now();
dbUtil.getReceivedTempUinMap().then(m => {
for (const [key, value] of Object.entries(m)) {
uidMaps[value] = key;
}
})
startReceiveHook().then();
NTQQGroupApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) {
ob11HTTPServer.start(config.ob11.httpPort)
}
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
}
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
}
log("LLOneBot start")
}
let getSelfNickCount = 0;
const init = async () => {
try {
log("start get self info")
const _ = await NTQQUserApi.getSelfInfo();
log("get self info api result:", _);
Object.assign(selfInfo, _);
selfInfo.nick = selfInfo.uin;
} catch (e) {
log("retry get self info", e);
}
if (!selfInfo.uin) {
selfInfo.uin = globalThis.authData?.uin;
selfInfo.uid = globalThis.authData?.uid;
selfInfo.nick = selfInfo.uin;
}
log("self info", selfInfo, globalThis.authData);
if (selfInfo.uin) {
async function getUserNick() {
try {
getSelfNickCount++;
const userInfo = (await NTQQUserApi.getUserDetailInfo(selfInfo.uid));
log("self info", userInfo);
if (userInfo) {
selfInfo.nick = userInfo.nick;
return
}
} catch (e) {
log("get self nickname failed", e.stack);
}
if (getSelfNickCount < 10) {
return setTimeout(getUserNick, 1000);
}
}
getUserNick().then()
start().then();
} else {
setTimeout(init, 1000)
}
}
setTimeout(init, 1000);
} }
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (selfInfo.uid) { if (![2, 4, 6].includes(window.id)) {
return return
} }
mainWindow = window; if (window.id === 2) {
log("window create", window.webContents.getURL().toString()) mainWindow = window
try { }
hookNTQQApiCall(window); //log('window create', window.webContents.getURL().toString())
hookNTQQApiReceive(window); try {
} catch (e) { hookNTQQApiCall(window, window.id !== 2)
log("LLOneBot hook error: ", e.toString()) hookNTQQApiReceive(window, window.id !== 2)
} } catch (e) {
log('LLOneBot hook error: ', String(e))
}
} }
try { try {
onLoad(); onLoad()
} catch (e: any) { } catch (e) {
console.log(e.toString()) console.log(e)
} }
// 这两个函数都是可选的 // 这两个函数都是可选的
export { export { onBrowserWindowCreated }
onBrowserWindowCreated
}

View File

@@ -1,60 +0,0 @@
import {Config} from "../common/types";
import {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;
}
}
}
}
log("old config", oldConfig)
log("配置已更新", config)
checkFfmpeg(config.ffmpeg).then()
}

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

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

View File

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

View File

@@ -1,61 +1,172 @@
import {Friend, FriendRequest} from "../types"; import { Friend, FriendV2, SimpleInfo, CategoryFriend } from '../types'
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; import { invoke, NTMethod, NTClass } from '../ntcall'
import {friendRequests} from "../../common/data"; import { getSession } from '@/ntqqapi/wrapper'
import { BuddyListReqType } from '../services'
export class NTQQFriendApi{ import { Dict, pick } from 'cosmokit'
static async getFriends(forced = false) { import { Service, Context } from 'cordis'
const data = await callNTQQApi<{
data: {
categoryId: number,
categroyName: string,
categroyMbCount: number,
buddyList: Friend[]
}[]
}>(
{
methodName: NTQQApiMethod.FRIENDS,
args: [{force_update: forced}, undefined],
cbCmd: ReceiveCmdS.FRIENDS
})
let _friends: Friend[] = [];
for (const fData of data.data) {
_friends.push(...fData.buddyList)
}
return _friends
}
static async likeFriend(uid: string, count = 1) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
}, null]
})
}
static async handleFriendRequest(flag: string, accept: boolean,) {
const request: FriendRequest = friendRequests[flag]
if (!request) {
throw `flat: ${flag}, 对应的好友请求不存在`
}
const result = await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_FRIEND_REQUEST,
args: [
{
"approvalInfo": {
"friendUid": request.friendUid,
"reqTime": request.reqTime,
accept
}
}
]
})
delete friendRequests[flag];
return result;
}
declare module 'cordis' {
interface Context {
ntFriendApi: NTQQFriendApi
}
}
export class NTQQFriendApi extends Service {
constructor(protected ctx: Context) {
super(ctx, 'ntFriendApi', true)
}
/** 大于或等于 26702 应使用 getBuddyV2 */
async getFriends() {
const data = await invoke<{
data: {
categoryId: number
categroyName: string
categroyMbCount: number
buddyList: Friend[]
}[]
}>(
'getBuddyList',
[],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
const _friends: Friend[] = []
for (const item of data.data) {
_friends.push(...item.buddyList)
}
return _friends
}
async handleFriendRequest(friendUid: string, reqTime: string, accept: boolean) {
const session = getSession()
if (session) {
return session.getBuddyService().approvalFriendRequest({
friendUid,
reqTime,
accept
})
} else {
return await invoke(NTMethod.HANDLE_FRIEND_REQUEST, [{
approvalInfo: {
friendUid,
reqTime,
accept,
},
}])
}
}
async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
const uids = data.buddyCategory.flatMap(item => item.buddyUids)
return Object.values(data.userSimpleInfos).filter(v => uids.includes(v.uid!))
}
/** uid => uin */
async getBuddyIdMap(refresh = false): Promise<Map<string, string>> {
const retMap: Map<string, string> = new Map()
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
for (const item of Object.values(data.userSimpleInfos)) {
if (retMap.size > 5000) {
break
}
retMap.set(item.uid!, item.uin!)
}
return retMap
}
async getBuddyV2ExWithCate(refresh = false) {
const session = getSession()
if (session) {
const uids: string[] = []
const categoryMap: Map<string, Dict> = 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 session.getProfileService().getCoreAndBaseInfo('nodeStore', uids)
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key)
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value
})
} else {
const data = await invoke<{
buddyCategory: CategoryFriend[]
userSimpleInfos: Record<string, SimpleInfo>
}>(
'getBuddyList',
[refresh],
{
className: NTClass.NODE_STORE_API,
cbCmd: ReceiveCmdS.FRIENDS,
afterFirstCmd: false,
}
)
const category: Map<number, Pick<CategoryFriend, 'buddyUids' | 'categroyName'>> = new Map()
for (const item of data.buddyCategory) {
category.set(item.categoryId, pick(item, ['buddyUids', 'categroyName']))
}
return Object.values(data.userSimpleInfos)
.filter(v => v.baseInfo && category.get(v.baseInfo.categoryId)?.buddyUids.includes(v.uid!))
.map(value => {
return {
...value,
categoryId: value.baseInfo.categoryId,
categroyName: category.get(value.baseInfo.categoryId)?.categroyName
}
})
}
}
async isBuddy(uid: string): Promise<boolean> {
const session = getSession()
if (session) {
return session.getBuddyService().isBuddy(uid)
} else {
return await invoke('nodeIKernelBuddyService/isBuddy', [{ uid }, null])
}
}
async getBuddyRecommendContact(uin: string) {
const ret = await invoke('nodeIKernelBuddyService/getBuddyRecommendContactArkJson', [{ uin }, null])
return ret.arkMsg
}
async setBuddyRemark(uid: string, remark: string) {
return await invoke('nodeIKernelBuddyService/setBuddyRemark', [{
remarkParams: { uid, remark }
}, null])
}
} }

View File

@@ -1,216 +1,336 @@
import {ReceiveCmdS} from "../hook"; import { ReceiveCmdS } from '../hook'
import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types"; import {
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; Group,
import {uidMaps} from "../../common/data"; GroupMember,
import {dbUtil} from "../../common/db"; GroupMemberRole,
import {log} from "../../common/utils/log"; GroupNotifies,
import {NTQQWindowApi, NTQQWindows} from "./window"; GroupRequestOperateTypes,
GetFileListParam,
OnGroupFileInfoUpdateParams,
PublishGroupBulletinReq
} from '../types'
import { invoke, NTClass, NTMethod } from '../ntcall'
import { GeneralCallResult } from '../services'
import { NTQQWindows } from './window'
import { getSession } from '../wrapper'
import { NodeIKernelGroupService } from '../services'
import { Service, Context } from 'cordis'
import { isNumeric } from '@/common/utils/misc'
export class NTQQGroupApi{ declare module 'cordis' {
static async getGroups(forced = false) { interface Context {
let cbCmd = ReceiveCmdS.GROUPS ntGroupApi: NTQQGroupApi
if (process.platform != "win32") { }
cbCmd = ReceiveCmdS.GROUPS_STORE }
}
const result = await callNTQQApi<{ export class NTQQGroupApi extends Service {
updateType: number, static inject = ['ntWindowApi']
groupList: Group[]
}>({methodName: NTQQApiMethod.GROUPS, args: [{force_update: forced}, undefined], cbCmd}) public groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>()
return result.groupList
} constructor(protected ctx: Context) {
static async getGroupMembers(groupQQ: string, num = 3000): Promise<GroupMember[]> { super(ctx, 'ntGroupApi', true)
const sceneId = await callNTQQApi({ }
methodName: NTQQApiMethod.GROUP_MEMBER_SCENE,
args: [{ async getGroups(): Promise<Group[]> {
groupCode: groupQQ, const result = await invoke<{
scene: "groupMemberList_MainWindow" updateType: number
}] groupList: Group[]
}) }>(
// log("get group member sceneId", sceneId); 'getGroupList',
try { [],
const result = await callNTQQApi<{ {
result: { infos: any } className: NTClass.NODE_STORE_API,
}>({ cbCmd: ReceiveCmdS.GROUPS_STORE,
methodName: NTQQApiMethod.GROUP_MEMBERS, afterFirstCmd: false,
args: [{ }
sceneId: sceneId, )
num: num return result.groupList
}, }
null
] async getGroupMembers(groupCode: string, num = 3000): Promise<Map<string, GroupMember>> {
}) const session = getSession()
// log("members info", typeof result.result.infos, Object.keys(result.result.infos)) let result: Awaited<ReturnType<NodeIKernelGroupService['getNextMemberList']>>
const values = result.result.infos.values() if (session) {
const groupService = session.getGroupService()
const members: GroupMember[] = Array.from(values) const sceneId = groupService.createMemberListScene(groupCode, 'groupMemberList_MainWindow')
for (const member of members) { result = await groupService.getNextMemberList(sceneId, undefined, num)
uidMaps[member.uid] = member.uin; } else {
} const sceneId = await invoke(NTMethod.GROUP_MEMBER_SCENE, [{ groupCode, scene: 'groupMemberList_MainWindow' }])
// log(uidMaps); result = await invoke(NTMethod.GROUP_MEMBERS, [{ sceneId, num }, null])
// log("members info", values); }
log(`get group ${groupQQ} members success`) if (result.errCode !== 0) {
return members throw ('获取群成员列表出错,' + result.errMsg)
} catch (e) { }
log(`get group ${groupQQ} members failed`, e) return result.result.infos
return [] }
}
} async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
static async getGroupNotifies() { const groupCodeStr = groupCode.toString()
// 获取管理员变更 const memberUinOrUidStr = memberUinOrUid.toString()
// 加群通知,退出通知,需要管理员权限 if (!this.groupMembers.has(groupCodeStr)) {
callNTQQApi<GeneralCallResult>({ try {
methodName: ReceiveCmdS.GROUP_NOTIFY, // 更新群成员列表
classNameIsRegister: true, this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr))
}).then() }
return await callNTQQApi<GroupNotifies>({ catch (e) {
methodName: NTQQApiMethod.GET_GROUP_NOTICE, return null
cbCmd: ReceiveCmdS.GROUP_NOTIFY, }
afterFirstCmd: false, }
args: [ let members = this.groupMembers.get(groupCodeStr)!
{"doubt": false, "startSeq": "", "number": 14}, const getMember = () => {
null let member: GroupMember | undefined = undefined
] if (isNumeric(memberUinOrUidStr)) {
}); member = Array.from(members.values()).find(member => member.uin === memberUinOrUidStr)
} } else {
static async getGroupIgnoreNotifies() { member = members.get(memberUinOrUidStr)
await NTQQGroupApi.getGroupNotifies(); }
return await NTQQWindowApi.openWindow(NTQQWindows.GroupNotifyFilterWindow,[], ReceiveCmdS.GROUP_NOTIFY); return member
} }
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) { let member = getMember()
const notify: GroupNotify = await dbUtil.getGroupNotify(seq) if (!member) {
if (!notify) { this.groupMembers.set(groupCodeStr, await this.getGroupMembers(groupCodeStr))
throw `${seq}对应的加群通知不存在` members = this.groupMembers.get(groupCodeStr)!
} member = getMember()
// delete groupNotifies[seq]; }
return await callNTQQApi<GeneralCallResult>({ return member
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST, }
args: [
{ async getGroupIgnoreNotifies() {
"doubt": false, await this.getSingleScreenNotifies(14)
"operateMsg": { return await this.ctx.ntWindowApi.openWindow<GeneralCallResult & GroupNotifies>(
"operateType": operateType, // 2 拒绝 NTQQWindows.GroupNotifyFilterWindow,
"targetMsg": { [],
"seq": seq, // 通知序列号 ReceiveCmdS.GROUP_NOTIFY,
"type": notify.type, )
"groupCode": notify.group.groupCode, }
"postscript": reason
} async getSingleScreenNotifies(num: number) {
} invoke(ReceiveCmdS.GROUP_NOTIFY, [], { classNameIsRegister: true })
}, return (await invoke<GroupNotifies>(
null 'nodeIKernelGroupService/getSingleScreenNotifies',
] [{ doubt: false, startSeq: '', number: num }, null],
}); {
} cbCmd: ReceiveCmdS.GROUP_NOTIFY,
static async quitGroup(groupQQ: string) { afterFirstCmd: false,
await callNTQQApi<GeneralCallResult>({ }
methodName: NTQQApiMethod.QUIT_GROUP, )).notifies
args: [ }
{"groupCode": groupQQ},
null async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
] const flagitem = flag.split('|')
}) const groupCode = flagitem[0]
} const seq = flagitem[1]
static async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') { const type = parseInt(flagitem[2])
return await callNTQQApi<GeneralCallResult>( const session = getSession()
{ if (session) {
methodName: NTQQApiMethod.KICK_MEMBER, return session.getGroupService().operateSysNotify(false, {
args: [ operateType, // 2 拒绝
{ targetMsg: {
groupCode: groupQQ, seq, // 通知序列号
kickUids, type,
refuseForever, groupCode,
kickReason, postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
} }
] })
} } else {
) return await invoke(NTMethod.HANDLE_GROUP_REQUEST, [{
} doubt: false,
static async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) { operateMsg: {
// timeStamp为秒数, 0为解除禁言 operateType,
return await callNTQQApi<GeneralCallResult>( targetMsg: {
{ seq,
methodName: NTQQApiMethod.MUTE_MEMBER, type,
args: [ groupCode,
{ postscript: reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
groupCode: groupQQ, },
memList, },
} }, null])
] }
} }
)
} async quitGroup(groupCode: string) {
static async banGroup(groupQQ: string, shutUp: boolean) { const session = getSession()
return await callNTQQApi<GeneralCallResult>({ if (session) {
methodName: NTQQApiMethod.MUTE_GROUP, return session.getGroupService().quitGroup(groupCode)
args: [ } else {
{ return await invoke(NTMethod.QUIT_GROUP, [{ groupCode }, null])
groupCode: groupQQ, }
shutUp }
}, null
] async kickMember(
}) groupCode: string,
} kickUids: string[],
static async setMemberCard(groupQQ: string, memberUid: string, cardName: string) { refuseForever = false,
return await callNTQQApi<GeneralCallResult>({ kickReason = '',
methodName: NTQQApiMethod.SET_MEMBER_CARD, ) {
args: [ const session = getSession()
{ if (session) {
groupCode: groupQQ, return session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason)
uid: memberUid, } else {
cardName return await invoke(NTMethod.KICK_MEMBER, [{ groupCode, kickUids, refuseForever, kickReason }])
}, null }
] }
})
} async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
static async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) { // timeStamp为秒数, 0为解除禁言
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_MEMBER_ROLE, if (session) {
args: [ return session.getGroupService().setMemberShutUp(groupCode, memList)
{ } else {
groupCode: groupQQ, return await invoke(NTMethod.MUTE_MEMBER, [{ groupCode, memList }])
uid: memberUid, }
role }
}, null
] async banGroup(groupCode: string, shutUp: boolean) {
}) const session = getSession()
} if (session) {
static async setGroupName(groupQQ: string, groupName: string) { return session.getGroupService().setGroupShutUp(groupCode, shutUp)
return await callNTQQApi<GeneralCallResult>({ } else {
methodName: NTQQApiMethod.SET_GROUP_NAME, return await invoke(NTMethod.MUTE_GROUP, [{ groupCode, shutUp }, null])
args: [ }
{ }
groupCode: groupQQ,
groupName async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
}, null const session = getSession()
] if (session) {
}) return session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName)
} } else {
return await invoke(NTMethod.SET_MEMBER_CARD, [{ groupCode, uid: memberUid, cardName }, null])
static async getGroupAtAllRemainCount(groupCode: string){ }
return await callNTQQApi<GeneralCallResult & {"atInfo":{"canAtAll": boolean,"RemainAtAllCountForUin": number,"RemainAtAllCountForGroup": number,"atTimesMsg": string,"canNotAtAllMsg":""}}>({ }
methodName: NTQQApiMethod.GROUP_AT_ALL_REMAIN_COUNT,
args: [ async setMemberRole(groupCode: string, memberUid: string, role: GroupMemberRole) {
{ const session = getSession()
groupCode if (session) {
}, null return session.getGroupService().modifyMemberRole(groupCode, memberUid, role)
] } else {
}) return await invoke(NTMethod.SET_MEMBER_ROLE, [{ groupCode, uid: memberUid, role }, null])
} }
}
// 头衔不可用
static async setGroupTitle(groupQQ: string, uid: string, title: string) { async setGroupName(groupCode: string, groupName: string) {
return await callNTQQApi<GeneralCallResult>({ const session = getSession()
methodName: NTQQApiMethod.SET_GROUP_TITLE, if (session) {
args: [ return session.getGroupService().modifyGroupName(groupCode, groupName, false)
{ } else {
groupCode: groupQQ, return await invoke(NTMethod.SET_GROUP_NAME, [{ groupCode, groupName }, null])
uid, }
title }
}, null
] async getGroupRemainAtTimes(groupCode: string) {
}) return await invoke(NTMethod.GROUP_AT_ALL_REMAIN_COUNT, [{ groupCode }, null])
} }
static publishGroupBulletin(groupQQ: string, title: string, content: string) {
async removeGroupEssence(groupCode: string, msgId: string) {
} const session = getSession()
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().removeGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/removeGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
}
async addGroupEssence(groupCode: string, msgId: string) {
const session = getSession()
if (session) {
const data = await session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return session.getGroupService().addGroupEssence({
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
})
} else {
const ntMsgApi = this.ctx.get('ntMsgApi')!
const data = await ntMsgApi.getMsgHistory({ chatType: 2, guildId: '', peerUid: groupCode }, msgId, 1, false)
return await invoke('nodeIKernelGroupService/addGroupEssence', [{
req: {
groupCode: groupCode,
msgRandom: Number(data?.msgList[0].msgRandom),
msgSeq: Number(data?.msgList[0].msgSeq)
}
}, null])
}
}
async createGroupFileFolder(groupId: string, folderName: string) {
return await invoke('nodeIKernelRichMediaService/createGroupFolder', [{ groupId, folderName }, null])
}
async deleteGroupFileFolder(groupId: string, folderId: string) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFolder', [{ groupId, folderId }, null])
}
async deleteGroupFile(groupId: string, fileIdList: string[], busIdList: number[]) {
return await invoke('nodeIKernelRichMediaService/deleteGroupFile', [{ groupId, busIdList, fileIdList }, null])
}
async getGroupFileList(groupId: string, fileListForm: GetFileListParam) {
invoke('nodeIKernelMsgListener/onGroupFileInfoUpdate', [], { classNameIsRegister: true })
const data = await invoke<{ fileInfo: OnGroupFileInfoUpdateParams }>(
'nodeIKernelRichMediaService/getGroupFileList',
[
{
groupId,
fileListForm
},
null,
],
{
cbCmd: 'nodeIKernelMsgListener/onGroupFileInfoUpdate',
afterFirstCmd: false,
cmdCB: (payload, result) => payload.fileInfo.reqId === result
}
)
return data.fileInfo.item
}
async publishGroupBulletin(groupCode: string, req: PublishGroupBulletinReq) {
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/publishGroupBulletin', [{ groupCode, psKey, req }, null])
}
async uploadGroupBulletinPic(groupCode: string, path: string) {
const ntUserApi = this.ctx.get('ntUserApi')!
const psKey = (await ntUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!
return await invoke('nodeIKernelGroupService/uploadGroupBulletinPic', [{ groupCode, psKey, path }, null])
}
async getGroupRecommendContact(groupCode: string) {
const ret = await invoke('nodeIKernelGroupService/getGroupRecommendContactArkJson', [{ groupCode }, null])
return ret.arkJson
}
async queryCachedEssenceMsg(groupCode: string, msgSeq = '0', msgRandom = '0') {
return await invoke('nodeIKernelGroupService/queryCachedEssenceMsg', [{
key: {
groupCode,
msgSeq: +msgSeq,
msgRandom: +msgRandom
}
}, null])
}
async getGroupHonorList(groupCode: string) {
// 还缺点东西
return await invoke('nodeIKernelGroupService/getGroupHonorList', [{
req: {
groupCode: [+groupCode]
}
}, null])
}
} }

View File

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

View File

@@ -1,221 +1,272 @@
import {callNTQQApi, GeneralCallResult, NTQQApiMethod} from "../ntcall"; import { invoke, NTMethod } from '../ntcall'
import {ChatType, RawMessage, SendMessageElement} from "../types"; import { GeneralCallResult } from '../services'
import {dbUtil} from "../../common/db"; import { RawMessage, SendMessageElement, Peer, ChatType } from '../types'
import {selfInfo} from "../../common/data"; import { getSession } from '@/ntqqapi/wrapper'
import {ReceiveCmdS, registerReceiveHook} from "../hook"; import { Service, Context } from 'cordis'
import {log} from "../../common/utils/log"; import { selfInfo } from '@/common/globalVars'
import {sleep} from "../../common/utils/helper";
import {isQQ998} from "../../common/utils";
export let sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void) | null> = {}// peerUid: callbackFunnc declare module 'cordis' {
interface Context {
export interface Peer { ntMsgApi: NTQQMsgApi
chatType: ChatType }
peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: ""
} }
export class NTQQMsgApi { export class NTQQMsgApi extends Service {
static async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) { static inject = ['ntUserApi']
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: NTQQApiMethod.GET_MULTI_MSG,
args: [{
peer,
rootMsgId,
parentMsgId
}, null]
})
}
static async activateChat(peer: Peer) { constructor(protected ctx: Context) {
// await this.fetchRecentContact(); super(ctx, 'ntMsgApi', true)
// await sleep(500); }
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_PREVIEW,
args: [{peer, cnt: 20}, null]
})
}
static async activateChatAndGetHistory(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.ACTIVE_CHAT_HISTORY,
// 参数似乎不是这样
args: [{peer, cnt: 20}, null]
})
}
static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新
return await callNTQQApi<GeneralCallResult & {msgList: RawMessage[]}>({
methodName: isQQ998 ? NTQQApiMethod.ACTIVE_CHAT_HISTORY : NTQQApiMethod.HISTORY_MSG,
args: [{
peer,
msgId,
cnt: count,
queryOrder: true,
}, null]
})
}
static async fetchRecentContact(){
await callNTQQApi({
methodName: NTQQApiMethod.RECENT_CONTACT,
args: [
{
fetchParam: {
anchorPointContact: {
contactId: '',
sortField: '',
pos: 0,
},
relativeMoveCount: 0,
listType: 2, // 1普通消息2群助手内的消息
count: 200,
fetchOld: true,
},
}
]
})
}
static async recallMsg(peer: Peer, msgIds: string[]) { async getTempChatInfo(chatType: ChatType, peerUid: string) {
return await callNTQQApi({ const session = getSession()
methodName: NTQQApiMethod.RECALL_MSG, if (session) {
args: [{ return session.getMsgService().getTempChatInfo(chatType, peerUid)
peer, } else {
msgIds return await invoke('nodeIKernelMsgService/getTempChatInfo', [{ chatType, peerUid }, null])
}, null]
})
} }
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, setEmoji: boolean = true) {
waitComplete = true, timeout = 10000) { // nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
const peerUid = peer.peerUid // 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
const session = getSession()
const emojiType = emojiId.length > 3 ? '2' : '1'
if (session) {
return session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiType, setEmoji)
} else {
return await invoke(NTMethod.EMOJI_LIKE, [{ peer, msgSeq, emojiId, emojiType, setEmoji }, null])
}
}
// 等待上一个相同的peer发送完 async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string) {
let checkLastSendUsingTime = 0; const session = getSession()
const waitLastSend = async () => { if (session) {
if (checkLastSendUsingTime > timeout) { return session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId)
throw ("发送超时") } else {
return await invoke(NTMethod.GET_MULTI_MSG, [{ peer, rootMsgId, parentMsgId }, null])
}
}
async activateChat(peer: Peer) {
return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_PREVIEW, [{ peer, cnt: 1 }, null])
}
async activateChatAndGetHistory(peer: Peer) {
return await invoke<GeneralCallResult>(NTMethod.ACTIVE_CHAT_HISTORY, [{ peer, cnt: 20 }, null])
}
async getAioFirstViewLatestMsgs(peer: Peer, cnt: number) {
return await invoke('nodeIKernelMsgService/getAioFirstViewLatestMsgs', [{ peer, cnt }, null])
}
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
if (!peer) throw new Error('peer is not allowed')
if (!msgIds) throw new Error('msgIds is not allowed')
const session = getSession()
if (session) {
return session.getMsgService().getMsgsByMsgId(peer, msgIds)
} else {
return await invoke('nodeIKernelMsgService/getMsgsByMsgId', [{ peer, msgIds }, null])
}
}
async getMsgHistory(peer: Peer, msgId: string, cnt: number, isReverseOrder: boolean = false) {
const session = getSession()
// 消息时间从旧到新
if (session) {
return session.getMsgService().getMsgsIncludeSelf(peer, msgId, cnt, isReverseOrder)
} else {
return await invoke(NTMethod.HISTORY_MSG, [{ peer, msgId, cnt, queryOrder: isReverseOrder }, null])
}
}
async recallMsg(peer: Peer, msgIds: string[]) {
const session = getSession()
if (session) {
return session.getMsgService().recallMsg(peer, msgIds)
} else {
return await invoke(NTMethod.RECALL_MSG, [{ peer, msgIds }, null])
}
}
async sendMsg(peer: Peer, msgElements: SendMessageElement[], timeout = 10000) {
const msgId = await this.generateMsgUniqueId(peer.chatType)
peer.guildId = msgId
const data = await invoke<{ msgList: RawMessage[] }>(
'nodeIKernelMsgService/sendMsg',
[
{
msgId: '0',
peer,
msgElements,
msgAttributeInfos: new Map()
},
null
],
{
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true
} }
let lastSending = sendMessagePool[peer.peerUid] }
if (lastSending) { return false
// log("有正在发送的消息,等待中...") },
await sleep(500); timeout
checkLastSendUsingTime += 500; }
return await waitLastSend(); )
} else { return data.msgList.find(msgRecord => msgRecord.guildId === msgId)
return; }
}
}
await waitLastSend();
let sentMessage: RawMessage = null; async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { const session = getSession()
delete sendMessagePool[peerUid]; if (session) {
sentMessage = rawMessage; return session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], [])
} } else {
return await invoke(NTMethod.FORWARD_MSG, [{
let checkSendCompleteUsingTime = 0; msgIds,
const checkSendComplete = async (): Promise<RawMessage> => { srcContact: srcPeer,
if (sentMessage) { dstContacts: [destPeer],
if (waitComplete) { commentElements: [],
if ((await dbUtil.getMsgByLongId(sentMessage.msgId)).sendStatus == 2) { msgAttributeInfos: new Map(),
return sentMessage }, null])
}
} else {
return sentMessage
}
// log(`给${peerUid}发送消息成功`)
}
checkSendCompleteUsingTime += 500
if (checkSendCompleteUsingTime > timeout) {
throw ('发送超时')
}
await sleep(500)
return await checkSendComplete()
}
callNTQQApi({
methodName: NTQQApiMethod.SEND_MSG,
args: [{
msgId: "0",
peer, msgElements,
msgAttributeInfos: new Map(),
}, null]
}).then()
return await checkSendComplete()
} }
}
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
return await callNTQQApi<GeneralCallResult>({ const senderShowName = await this.ctx.ntUserApi.getSelfNick(true)
methodName: NTQQApiMethod.FORWARD_MSG, const msgInfos = msgIds.map(id => {
args: [ return { msgId: id, senderShowName }
{ })
msgIds: msgIds, const selfUid = selfInfo.uid
srcContact: srcPeer, const data = await invoke<{ msgList: RawMessage[] }>(
dstContacts: [ 'nodeIKernelMsgService/multiForwardMsgWithComment',
destPeer [
], {
commentElements: [], msgInfos,
msgAttributeInfos: new Map() srcContact: srcPeer,
}, dstContact: destPeer,
null, commentElements: [],
] msgAttributeInfos: new Map(),
}) },
null,
],
{
cbCmd: 'nodeIKernelMsgListener/onMsgInfoListUpdate',
afterFirstCmd: false,
cmdCB: payload => {
for (const msgRecord of payload.msgList) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfUid) {
return true
}
}
return false
},
}
)
for (const msg of data.msgList) {
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
continue
}
const forwardData = JSON.parse(arkElement.arkElement!.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
continue
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfUid) {
return msg
}
} }
throw new Error('转发消息超时')
}
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { async getMsgsBySeqAndCount(peer: Peer, msgSeq: string, count: number, desc: boolean, z: boolean) {
const msgInfos = msgIds.map(id => { const session = getSession()
return {msgId: id, senderShowName: selfInfo.nick} if (session) {
}) return await session.getMsgService().getMsgsBySeqAndCount(peer, msgSeq, count, desc, z)
const apiArgs = [ } else {
{ return await invoke('nodeIKernelMsgService/getMsgsBySeqAndCount', [{
msgInfos, peer,
srcContact: srcPeer, cnt: count,
dstContact: destPeer, msgSeq,
commentElements: [], queryOrder: desc
msgAttributeInfos: new Map() }, null])
},
null,
]
return await new Promise<RawMessage>((resolve, reject) => {
let complete = false
setTimeout(() => {
if (!complete) {
reject("转发消息超时");
}
}, 5000)
registerReceiveHook(ReceiveCmdS.SELF_SEND_MSG, async (payload: { msgRecord: RawMessage }) => {
const msg = payload.msgRecord
// 需要判断它是转发的消息,并且识别到是当前转发的这一条
const arkElement = msg.elements.find(ele => ele.arkElement)
if (!arkElement) {
// log("收到的不是转发消息")
return
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData)
if (forwardData.app != 'com.tencent.multimsg') {
return
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
complete = true
await dbUtil.addMsg(msg)
resolve(msg)
log('转发消息成功:', payload)
}
})
callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.MULTI_FORWARD_MSG,
args: apiArgs
}).then(result => {
log("转发消息结果:", result, apiArgs)
if (result.result !== 0) {
complete = true;
reject("转发消息失败," + JSON.stringify(result));
}
})
})
} }
}
async getSingleMsg(peer: Peer, msgSeq: string) {
const session = getSession()
if (session) {
return await session.getMsgService().getSingleMsg(peer, msgSeq)
} else {
return await invoke('nodeIKernelMsgService/getSingleMsg', [{ peer, msgSeq }, null])
}
}
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, null])
}
async queryMsgsWithFilterExBySeq(peer: Peer, msgSeq: string, filterMsgTime: string, filterSendersUid: string[] = []) {
return await invoke('nodeIKernelMsgService/queryMsgsWithFilterEx', [{
msgId: '0',
msgTime: '0',
msgSeq,
params: {
chatInfo: peer,
filterMsgType: [],
filterSendersUid,
filterMsgToTime: filterMsgTime,
filterMsgFromTime: filterMsgTime,
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
}
}, null])
}
async setMsgRead(peer: Peer) {
return await invoke('nodeIKernelMsgService/setMsgRead', [{ peer }, null])
}
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number) {
return await invoke('nodeIKernelMsgService/getMsgEmojiLikesList', [{
peer,
msgSeq,
emojiId,
emojiType,
cnt: count
}, null])
}
async fetchFavEmojiList(count: number) {
return await invoke('nodeIKernelMsgService/fetchFavEmojiList', [{
resId: '',
count,
backwardFetch: true,
forceRefresh: true
}, null])
}
async generateMsgUniqueId(chatType: number) {
return await invoke('nodeIKernelMsgService/generateMsgUniqueId', [{ chatType }])
}
} }

View File

@@ -1,117 +1,291 @@
import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import { invoke } from '../ntcall'
import {SelfInfo, User} from "../types"; import { User, UserDetailInfoByUin, UserDetailInfoByUinV2, UserDetailInfoListenerArg } from '../types'
import {ReceiveCmdS} from "../hook"; import { getBuildVersion } from '@/common/utils'
import {uidMaps} from "../../common/data"; import { getSession } from '@/ntqqapi/wrapper'
import {NTQQWindowApi, NTQQWindows} from "./window"; import { RequestUtil } from '@/common/utils/request'
import {isQQ998, sleep} from "../../common/utils"; import { UserDetailSource, ProfileBizType } from '../services'
import { Time } from 'cosmokit'
let userInfoCache: Record<string, User> = {}; // uid: User import { Service, Context } from 'cordis'
import { selfInfo } from '@/common/globalVars'
export class NTQQUserApi{
static async setQQAvatar(filePath: string) {
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.SET_QQ_AVATAR,
args: [{
path:filePath
}, null],
timeoutSecond: 10 // 10秒不一定够
});
}
static async getSelfInfo() {
return await callNTQQApi<SelfInfo>({
className: NTQQApiClass.GLOBAL_DATA,
methodName: NTQQApiMethod.SELF_INFO, timeoutSecond: 2
})
}
static async getUserInfo(uid: string) {
const result = await callNTQQApi<{ profiles: Map<string, User> }>({
methodName: NTQQApiMethod.USER_INFO,
args: [{force: true, uids: [uid]}, undefined],
cbCmd: ReceiveCmdS.USER_INFO
})
return result.profiles.get(uid)
}
static async getUserDetailInfo(uid: string, getLevel=false) {
// this.getUserInfo(uid);
let methodName = !isQQ998 ? NTQQApiMethod.USER_DETAIL_INFO : NTQQApiMethod.USER_DETAIL_INFO_WITH_BIZ_INFO
const fetchInfo = async ()=>{
const result = await callNTQQApi<{ info: User }>({
methodName,
cbCmd: ReceiveCmdS.USER_DETAIL_INFO,
afterFirstCmd: false,
cmdCB: (payload) => {
const success = payload.info.uid == uid
// log("get user detail info", success, uid, payload)
return success
},
args: [
{
uid
},
null
]
})
const info = result.info
if (info?.uin) {
uidMaps[info.uid] = info.uin
}
return info
}
// 首次请求两次才能拿到的等级信息
if (!userInfoCache[uid] && getLevel) {
await fetchInfo()
await sleep(1000);
}
let userInfo = await fetchInfo()
userInfoCache[uid] = userInfo
return userInfo
}
static async getPSkey() {
return await callNTQQApi<string>({
className: NTQQApiClass.GROUP_HOME_WORK,
methodName: NTQQApiMethod.UPDATE_SKEY,
args: [
{
domain: "qun.qq.com"
}
]
})
}
static async getSkey(groupName: string, groupCode: string): Promise<{data: string}> {
return await NTQQWindowApi.openWindow<{data: string}>(NTQQWindows.GroupHomeWorkWindow, [{
groupName,
groupCode,
"source": "funcbar"
}], ReceiveCmdS.SKEY_UPDATE, 1);
// return await callNTQQApi<string>({
// className: NTQQApiClass.GROUP_HOME_WORK,
// methodName: NTQQApiMethod.UPDATE_SKEY,
// args: [
// {
// domain: "qun.qq.com"
// }
// ]
// })
// return await callNTQQApi<GeneralCallResult>({
// methodName: NTQQApiMethod.GET_SKEY,
// args: [
// {
// "domains": [
// "qzone.qq.com",
// "qlive.qq.com",
// "qun.qq.com",
// "gamecenter.qq.com",
// "vip.qq.com",
// "qianbao.qq.com",
// "qidian.qq.com"
// ],
// "isForNewPCQQ": false
// },
// null
// ]
// })
}
declare module 'cordis' {
interface Context {
ntUserApi: NTQQUserApi
}
}
export class NTQQUserApi extends Service {
static inject = ['ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context) {
super(ctx, 'ntUserApi', true)
}
async setQQAvatar(path: string) {
return await invoke(
'nodeIKernelProfileService/setHeader',
[
{ path },
null,
],
{
timeout: 10 * Time.second, // 10秒不一定够
}
)
}
async fetchUserDetailInfo(uid: string) {
const result = await invoke<{ info: UserDetailInfoListenerArg }>(
'nodeIKernelProfileService/fetchUserDetailInfo',
[
{
callFrom: 'BuddyProfileStore',
uid: [uid],
source: UserDetailSource.KSERVER,
bizList: [ProfileBizType.KALL]
},
null
],
{
cbCmd: 'nodeIKernelProfileListener/onUserDetailInfoChanged',
afterFirstCmd: false,
cmdCB: payload => payload.info.uid === uid,
}
)
const { info } = result
const ret: User = {
...info.simpleInfo.coreInfo,
...info.simpleInfo.status,
...info.simpleInfo.vasInfo,
...info.commonExt,
...info.simpleInfo.baseInfo,
qqLevel: info.commonExt?.qqLevel,
pendantId: ''
}
return ret
}
async getUserDetailInfo(uid: string) {
if (getBuildVersion() >= 26702) {
return this.fetchUserDetailInfo(uid)
}
const result = await invoke<{ info: User }>(
'nodeIKernelProfileService/getUserDetailInfoWithBizInfo',
[
{
uid,
bizList: [0]
},
null,
],
{
cbCmd: 'nodeIKernelProfileListener/onProfileDetailInfoChanged',
afterFirstCmd: false,
cmdCB: (payload) => payload.info.uid === uid,
}
)
return result.info
}
async getCookies(domain: string) {
const clientKeyData = await this.forceFetchClientKey()
if (clientKeyData?.result !== 0) {
throw new Error('获取clientKey失败')
}
const uin = selfInfo.uin
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + uin + '&clientkey=' + clientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + uin + '%2Finfocenter&keyindex=19%27'
const cookies: { [key: string]: string } = await RequestUtil.HttpsGetCookies(requestUrl)
return cookies
}
async getPSkey(domains: string[]) {
return await invoke('nodeIKernelTipOffService/getPskey', [{ domains, isForNewPCQQ: true }, null])
}
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(
'nodeIKernelProfileLikeService/setBuddyProfileLike',
[
{
doLikeUserInfo: {
friendUid: uid,
sourceId: 71,
doLikeCount: count,
doLikeTollCount: 0
}
},
null,
],
)
}
}
async getUidByUinV1(uin: string) {
const session = getSession()
// 通用转换开始尝试
let uid = (await session?.getUixConvertService().getUid([uin]))?.uidInfo.get(uin)
if (!uid) {
for (const membersList of this.ctx.ntGroupApi.groupMembers.values()) { //从群友列表转
for (const member of membersList.values()) {
if (member.uin === uin) {
uid = member.uid
break
}
}
if (uid) break
}
}
if (!uid) {
const unveifyUid = (await this.getUserDetailInfoByUin(uin)).info.uid //特殊转换
if (unveifyUid.indexOf('*') === -1) {
uid = unveifyUid
}
}
if (!uid) {
const friends = await this.ctx.ntFriendApi.getFriends() //从好友列表转
uid = friends.find(item => item.uin === uin)?.uid
}
return uid
}
async getUidByUinV2(uin: string) {
const session = getSession()
if (session) {
let uid = (await session.getGroupService().getUidByUins([uin])).uids.get(uin)
if (uid) return uid
uid = (await session.getProfileService().getUidByUin('FriendsServiceImpl', [uin])).get(uin)
if (uid) return uid
uid = (await session.getUixConvertService().getUid([uin])).uidInfo.get(uin)
if (uid) return uid
} else {
let uid = (await invoke('nodeIKernelGroupService/getUidByUins', [{ uin: [uin] }])).uids.get(uin)
if (uid) return uid
uid = (await invoke('nodeIKernelProfileService/getUidByUin', [{ callFrom: 'FriendsServiceImpl', uin: [uin] }])).get(uin)
if (uid) return uid
uid = (await invoke('nodeIKernelUixConvertService/getUid', [{ uins: [uin] }])).uidInfo.get(uin)
if (uid) return uid
}
const unveifyUid = (await this.getUserDetailInfoByUinV2(uin)).detail.uid //从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) return unveifyUid
}
async getUidByUin(uin: string) {
if (getBuildVersion() >= 26702) {
return this.getUidByUinV2(uin)
}
return this.getUidByUinV1(uin)
}
async getUserDetailInfoByUinV2(uin: string) {
return await invoke<UserDetailInfoByUinV2>(
'nodeIKernelProfileService/getUserDetailInfoByUin',
[
{ uin },
null,
],
)
}
async getUserDetailInfoByUin(uin: string) {
return await invoke<UserDetailInfoByUin>(
'nodeIKernelProfileService/getUserDetailInfoByUin',
[
{ uin },
null,
],
)
}
async getUinByUidV1(uid: string) {
const ret = await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])
let uin = ret.uinInfo.get(uid)
if (!uin) {
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
}
return uin
}
async getUinByUidV2(uid: string) {
const session = getSession()
if (session) {
let uin = (await session.getGroupService().getUinByUids([uid])).uins.get(uid)
if (uin) return uin
uin = (await session.getProfileService().getUinByUid('FriendsServiceImpl', [uid])).get(uid)
if (uin) return uin
uin = (await session.getUixConvertService().getUin([uid])).uinInfo.get(uid)
if (uin) return uin
} else {
let uin = (await invoke('nodeIKernelGroupService/getUinByUids', [{ uid: [uid] }])).uins.get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelProfileService/getUinByUid', [{ callFrom: 'FriendsServiceImpl', uid: [uid] }])).get(uid)
if (uin) return uin
uin = (await invoke('nodeIKernelUixConvertService/getUin', [{ uids: [uid] }])).uinInfo.get(uid)
if (uin) return uin
}
let uin = (await this.ctx.ntFriendApi.getBuddyIdMap(true)).get(uid)
if (uin) return uin
uin = (await this.getUserDetailInfo(uid)).uin //从QQ Native 转换
return uin
}
async getUinByUid(uid: string) {
if (getBuildVersion() >= 26702) {
return this.getUinByUidV2(uid)
}
return this.getUinByUidV1(uid)
}
async forceFetchClientKey() {
const session = getSession()
if (session) {
return await session.getTicketService().forceFetchClientKey('')
} else {
return await invoke('nodeIKernelTicketService/forceFetchClientKey', [{ domain: '' }, null])
}
}
async getSelfNick(refresh = false) {
if ((refresh || !selfInfo.nick) && selfInfo.uid) {
const userInfo = await this.getUserDetailInfo(selfInfo.uid)
if (userInfo) {
Object.assign(selfInfo, { nick: userInfo.nick })
return userInfo.nick
}
}
return selfInfo.nick
}
async setSelfStatus(status: number, extStatus: number, batteryStatus: number) {
return await invoke('nodeIKernelMsgService/setStatus', [{
statusReq: {
status,
extStatus,
batteryStatus,
}
}, null])
}
async getProfileLike(uid: string) {
return await invoke('nodeIKernelProfileLikeService/getBuddyProfileLike', [{
req: {
friendUids: [uid],
basic: 1,
vote: 1,
favorite: 0,
userProfile: 1,
type: 2,
start: 0,
limit: 20,
}
}, null])
}
} }

View File

@@ -1,86 +1,156 @@
import {groups} from "../../common/data"; import { RequestUtil } from '@/common/utils/request'
import {log} from "../../common/utils"; import { Service, Context } from 'cordis'
import {NTQQUserApi} from "./user"; import { Dict } from 'cosmokit'
export class WebApi{ declare module 'cordis' {
private static bkn: string; interface Context {
private static skey: string; ntWebApi: NTQQWebApi
private static pskey: string; }
private static cookie: string }
private defaultHeaders: Record<string,string> = {
"User-Agent": "QQ/8.9.28.635 CFNetwork/1312 Darwin/21.0.0" export enum WebHonorType {
} ALL = 'all',
TALKACTIVE = 'talkative',
constructor() { PERFROMER = 'performer',
LEGEND = 'legend',
} STORONGE_NEWBI = 'strong_newbie',
EMOTION = 'emotion'
public async addGroupDigest(groupCode: string, msgSeq: string){ }
const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`
const res = await this.request(url) export class NTQQWebApi extends Service {
return await res.json() static inject = ['ntUserApi']
}
constructor(protected ctx: Context) {
public async getGroupDigest(groupCode: string){ super(ctx, 'ntWebApi', true)
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20` }
const res = await this.request(url)
log(res.headers) genBkn(sKey: string) {
return await res.json() sKey = sKey || ''
} let hash = 5381
for (let i = 0; i < sKey.length; i++) {
private genBkn(sKey: string){ const code = sKey.charCodeAt(i)
sKey = sKey || ""; hash = hash + (hash << 5) + code
let hash = 5381; }
return (hash & 0x7FFFFFFF).toString()
for (let i = 0; i < sKey.length; i++) { }
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code; 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
return (hash & 0x7FFFFFFF).toString(); let resJson
} try {
private async init(){ const res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': cookieStr })
if (!WebApi.bkn) { const match = res.match(/window\.__INITIAL_STATE__=(.*?);/)
const group = groups[0]; if (match) {
WebApi.skey = (await NTQQUserApi.getSkey(group.groupName, group.groupCode)).data; resJson = JSON.parse(match[1].trim())
WebApi.bkn = this.genBkn(WebApi.skey); }
let cookie = await NTQQUserApi.getPSkey(); if (type === 1) {
const pskeyRegex = /p_skey=([^;]+)/; return resJson?.talkativeList
const match = cookie.match(pskeyRegex); } else {
const pskeyValue = match ? match[1] : null; return resJson?.actorList
WebApi.pskey = pskeyValue; }
if (cookie.indexOf("skey=;") !== -1) { } catch (e) {
cookie = cookie.replace("skey=;", `skey=${WebApi.skey};`); this.ctx.logger.error('获取当前群荣耀失败', url, e)
} }
WebApi.cookie = cookie; return undefined
// for(const kv of WebApi.cookie.split(";")){ }
// const [key, value] = kv.split("=");
// } const honorInfo: Dict = { group_id: groupCode }
// log("set cookie", key, value) const cookieObject = await this.ctx.ntUserApi.getCookies('qun.qq.com')
// await session.defaultSession.cookies.set({ const cookieStr = this.cookieToString(cookieObject)
// url: 'https://qun.qq.com', // 你要请求的域名
// name: key.trim(), if (getType === WebHonorType.TALKACTIVE || getType === WebHonorType.ALL) {
// value: value.trim(), try {
// expirationDate: Date.now() / 1000 + 300000, // Cookie 过期时间例如设置为当前时间之后的300秒 const RetInternal = await getDataInternal(groupCode, 1)
// }); if (!RetInternal) {
// } throw new Error('获取龙王信息失败')
} }
} honorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
private async request(url: string, method: "GET" | "POST" = "GET", headers: Record<string, string> = {}){ avatar: RetInternal[0]?.avatar,
nickname: RetInternal[0]?.name,
await this.init(); day_count: 0,
url += "&bkn=" + WebApi.bkn; description: RetInternal[0]?.desc
let _headers: Record<string, string> = { }
...this.defaultHeaders, ...headers, honorInfo.talkative_list = [];
"Cookie": WebApi.cookie, for (const talkative_ele of RetInternal) {
credentials: 'include' honorInfo.talkative_list.push({
} user_id: talkative_ele?.uin,
log("request", url, _headers) avatar: talkative_ele?.avatar,
const options = { description: talkative_ele?.desc,
method: method, day_count: 0,
headers: _headers nickname: talkative_ele?.name
} })
return fetch(url, options) }
} } catch (e) {
this.ctx.logger.error(e)
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
const RetInternal = await getDataInternal(groupCode, 2)
if (!RetInternal) {
throw new Error('获取群聊之火失败')
}
honorInfo.performer_list = []
for (const performer_ele of RetInternal) {
honorInfo.performer_list.push({
user_id: performer_ele?.uin,
nickname: performer_ele?.name,
avatar: performer_ele?.avatar,
description: performer_ele?.desc
})
}
} catch (e) {
this.ctx.logger.error(e)
}
}
if (getType === WebHonorType.PERFROMER || getType === WebHonorType.ALL) {
try {
const RetInternal = await getDataInternal(groupCode, 3)
if (!RetInternal) {
throw new Error('获取群聊炽焰失败')
}
honorInfo.legend_list = []
for (const legend_ele of RetInternal) {
honorInfo.legend_list.push({
user_id: legend_ele?.uin,
nickname: legend_ele?.name,
avatar: legend_ele?.avatar,
desc: legend_ele?.description
})
}
} catch (e) {
this.ctx.logger.error('获取群聊炽焰失败', e)
}
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try {
const RetInternal = await getDataInternal(groupCode, 6)
if (!RetInternal) {
throw new Error('获取快乐源泉失败')
}
honorInfo.emotion_list = []
for (const emotion_ele of RetInternal) {
honorInfo.emotion_list.push({
user_id: emotion_ele?.uin,
nickname: emotion_ele?.name,
avatar: emotion_ele?.avatar,
desc: emotion_ele?.description
})
}
} catch (e) {
this.ctx.logger.error('获取快乐源泉失败', e)
}
}
//冒尖小春笋好像已经被tx扬了
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
honorInfo.strong_newbie_list = []
}
return honorInfo
}
private cookieToString(cookieObject: Dict) {
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ')
}
} }

View File

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

View File

@@ -1,256 +0,0 @@
import {
AtType,
ElementType,
PicType,
SendArkElement,
SendFaceElement,
SendFileElement,
SendPicElement,
SendPttElement,
SendReplyElement,
SendTextElement,
SendVideoElement
} from "./types";
import {promises as fs} from "node:fs";
import ffmpeg from "fluent-ffmpeg"
import {NTQQFileApi} from "./api/file";
import {calculateFileMD5, isGIF} from "../common/utils/file";
import {log} from "../common/utils/log";
import {defaultVideoThumb, getVideoInfo} from "../common/utils/video";
import {encodeSilk} from "../common/utils/audio";
export class SendMsgElementConstructor {
static poke(groupCode: string, uin: string){
return null
}
static text(content: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: "",
textElement: {
content,
atType: AtType.notAt,
atUid: "",
atTinyId: "",
atNtUid: "",
},
};
}
static at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement {
return {
elementType: ElementType.TEXT,
elementId: "",
textElement: {
content: `@${atName}`,
atType,
atUid,
atTinyId: "",
atNtUid,
},
};
}
static reply(msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement {
return {
elementType: ElementType.REPLY,
elementId: "",
replyElement: {
replayMsgSeq: msgSeq, // raw.msgSeq
replayMsgId: msgId, // raw.msgId
senderUin: senderUin,
senderUinStr: senderUinStr,
}
}
}
static async pic(picPath: string, summary: string = "", subType: 0|1=0): Promise<SendPicElement> {
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType);
if (fileSize === 0) {
throw "文件异常大小为0";
}
const imageSize = await NTQQFileApi.getImageSize(picPath);
const picElement = {
md5HexStr: md5,
fileSize: fileSize.toString(),
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: "",
fileSubId: "",
thumbFileSize: 0,
summary
};
log("图片信息", picElement)
return {
elementType: ElementType.PIC,
elementId: "",
picElement,
};
}
static async file(filePath: string, fileName: string = ""): Promise<SendFileElement> {
const {md5, fileName: _fileName, path, fileSize} = await NTQQFileApi.uploadFile(filePath, ElementType.FILE);
if (fileSize === 0) {
throw "文件异常大小为0";
}
let element: SendFileElement = {
elementType: ElementType.FILE,
elementId: "",
fileElement: {
fileName: fileName || _fileName,
"filePath": path,
"fileSize": (fileSize).toString(),
}
}
return element;
}
static async video(filePath: string, fileName: string = "", diyThumbPath: string = ""): Promise<SendVideoElement> {
let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw "文件异常大小为0";
}
const pathLib = require("path");
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`)
thumb = pathLib.dirname(thumb)
// log("thumb 目录", thumb)
let videoInfo = {
width: 1920, height: 1080,
time: 15,
format: "mp4",
size: fileSize,
filePath
};
try {
videoInfo = await getVideoInfo(path);
log("视频信息", videoInfo)
} catch (e) {
log("获取视频信息失败", e)
}
const createThumb = new Promise<string>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`
const thumbPath = pathLib.join(thumb, thumbFileName)
ffmpeg(filePath)
.on("end", () => {
})
.on("error", (err) => {
log("获取视频封面失败,使用默认封面", err)
if (diyThumbPath) {
fs.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
}).catch(reject)
} else {
fs.writeFile(thumbPath, defaultVideoThumb).then(() => {
resolve(thumbPath);
}).catch(reject)
}
})
.screenshots({
timestamps: [0],
filename: thumbFileName,
folder: thumb,
size: videoInfo.width + "x" + videoInfo.height
}).on("end", () => {
resolve(thumbPath);
});
})
let thumbPath = new Map()
const _thumbPath = await createThumb;
const thumbSize = (await fs.stat(_thumbPath)).size;
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath);
let element: SendVideoElement = {
elementType: ElementType.VIDEO,
elementId: "",
videoElement: {
fileName: fileName || _fileName,
filePath: path,
videoMd5: md5,
thumbMd5,
fileTime: videoInfo.time,
thumbPath: thumbPath,
thumbSize,
thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height,
fileSize: "" + fileSize,
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// sourceVideoCodecFormat: 2
}
}
return element;
}
static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQFileApi.uploadFile(silkPath, ElementType.PTT);
if (fileSize === 0) {
throw "文件异常大小为0";
}
if (converted) {
fs.unlink(silkPath).then();
}
return {
elementType: ElementType.PTT,
elementId: "",
pttElement: {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
duration: duration,
formatType: 1,
voiceType: 1,
voiceChangeType: 0,
canConvert2Text: true,
waveAmplitudes: [
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
],
fileSubId: "",
playState: 1,
autoConvertText: 0,
}
};
}
static face(faceId: number): SendFaceElement {
return {
elementType: ElementType.FACE,
elementId: "",
faceElement: {
faceIndex: faceId,
faceType: 1
}
}
}
static ark(data: any): SendArkElement {
return {
elementType: ElementType.ARK,
elementId: "",
arkElement: {
bytesData: data,
linkInfo: null,
subElementType: null
}
}
}
}

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

@@ -0,0 +1,233 @@
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 { llonebotError } from '../common/globalVars'
import { isNumeric } from '../common/utils/misc'
import { NTMethod } from './ntcall'
import {
RawMessage,
GroupNotify,
FriendRequestNotify,
FriendRequest,
GroupMember,
CategoryFriend,
SimpleInfo,
ChatType
} from './types'
import { selfInfo } from '../common/globalVars'
import { version } from '../version'
import { invoke } from './ntcall'
declare module 'cordis' {
interface Context {
app: Core
}
interface Events {
'nt/message-created': (input: RawMessage[]) => void
'nt/message-deleted': (input: RawMessage) => void
'nt/message-sent': (input: RawMessage) => void
'nt/group-notify': (input: GroupNotify[]) => void
'nt/friend-request': (input: FriendRequest[]) => void
'nt/group-member-info-updated': (input: { groupCode: string, members: GroupMember[] }) => void
'nt/system-message-created': (input: Uint8Array) => void
}
}
class Core extends Service {
static inject = ['ntMsgApi', 'ntFriendApi', 'ntGroupApi']
constructor(protected ctx: Context, public config: Core.Config) {
super(ctx, 'app', true)
}
public start() {
llonebotError.otherError = ''
this.registerListener()
this.ctx.logger.info(`LLOneBot/${version}`)
this.ctx.on('llonebot/config-updated', input => {
Object.assign(this.config, input)
})
}
private registerListener() {
registerReceiveHook<{
data?: CategoryFriend[]
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.friend })
}
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.temp) {
this.ctx.ntMsgApi.activateChatAndGetHistory(peer).then(() => {
this.ctx.ntMsgApi.getMsgHistory(peer, '', 20).then(({ msgList }) => {
const lastTempMsg = msgList.at(-1)
if (Date.now() / 1000 - Number(lastTempMsg?.msgTime) < 5) {
this.ctx.parallel('nt/message-created', [lastTempMsg!])
}
})
})
}
else {
this.ctx.ntMsgApi.activateChat(peer)
}
}
}
})
registerCallHook(NTMethod.DELETE_ACTIVE_CHAT, async (payload) => {
const peerUid = payload[0] as string
this.ctx.logger.info('激活的聊天窗口被删除,准备重新激活', peerUid)
let chatType = ChatType.friend
if (isNumeric(peerUid)) {
chatType = ChatType.group
}
else if (!(await this.ctx.ntFriendApi.isBuddy(peerUid))) {
chatType = ChatType.temp
}
const peer = { peerUid, chatType }
await this.ctx.sleep(1000)
this.ctx.ntMsgApi.activateChat(peer).then((r) => {
this.ctx.logger.info('重新激活聊天窗口', peer, { result: r.result, errMsg: r.errMsg })
})
})
registerReceiveHook<{
groupCode: string
dataSource: number
members: Set<GroupMember>
}>(ReceiveCmdS.GROUP_MEMBER_INFO_UPDATE, async (payload) => {
const groupCode = payload.groupCode
const members = Array.from(payload.members.values())
this.ctx.parallel('nt/group-member-info-updated', { groupCode, members })
})
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.NEW_MSG, ReceiveCmdS.NEW_ACTIVE_MSG], payload => {
this.ctx.parallel('nt/message-created', payload.msgList)
})
const sentMsgIds = new Map<string, boolean>()
const recallMsgIds: string[] = [] // 避免重复上报
registerReceiveHook<{ msgList: RawMessage[] }>([ReceiveCmdS.UPDATE_MSG], payload => {
for (const msg of payload.msgList) {
if (msg.recallTime !== '0' && !recallMsgIds.includes(msg.msgId)) {
recallMsgIds.push(msg.msgId)
this.ctx.parallel('nt/message-deleted', msg)
} else if (sentMsgIds.get(msg.msgId)) {
sentMsgIds.delete(msg.msgId)
this.ctx.parallel('nt/message-sent', msg)
}
}
})
registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmdS.SELF_SEND_MSG, payload => {
if (!this.config.reportSelfMessage) {
return
}
sentMsgIds.set(payload.msgRecord.msgId, true)
})
const groupNotifyFlags: string[] = []
registerReceiveHook<{
doubt: boolean
oldestUnreadSeq: string
unreadCount: number
}>(ReceiveCmdS.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
let notifies: GroupNotify[]
try {
notifies = (await this.ctx.ntGroupApi.getSingleScreenNotifies(14)).slice(0, payload.unreadCount)
} catch (e) {
return
}
const list = notifies.filter(v => {
const flag = v.group.groupCode + '|' + v.seq + '|' + v.type
if (groupNotifyFlags.includes(flag)) {
return false
}
groupNotifyFlags.push(flag)
return true
})
this.ctx.parallel('nt/group-notify', list)
}
})
registerReceiveHook<FriendRequestNotify>(ReceiveCmdS.FRIEND_REQUEST, payload => {
this.ctx.parallel('nt/friend-request', payload.data.buddyReqs)
})
invoke('nodeIKernelMsgListener/onRecvSysMsg', [], { classNameIsRegister: true })
registerReceiveHook<{
msgBuf: number[]
}>('nodeIKernelMsgListener/onRecvSysMsg', payload => {
this.ctx.parallel('nt/system-message-created', Uint8Array.from(payload.msgBuf))
})
}
}
namespace Core {
export interface Config extends LLOBConfig {
}
}
export default Core

377
src/ntqqapi/entities.ts Normal file
View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,211 +1,181 @@
import {ipcMain} from "electron"; import { ipcMain } from 'electron'
import {hookApiCallbacks, ReceiveCmd, ReceiveCmdS, registerReceiveHook, removeReceiveHook} from "./hook"; import { hookApiCallbacks, registerReceiveHook, removeReceiveHook } from './hook'
import { log } from '../common/utils/legacyLog'
import { randomUUID } from 'node:crypto'
import {
GeneralCallResult,
NodeIKernelBuddyService,
NodeIKernelProfileService,
NodeIKernelGroupService,
NodeIKernelProfileLikeService,
NodeIKernelMsgService,
NodeIKernelMSFService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService
} from './services'
import {v4 as uuidv4} from "uuid" export enum NTClass {
import {log} from "../common/utils/log"; NT_API = 'ns-ntApi',
import {NTQQWindow, NTQQWindowApi, NTQQWindows} from "./api/window"; FS_API = 'ns-FsApi',
import {WebApi} from "./api/webapi"; OS_API = 'ns-OsApi',
import {HOOK_LOG} from "../common/config"; WINDOW_API = 'ns-WindowApi',
HOTUPDATE_API = 'ns-HotUpdateApi',
export enum NTQQApiClass { BUSINESS_API = 'ns-BusinessApi',
NT_API = "ns-ntApi", GLOBAL_DATA = 'ns-GlobalDataApi',
FS_API = "ns-FsApi", SKEY_API = 'ns-SkeyApi',
OS_API = "ns-OsApi", GROUP_HOME_WORK = 'ns-GroupHomeWork',
WINDOW_API = "ns-WindowApi", GROUP_ESSENCE = 'ns-GroupEssence',
HOTUPDATE_API = "ns-HotUpdateApi", NODE_STORE_API = 'ns-NodeStoreApi'
BUSINESS_API = "ns-BusinessApi",
GLOBAL_DATA = "ns-GlobalDataApi",
SKEY_API = "ns-SkeyApi",
GROUP_HOME_WORK = "ns-GroupHomeWork",
GROUP_ESSENCE = "ns-GroupEssence",
} }
export enum NTQQApiMethod { 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',
MEDIA_FILE_PATH = 'nodeIKernelMsgService/getRichMediaFilePathForGuild',
RECALL_MSG = 'nodeIKernelMsgService/recallMsg',
EMOJI_LIKE = 'nodeIKernelMsgService/setMsgEmojiLikes',
FORWARD_MSG = 'nodeIKernelMsgService/forwardMsgWithComment',
LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", SELF_INFO = 'fetchAuthData',
SELF_INFO = "fetchAuthData", FILE_TYPE = 'getFileType',
FRIENDS = "nodeIKernelBuddyService/getBuddyList", FILE_MD5 = 'getFileMd5',
GROUPS = "nodeIKernelGroupService/getGroupList", FILE_COPY = 'copyFile',
GROUP_MEMBER_SCENE = "nodeIKernelGroupService/createMemberListScene", IMAGE_SIZE = 'getImageSizeFromPath',
GROUP_MEMBERS = "nodeIKernelGroupService/getNextMemberList", FILE_SIZE = 'getFileSize',
USER_INFO = "nodeIKernelProfileService/getUserSimpleInfo", CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath',
USER_DETAIL_INFO = "nodeIKernelProfileService/getUserDetailInfo", CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath',
USER_DETAIL_INFO_WITH_BIZ_INFO = "nodeIKernelProfileService/getUserDetailInfoWithBizInfo", CACHE_PATH_SESSION = 'getCleanableAppSessionPathList',
FILE_TYPE = "getFileType", OPEN_EXTRA_WINDOW = 'openExternalWindow',
FILE_MD5 = "getFileMd5",
FILE_COPY = "copyFile",
IMAGE_SIZE = "getImageSizeFromPath",
FILE_SIZE = "getFileSize",
MEDIA_FILE_PATH = "nodeIKernelMsgService/getRichMediaFilePathForGuild",
RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
FORWARD_MSG = "nodeIKernelMsgService/forwardMsgWithComment",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
GROUP_AT_ALL_REMAIN_COUNT = "nodeIKernelGroupService/getGroupRemainAtTimes",
// READ_FRIEND_REQUEST = "nodeIKernelBuddyListener/onDoubtBuddyReqUnreadNumChange"
HANDLE_FRIEND_REQUEST = "nodeIKernelBuddyService/approvalFriendRequest",
KICK_MEMBER = "nodeIKernelGroupService/kickMember",
MUTE_MEMBER = "nodeIKernelGroupService/setMemberShutUp",
MUTE_GROUP = "nodeIKernelGroupService/setGroupShutUp",
SET_MEMBER_CARD = "nodeIKernelGroupService/modifyMemberCardName",
SET_MEMBER_ROLE = "nodeIKernelGroupService/modifyMemberRole",
PUBLISH_GROUP_BULLETIN = "nodeIKernelGroupService/publishGroupBulletinBulletin",
SET_GROUP_NAME = "nodeIKernelGroupService/modifyGroupName",
SET_GROUP_TITLE = "nodeIKernelGroupService/modifyMemberSpecialTitle",
CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan', GROUP_MEMBER_SCENE = 'nodeIKernelGroupService/createMemberListScene',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths', GROUP_MEMBERS = 'nodeIKernelGroupService/getNextMemberList',
CACHE_PATH_HOT_UPDATE = 'getHotUpdateCachePath', HANDLE_GROUP_REQUEST = 'nodeIKernelGroupService/operateSysNotify',
CACHE_PATH_DESKTOP_TEMP = 'getDesktopTmpPath', QUIT_GROUP = 'nodeIKernelGroupService/quitGroup',
CACHE_PATH_SESSION = 'getCleanableAppSessionPathList', GROUP_AT_ALL_REMAIN_COUNT = 'nodeIKernelGroupService/getGroupRemainAtTimes',
CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache', KICK_MEMBER = 'nodeIKernelGroupService/kickMember',
CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys', MUTE_MEMBER = 'nodeIKernelGroupService/setMemberShutUp',
MUTE_GROUP = 'nodeIKernelGroupService/setGroupShutUp',
SET_MEMBER_CARD = 'nodeIKernelGroupService/modifyMemberCardName',
SET_MEMBER_ROLE = 'nodeIKernelGroupService/modifyMemberRole',
SET_GROUP_NAME = 'nodeIKernelGroupService/modifyGroupName',
CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo', HANDLE_FRIEND_REQUEST = 'nodeIKernelBuddyService/approvalFriendRequest',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
OPEN_EXTRA_WINDOW = 'openExternalWindow', CACHE_SET_SILENCE = 'nodeIKernelStorageCleanService/setSilentScan',
CACHE_ADD_SCANNED_PATH = 'nodeIKernelStorageCleanService/addCacheScanedPaths',
SET_QQ_AVATAR = 'nodeIKernelProfileService/setHeader', CACHE_SCAN = 'nodeIKernelStorageCleanService/scanCache',
GET_SKEY = "nodeIKernelTipOffService/getPskey", CACHE_CLEAR = 'nodeIKernelStorageCleanService/clearCacheDataByKeys',
UPDATE_SKEY = "updatePskey", CACHE_CHAT_GET = 'nodeIKernelStorageCleanService/getChatCacheInfo',
CACHE_FILE_GET = 'nodeIKernelStorageCleanService/getFileCacheInfo',
FETCH_UNITED_COMMEND_CONFIG = "nodeIKernelUnitedConfigService/fetchUnitedCommendConfig" // 发包需要调用的 CACHE_CHAT_CLEAR = 'nodeIKernelStorageCleanService/clearChatCacheInfo',
} }
enum NTQQApiChannel { export enum NTChannel {
IPC_UP_2 = "IPC_UP_2", IPC_UP_1 = 'IPC_UP_1',
IPC_UP_3 = "IPC_UP_3", IPC_UP_2 = 'IPC_UP_2',
IPC_UP_1 = "IPC_UP_1", IPC_UP_3 = 'IPC_UP_3',
IPC_UP_4 = 'IPC_UP_4'
} }
interface NTQQApiParams { interface NTService {
methodName: NTQQApiMethod | string, nodeIKernelBuddyService: NodeIKernelBuddyService
className?: NTQQApiClass, nodeIKernelProfileService: NodeIKernelProfileService
channel?: NTQQApiChannel, nodeIKernelGroupService: NodeIKernelGroupService
classNameIsRegister?: boolean nodeIKernelProfileLikeService: NodeIKernelProfileLikeService
args?: unknown[], nodeIKernelMsgService: NodeIKernelMsgService
cbCmd?: ReceiveCmd | null, nodeIKernelMSFService: NodeIKernelMSFService
cmdCB?: (payload: any) => boolean; nodeIKernelUixConvertService: NodeIKernelUixConvertService
afterFirstCmd?: boolean, // 是否在methodName调用完之后再去hook cbCmd nodeIKernelRichMediaService: NodeIKernelRichMediaService
timeoutSecond?: number, nodeIKernelTicketService: NodeIKernelTicketService
nodeIKernelTipOffService: NodeIKernelTipOffService
} }
export function callNTQQApi<ReturnType>(params: NTQQApiParams) { interface InvokeOptions<ReturnType> {
let { className?: NTClass
className, methodName, channel, args, channel?: NTChannel
cbCmd, timeoutSecond: timeout, classNameIsRegister?: boolean
classNameIsRegister, cmdCB, afterFirstCmd cbCmd?: string | string[]
} = params; cmdCB?: (payload: ReturnType, result: unknown) => boolean
className = className ?? NTQQApiClass.NT_API; afterFirstCmd?: boolean // 是否在methodName调用完之后再去hook cbCmd
channel = channel ?? NTQQApiChannel.IPC_UP_2; timeout?: number
args = args ?? []; }
timeout = timeout ?? 5;
afterFirstCmd = afterFirstCmd ?? true; export function invoke<
const uuid = uuidv4(); R extends Awaited<ReturnType<Extract<NTService[S][M], (...args: any) => unknown>>>,
HOOK_LOG && log("callNTQQApi", channel, className, methodName, args, uuid) S extends keyof NTService = any,
return new Promise((resolve: (data: ReturnType) => void, reject) => { M extends keyof NTService[S] & string = any
// log("callNTQQApiPromise", channel, className, methodName, args, uuid) >(method: Extract<unknown, `${S}/${M}`> | string, args: unknown[], options: InvokeOptions<R> = {}) {
const _timeout = timeout * 1000 const className = options.className ?? NTClass.NT_API
let success = false const channel = options.channel ?? NTChannel.IPC_UP_2
let eventName = className + "-" + channel[channel.length - 1]; const timeout = options.timeout ?? 5000
if (classNameIsRegister) { const afterFirstCmd = options.afterFirstCmd ?? true
eventName += "-register"; let eventName = className + '-' + channel[channel.length - 1]
} if (options.classNameIsRegister) {
const apiArgs = [methodName, ...args] eventName += '-register'
if (!cbCmd) { }
// QQ后端会返回结果并且可以根据uuid识别 return new Promise<R>((resolve, reject) => {
hookApiCallbacks[uuid] = (r: ReturnType) => { const apiArgs = [method, ...args]
success = true const callbackId = randomUUID()
resolve(r) const timeoutId = setTimeout(() => {
}; log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs)
} else { reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`)
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据 }, timeout)
const secondCallback = () => {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => { if (!options.cbCmd) {
// log(methodName, "second callback", cbCmd, payload, cmdCB); // QQ后端会返回结果并且可以根据uuid识别
if (!!cmdCB) { hookApiCallbacks[callbackId] = res => {
if (cmdCB(payload)) { clearTimeout(timeoutId)
removeReceiveHook(hookId); resolve(res)
success = true }
resolve(payload); }
} else {
} else { let result: unknown
removeReceiveHook(hookId); // 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
success = true const secondCallback = () => {
resolve(payload); const hookId = registerReceiveHook<R>(options.cbCmd!, (payload) => {
} if (options.cmdCB) {
}) if (options.cmdCB(payload, result)) {
removeReceiveHook(hookId)
clearTimeout(timeoutId)
resolve(payload)
} }
!afterFirstCmd && secondCallback(); }
hookApiCallbacks[uuid] = (result: GeneralCallResult) => { else {
log(`${methodName} callback`, result) removeReceiveHook(hookId)
if (result?.result == 0 || result === undefined) { clearTimeout(timeoutId)
afterFirstCmd && secondCallback(); resolve(payload)
} else { }
success = true
reject(`ntqq api call failed, ${result.errMsg}`);
}
}
}
setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName)
if (!success) {
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs);
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
}
}, _timeout)
ipcMain.emit(
channel,
{
sender: {
send: (..._args: unknown[]) => {
},
},
},
{type: 'request', callbackId: uuid, eventName},
apiArgs
)
})
}
export interface GeneralCallResult {
result: number, // 0: success
errMsg: string
}
export class NTQQApi {
static async call(className: NTQQApiClass, cmdName: string, args: any[],) {
return await callNTQQApi<GeneralCallResult>({
className,
methodName: cmdName,
args: [
...args,
]
}) })
}
!afterFirstCmd && secondCallback()
hookApiCallbacks[callbackId] = (res: GeneralCallResult) => {
result = res
if (res?.result === 0 || ['undefined', 'number'].includes(typeof res)) {
afterFirstCmd && secondCallback()
}
else {
log('ntqq api call failed,', method, res)
clearTimeout(timeoutId)
reject(`ntqq api call failed, ${method}, ${res.errMsg}`)
}
}
} }
static async fetchUnitedCommendConfig() { ipcMain.emit(
return await callNTQQApi<GeneralCallResult>({ channel,
methodName: NTQQApiMethod.FETCH_UNITED_COMMEND_CONFIG, {
args:[ sender: {
{ send: () => {
groups: ['100243'] },
} },
] },
}) { type: 'request', callbackId, eventName },
} 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

@@ -0,0 +1,31 @@
import { GeneralCallResult } from './common'
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export interface NodeIKernelBuddyService {
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
data: {
categoryId: number
categorySortId: number
categroyName: string
categroyMbCount: number
onlineCount: number
buddyUids: string[]
}[]
}>
setBuddyRemark(uid: number, remark: string): void
isBuddy(uid: string): boolean
approvalFriendRequest(arg: {
friendUid: string
reqTime: string
accept: boolean
}): Promise<void>
getBuddyRecommendContactArkJson(uid: string, phoneNumber: string): Promise<GeneralCallResult & { arkMsg: string }>
}

View File

@@ -0,0 +1,126 @@
import {
GroupMember,
GroupMemberRole,
GroupNotifyType,
GroupRequestOperateTypes,
} from '@/ntqqapi/types'
import { GeneralCallResult } from './common'
export interface NodeIKernelGroupService {
getGroupHonorList(req: { groupCode: number[] }): Promise<{
errCode: number
errMsg: string
groupMemberHonorList: {
honorList: {
groupCode: string
id: number[]
isGray: number
}[]
cacheTs: number
honorInfos: unknown[]
joinTime: number
}
}>
getUinByUids(uins: string[]): Promise<{
errCode: number
errMsg: string
uins: Map<string, string>
}>
getUidByUins(uins: string[]): Promise<{
errCode: number
errMsg: string
uids: Map<string, string>
}>
queryCachedEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<{
items: {
groupCode: string
msgSeq: number
msgRandom: number
msgSenderUin: string
msgSenderNick: string
opType: number
opUin: string
opNick: string
opTime: number
grayTipSeq: string
}[]
}>
setHeader(uid: string, path: string): unknown
createMemberListScene(groupCode: string, scene: string): string
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number
errMsg: string
result: {
ids: {
uid: string
index: number
}[]
infos: Map<string, GroupMember>
finish: boolean
hasRobot: boolean
}
}>
kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>
modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void
modifyMemberCardName(groupCode: string, uid: string, cardName: string): void
modifyGroupName(groupCode: string, groupName: string, arg: false): void
quitGroup(groupCode: string): void
getSingleScreenNotifies(force: boolean, startSeq: string, num: number): Promise<GeneralCallResult>
operateSysNotify(
doubt: boolean,
operateMsg: {
operateType: GroupRequestOperateTypes // 2 拒绝
targetMsg: {
seq: string // 通知序列号
type: GroupNotifyType
groupCode: string
postscript: string
}
}
): Promise<void>
publishGroupBulletin(groupCode: string, pskey: string, data: unknown): Promise<GeneralCallResult>
uploadGroupBulletinPic(groupCode: string, pskey: string, imagePath: string): Promise<{
errCode: number
errMsg: string
picInfo?: {
id: string
width: number
height: number
}
}>
getGroupRemainAtTimes(groupCode: string): Promise<GeneralCallResult & {
atInfo: {
canAtAll: boolean
RemainAtAllCountForUin: number
RemainAtAllCountForGroup: number
atTimesMsg: string
canNotAtAllMsg: ''
}
}>
setGroupShutUp(groupCode: string, shutUp: boolean): void
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>
getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>
addGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
removeGroupEssence(param: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>
}

View File

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

View File

@@ -0,0 +1,118 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/ntqqapi/types'
import { GeneralCallResult } from './common'
export interface QueryMsgsParams {
chatInfo: Peer
filterMsgType: []
filterSendersUid: string[]
filterMsgFromTime: string
filterMsgToTime: string
pageLimit: number
isReverseOrder: boolean
isIncludeCurrent: boolean
}
export interface TmpChatInfoApi {
errMsg: string
result: number
tmpChatInfo?: TmpChatInfo
}
export interface TmpChatInfo {
chatType: number
fromNick: string
groupCode: string
peerUid: string
sessionType: number
sig: string
}
export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<unknown, unknown>): Promise<GeneralCallResult>
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>
setStatus(args: { status: number, extStatus: number, batteryStatus: number }): Promise<GeneralCallResult>
forwardMsg(msgIds: string[], srcContact: Peer, dstContacts: Peer[], commentElements: MessageElement[]): Promise<GeneralCallResult>
forwardMsgWithComment(...args: unknown[]): Promise<GeneralCallResult>
multiForwardMsgWithComment(...args: unknown[]): unknown
getAioFirstViewLatestMsgs(peer: Peer, num: number): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: 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[] }>
getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getSingleMsg(peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>
queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>
setMsgRead(peer: Peer): Promise<GeneralCallResult>
getRichMediaFilePathForGuild(arg: {
md5HexStr: string
fileName: string
elementType: ElementType
elementSubType: number
thumbSize: 0
needCreate: true
downloadType: 1
file_uuid: ''
}): string
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: {
uin: string
emoId: number
emoPath: string
isExist: boolean
resId: string
url: string
md5: string
emoOriginalPath: string
thumbPath: string
RomaingType: string
isAPNG: false
isMarkFace: false
eId: string
epId: string
ocrWord: string
modifyWord: string
exposeNum: number
clickNum: number
desc: string
}[]
}>
downloadRichMedia(...args: unknown[]): unknown
setMsgEmojiLikes(...args: unknown[]): unknown
getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number
errMsg: string
emojiLikesList: {
tinyId: string
nickName: string
headUrl: string
}[]
cookie: string
isLastPage: boolean
isFirstPage: boolean
}>
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi>
}

View File

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

View File

@@ -0,0 +1,31 @@
import { SimpleInfo } from '../types'
import { GeneralCallResult } from './common'
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}
export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string, string>>
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string, string>>
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>
setHeader(arg: string): Promise<GeneralCallResult>
getUserDetailInfoWithBizInfo(uid: string, biz: unknown[]): Promise<GeneralCallResult>
getUserDetailInfoByUin(uin: string): Promise<unknown>
}

View File

@@ -0,0 +1,43 @@
import { GetFileListParam, Peer } from '../types'
import { GeneralCallResult } from './common'
export interface NodeIKernelRichMediaService {
getVideoPlayUrlV2(peer: Peer, msgId: string, elemId: string, videoCodecFormat: number, exParams: { downSourceType: number, triggerType: number }): Promise<GeneralCallResult & {
urlResult: {
v4IpUrl: []
v6IpUrl: []
domainUrl: {
url: string
isHttps: boolean
httpsDomain: string
}[]
videoCodecFormat: number
}
}>
deleteGroupFolder(groupCode: string, folderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>
createGroupFolder(groupCode: string, folderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: unknown, groupItem: unknown[] } }>
getGroupFileList(groupCode: string, params: GetFileListParam): Promise<GeneralCallResult & {
groupSpaceResult: {
retCode: number
retMsg: string
clientWording: string
totalSpace: number
usedSpace: number
allUpload: boolean
}
}>
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown
deleteGroupFile(groupCode: string, params: Array<number>, files: Array<string>): Promise<GeneralCallResult & {
transGroupFileResult: {
result: unknown
successFileIdList: Array<unknown>
failFileIdList: Array<unknown>
}
}>
}

View File

@@ -0,0 +1,10 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelTicketService {
forceFetchClientKey(arg: string): Promise<GeneralCallResult & {
url: string
keyIndex: string
clientKey: string
expireTime: string
}>
}

View File

@@ -0,0 +1,5 @@
import { GeneralCallResult } from './common'
export interface NodeIKernelTipOffService {
getPskey(domainList: string[], nocache: boolean): Promise<GeneralCallResult & { domainPskeyMap: Map<string, string> }>
}

View File

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

View File

@@ -0,0 +1,8 @@
export enum GeneralCallResultStatus {
OK = 0
}
export interface GeneralCallResult {
result: GeneralCallResultStatus
errMsg: string
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,410 +1,567 @@
import {GroupMemberRole} from "./group"; import { GroupMemberRole } from './group'
import exp from "constants";
export interface GetFileListParam {
sortType: number
fileCount: number
startIndex: number
sortOrder: number
showOnlinedocFolder: number
folderId?: string
}
export enum ElementType { export enum ElementType {
TEXT = 1, UNKNOWN = 0,
PIC = 2, TEXT = 1,
FILE = 3, PIC = 2,
PTT = 4, FILE = 3,
VIDEO = 5, PTT = 4,
FACE = 6, VIDEO = 5,
REPLY = 7, FACE = 6,
ARK = 10, REPLY = 7,
WALLET = 9,
GreyTip = 8, //Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10,
MFACE = 11,
LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
} }
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT, elementType: ElementType.TEXT
elementId: "", elementId: ''
textElement: { textElement: TextElement
content: string,
atType: number,
atUid: string,
atTinyId: string,
atNtUid: string,
}
} }
export interface SendPttElement { export interface SendPttElement {
elementType: ElementType.PTT, elementType: ElementType.PTT
elementId: "", elementId: ''
pttElement: { pttElement: {
fileName: string, fileName: string
filePath: string, filePath: string
md5HexStr: string, md5HexStr: string
fileSize: number, fileSize: number
duration: number, // 单位是秒 duration: number // 单位是秒
formatType: number, formatType: number
voiceType: number, voiceType: number
voiceChangeType: number, voiceChangeType: number
canConvert2Text: boolean, canConvert2Text: boolean
waveAmplitudes: number[], waveAmplitudes: number[]
fileSubId: "", fileSubId: ''
playState: number, playState: number
autoConvertText: number, autoConvertText: number
} }
} }
export enum PicType { export enum PicType {
gif = 2000, gif = 2000,
jpg = 1000 jpg = 1000,
} }
export enum PicSubType { export enum PicSubType {
normal = 0, // 普通图片,大图 normal = 0, // 普通图片,大图
face = 1 // 表情包小图 face = 1, // 表情包小图
} }
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC, elementType: ElementType.PIC
elementId: "", elementId: ''
picElement: { picElement: {
md5HexStr: string, md5HexStr: string
fileSize: number | string, fileSize: number | string
picWidth: number, picWidth: number
picHeight: number, picHeight: number
fileName: string, fileName: string
sourcePath: string, sourcePath: string
original: boolean, original: boolean
picType: PicType, picType: PicType
picSubType: PicSubType, picSubType: PicSubType
fileUuid: string, fileUuid: string
fileSubId: string, fileSubId: string
thumbFileSize: number, thumbFileSize: number
summary: string, summary: string
}, }
} }
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY, elementType: ElementType.REPLY
elementId: "", elementId: ''
replyElement: { replyElement: Partial<ReplyElement>
replayMsgSeq: string,
replayMsgId: string,
senderUin: string,
senderUinStr: string,
}
} }
export interface SendFaceElement { export interface SendFaceElement {
elementType: ElementType.FACE, elementType: ElementType.FACE
elementId: "", elementId: ''
faceElement: FaceElement faceElement: FaceElement
}
export interface SendMarketFaceElement {
elementType: ElementType.MFACE
marketFaceElement: MarketFaceElement
}
export interface TextElement {
content: string
atType: number
atUid: string
atTinyId: string
atNtUid: string
}
export interface ReplyElement {
replayMsgSeq: string
replayMsgId: string
senderUin: string
senderUinStr: string
sourceMsgIdInRecords: string
senderUid: string
senderUidStr: string
sourceMsgIsIncPic: boolean // 原消息是否有图片
sourceMsgText: string
replyMsgTime: string
} }
export interface FileElement { export interface FileElement {
"fileMd5"?: "", fileMd5?: string
"fileName": string, fileName: string
"filePath": string, filePath: string
"fileSize": string, fileSize: string
"picHeight"?: number, picHeight?: number
"picWidth"?: number, picWidth?: number
"picThumbPath"?: {}, folderId?: string
"file10MMd5"?: "", picThumbPath?: Map<number, string>
"fileSha"?: "", file10MMd5?: string
"fileSha3"?: "", fileSha?: string
"fileUuid"?: "", fileSha3?: string
"fileSubId"?: "", fileUuid?: string
"thumbFileSize"?: number, fileSubId?: string
fileBizId?: number thumbFileSize?: number
fileBizId?: number
} }
export interface SendFileElement { export interface SendFileElement {
elementType: ElementType.FILE elementType: ElementType.FILE
elementId: "", elementId: ''
fileElement: FileElement fileElement: FileElement
} }
export interface SendVideoElement { export interface SendVideoElement {
elementType: ElementType.VIDEO elementType: ElementType.VIDEO
elementId: "", elementId: ''
videoElement: VideoElement videoElement: VideoElement
} }
export interface SendArkElement { export interface SendArkElement {
elementType: ElementType.ARK, elementType: ElementType.ARK
elementId: "", elementId: ''
arkElement: ArkElement arkElement: ArkElement
} }
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement =
SendPicElement | SendReplyElement | SendFaceElement | SendFileElement | SendVideoElement | SendArkElement | SendTextElement
| SendPttElement
| SendPicElement
| SendReplyElement
| SendFaceElement
| SendMarketFaceElement
| SendFileElement
| SendVideoElement
| SendArkElement
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
atAll = 1, atAll = 1,
atUser = 2 atUser = 2,
} }
export enum ChatType { export enum ChatType {
friend = 1, friend = 1,
group = 2, group = 2,
temp = 100 temp = 100,
} }
export interface PttElement { export interface PttElement {
canConvert2Text: boolean; canConvert2Text: boolean
duration: number; // 秒数 duration: number // 秒数
fileBizId: null; fileBizId: null
fileId: number; // 0 fileId: number // 0
fileName: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr" fileName: string // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
filePath: string; // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr" filePath: string // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
fileSize: string; // "4261" fileSize: string // "4261"
fileSubId: string; // "0" fileSubId: string // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV" fileUuid: string // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1 formatType: string // 1
invalidState: number; // 0 invalidState: number // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6" md5HexStr: string // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0 playState: number // 0
progress: number; // 0 progress: number // 0
text: string; // "" text: string // ""
transferStatus: number; // 0 transferStatus: number // 0
translateStatus: number; // 0 translateStatus: number // 0
voiceChangeType: number; // 0 voiceChangeType: number // 0
voiceType: number; // 0 voiceType: number // 0
waveAmplitudes: number[]; waveAmplitudes: number[]
} }
export interface ArkElement { export interface ArkElement {
bytesData: string; bytesData: string
linkInfo: null, linkInfo: null
subElementType: null subElementType: null
} }
export const IMAGE_HTTP_HOST = "https://gchat.qpic.cn" export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn'
export const IMAGE_HTTP_HOST_NT = "https://multimedia.nt.qq.com.cn" export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'
export interface PicElement { export interface PicElement {
originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn picSubType: PicSubType
originImageMd5?: string; picType: PicType // 有这玩意儿吗
sourcePath: string; // 图片本地路径 originImageUrl: string // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
thumbPath: Map<number, string>; originImageMd5?: string
picWidth: number; sourcePath: string // 图片本地路径
picHeight: number; thumbPath: Map<number, string>
fileSize: number; picWidth: number
fileName: string; picHeight: number
fileUuid: string; fileSize: string
md5HexStr?: string; fileName: string
fileUuid: string
md5HexStr?: string
} }
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
INVITE_NEW_MEMBER = 12, REVOKE = 1,
MEMBER_NEW_TITLE = 17 PROCLAMATION = 2,
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
operatorRemark: string; operatorRemark: string
operatorMemRemark?: string; operatorMemRemark?: string
wording: string; // 自定义的撤回提示语 origMsgSenderUid?: string
} isSelfOperate?: boolean
aioOpGrayTipElement: TipAioOpGrayTipElement, wording: string // 自定义的撤回提示语
groupElement: TipGroupElement, }
xmlElement: { aioOpGrayTipElement?: TipAioOpGrayTipElement
content: string; groupElement?: TipGroupElement
}, xmlElement?: {
jsonGrayTipElement: { templId: string
jsonStr: string; content: string
} }
jsonGrayTipElement?: {
busiId: string
jsonStr: string
}
}
export enum FaceIndex {
dice = 358,
RPS = 359, // 石头剪刀布
} }
export interface FaceElement { export interface FaceElement {
faceIndex: number, faceIndex: number
faceType: 1 faceType: number
faceText?: string
packId?: string
stickerId?: string
sourceType?: number
stickerType?: number
resultId?: string
surpriseId?: string
randomType?: number
pokeType?: number
} }
export interface MarketFaceElement { export interface MarketFaceElement {
"itemType": 6, emojiPackageId: number
"faceInfo": 1, faceName?: string
"emojiPackageId": 203875, emojiId: string
"subType": 3, key: string
"mediaType": 0, imageWidth?: number
"imageWidth": 200, imageHeight?: number
"imageHeight": 200,
"faceName": string,
"emojiId": "094d53bd1c9ac5d35d04b08e8a6c992c",
"key": "a8b1dd0aebc8d910",
"param": null,
"mobileParam": null,
"sourceType": null,
"startTime": null,
"endTime": null,
"emojiType": 1,
"hasIpProduct": null,
"voiceItemHeightArr": null,
"sourceName": null,
"sourceJumpUrl": null,
"sourceTypeName": null,
"backColor": null,
"volumeColor": null,
"staticFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c_aio.png",
"dynamicFacePath": "E:\\SystemDocuments\\QQ\\721011692\\nt_qq\\nt_data\\Emoji\\marketface\\203875\\094d53bd1c9ac5d35d04b08e8a6c992c",
"supportSize": [
{
"width": 300,
"height": 300
},
{
"width": 200,
"height": 200
}
],
"apngSupportSize": null
} }
export interface VideoElement { export interface VideoElement {
"filePath": string, filePath: string
"fileName": string, fileName: string
"videoMd5"?: string, videoMd5?: string
"thumbMd5"?: string thumbMd5?: string
"fileTime"?: number, // second fileTime?: number // second
"thumbSize"?: number, // byte thumbSize?: number // byte
"fileFormat"?: number, // 2表示mp4 fileFormat?: number // 2表示mp4
"fileSize"?: string, // byte fileSize?: string // byte
"thumbWidth"?: number, thumbWidth?: number
"thumbHeight"?: number, thumbHeight?: number
"busiType"?: 0, // 未知 busiType?: 0 // 未知
"subBusiType"?: 0, // 未知 subBusiType?: 0 // 未知
"thumbPath"?: Map<number, any>, thumbPath?: Map<number, string>
"transferStatus"?: 0, // 未知 transferStatus?: 0 // 未知
"progress"?: 0, // 下载进度? progress?: 0 // 下载进度?
"invalidState"?: 0, // 未知 invalidState?: 0 // 未知
"fileUuid"?: string, // 可以用于下载链接? fileUuid?: string // 可以用于下载链接?
"fileSubId"?: "", fileSubId?: ''
"fileBizId"?: null, fileBizId?: null
"originVideoMd5"?: "", originVideoMd5?: ''
"import_rich_media_context"?: null, import_rich_media_context?: null
"sourceVideoCodecFormat"?: number sourceVideoCodecFormat?: number
} }
export interface MarkdownElement { export interface MarkdownElement {
content: string, content: string
} }
export interface InlineKeyboardElementRowButton{ export interface InlineKeyboardElementRowButton {
"id": "", id: ''
"label": string, label: string
"visitedLabel": string, visitedLabel: string
"style": 1, // 未知 style: 1 // 未知
"type": 2, // 未知 type: 2 // 未知
"clickLimit": 0, // 未知 clickLimit: 0 // 未知
"unsupportTips": "请升级新版手机QQ", unsupportTips: '请升级新版手机QQ'
"data": string, data: string
"atBotShowChannelList": false, atBotShowChannelList: false
"permissionType": 2, permissionType: 2
"specifyRoleIds": [], specifyRoleIds: []
"specifyTinyids": [], specifyTinyids: []
"isReply": false, isReply: false
"anchor": 0, anchor: 0
"enter": false, enter: false
"subscribeDataTemplateIds": [] subscribeDataTemplateIds: []
} }
export interface InlineKeyboardElement { export interface InlineKeyboardElement {
rows: [{ rows: [
buttons: InlineKeyboardElementRowButton[] {
}] buttons: InlineKeyboardElementRowButton[]
},
]
} }
export interface TipAioOpGrayTipElement { // 这是什么提示来着? export interface TipAioOpGrayTipElement {
operateType: number, // 这是什么提示来着?
peerUid: string, operateType: number
fromGrpCodeOfTmpChat: string, peerUid: string
fromGrpCodeOfTmpChat: string
} }
export enum TipGroupElementType { export enum TipGroupElementType {
memberIncrease = 1, memberIncrease = 1,
kicked = 3, // 被移出群 kicked = 3, // 被移出群
ban = 8 ban = 8,
} }
export interface TipGroupElement { export interface TipGroupElement {
"type": TipGroupElementType, // 1是表示有人加入群, 自己加入群也会收到这个 type: TipGroupElementType // 1是表示有人加入群, 自己加入群也会收到这个
"role": 0, // 暂时不知 role: 0 // 暂时不知
"groupName": string, // 暂时获取不到 groupName: string // 暂时获取不到
"memberUid": string, memberUid: string
"memberNick": string, memberNick: string
"memberRemark": string, memberRemark: string
"adminUid": string, adminUid: string
"adminNick": string, adminNick: string
"adminRemark": string, adminRemark: string
"createGroup": null, createGroup: null
"memberAdd"?: { memberAdd?: {
"showType": 1, showType: 1
"otherAdd": null, otherAdd: null
"otherAddByOtherQRCode": null, otherAddByOtherQRCode: null
"otherAddByYourQRCode": null, otherAddByYourQRCode: null
"youAddByOtherQRCode": null, youAddByOtherQRCode: null
"otherInviteOther": null, otherInviteOther: null
"otherInviteYou": null, otherInviteYou: null
"youInviteOther": null youInviteOther: null
}, }
"shutUp"?: { shutUp?: {
"curTime": string, curTime: string
"duration": string, // 禁言时间,秒 duration: string // 禁言时间,秒
"admin": { admin: {
"uid": string, uid: string
"card": string, card: string
"name": string, name: string
"role": GroupMemberRole role: GroupMemberRole
},
"member": {
"uid": string
"card": string,
"name": string,
"role": GroupMemberRole
}
} }
member: {
uid: string
card: string
name: string
role: GroupMemberRole
}
}
} }
export interface MultiForwardMsgElement{ export interface MultiForwardMsgElement {
xmlContent: string, // xml格式的消息内容 xmlContent: string // xml格式的消息内容
resId: string, resId: string
fileName: string, fileName: string
} }
export interface RawMessage { export interface RawMessage {
msgId: string; msgId: string
msgShortId?: number; // 自己维护的消息id msgType: number
msgTime: string; // 时间戳,秒 subMsgType: number
msgSeq: string; msgShortId?: number // 自己维护的消息id
senderUid: string; msgTime: string // 时间戳,秒
senderUin?: string; // 发送者QQ号 msgSeq: string
peerUid: string; // 群号 或者 QQ uid msgRandom: string
peerUin: string; // 群号 或者 发送者QQ号 senderUid: string
sendNickName: string; senderUin: string // 发送者QQ号
sendMemberName?: string; // 发送者群名片 peerUid: string // 群号 或者 QQ uid
chatType: ChatType; peerUin: string // 群号 或者 发送者QQ号
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送 guildId: string
recallTime: string; // 撤回时间, "0"是没有撤回 sendNickName: string
elements: { sendMemberName?: string // 发送者群名片
elementId: string, chatType: ChatType
elementType: ElementType; sendStatus?: number // 消息状态别人发的2是已撤回自己发的2是已发送
replyElement: { recallTime: string // 撤回时间, "0"是没有撤回
senderUid: string; // 原消息发送者QQ号 records: RawMessage[]
sourceMsgIsIncPic: boolean; // 原消息是否有图片 elements: MessageElement[]
sourceMsgText: string; }
replayMsgSeq: string; // 源消息的msgSeq可以通过这个找到源消息的msgId
}; export interface Peer {
textElement: { chatType: ChatType
atType: AtType; peerUid: string // 如果是群聊uid为群号私聊uid就是加密的字符串
atUid: string; // QQ号 guildId?: string
content: string; }
atNtUid: string; // uid号
}; export interface MessageElement {
picElement: PicElement; elementType: ElementType
pttElement: PttElement; elementId: string
arkElement: ArkElement; extBufForUI: string //"0x"
grayTipElement: GrayTipElement; textElement?: TextElement
faceElement: FaceElement; faceElement?: FaceElement
videoElement: VideoElement; marketFaceElement?: MarketFaceElement
fileElement: FileElement; replyElement?: ReplyElement
marketFaceElement: MarketFaceElement; picElement?: PicElement
inlineKeyboardElement: InlineKeyboardElement; pttElement?: PttElement
markdownElement: MarkdownElement; videoElement?: VideoElement
multiForwardMsgElement: MultiForwardMsgElement; grayTipElement?: GrayTipElement
}[]; arkElement?: ArkElement
fileElement?: FileElement
liveGiftElement?: unknown
markdownElement?: MarkdownElement
structLongMsgElement?: unknown
multiForwardMsgElement?: MultiForwardMsgElement
giphyElement?: unknown
walletElement?: unknown
inlineKeyboardElement?: InlineKeyboardElement
textGiftElement?: unknown //????
calendarElement?: unknown
yoloGameResultElement?: unknown
avRecordElement?: unknown
structMsgElement?: unknown
faceBubbleElement?: unknown
shareLocationElement?: unknown
tofuRecordElement?: unknown
taskTopMsgElement?: unknown
recommendedMsgElement?: unknown
actionBarElement?: unknown
}
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string
msgElementId: string
msgId: string
fileId: string
fileProgress: string // '0'
fileSpeed: string // '0'
fileErrCode: string // '0'
fileErrMsg: string
fileDownType: number // 暂时未知
thumbSize: number
filePath: string
totalSize: string
trasferStatus: number
step: number
commonFileInfo: unknown
fileSrvErrCode: string
clientMsg: string
businessId: number
userTotalSpacePerDay: unknown
userUsedSpacePerDay: unknown
}
export interface OnGroupFileInfoUpdateParams {
retCode: number
retMsg: string
clientWording: string
isEnd: boolean
item: {
peerId: string
type: number
folderInfo?: {
folderId: string
parentFolderId: string
folderName: string
createTime: number
modifyTime: number
createUin: string
creatorName: string
totalFileCount: number
modifyUin: string
modifyName: string
usedSpace: string
}
fileInfo?: {
fileModelId: string
fileId: string
fileName: string
fileSize: string
busId: number
uploadedSize: string
uploadTime: number
deadTime: number
modifyTime: number
downloadTimes: number
sha: string
sha3: string
md5: string
uploaderLocalPath: string
uploaderName: string
uploaderUin: string
parentFolderId: string
localPath: string
transStatus: number
transType: number
elementId: string
isFolder: boolean
}
}[]
allFileCount: number
nextIndex: number
reqId: number
} }

View File

@@ -1,65 +1,131 @@
export enum GroupNotifyType {
export enum GroupNotifyTypes { INVITED_BY_MEMBER = 1,
INVITE_ME = 1, REFUSE_INVITED,
INVITED_JOIN = 4, // 有人接受了邀请入群 REFUSED_BY_ADMINI_STRATOR,
JOIN_REQUEST = 7, AGREED_TOJOIN_DIRECT, // 有人接受了邀请入群
ADMIN_SET = 8, INVITED_NEED_ADMINI_STRATOR_PASS, // 有人邀请了别人入群
KICK_MEMBER = 9, AGREED_TO_JOIN_BY_ADMINI_STRATOR,
MEMBER_EXIT = 11, // 主动退出 REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS,
ADMIN_UNSET = 12, SET_ADMIN,
KICK_MEMBER_NOTIFY_ADMIN,
KICK_MEMBER_NOTIFY_KICKED,
MEMBER_LEAVE_NOTIFY_ADMIN, // 主动退出
CANCEL_ADMIN_NOTIFY_CANCELED, // 我被取消管理员
CANCEL_ADMIN_NOTIFY_ADMIN, // 其他人取消管理员
TRANSFER_GROUP_NOTIFY_OLDOWNER,
TRANSFER_GROUP_NOTIFY_ADMIN
} }
export interface GroupNotifies { export interface GroupNotifies {
doubt: boolean, doubt: boolean
nextStartSeq: string, nextStartSeq: string
notifies: GroupNotify[], notifies: GroupNotify[]
} }
export enum GroupNotifyStatus { export enum GroupNotifyStatus {
IGNORE = 0, KINIT, // 初始化
WAIT_HANDLE = 1, KUNHANDLE, // 未处理
APPROVE = 2, KAGREED, // 同意
REJECT = 3 KREFUSED, // 拒绝
KIGNORED // 忽略
} }
export interface GroupNotify { export interface GroupNotify {
time: number; // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify time: number // 自己添加的字段,时间戳,毫秒, 用于判断收到短时间内收到重复的notify
seq: string, // 唯一标识符转成数字再除以1000应该就是时间戳 seq: string // 唯一标识符转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes, type: GroupNotifyType
status: GroupNotifyStatus, // 0是已忽略1是未处理2是已同意 status: GroupNotifyStatus
group: { groupCode: string, groupName: string }, group: { groupCode: string; groupName: string }
user1: { uid: string, nickName: string }, // 被设置管理员的人 user1: { uid: string; nickName: string } // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者 user2: { uid: string; nickName: string } // 操作者
actionUser: { uid: string, nickName: string }, //未知 actionUser: { uid: string; nickName: string } //未知
actionTime: string, actionTime: string
invitationExt: { invitationExt: {
srcType: number, // 0?未知 srcType: number // 0?未知
groupCode: string, waitStatus: number groupCode: string
}, waitStatus: number
postscript: string, // 加群用户填写的验证信息 }
repeatSeqs: [], postscript: string // 加群用户填写的验证信息
warningTips: string repeatSeqs: []
warningTips: string
} }
export enum GroupRequestOperateTypes { export enum GroupRequestOperateTypes {
approve = 1, approve = 1,
reject = 2 reject = 2,
}
export enum BuddyReqType {
KMEINITIATOR,
KPEERINITIATOR,
KMEAGREED,
KMEAGREEDANDADDED,
KPEERAGREED,
KPEERAGREEDANDADDED,
KPEERREFUSED,
KMEREFUSED,
KMEIGNORED,
KMEAGREEANYONE,
KMESETQUESTION,
KMEAGREEANDADDFAILED,
KMSGINFO,
KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
friendUid: string, isInitiator?: boolean
reqTime: string, // 时间戳,秒 isDecide: boolean
extWords: string, // 申请人填写的验证消息 friendUid: string
isUnread: boolean, reqType: BuddyReqType
friendNick: string, reqTime: string // 时间戳,秒
sourceId: number, extWords: string // 申请人填写的验证消息
groupCode: string isUnread: boolean
friendNick: string
sourceId: number
groupCode: string
} }
export interface FriendRequestNotify { export interface FriendRequestNotify {
data: { data: {
unreadNums: number, unreadNums: number
buddyReqs: FriendRequest[] buddyReqs: FriendRequest[]
} }
}
export enum MemberExtSourceType {
DEFAULTTYPE = 0,
TITLETYPE = 1,
NEWGROUPTYPE = 2,
}
export interface GroupExtParam {
groupCode: string
seq: string
beginUin: string
dataTime: string
uinList: Array<string>
uinNum: string
groupType: string
richCardNameVer: string
sourceType: MemberExtSourceType
memberExtFilter: {
memberLevelInfoUin: number
memberLevelInfoPoint: number
memberLevelInfoActiveDay: number
memberLevelInfoLevel: number
memberLevelInfoName: number
levelName: number
dataTime: number
userShowFlag: number
sysShowFlag: number
timeToUpdate: number
nickName: number
specialTitle: number
levelNameNew: number
userShowFlagNew: number
msgNeedField: number
cmdUinFlagExt3Grocery: number
memberIcon: number
memberInfoSeq: number
}
} }

View File

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

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

@@ -0,0 +1,54 @@
import {
NodeIKernelBuddyService,
NodeIKernelGroupService,
NodeIKernelProfileService,
NodeIKernelProfileLikeService,
NodeIKernelMSFService,
NodeIKernelMsgService,
NodeIKernelUixConvertService,
NodeIKernelRichMediaService,
NodeIKernelTicketService,
NodeIKernelTipOffService
} from './services'
import { constants } from 'node:os'
import { Dict } from 'cosmokit'
const Process = require('node:process')
export interface NodeIQQNTWrapperSession {
getBuddyService(): NodeIKernelBuddyService
getGroupService(): NodeIKernelGroupService
getProfileService(): NodeIKernelProfileService
getProfileLikeService(): NodeIKernelProfileLikeService
getMsgService(): NodeIKernelMsgService
getMSFService(): NodeIKernelMSFService
getUixConvertService(): NodeIKernelUixConvertService
getRichMediaService(): NodeIKernelRichMediaService
getTicketService(): NodeIKernelTicketService
getTipOffService(): NodeIKernelTipOffService
}
export interface WrapperApi {
NodeIQQNTWrapperSession?: NodeIQQNTWrapperSession
}
const wrapperApi: WrapperApi = {}
Process.dlopenOrig = Process.dlopen
Process.dlopen = function (module: Dict, filename: string, flags = constants.dlopen.RTLD_LAZY) {
const dlopenRet = this.dlopenOrig(module, filename, flags)
for (const export_name in module.exports) {
module.exports[export_name] = new Proxy(module.exports[export_name], {
construct: (target, args) => {
const ret = new target(...args)
if (export_name === 'NodeIQQNTWrapperSession') wrapperApi.NodeIQQNTWrapperSession = ret
return ret
}
})
}
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 type Adapter from '../adapter'
import {log} from "../../common/utils/log"; abstract class BaseAction<PayloadType, ReturnDataType> {
abstract actionName: ActionName
protected ctx: Context
payloadSchema?: Schema<PayloadType>
class BaseAction<PayloadType, ReturnDataType> { constructor(protected adapter: Adapter) {
actionName: ActionName this.ctx = adapter.ctx
}
protected async check(payload: PayloadType): Promise<BaseCheckResult> { public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
return { let params: PayloadType
valid: true, try {
} params = this.payloadSchema ? new this.payloadSchema(payload) : payload
} catch (e) {
return OB11Response.error((e as Error).message, 400)
} }
try {
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> { const resData = await this._handle(params)
const result = await this.check(payload); return OB11Response.ok(resData)
if (!result.valid) { } catch (e) {
return OB11Response.error(result.message, 400); this.ctx.logger.error('发生错误', e)
} return OB11Response.error((e as Error)?.toString() || (e as Error)?.stack?.toString() || '未知错误,可能操作超时', 200)
try {
const resData = await this._handle(payload);
return OB11Response.ok(resData);
} catch (e) {
log("发生错误", e)
return OB11Response.error(e?.toString() || e?.stack?.toString() || "未知错误,可能操作超时", 200);
}
} }
}
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> { public async websocketHandle(payload: PayloadType, echo: 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) {
try { return OB11Response.error((e as Error).message, 1400)
const resData = await this._handle(payload)
return OB11Response.ok(resData, echo);
} catch (e) {
log("发生错误", e)
return OB11Response.error(e.stack?.toString() || e.toString(), 1200, echo)
}
} }
try {
const resData = await this._handle(params)
return OB11Response.ok(resData, echo)
} catch (e) {
this.ctx.logger.error('发生错误', e)
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,32 +1,31 @@
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> {
return { return {
status: status, status: status,
retcode: retcode, retcode: retcode,
data: data, data: data,
message: message, message: message,
wording: message, wording: message,
echo: null echo: undefined,
}
} }
}
static ok<T>(data: T, echo: any = null) { static ok<T>(data: T, echo?: unknown) {
let res = OB11Response.res<T>(data, "ok", 0) const res = OB11Response.res<T>(data, 'ok', 0)
if (!isNull(echo)) { if (!isNullable(echo)) {
res.echo = echo; res.echo = echo
}
return res;
} }
return res
}
static error(err: string, retcode: number, echo: any = null) { static error(err: string, retcode: number, echo?: unknown) {
let res = OB11Response.res(null, "failed", retcode, err) const res = OB11Response.res(null, 'failed', retcode, err)
if (!isNull(echo)) { if (!isNullable(echo)) {
res.echo = echo; res.echo = echo
}
return res;
} }
return res
}
} }

View File

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

View File

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

View File

@@ -1,15 +1,29 @@
import {GetFileBase, GetFilePayload, GetFileResponse} from "./GetFile"; import path from 'node:path'
import {ActionName} from "../types"; import { GetFileBase, GetFilePayload, GetFileResponse } from './GetFile'
import { ActionName } from '../types'
import { decodeSilk } from '@/common/utils/audio'
import { Schema } from '../BaseAction'
import { stat, readFile } from 'node:fs/promises'
interface Payload extends GetFilePayload { interface Payload extends GetFilePayload {
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac' out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac'
} }
export default class GetRecord extends GetFileBase { export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord actionName = ActionName.GetRecord
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 = super._handle(payload); const res = await super._handle(payload)
return res; res.file = await decodeSilk(this.ctx, res.file!, payload.out_format)
res.file_name = path.basename(res.file)
res.file_size = (await stat(res.file)).size.toString()
if (this.adapter.config.enableLocalFile2Url) {
res.base64 = await readFile(res.file, 'base64')
} }
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

@@ -0,0 +1,24 @@
import { BaseAction, Schema } from '../BaseAction'
import { ActionName } from '../types'
interface Payload {
message_id: number | string
}
export class DeleteEssenceMsg extends BaseAction<Payload, unknown> {
actionName = ActionName.GoCQHTTP_DelEssenceMsg
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')
}
return await this.ctx.ntGroupApi.removeGroupEssence(
msg.peer.peerUid,
msg.msgId,
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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