mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
588 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
34d9f04f15 | ||
![]() |
be5da7cc6f | ||
![]() |
8d32ccb5d4 | ||
![]() |
6acceb884c | ||
![]() |
4c834fd640 | ||
![]() |
301278c7a9 | ||
![]() |
42ee83c54f | ||
![]() |
e631f69621 | ||
![]() |
ce8760a39a | ||
![]() |
ff952956de | ||
![]() |
28f3ff4971 | ||
![]() |
19e728c3cb | ||
![]() |
269773ed6b | ||
![]() |
e0d32417e1 | ||
![]() |
9fa6083bed | ||
![]() |
4d2fccdfb4 | ||
![]() |
c1c4bdfe94 | ||
![]() |
8a0e9e8b61 | ||
![]() |
1190e14171 | ||
![]() |
00292b177a | ||
![]() |
88de57f984 | ||
![]() |
61ddf38892 | ||
![]() |
52b3540ec3 | ||
![]() |
5f831958c3 | ||
![]() |
c3d4698af3 | ||
![]() |
bd6e83217d | ||
![]() |
50ec49d9a2 | ||
![]() |
dc3a089070 | ||
![]() |
530e380178 | ||
![]() |
10e4387add | ||
![]() |
e925bc3aa8 | ||
![]() |
427b3a7560 | ||
![]() |
c8da950725 | ||
![]() |
743c5b8196 | ||
![]() |
5e62abea57 | ||
![]() |
6bfc545582 | ||
![]() |
411108a2d2 | ||
![]() |
308a6fa9e4 | ||
![]() |
2dc7b785d0 | ||
![]() |
0e69e9e839 | ||
![]() |
b83229b5da | ||
![]() |
6f053f5f7d | ||
![]() |
c3dc53eaaf | ||
![]() |
ffdc34cfe2 | ||
![]() |
4825a0e341 | ||
![]() |
95a00d7f35 | ||
![]() |
d885bab426 | ||
![]() |
e2a6a0bc02 | ||
![]() |
ff7d8609ce | ||
![]() |
7507b90e03 | ||
![]() |
2b226a4b27 | ||
![]() |
8b0232c4fe | ||
![]() |
0728ee9ad6 | ||
![]() |
8c6f04d0bc | ||
![]() |
c67fad789e | ||
![]() |
4072339d70 | ||
![]() |
3a244f5804 | ||
![]() |
f12cf59137 | ||
![]() |
c76f556a11 | ||
![]() |
e0f3d07b98 | ||
![]() |
378d85dc67 | ||
![]() |
875e91fc0e | ||
![]() |
15f7cd9814 | ||
![]() |
1eb5cd6237 | ||
![]() |
ad2f843c8f | ||
![]() |
8e550e216e | ||
![]() |
9f07b07c82 | ||
![]() |
0be6effc32 | ||
![]() |
7ab6a10fc9 | ||
![]() |
fb09af0e64 | ||
![]() |
0d99d30b2d | ||
![]() |
0000ec8b5b | ||
![]() |
0085bd8a1f | ||
![]() |
617139dfa4 | ||
![]() |
4eb4a612d0 | ||
![]() |
cda5e784f6 | ||
![]() |
d93a280ab3 | ||
![]() |
f7e2b3a4a7 | ||
![]() |
39d9c8fa74 | ||
![]() |
8823895a03 | ||
![]() |
b44a9e696c | ||
![]() |
cf28a3dc17 | ||
![]() |
7416e6caf6 | ||
![]() |
90f6896f3c | ||
![]() |
eebcd0700d | ||
![]() |
133eee0c66 | ||
![]() |
640fb75f74 | ||
![]() |
51dcc1add6 | ||
![]() |
730c928f91 | ||
![]() |
c3b7e111b9 | ||
![]() |
1874e48925 | ||
![]() |
e7a082c91c | ||
![]() |
5d4f45407e | ||
![]() |
17c37ec32f | ||
![]() |
b5f8140c79 | ||
![]() |
63f746c237 | ||
![]() |
dac6709f27 | ||
![]() |
470c8d0b29 | ||
![]() |
b0d35e803b | ||
![]() |
a71475be8b | ||
![]() |
b9f2cc5142 | ||
![]() |
2d46e55b9b | ||
![]() |
684e254996 | ||
![]() |
a2f7903960 | ||
![]() |
c0c757d6bd | ||
![]() |
da0fad743d | ||
![]() |
80b10d6025 | ||
![]() |
a27c2a69c4 | ||
![]() |
9ed2a2fd19 | ||
![]() |
aa9d96718c | ||
![]() |
aa67a2b71c | ||
![]() |
d3405edd42 | ||
![]() |
3612098d62 | ||
![]() |
2f08b72d69 | ||
![]() |
ab66904c1a | ||
![]() |
55542a3dbe | ||
![]() |
8569a45114 | ||
![]() |
c790311fc3 | ||
![]() |
3c45c8bd80 | ||
![]() |
d5b7b3ae31 | ||
![]() |
43e73a5f24 | ||
![]() |
698947ed97 | ||
![]() |
f3d967ae07 | ||
![]() |
dbe72fa07e | ||
![]() |
801a97d85b | ||
![]() |
9f8f938c47 | ||
![]() |
8fe37d1c1e | ||
![]() |
5cca8457e7 | ||
![]() |
e9332e7646 | ||
![]() |
31365505d8 | ||
![]() |
b3fbe9e34a | ||
![]() |
4082b651c5 | ||
![]() |
0081000ef0 | ||
![]() |
ad4d6a1070 | ||
![]() |
5190b26399 | ||
![]() |
29a8db96f4 | ||
![]() |
1a4c2cabfd | ||
![]() |
ef9189055c | ||
![]() |
5cc3719125 | ||
![]() |
5d46f41348 | ||
![]() |
3c2c1963f4 | ||
![]() |
4896ca9279 | ||
![]() |
f0afba6cd9 | ||
![]() |
bd717c298a | ||
![]() |
baaa8a70dc | ||
![]() |
6d561c6e6f | ||
![]() |
e6b6947d49 | ||
![]() |
52e99a2175 | ||
![]() |
052d17a46f | ||
![]() |
1aa1f4c212 | ||
![]() |
c3a48e3344 | ||
![]() |
1d5483dc28 | ||
![]() |
54277fa0df | ||
![]() |
ab04bd262f | ||
![]() |
fb23087b65 | ||
![]() |
846fee7ac8 | ||
![]() |
977eacc679 | ||
![]() |
dacfefe644 | ||
![]() |
345e941e11 | ||
![]() |
6cb7d45464 | ||
![]() |
e7222653fa | ||
![]() |
014f0758f5 | ||
![]() |
0e8b416f6d | ||
![]() |
09a60a2204 | ||
![]() |
b0eae307c2 | ||
![]() |
f5d2b54cca | ||
![]() |
3eefec3899 | ||
![]() |
b6a8094554 | ||
![]() |
4083b35436 | ||
![]() |
bb72d70baf | ||
![]() |
95d1a77f52 | ||
![]() |
051729886e | ||
![]() |
0f00123dc7 | ||
![]() |
0b0a089d86 | ||
![]() |
c711a7d99a | ||
![]() |
43f1d8c88c | ||
![]() |
e818e79d20 | ||
![]() |
cbad3ff1de | ||
![]() |
16a2e5e996 | ||
![]() |
331c6a50d0 | ||
![]() |
31c4540ec6 | ||
![]() |
1e6116554f | ||
![]() |
a12ea0e761 | ||
![]() |
c9e3bbcd9f | ||
![]() |
9c17dc1b8f | ||
![]() |
69d1cae686 | ||
![]() |
1c2404b6af | ||
![]() |
b33b33739d | ||
![]() |
2b7886c682 | ||
![]() |
106d1f6374 | ||
![]() |
e601786bd7 | ||
![]() |
fda2a98b40 | ||
![]() |
c01d70b8fc | ||
![]() |
eccbcc3e28 | ||
![]() |
7a4a255a89 | ||
![]() |
83bced82b1 | ||
![]() |
f3033ce732 | ||
![]() |
5c21a1727c | ||
![]() |
93aab437b7 | ||
![]() |
34e797270f | ||
![]() |
0f337a8d8c | ||
![]() |
cc9b83089e | ||
![]() |
a565929686 | ||
![]() |
6adacea774 | ||
![]() |
47ab5421ed | ||
![]() |
10c404d455 | ||
![]() |
dfdca11155 | ||
![]() |
698e095364 | ||
![]() |
524fd258d8 | ||
![]() |
17e70a4360 | ||
![]() |
e4a533e7b7 | ||
![]() |
0cb68d3737 | ||
![]() |
9faeadbebe | ||
![]() |
35d201cfb8 | ||
![]() |
205174255f | ||
![]() |
8873a030ab | ||
![]() |
0ab61bac12 | ||
![]() |
b1157f60f5 | ||
![]() |
bb93df06b2 | ||
![]() |
82e807fd80 | ||
![]() |
29da539467 | ||
![]() |
659aa005b0 | ||
![]() |
3f20733e7e | ||
![]() |
b15e1174d6 | ||
![]() |
05b05fd74e | ||
![]() |
d30d467a21 | ||
![]() |
cd62e8ca37 | ||
![]() |
f9e44820c1 | ||
![]() |
169ae6a4d0 | ||
![]() |
030ba15952 | ||
![]() |
964874bdad | ||
![]() |
7affa081ac | ||
![]() |
10e281ed35 | ||
![]() |
27081ae599 | ||
![]() |
61cbcdffe8 | ||
![]() |
eeb15ea564 | ||
![]() |
565c820925 | ||
![]() |
325dff5735 | ||
![]() |
397c2cf5f0 | ||
![]() |
1fbc339a42 | ||
![]() |
f2c719c60d | ||
![]() |
08505fcc9a | ||
![]() |
a79c933693 | ||
![]() |
b4cb3ddf1c | ||
![]() |
aa188a6e89 | ||
![]() |
a04b6b8a70 | ||
![]() |
11149d2743 | ||
![]() |
86bfd990db | ||
![]() |
9304430889 | ||
![]() |
095f1c270b | ||
![]() |
d3f91a832b | ||
![]() |
4790a1170f | ||
![]() |
501c392028 | ||
![]() |
9200520f70 | ||
![]() |
8122561337 | ||
![]() |
c6dc86ef8d | ||
![]() |
bea3b8485f | ||
![]() |
b807b89cdc | ||
![]() |
daac2f7fd9 | ||
![]() |
f0a5523174 | ||
![]() |
eda8fbb178 | ||
![]() |
67ca6184e9 | ||
![]() |
d79e91fc1e | ||
![]() |
1cdb93baa2 | ||
![]() |
f91991e25c | ||
![]() |
d21da47a7d | ||
![]() |
b4e22a345d | ||
![]() |
30e594ae5f | ||
![]() |
ffba3573ba | ||
![]() |
9df5bee8d3 | ||
![]() |
71c0728622 | ||
![]() |
476d8ba14d | ||
![]() |
274c956f16 | ||
![]() |
3068f9ee3d | ||
![]() |
a0c49d5f7f | ||
![]() |
a8534974fe | ||
![]() |
c517790391 | ||
![]() |
b7e875c77f | ||
![]() |
befd9c0624 | ||
![]() |
7a46f11089 | ||
![]() |
dc168bf8b9 | ||
![]() |
eef5293ca0 | ||
![]() |
a2c4498694 | ||
![]() |
938a84a460 | ||
![]() |
978d2c24ee | ||
![]() |
cdd00d665d | ||
![]() |
bb8b06c044 | ||
![]() |
604c5dcdc1 | ||
![]() |
6bc2ecdbf0 | ||
![]() |
e91c81def7 | ||
![]() |
bedd2fa15a | ||
![]() |
50465eef54 | ||
![]() |
07689adfcd | ||
![]() |
8f4f898675 | ||
![]() |
968bd7a437 | ||
![]() |
eba5900ba8 | ||
![]() |
69c477b104 | ||
![]() |
c8df8f4f54 | ||
![]() |
d35a19b4fd | ||
![]() |
a97437a6e5 | ||
![]() |
39c4473367 | ||
![]() |
b882bc721d | ||
![]() |
405cace489 | ||
![]() |
402a7b7fc9 | ||
![]() |
8ad805e654 | ||
![]() |
b23c357f73 | ||
![]() |
f561c2b0fa | ||
![]() |
5a8eea668f | ||
![]() |
777143e502 | ||
![]() |
0d8c9a82fe | ||
![]() |
d10ab1cce3 | ||
![]() |
ec25e09d73 | ||
![]() |
cba9c78ab1 | ||
![]() |
c32db4a881 | ||
![]() |
871add3071 | ||
![]() |
e661c617a3 | ||
![]() |
d4bf721540 | ||
![]() |
d91b55faed | ||
![]() |
9687832d4d | ||
![]() |
fc3e436744 | ||
![]() |
da90245f7b | ||
![]() |
410d6a85d7 | ||
![]() |
b693342e4f | ||
![]() |
acca361f2e | ||
![]() |
b663f47713 | ||
![]() |
d332b199b5 | ||
![]() |
78bac1dbd1 | ||
![]() |
724ff215f9 | ||
![]() |
68ea146469 | ||
![]() |
82583e616f | ||
![]() |
bfc339c58d | ||
![]() |
fe4427c076 | ||
![]() |
5745f388a9 | ||
![]() |
377e3c253f | ||
![]() |
3007a0c00e | ||
![]() |
f51ffc091d | ||
![]() |
c37c364a08 | ||
![]() |
331a106e9a | ||
![]() |
cd74687b7b | ||
![]() |
b3e145c1e6 | ||
![]() |
d8e1547736 | ||
![]() |
8617f01924 | ||
![]() |
55f9e75e6a | ||
![]() |
b93e7b7ed1 | ||
![]() |
89cc79ad60 | ||
![]() |
8dd0e60eea | ||
![]() |
df6113fdf6 | ||
![]() |
3a3095d15a | ||
![]() |
fb4d07391e | ||
![]() |
9bef9c85cf | ||
![]() |
b77b3f227f | ||
![]() |
6a065f0a34 | ||
![]() |
4e1e190797 | ||
![]() |
1ce8cd2100 | ||
![]() |
c03af6b9ad | ||
![]() |
adca850075 | ||
![]() |
e3616b484e | ||
![]() |
cfd7808169 | ||
![]() |
addcedc588 | ||
![]() |
bfea786088 | ||
![]() |
50e84c3c9e | ||
![]() |
dc92ace85e | ||
![]() |
1a543928b1 | ||
![]() |
652fe8d21e | ||
![]() |
199690f45f | ||
![]() |
37a4dd4b00 | ||
![]() |
34d4358bfc | ||
![]() |
90906b9019 | ||
![]() |
1c212ff2b4 | ||
![]() |
7d709f44a8 | ||
![]() |
ea9e88a18a | ||
![]() |
0be8a9c805 | ||
![]() |
fcf8139afe | ||
![]() |
62f969b50b | ||
![]() |
6726062500 | ||
![]() |
cf1f4bdcaf | ||
![]() |
b09a14ad4e | ||
![]() |
1dc62c9ca3 | ||
![]() |
beaa89a2dc | ||
![]() |
f39a000b49 | ||
![]() |
013a74fb14 | ||
![]() |
7c4964753b | ||
![]() |
8353533d60 | ||
![]() |
c06df27424 | ||
![]() |
ad82919ddf | ||
![]() |
44dbba17e1 | ||
![]() |
5ba110e1da | ||
![]() |
b6e392fdb2 | ||
![]() |
2280e83aa2 | ||
![]() |
f49b94edb9 | ||
![]() |
2428a12221 | ||
![]() |
9c353f3760 | ||
![]() |
5b86d25d7f | ||
![]() |
2b168e8bbc | ||
![]() |
537db32847 | ||
![]() |
498b7f9f2b | ||
![]() |
9935568597 | ||
![]() |
467003af8c | ||
![]() |
4c9edcc47b | ||
![]() |
24bf9cf121 | ||
![]() |
e06f6f39a9 | ||
![]() |
98ee0c307b | ||
![]() |
5e53ea0bc3 | ||
![]() |
847d88ea77 | ||
![]() |
d5046cc2b3 | ||
![]() |
3ad64b7cbb | ||
![]() |
0dbfe8ca55 | ||
![]() |
91b794d66d | ||
![]() |
0d65e1e314 | ||
![]() |
2d8f58c6d8 | ||
![]() |
65888fa816 | ||
![]() |
857e882c6e | ||
![]() |
add2931834 | ||
![]() |
cdda5f45ee | ||
![]() |
5f73d6a913 | ||
![]() |
0637882fbc | ||
![]() |
3f785bab20 | ||
![]() |
a4ca89bdd6 | ||
![]() |
1a64e796bd | ||
![]() |
a8b85a34f7 | ||
![]() |
e7bec7d6b0 | ||
![]() |
a582026037 | ||
![]() |
1a67a001c5 | ||
![]() |
406deac592 | ||
![]() |
e719ae0676 | ||
![]() |
d8b7726440 | ||
![]() |
49f642e712 | ||
![]() |
70117016ce | ||
![]() |
a4738f6281 | ||
![]() |
b1fc72d696 | ||
![]() |
457c2c2b50 | ||
![]() |
48848d7d1a | ||
![]() |
55b07ca3ab | ||
![]() |
a1d4882e18 | ||
![]() |
3843795d8f | ||
![]() |
f2bf8d42da | ||
![]() |
a3b244e114 | ||
![]() |
3093bdbc68 | ||
![]() |
9ab0799283 | ||
![]() |
236bec11ed | ||
![]() |
de48b0f940 | ||
![]() |
4885d4db86 | ||
![]() |
0c7bbda936 | ||
![]() |
fa07c2c1fb | ||
![]() |
5d17a191f6 | ||
![]() |
67fb74d3c2 | ||
![]() |
dc04cfc1b3 | ||
![]() |
d61d481965 | ||
![]() |
6b346ee1de | ||
![]() |
d0f248aaf9 | ||
![]() |
85c9227515 | ||
![]() |
73b6d3be84 | ||
![]() |
1ff6ce2343 | ||
![]() |
c145935d46 | ||
![]() |
e9ede6924e | ||
![]() |
515a21761d | ||
![]() |
8d6397028b | ||
![]() |
eb4828d81f | ||
![]() |
7e74578312 | ||
![]() |
640e3516d4 | ||
![]() |
bd295a4632 | ||
![]() |
166c30fe2c | ||
![]() |
66c1bab629 | ||
![]() |
66656304f9 | ||
![]() |
07f66e379d | ||
![]() |
7ae8fd60c4 | ||
![]() |
7275066994 | ||
![]() |
385adec186 | ||
![]() |
96b5bec5ab | ||
![]() |
6a9ec4e5f0 | ||
![]() |
d9851493df | ||
![]() |
efdb520414 | ||
![]() |
5548644aeb | ||
![]() |
e3fcd91b2d | ||
![]() |
2cae30ba88 | ||
![]() |
58cd38c4a8 | ||
![]() |
3300304feb | ||
![]() |
f0e376d06b | ||
![]() |
16f7bb48f2 | ||
![]() |
7f383dd29b | ||
![]() |
3dc529edf4 | ||
![]() |
45dedb4872 | ||
![]() |
afcdd01c0d | ||
![]() |
1164877e9a | ||
![]() |
fe92a449ba | ||
![]() |
401b0e2bd0 | ||
![]() |
cf9c71fcc1 | ||
![]() |
15a2400069 | ||
![]() |
d68a39b49e | ||
![]() |
066ca22e24 | ||
![]() |
0418b926fe | ||
![]() |
be40bbdf40 | ||
![]() |
df4f42e79e | ||
![]() |
5f80058f70 | ||
![]() |
0cbe59052d | ||
![]() |
af28a26e37 | ||
![]() |
70c596df93 | ||
![]() |
748b51428c | ||
![]() |
8ad746397c | ||
![]() |
45baed2f9a | ||
![]() |
74185f2d33 | ||
![]() |
90a91e4105 | ||
![]() |
11aa3a0315 | ||
![]() |
0c2e39214f | ||
![]() |
d89620d7a6 | ||
![]() |
edf80775b7 | ||
![]() |
46e56ac726 | ||
![]() |
40b2f6bfd6 | ||
![]() |
911e4921e2 | ||
![]() |
1db9bb419d | ||
![]() |
c6241a94e3 | ||
![]() |
1cbf75ca36 | ||
![]() |
8f85c897c8 | ||
![]() |
29c31b7aba | ||
![]() |
402919d6f2 | ||
![]() |
82608dd5ff | ||
![]() |
f312368df2 | ||
![]() |
374fc64427 | ||
![]() |
95bd74bb0d | ||
![]() |
a9f5069649 | ||
![]() |
957f7ffd8d | ||
![]() |
336dd3ce10 | ||
![]() |
47a7295477 | ||
![]() |
341a0e1c2a | ||
![]() |
c4f73d0eb8 | ||
![]() |
bd9258bae4 | ||
![]() |
e3b3260aa0 | ||
![]() |
676766c99e | ||
![]() |
1025a07593 | ||
![]() |
00c3fcd033 | ||
![]() |
b8457d4aff | ||
![]() |
a2ecf10d19 | ||
![]() |
1e63a2a7e7 | ||
![]() |
964014fc5c | ||
![]() |
fc2bb6d8c3 | ||
![]() |
1b10252d76 | ||
![]() |
ad8af12a10 | ||
![]() |
b040c9b118 | ||
![]() |
f6da7da90b | ||
![]() |
a745185408 | ||
![]() |
d3336f9027 | ||
![]() |
daf42c8203 | ||
![]() |
0a18bae3b5 | ||
![]() |
919705966c | ||
![]() |
2c54aee63e | ||
![]() |
3f80bdf2a3 | ||
![]() |
1c429b8dd3 | ||
![]() |
5669e2b0b7 | ||
![]() |
1a6a43babf | ||
![]() |
2650db5ddc | ||
![]() |
255491a107 | ||
![]() |
5c64147dfa | ||
![]() |
39f4118577 | ||
![]() |
f7f6e4736a | ||
![]() |
c635da7ebb | ||
![]() |
58124b006a | ||
![]() |
563aeccd0f | ||
![]() |
bd1a95a7f5 | ||
![]() |
cdb25828f2 | ||
![]() |
45803b3b23 | ||
![]() |
0e5e3d3383 | ||
![]() |
4672930037 | ||
![]() |
09be7131c3 | ||
![]() |
a804f90b9c | ||
![]() |
264cb6bbd2 | ||
![]() |
b7772e867b | ||
![]() |
cc0e77abfb | ||
![]() |
537d1c6f4f | ||
![]() |
80facadd67 | ||
![]() |
ba097dad23 | ||
![]() |
c13c15d046 | ||
![]() |
4f52128a06 | ||
![]() |
500b2d0e6d | ||
![]() |
e59d094feb | ||
![]() |
a8372f14f8 | ||
![]() |
5174ff422d | ||
![]() |
5c06751c3b | ||
![]() |
ac2b0118a6 | ||
![]() |
3eb8fd4abe | ||
![]() |
48b389ebe3 | ||
![]() |
065adeb2cd | ||
![]() |
269d0a06fe | ||
![]() |
8eca26b1a5 | ||
![]() |
3019ef7de4 | ||
![]() |
522311b547 | ||
![]() |
21061561ec | ||
![]() |
b83c41ad56 | ||
![]() |
e80a1cc64a |
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -10,6 +10,7 @@ body:
|
||||
在提交新的 Bug 反馈前,请确保您:
|
||||
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
||||
* 不与现有的某一 issue 重复
|
||||
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
|
||||
- type: input
|
||||
id: system-version
|
||||
attributes:
|
||||
@@ -78,4 +79,4 @@ body:
|
||||
attributes:
|
||||
label: OneBot 客户端运行日志
|
||||
description: 粘贴 OneBot 客户端的相关日志内容到此处
|
||||
render: shell
|
||||
render: shell
|
||||
|
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@@ -93,15 +93,18 @@ jobs:
|
||||
needs: [Build-LiteLoader,Build-Shell]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'NapNeko/NapCatQQ'
|
||||
submodules: true
|
||||
ref: main
|
||||
token: ${{ secrets.NAPCAT_BUILD }}
|
||||
|
||||
- name: Download All Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
# - name: Compress subdirectories
|
||||
# run: |
|
||||
# cd ./NapCat.Shell/
|
||||
# zip -q -r NapCat.Shell.zip *
|
||||
# cd ..
|
||||
# rm ./NapCat.Shell.zip -rf
|
||||
# mv ./NapCat.Shell/NapCat.Shell.zip ./
|
||||
|
||||
- name: Compress subdirectories
|
||||
run: |
|
||||
cd ./NapCat.Shell/
|
||||
@@ -114,6 +117,20 @@ jobs:
|
||||
rm ./NapCat.Framework.zip -rf
|
||||
mv ./NapCat.Shell/NapCat.Shell.zip ./
|
||||
mv ./NapCat.Framework/NapCat.Framework.zip ./
|
||||
|
||||
mkdir ./NapCat.Framework.Windows.Once
|
||||
unzip -q ./external/LiteLoaderWrapper.zip -d ./NapCat.Framework.Windows.Once
|
||||
cd ./NapCat.Framework.Windows.Once
|
||||
ls
|
||||
mkdir -p ./LL/plugins/NapCatQQ
|
||||
unzip -q ../NapCat.Framework.zip -d ./LL/plugins/NapCatQQ
|
||||
zip -q -r NapCat.Framework.Windows.Once.zip *
|
||||
cd ..
|
||||
mv ./NapCat.Framework.Windows.Once/NapCat.Framework.Windows.Once.zip ./
|
||||
mv ./external/packet/napcat.packet.arm64 ./
|
||||
mv ./external/packet/napcat.packet.exe ./
|
||||
mv ./external/packet/napcat.packet.linux ./
|
||||
mv ./external/packet/napcat.packet.production.py ./
|
||||
- name: Extract version from tag
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
@@ -129,4 +146,9 @@ jobs:
|
||||
files: |
|
||||
NapCat.Framework.zip
|
||||
NapCat.Shell.zip
|
||||
NapCat.Framework.Windows.Once.zip
|
||||
napcat.packet.arm64
|
||||
napcat.packet.exe
|
||||
napcat.packet.linux
|
||||
napcat.packet.production.py
|
||||
draft: true
|
||||
|
33
README.md
33
README.md
@@ -1,37 +1,50 @@
|
||||
<div align="center">
|
||||
<img src="https://socialify.git.ci/NapNeko/NapCatQQ/image?description=1&language=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2FNapNeko%2FNapCatQQ%2Fmain%2Flogo.png&name=1&stargazers=1&theme=Auto" alt="NapCatQQ" width="640" height="320" />
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
## 欢迎回来
|
||||
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现。
|
||||
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
## 猫猫技能
|
||||
- [x] **高性能**:1K+ 群聊数目、20 线程并行发送消息毫无压力
|
||||
- [x] **多种启动方式**:支持以无头、LiteLoader 插件、仅 QQ GUI 三种方式启动
|
||||
- [x] **多平台支持**: 支持Windows/Linux(可选Docker)/Android Termux/MacOs覆盖全平台
|
||||
- [x] **超高性能**:轻松数千群聊 独创消息队列
|
||||
- [x] **启动方式**:支持以无头、LiteLoader 插件、仅 QQ GUI 三种方式启动
|
||||
- [x] **覆盖平台**: 覆盖 Windows / Linux (可选 Docker) / Android Termux / MacOS
|
||||
- [x] **安装简单**: 支持一键脚本/程序自动部署/镜像部署等多种覆盖范围
|
||||
- [x] **低占用**:无头模式占用资源极低,适合在服务器上运行
|
||||
- [x] **超低占用**:无头模式占用资源极低,适合在服务器上运行
|
||||
- [x] **超多接口**:实现大部分 OneBot 和 go-cqhttp 接口,超多扩展 API
|
||||
- [x] **WebUI**:自带 WebUI 支持,远程管理更加便捷
|
||||
- [x] **低故障率**:快速适配最新版本,日常保证 0 Issue
|
||||
- [x] **远程管理**:自带 WebUI 支持,远程管理更加便捷
|
||||
- [x] **扩展支持**:基于 MoeHoo 的Native 可实现发包与收包
|
||||
|
||||
## 使用猫猫
|
||||
|
||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
||||
|
||||
**首次使用**请务必前往[官方文档](https://napneko.github.io/)查看使用教程。
|
||||
**首次使用**请务必查看如下文档看使用教程
|
||||
|
||||
### 文档地址
|
||||
|
||||
[Cloudflare.Worker](https://doc.napneko.icu/)
|
||||
|
||||
[Cloudflare.HKServer](https://napcat.napneko.icu/)
|
||||
|
||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||
|
||||
[Github.IO](https://napneko.github.io/)
|
||||
## 回家旅途
|
||||
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS)
|
||||
|
||||
[Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl)
|
||||
|
||||
## 猫猫朋友
|
||||
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot) 提供初始版本基础
|
||||
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
|
||||
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持
|
||||
|
||||
不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||
|
||||
---
|
||||
|
||||
## 约法三章
|
||||
|
BIN
external/LiteLoaderWrapper.zip
vendored
Normal file
BIN
external/LiteLoaderWrapper.zip
vendored
Normal file
Binary file not shown.
BIN
external/packet/napcat.packet.arm64
vendored
Normal file
BIN
external/packet/napcat.packet.arm64
vendored
Normal file
Binary file not shown.
BIN
external/packet/napcat.packet.exe
vendored
Normal file
BIN
external/packet/napcat.packet.exe
vendored
Normal file
Binary file not shown.
BIN
external/packet/napcat.packet.linux
vendored
Normal file
BIN
external/packet/napcat.packet.linux
vendored
Normal file
Binary file not shown.
102
external/packet/napcat.packet.production.py
vendored
Normal file
102
external/packet/napcat.packet.production.py
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -5,11 +5,11 @@ if %errorLevel% == 0 (
|
||||
echo Administrator mode detected.
|
||||
) else (
|
||||
echo Please run this script in administrator mode.
|
||||
powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\"' -Verb runAs"
|
||||
powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
|
||||
set NAPCAT_PATCH_PATH=%cd%\patchNapCat.js
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
@@ -33,6 +33,8 @@ if not exist "%QQpath%" (
|
||||
exit /b
|
||||
)
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > %NAPCAT_LOAD_PATH%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%"
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
|
@@ -5,11 +5,11 @@ if %errorLevel% == 0 (
|
||||
echo Administrator mode detected.
|
||||
) else (
|
||||
echo Please run this script in administrator mode.
|
||||
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\"' -Verb runAs"
|
||||
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
|
||||
set NAPCAT_PATCH_PATH=%cd%\patchNapCat.js
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
@@ -34,6 +34,6 @@ if not exist "%QQpath%" (
|
||||
)
|
||||
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > %NAPCAT_LOAD_PATH%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%"
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
@@ -1 +0,0 @@
|
||||
require('./launcher.node').load('external_index', module);
|
26
launcher/qqnt.json
Normal file
26
launcher/qqnt.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "qq-chat",
|
||||
"version": "9.9.16-28788",
|
||||
"verHash": "73b0c8f6",
|
||||
"linuxVersion": "3.2.13-28788",
|
||||
"linuxVerHash": "55fb6434",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "QQ",
|
||||
"productName": "QQ",
|
||||
"author": {
|
||||
"name": "Tencent",
|
||||
"email": "QQ-Team@tencent.com"
|
||||
},
|
||||
"homepage": "https://im.qq.com",
|
||||
"sideEffects": true,
|
||||
"bin": {
|
||||
"qd": "externals/devtools/cli/index.js"
|
||||
},
|
||||
"main": "./loadNapCat.js",
|
||||
"buildVersion": "28788",
|
||||
"isPureShell": true,
|
||||
"isByteCodeShell": true,
|
||||
"platform": "win32",
|
||||
"eleArch": "x64"
|
||||
}
|
4
launcher/quickLoginExample.bat
Normal file
4
launcher/quickLoginExample.bat
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
REM ./launcher.bat 123456
|
||||
REM ./launcher-win10.bat 123456
|
||||
REM 带有REM的为注释 删掉你需要的系统的那行REM这三个单词 修改QQ本脚本启动即可
|
BIN
logo.png
BIN
logo.png
Binary file not shown.
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 335 KiB |
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "2.3.0",
|
||||
"version": "3.4.2",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
117
package.json
117
package.json
@@ -1,65 +1,52 @@
|
||||
{
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "2.3.0",
|
||||
"scripts": {
|
||||
"build:framework": "vite build --mode framework",
|
||||
"build:shell": "vite build --mode shell",
|
||||
"build:webui": "cd ./src/webui && vite build",
|
||||
"lint": "eslint --fix src/**/*.{js,ts}",
|
||||
"depend": "cd dist && npm install --omit=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@protobuf-ts/plugin": "^2.9.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/figlet": "^1.5.8",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.0.1",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"i": "^0.3.7",
|
||||
"javascript-obfuscator": "^4.1.0",
|
||||
"rollup": "^4.13.2",
|
||||
"rollup-plugin-dts": "^6.1.0",
|
||||
"rollup-plugin-obfuscator": "^1.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.6",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-plugin-dts": "^3.8.2",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.0.0-beta.2",
|
||||
"fast-xml-parser": "^4.3.6",
|
||||
"file-type": "^19.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"image-size": "^1.1.1",
|
||||
"json-schema-to-ts": "^3.1.0",
|
||||
"log4js": "^6.9.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"strtok3": "8.0.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "3.4.2",
|
||||
"scripts": {
|
||||
"build:framework": "vite build --mode framework",
|
||||
"build:shell": "vite build --mode shell",
|
||||
"build:webui": "cd ./src/webui && vite build",
|
||||
"lint": "eslint --fix src/**/*.{js,ts}",
|
||||
"depend": "cd dist && npm install --omit=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@protobuf-ts/runtime": "^2.9.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/node": "^22.0.1",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"fast-xml-parser": "^4.3.6",
|
||||
"file-type": "^19.0.0",
|
||||
"image-size": "^1.1.1",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.6",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"log4js": "^6.9.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
@@ -1,45 +0,0 @@
|
||||
# Dont Use This Script
|
||||
# 2024.7.3
|
||||
function Get-QQpath {
|
||||
try {
|
||||
$key = Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ"
|
||||
$uninstallString = $key.UninstallString
|
||||
return [System.IO.Path]::GetDirectoryName($uninstallString) + "\QQ.exe"
|
||||
}
|
||||
catch {
|
||||
throw "get QQ path error: $_"
|
||||
}
|
||||
}
|
||||
function Select-QQPath {
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
[System.Windows.Forms.Application]::EnableVisualStyles()
|
||||
|
||||
$dialogTitle = "Select QQ.exe"
|
||||
|
||||
$filePicker = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$filePicker.Title = $dialogTitle
|
||||
$filePicker.Filter = "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*"
|
||||
$filePicker.FilterIndex = 1
|
||||
$null = $filePicker.ShowDialog()
|
||||
if (-not ($filePicker.FileName)) {
|
||||
throw "User did not select an .exe file."
|
||||
}
|
||||
return $filePicker.FileName
|
||||
}
|
||||
|
||||
$params = $args -join " "
|
||||
Try {
|
||||
$QQpath = Get-QQpath
|
||||
}
|
||||
Catch {
|
||||
$QQpath = Select-QQPath
|
||||
}
|
||||
|
||||
if (!(Test-Path $QQpath)) {
|
||||
throw "provided QQ path is invalid: $QQpath"
|
||||
}
|
||||
|
||||
$Bootfile = Join-Path $PSScriptRoot "napcat.mjs"
|
||||
$env:ELECTRON_RUN_AS_NODE = 1
|
||||
$commandInfo = Get-Command $QQpath -ErrorAction Stop
|
||||
Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& chcp 65001;& '$($commandInfo.Path)' --enable-logging }"
|
@@ -1,90 +0,0 @@
|
||||
@echo off
|
||||
REM 检查当前会话是否具有管理员权限
|
||||
openfiles >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
REM 如果不是管理员,则重新启动脚本以管理员模式运行
|
||||
echo 请求管理员权限...
|
||||
powershell -Command "Start-Process cmd -ArgumentList '/c %~f0 %*' -Verb RunAs"
|
||||
exit /b
|
||||
)
|
||||
|
||||
REM 设置当前工作目录
|
||||
cd /d %~dp0
|
||||
|
||||
REM 获取当前目录路径
|
||||
set currentPath=%cd%
|
||||
set currentPath=%currentPath:\=/%
|
||||
|
||||
REM 生成JavaScript代码
|
||||
set "jsCode=(async () =^>await import('file:///%currentPath%/napcat.mjs'))();"
|
||||
|
||||
REM 将JavaScript代码保存到文件中
|
||||
echo %jsCode% > loadScript.js
|
||||
echo JavaScript code has been generated and saved to loadScript.js
|
||||
|
||||
REM 设置NAPCAT_PATH环境变量为 当前目录的loadScript.js地址
|
||||
set NAPCAT_PATH=%cd%\loadScript.js
|
||||
|
||||
REM 获取QQ路径
|
||||
|
||||
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in (%RetString%) do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
REM 拿不到QQ路径则退出
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
REM 收集dbghelp.dll路径和HASH信息
|
||||
set QQdir=%~dp0
|
||||
set oldDllPath=%QQdir%dbghelp.dll
|
||||
set newDllPath=%currentPath%\dbghelp.dll
|
||||
|
||||
for /f "tokens=*" %%A in ('certutil -hashfile "%oldDllPath%" MD5') do (
|
||||
if not defined oldDllHash set oldDllHash=%%A
|
||||
)
|
||||
for /f "tokens=*" %%A in ('certutil -hashfile "%newDllPath%" MD5') do (
|
||||
if not defined newDllHash set newDllHash=%%A
|
||||
)
|
||||
|
||||
REM 如果文件一致则跳过
|
||||
if "%oldDllHash%" neq "%newDllHash%" (
|
||||
tasklist /fi "imagename eq QQ.exe" 2>nul | find /i "QQ.exe" >nul
|
||||
if %errorlevel% equ 0 (
|
||||
REM 文件占用则退出
|
||||
echo dbghelp.dll is in use, cannot continue.
|
||||
) else (
|
||||
REM 文件未占用则尝试覆盖
|
||||
copy /y "%newDllPath%" "%oldDllPath%"
|
||||
if %errorlevel% neq 0 (
|
||||
echo Failed to copy dbghelp.dll
|
||||
pause
|
||||
exit /b
|
||||
) else (
|
||||
echo dbghelp.dll has been copied to %QQdir%
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM 带参数启动QQ
|
||||
REM 判断wt是否存在,存在则通过wt启动,不存在则通过cmd启动
|
||||
REM %QQPath% --enable-logging %*
|
||||
where wt >nul 2>nul
|
||||
if %errorlevel% equ 0 (
|
||||
wt "cmd" /c "%QQPath%" --enable-logging %*
|
||||
) else (
|
||||
"%QQPath%" --enable-logging %*
|
||||
)
|
@@ -1,123 +0,0 @@
|
||||
# 检查当前会话是否具有管理员权限
|
||||
function Test-Administrator {
|
||||
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
(New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
|
||||
}
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
# 如果不是管理员,则重新启动脚本以管理员模式运行
|
||||
$scriptPath = $myInvocation.MyCommand.Path
|
||||
if (-not $scriptPath) {
|
||||
$scriptPath = $PSCommandPath
|
||||
}
|
||||
$newProcess = New-Object System.Diagnostics.ProcessStartInfo "powershell";
|
||||
$newProcess.Arguments = "-File `"$scriptPath`" $args"
|
||||
$newProcess.Verb = "runas";
|
||||
[System.Diagnostics.Process]::Start($newProcess);
|
||||
exit
|
||||
}
|
||||
|
||||
function Get-QQpath {
|
||||
try {
|
||||
$key = Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ"
|
||||
$uninstallString = $key.UninstallString
|
||||
return [System.IO.Path]::GetDirectoryName($uninstallString) + "\QQ.exe"
|
||||
}
|
||||
catch {
|
||||
throw "get QQ path error: $_"
|
||||
}
|
||||
}
|
||||
function Select-QQPath {
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
[System.Windows.Forms.Application]::EnableVisualStyles()
|
||||
|
||||
$dialogTitle = "Select QQ.exe"
|
||||
|
||||
$filePicker = New-Object System.Windows.Forms.OpenFileDialog
|
||||
$filePicker.Title = $dialogTitle
|
||||
$filePicker.Filter = "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*"
|
||||
$filePicker.FilterIndex = 1
|
||||
$null = $filePicker.ShowDialog()
|
||||
if (-not ($filePicker.FileName)) {
|
||||
throw "User did not select an .exe file."
|
||||
}
|
||||
return $filePicker.FileName
|
||||
}
|
||||
|
||||
# 设置当前工作目录
|
||||
$scriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
|
||||
Set-Location $scriptDirectory
|
||||
|
||||
# 获取当前目录路径
|
||||
$currentPath = Get-Location
|
||||
|
||||
# 替换\为/
|
||||
$currentPath = $currentPath -replace '\\', '/'
|
||||
|
||||
# 生成JavaScript代码
|
||||
$jsCode = @"
|
||||
(async () => {
|
||||
await import('file:///$currentPath/napcat.mjs');
|
||||
})();
|
||||
"@
|
||||
|
||||
# 将JavaScript代码保存到文件中
|
||||
$jsFilePath = Join-Path $currentPath "loadScript.js"
|
||||
$jsCode | Out-File -FilePath $jsFilePath -Encoding UTF8
|
||||
|
||||
Write-Output "JavaScript code has been generated and saved to $jsFilePath"
|
||||
# 设置NAPCAT_PATH环境变量为 当前目录的loadScript.js地址
|
||||
$env:NAPCAT_PATH = $jsFilePath
|
||||
|
||||
$params = $args -join " "
|
||||
Try {
|
||||
$QQpath = Get-QQpath
|
||||
}
|
||||
Catch {
|
||||
$QQpath = Select-QQPath
|
||||
}
|
||||
# 拿不到QQ路径则退出
|
||||
if (!(Test-Path $QQpath)) {
|
||||
Write-Output "provided QQ path is invalid: $QQpath"
|
||||
Read-Host "Press any key to continue..."
|
||||
exit
|
||||
}
|
||||
|
||||
$commandInfo = Get-Command $QQpath -ErrorAction Stop
|
||||
|
||||
# 收集dbghelp.dll路径和HASH信息
|
||||
$QQpath = Split-Path $QQpath
|
||||
$oldDllPath = Join-Path $QQpath "dbghelp.dll"
|
||||
$oldDllHash = Get-FileHash $oldDllPath -Algorithm MD5
|
||||
$newDllPath = Join-Path $currentPath "dbghelp.dll"
|
||||
$newDllHash = Get-FileHash $newDllPath -Algorithm MD5
|
||||
# 如果文件一致则跳过
|
||||
if ($oldDllHash.Hash -ne $newDllHash.Hash) {
|
||||
$processes = Get-Process -Name QQ -ErrorAction SilentlyContinue
|
||||
if ($processes) {
|
||||
# 文件占用则退出
|
||||
Write-Output "dbghelp.dll is in use by the following processes:"
|
||||
$processes | ForEach-Object { Write-Output "$($_.Id) $($_.Name) $($_.Path)" }
|
||||
Write-Output "dbghelp.dll is in use, cannot continue."
|
||||
Read-Host "Press any key to continue..."
|
||||
exit
|
||||
} else {
|
||||
# 文件未占用则尝试覆盖
|
||||
try {
|
||||
Copy-Item -Path "$newDllPath" -Destination "$oldDllPath" -Force
|
||||
Write-Output "dbghelp.dll has been copied to $QQpath"
|
||||
} catch {
|
||||
Write-Output "Failed to copy dbghelp.dll: $_"
|
||||
Read-Host "Press any key to continue..."
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 带参数启动QQ
|
||||
try {
|
||||
Start-Process powershell -ArgumentList '-noexit', '-noprofile', "-command &{& chcp 65001;& '$($commandInfo.Path)' --enable-logging $params}" -NoNewWindow -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Output "Failed to start process as administrator: $_"
|
||||
Read-Host "Press any key to continue..."
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
@echo off
|
||||
REM 检查当前会话是否具有管理员权限
|
||||
openfiles >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
REM 如果不是管理员,则重新启动脚本以管理员模式运行
|
||||
echo 请求管理员权限...
|
||||
where wt >nul 2>nul
|
||||
if %errorlevel% equ 0 (
|
||||
powershell -Command "Start-Process cmd -ArgumentList ' /c %~f0 %*' -Verb RunAs"
|
||||
) else (
|
||||
powershell -Command "Start-Process wt -ArgumentList 'cmd /c %~f0 %*' -Verb RunAs"
|
||||
)
|
||||
|
||||
REM wt "cmd" /c "%~f0 %*"
|
||||
exit /b
|
||||
)
|
||||
|
||||
REM 设置当前工作目录
|
||||
cd /d %~dp0
|
||||
|
||||
REM 获取当前目录路径
|
||||
set currentPath=%cd%
|
||||
set currentPath=%currentPath:\=/%
|
||||
|
||||
REM 生成JavaScript代码
|
||||
set "jsCode=(async () =^>await import('file:///%currentPath%/napcat.mjs'))();"
|
||||
|
||||
REM 将JavaScript代码保存到文件中
|
||||
echo %jsCode% > loadScript.js
|
||||
echo JavaScript code has been generated and saved to loadScript.js
|
||||
|
||||
REM 设置NAPCAT_PATH环境变量为 当前目录的loadScript.js地址
|
||||
set NAPCAT_PATH=%cd%\loadScript.js
|
||||
|
||||
REM 获取QQ路径
|
||||
|
||||
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in (%RetString%) do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
REM 拿不到QQ路径则退出
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
REM 收集dbghelp.dll路径和HASH信息
|
||||
set QQdir=%~dp0
|
||||
set oldDllPath=%QQdir%dbghelp.dll
|
||||
set newDllPath=%currentPath%\dbghelp.dll
|
||||
|
||||
for /f "tokens=*" %%A in ('certutil -hashfile "%oldDllPath%" MD5') do (
|
||||
if not defined oldDllHash set oldDllHash=%%A
|
||||
)
|
||||
for /f "tokens=*" %%A in ('certutil -hashfile "%newDllPath%" MD5') do (
|
||||
if not defined newDllHash set newDllHash=%%A
|
||||
)
|
||||
|
||||
REM 如果文件一致则跳过
|
||||
if "%oldDllHash%" neq "%newDllHash%" (
|
||||
tasklist /fi "imagename eq QQ.exe" 2>nul | find /i "QQ.exe" >nul
|
||||
if %errorlevel% equ 0 (
|
||||
REM 文件占用则退出
|
||||
echo dbghelp.dll is in use, cannot continue.
|
||||
) else (
|
||||
REM 文件未占用则尝试覆盖
|
||||
copy /y "%newDllPath%" "%oldDllPath%"
|
||||
if %errorlevel% neq 0 (
|
||||
echo Failed to copy dbghelp.dll
|
||||
pause
|
||||
exit /b
|
||||
) else (
|
||||
echo dbghelp.dll has been copied to %QQdir%
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
REM 带参数启动QQ
|
||||
REM 判断wt是否存在,存在则通过wt启动,不存在则通过cmd启动
|
||||
REM %QQPath% --enable-logging %*
|
||||
chcp 65001
|
||||
"%QQPath%" --enable-logging %*
|
@@ -1,77 +0,0 @@
|
||||
@echo off
|
||||
REM Check if the script is running as administrator
|
||||
openfiles >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
REM If not, restart the script in administrator mode
|
||||
echo Requesting administrator privileges...
|
||||
powershell -Command "Start-Process cmd -ArgumentList '/c %~f0 %*' -Verb RunAs"
|
||||
exit /b
|
||||
)
|
||||
|
||||
cd /d %~dp0
|
||||
|
||||
set currentPath=%cd%
|
||||
set currentPath=%currentPath:\=/%
|
||||
|
||||
REM Generate JavaScript code
|
||||
set "jsCode=(async () =^>await import('file:///%currentPath%/napcat.mjs'))();"
|
||||
|
||||
REM Save JavaScript code to a file
|
||||
echo %jsCode% > loadScript.js
|
||||
echo JavaScript code has been generated and saved to loadScript.js
|
||||
|
||||
REM Set NAPCAT_PATH environment variable to the address of loadScript.js in the current directory
|
||||
set NAPCAT_PATH=%cd%\loadScript.js
|
||||
|
||||
REM Get QQ path and cache it
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set "RetString=%%b"
|
||||
)
|
||||
|
||||
set "pathWithoutUninstall=%RetString:Uninstall.exe=%"
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
echo %QQPath%>qq_path_cache.txt
|
||||
echo QQ path %QQPath% has been cached to qq_path_cache.txt
|
||||
|
||||
REM Exit if QQ path is invalid
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
REM Collect dbghelp.dll path and HASH information
|
||||
set QQdir=%~dp0
|
||||
set oldDllPath=%QQdir%dbghelp.dll
|
||||
set newDllPath=%currentPath%\dbghelp.dll
|
||||
|
||||
for /f "tokens=*" %%A in ('certutil -hashfile "%oldDllPath%" MD5') do (
|
||||
if not defined oldDllHash set oldDllHash=%%A
|
||||
)
|
||||
for /f "tokens=*" %%A in ('certutil -hashfile "%newDllPath%" MD5') do (
|
||||
if not defined newDllHash set newDllHash=%%A
|
||||
)
|
||||
|
||||
REM Compare the HASH of the old and new dbghelp.dll, and replace the old one if they are different
|
||||
if "%oldDllHash%" neq "%newDllHash%" (
|
||||
tasklist /fi "imagename eq QQ.exe" 2>nul | find /i "QQ.exe" >nul
|
||||
if %errorlevel% equ 0 (
|
||||
REM If the file is in use, prompt the user to close QQ
|
||||
echo dbghelp.dll is in use, please close QQ first.
|
||||
) else (
|
||||
copy /y "%newDllPath%" "%oldDllPath%"
|
||||
if %errorlevel% neq 0 (
|
||||
echo Copy dbghelp.dll failed, please check and try again.
|
||||
pause
|
||||
exit /b
|
||||
) else (
|
||||
echo dbghelp.dll has been updated.
|
||||
echo Please run BootWay05_run.bat to start QQ.
|
||||
echo If you update QQ in the future, please run BootWay05_init.bat again.
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
)
|
||||
)
|
@@ -1,10 +0,0 @@
|
||||
@echo off
|
||||
set /p QQPath=<qq_path_cache.txt
|
||||
echo QQ path %QQPath% has been read from qq_path_cache.txt
|
||||
echo If failed to start QQ, please try running this script in administrator mode.
|
||||
|
||||
set NAPCAT_PATH=%cd%\loadScript.js
|
||||
|
||||
REM Launch QQ.exe with params provided
|
||||
|
||||
"%QQPath%" --enable-logging %*
|
@@ -1,13 +0,0 @@
|
||||
@echo off
|
||||
|
||||
chcp 65001
|
||||
|
||||
set /p QQPath=<qq_path_cache.txt
|
||||
echo QQ path %QQPath% has been read from qq_path_cache.txt
|
||||
echo If failed to start QQ, please try running this script in administrator mode.
|
||||
|
||||
set NAPCAT_PATH=%cd%\loadScript.js
|
||||
|
||||
REM Launch QQ.exe with params provided
|
||||
|
||||
"%QQPath%" --enable-logging %*
|
@@ -4,16 +4,27 @@ const process = require("process");
|
||||
console.log("[NapCat] [CheckVersion] 开始检测当前仓库版本...");
|
||||
try {
|
||||
const packageJson = require("../package.json");
|
||||
const manifsetJson = require("../manifest.json");
|
||||
|
||||
const currentVersion = packageJson.version;
|
||||
const targetVersion = process.env.VERSION;
|
||||
|
||||
const manifestCurrentVersion = manifsetJson.version;
|
||||
const manifestTargetVersion = process.env.VERSION;
|
||||
|
||||
console.log("[NapCat] [CheckVersion] currentVersion:", currentVersion, "targetVersion:", targetVersion);
|
||||
console.log("[NapCat] [CheckVersion] manifestCurrentVersion:", manifestCurrentVersion, "manifestTargetVersion:", manifestTargetVersion);
|
||||
|
||||
// 验证 targetVersion 格式
|
||||
if (!targetVersion || typeof targetVersion !== 'string') {
|
||||
console.log("[NapCat] [CheckVersion] 目标版本格式不正确或未设置!");
|
||||
return;
|
||||
}
|
||||
// 验证 manifestTargetVersion 格式
|
||||
if (!manifestTargetVersion || typeof manifestTargetVersion !== 'string') {
|
||||
console.log("[NapCat] [CheckVersion] manifest目标版本格式不正确或未设置!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 写入脚本文件的统一函数
|
||||
const writeScriptToFile = (content) => {
|
||||
@@ -21,7 +32,7 @@ try {
|
||||
console.log("[NapCat] [CheckVersion] checkVersion.sh 文件已更新。");
|
||||
};
|
||||
|
||||
if (currentVersion === targetVersion) {
|
||||
if (currentVersion === targetVersion && manifestCurrentVersion === manifestTargetVersion) {
|
||||
// 不需要更新版本,写入一个简单的脚本
|
||||
const simpleScript = "#!/bin/bash\necho \"CheckVersion Is Done\"";
|
||||
writeScriptToFile(simpleScript);
|
||||
@@ -29,11 +40,14 @@ try {
|
||||
// 更新版本,构建安全的sed命令
|
||||
const safeScriptContent = `
|
||||
#!/bin/bash
|
||||
git config --global user.email "bot@test.wumiao.wang"
|
||||
git config --global user.name "Version"
|
||||
sed -i "s/\\\"version\\\": \\\"${currentVersion}\\\"/\\\"version\\\": \\\"${targetVersion}\\\"/g" package.json
|
||||
git config --global user.email "nanaeonn@outlook.com"
|
||||
git config --global user.name "Mlikiowa"
|
||||
sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json
|
||||
sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json
|
||||
sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts
|
||||
sed -i "s/SettingButton(\\"V.*\\", \\"napcat-update-button\\", \\"secondary\\")/SettingButton(\\"V${targetVersion}\\", \\"napcat-update-button\\", \\"secondary\\")/g" ./static/assets/renderer.js
|
||||
git add .
|
||||
git commit -m "chore:version change"
|
||||
git commit -m "release: v${targetVersion}"
|
||||
git push -u origin main`;
|
||||
writeScriptToFile(safeScriptContent);
|
||||
}
|
||||
|
Binary file not shown.
@@ -1,66 +1,67 @@
|
||||
import fs from 'fs';
|
||||
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
|
||||
import fsPromise from 'fs/promises';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
|
||||
import { LogWrapper } from './log';
|
||||
|
||||
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
|
||||
async function guessDuration(pttPath: string) {
|
||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||
let duration = pttFileInfo.size / 1024 / 3; // 3kb/s
|
||||
duration = Math.floor(duration);
|
||||
duration = Math.max(1, duration);
|
||||
logger.log('通过文件大小估算语音的时长:', duration);
|
||||
return duration;
|
||||
}
|
||||
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
||||
const EXIT_CODES = [0, 255];
|
||||
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
|
||||
|
||||
async function guessDuration(pttPath: string, logger: LogWrapper) {
|
||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
|
||||
logger.log('通过文件大小估算语音的时长:', duration);
|
||||
return duration;
|
||||
}
|
||||
|
||||
async function convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
const cp = spawn(FFMPEG_PATH, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
|
||||
cp.on('error', (err: Error) => {
|
||||
logger.log('FFmpeg处理转换出错: ', err.message);
|
||||
reject(err);
|
||||
});
|
||||
cp.on('exit', async (code, signal) => {
|
||||
if (code == null || EXIT_CODES.includes(code)) {
|
||||
try {
|
||||
const data = await fsPromise.readFile(pcmPath);
|
||||
await fsPromise.unlink(pcmPath);
|
||||
resolve(data);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
|
||||
reject(new Error('FFmpeg处理转换失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleWavFile(
|
||||
file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper
|
||||
): Promise<{input: Buffer, sampleRate: number}> {
|
||||
const { fmt } = getWavFileInfo(file);
|
||||
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
|
||||
return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||
}
|
||||
return { input: file, sampleRate: fmt.sampleRate };
|
||||
}
|
||||
|
||||
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
|
||||
try {
|
||||
const file = await fsPromise.readFile(filePath);
|
||||
const pttPath = path.join(TEMP_DIR, randomUUID());
|
||||
if (!isSilk(file)) {
|
||||
logger.log(`语音文件${filePath}需要转换成silk`);
|
||||
const _isWav = isWav(file);
|
||||
const pcmPath = pttPath + '.pcm';
|
||||
let sampleRate = 0;
|
||||
const convert = () => {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
// todo: 通过配置文件获取ffmpeg路径
|
||||
const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
|
||||
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
|
||||
cp.on('error', err => {
|
||||
logger.log('FFmpeg处理转换出错: ', err.message);
|
||||
return reject(err);
|
||||
});
|
||||
cp.on('exit', (code, signal) => {
|
||||
const EXIT_CODES = [0, 255];
|
||||
if (code == null || EXIT_CODES.includes(code)) {
|
||||
sampleRate = 24000;
|
||||
const data = fs.readFileSync(pcmPath);
|
||||
fs.unlink(pcmPath, (err) => {
|
||||
});
|
||||
return resolve(data);
|
||||
}
|
||||
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
|
||||
reject(Error('FFmpeg处理转换失败'));
|
||||
});
|
||||
});
|
||||
};
|
||||
let input: Buffer;
|
||||
if (!_isWav) {
|
||||
input = await convert();
|
||||
} else {
|
||||
input = file;
|
||||
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
||||
const { fmt } = getWavFileInfo(input);
|
||||
// log(`wav文件信息`, fmt)
|
||||
if (!allowSampleRate.includes(fmt.sampleRate)) {
|
||||
input = await convert();
|
||||
}
|
||||
}
|
||||
const pcmPath = `${pttPath}.pcm`;
|
||||
const { input, sampleRate } = isWav(file)
|
||||
? (await handleWavFile(file, filePath, pcmPath, logger))
|
||||
: { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||
const silk = await encode(input, sampleRate);
|
||||
fs.writeFileSync(pttPath, silk.data);
|
||||
await fsPromise.writeFile(pttPath, silk.data);
|
||||
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
|
||||
return {
|
||||
converted: true,
|
||||
@@ -68,15 +69,13 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
||||
duration: silk.duration / 1000,
|
||||
};
|
||||
} else {
|
||||
const silk = file;
|
||||
let duration = 0;
|
||||
try {
|
||||
duration = getDuration(silk) / 1000;
|
||||
duration = getDuration(file) / 1000;
|
||||
} catch (e: any) {
|
||||
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack);
|
||||
duration = await guessDuration(filePath);
|
||||
duration = await guessDuration(filePath, logger);
|
||||
}
|
||||
|
||||
return {
|
||||
converted: false,
|
||||
path: filePath,
|
||||
@@ -84,7 +83,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.logError('convert silk failed', error.stack);
|
||||
logger.logError.bind(logger)('convert silk failed', error.stack);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
@@ -40,7 +40,7 @@ export abstract class ConfigBase<T> {
|
||||
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
||||
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
||||
} catch (e: any) {
|
||||
logger.logError(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
||||
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
@@ -49,9 +49,9 @@ export abstract class ConfigBase<T> {
|
||||
return this.configData;
|
||||
} catch (e: any) {
|
||||
if (e instanceof SyntaxError) {
|
||||
logger.logError(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
||||
logger.logError.bind(logger)(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
||||
} else {
|
||||
logger.logError(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
||||
logger.logError.bind(logger)(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
||||
}
|
||||
return {} as T;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export abstract class ConfigBase<T> {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
|
||||
} catch (e: any) {
|
||||
logger.logError(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
||||
logger.logError.bind(logger)(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,133 +0,0 @@
|
||||
import type { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
|
||||
import EventEmitter from 'node:events';
|
||||
|
||||
export type ListenerClassBase = Record<string, string>;
|
||||
|
||||
export interface ListenerIBase {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-new
|
||||
new(listener: any): ListenerClassBase;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class NTEventWrapperV2 extends EventEmitter {
|
||||
private wrapperApi: WrapperNodeApi;
|
||||
private wrapperSession: NodeIQQNTWrapperSession;
|
||||
private listenerRefStorage = new Map<string, ListenerIBase>();
|
||||
|
||||
constructor(WrapperApi: WrapperNodeApi, WrapperSession: NodeIQQNTWrapperSession) {
|
||||
super();
|
||||
this.on('error', () => {
|
||||
});
|
||||
this.wrapperApi = WrapperApi;
|
||||
this.wrapperSession = WrapperSession;
|
||||
}
|
||||
|
||||
dispatcherListener(ListenerEvent: string, ...args: any[]) {
|
||||
this.emit(ListenerEvent, ...args);
|
||||
}
|
||||
|
||||
createProxyDispatch(ListenerMainName: string) {
|
||||
const dispatcherListener = this.dispatcherListener.bind(this);
|
||||
return new Proxy({}, {
|
||||
get(_target: any, prop: any, _receiver: any) {
|
||||
return (...args: any[]) => {
|
||||
dispatcherListener(ListenerMainName + '/' + prop, ...args);
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOrInitListener<T>(listenerMainName: string): Promise<T> {
|
||||
const ListenerType = this.wrapperApi[listenerMainName];
|
||||
//获取NTQQ 外部 Listener包装
|
||||
if (!ListenerType) throw new Error('Init Listener not found');
|
||||
let Listener = this.listenerRefStorage.get(listenerMainName);
|
||||
//判断是否已创建 创建则跳过
|
||||
if (!Listener && ListenerType) {
|
||||
Listener = new ListenerType(this.createProxyDispatch(listenerMainName));
|
||||
if (!Listener) throw new Error('Init Listener failed');
|
||||
//实例化NTQQ Listener外包装
|
||||
const ServiceSubName = /^NodeIKernel(.*?)Listener$/.exec(listenerMainName)![1];
|
||||
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener';
|
||||
const addfunc = this.createEventFunction<(listener: T) => number>(Service);
|
||||
//添加Listener到NTQQ
|
||||
addfunc!(Listener as T);
|
||||
this.listenerRefStorage.set(listenerMainName, Listener);
|
||||
//保存Listener实例
|
||||
}
|
||||
return Listener as T;
|
||||
}
|
||||
|
||||
async createEventWithListener<EventType extends (...args: any) => any, ListenerType extends (...args: any) => any>
|
||||
(
|
||||
eventName: string,
|
||||
listenerName: string,
|
||||
waitTimes = 1,
|
||||
timeout: number = 3000,
|
||||
checker: (...args: Parameters<ListenerType>) => boolean,
|
||||
...eventArg: Parameters<EventType>
|
||||
) {
|
||||
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
|
||||
const ListenerNameList = listenerName.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
//const ListenerSubName = ListenerNameList[1];
|
||||
this.getOrInitListener<ListenerType>(ListenerMainName);
|
||||
let complete = 0;
|
||||
const retData: Parameters<ListenerType> | undefined = undefined;
|
||||
let retEvent: any = {};
|
||||
const databack = () => {
|
||||
if (complete == 0) {
|
||||
reject(new Error('Timeout: NTEvent EventName:' + eventName + ' ListenerName:' + listenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'));
|
||||
} else {
|
||||
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
|
||||
}
|
||||
};
|
||||
const Timeouter = setTimeout(databack, timeout);
|
||||
const callback = (...args: Parameters<ListenerType>) => {
|
||||
if (checker(...args)) {
|
||||
complete++;
|
||||
if (complete >= waitTimes) {
|
||||
clearTimeout(Timeouter);
|
||||
this.removeListener(listenerName, callback);
|
||||
databack();
|
||||
}
|
||||
}
|
||||
};
|
||||
this.on(listenerName, callback);
|
||||
const EventFunc = this.createEventFunction<EventType>(eventName);
|
||||
retEvent = await EventFunc!(...(eventArg as any[]));
|
||||
});
|
||||
}
|
||||
|
||||
private createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
|
||||
const eventNameArr = eventName.split('/');
|
||||
type eventType = {
|
||||
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
|
||||
}
|
||||
if (eventNameArr.length > 1) {
|
||||
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '');
|
||||
const eventName = eventNameArr[1];
|
||||
//getNodeIKernelGroupListener,GroupService
|
||||
//console.log('2', eventName);
|
||||
const services = (this.wrapperSession as unknown as eventType)[serviceName]();
|
||||
const event = services[eventName]
|
||||
//重新绑定this
|
||||
.bind(services);
|
||||
if (event) {
|
||||
return event as T;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async callEvent<EventType extends (...args: any[]) => Promise<any> | any>(
|
||||
EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
|
||||
return new Promise<Awaited<ReturnType<EventType>>>((resolve) => {
|
||||
const EventFunc = this.createEventFunction<EventType>(EventName);
|
||||
EventFunc!(...args).then((retData: Awaited<ReturnType<EventType>> | PromiseLike<Awaited<ReturnType<EventType>>>) => resolve(retData));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//NTEvent2.0
|
@@ -9,6 +9,15 @@ interface InternalMapKey {
|
||||
checker: ((...args: any[]) => boolean) | undefined;
|
||||
}
|
||||
|
||||
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
|
||||
|
||||
type FuncKeys<T> = Extract<
|
||||
{
|
||||
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
|
||||
}[keyof T],
|
||||
string
|
||||
>;
|
||||
|
||||
export type ListenerClassBase = Record<string, string>;
|
||||
|
||||
export class NTEventWrapper {
|
||||
@@ -43,10 +52,8 @@ export class NTEventWrapper {
|
||||
|
||||
createEventFunction<
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
T extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
|
||||
const eventNameArr = eventName.split('/');
|
||||
type eventType = {
|
||||
@@ -98,10 +105,8 @@ export class NTEventWrapper {
|
||||
|
||||
async callNoListenerEvent<
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||
>(
|
||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||
...args: Parameters<EventType>
|
||||
@@ -111,10 +116,8 @@ export class NTEventWrapper {
|
||||
|
||||
async registerListen<
|
||||
Listener extends keyof ListenerNamingMapping,
|
||||
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod],
|
||||
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
|
||||
>(
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
waitTimes = 1,
|
||||
@@ -164,15 +167,11 @@ export class NTEventWrapper {
|
||||
|
||||
async callNormalEventV2<
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
Listener extends keyof ListenerNamingMapping,
|
||||
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod]
|
||||
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
|
||||
>(
|
||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
@@ -236,7 +235,7 @@ export class NTEventWrapper {
|
||||
this.createListenerFunction(ListenerMainName);
|
||||
const eventFunction = this.createEventFunction(serviceAndMethod);
|
||||
retEvent = await eventFunction!(...(args));
|
||||
if (!checkerEvent(retEvent)) {
|
||||
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
|
||||
clearTimeout(timeoutRef);
|
||||
reject(
|
||||
new Error(
|
||||
@@ -250,86 +249,8 @@ export class NTEventWrapper {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
async callNormalEvent<
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
|
||||
Listener extends keyof ListenerNamingMapping,
|
||||
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod]
|
||||
>(
|
||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
waitTimes = 1,
|
||||
timeout: number = 3000,
|
||||
checker: (...args: Parameters<ListenerType>) => boolean,
|
||||
...args: Parameters<EventType>
|
||||
) {
|
||||
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
|
||||
async (resolve, reject) => {
|
||||
const id = randomUUID();
|
||||
let complete = 0;
|
||||
let retData: Parameters<ListenerType> | undefined = undefined;
|
||||
let retEvent: any = {};
|
||||
const databack = () => {
|
||||
if (complete == 0) {
|
||||
reject(
|
||||
new Error(
|
||||
'Timeout: NTEvent EventName:' +
|
||||
serviceAndMethod +
|
||||
' ListenerName:' +
|
||||
listenerAndMethod +
|
||||
' EventRet:\n' +
|
||||
JSON.stringify(retEvent, null, 4) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
|
||||
}
|
||||
};
|
||||
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
const ListenerSubName = ListenerNameList[1];
|
||||
|
||||
const Timeouter = setTimeout(databack, timeout);
|
||||
|
||||
const eventCallbak = {
|
||||
timeout: timeout,
|
||||
createtime: Date.now(),
|
||||
checker: checker,
|
||||
func: (...args: any[]) => {
|
||||
complete++;
|
||||
//console.log('func', ...args);
|
||||
retData = args as Parameters<ListenerType>;
|
||||
if (complete >= waitTimes) {
|
||||
clearTimeout(Timeouter);
|
||||
databack();
|
||||
}
|
||||
},
|
||||
};
|
||||
if (!this.EventTask.get(ListenerMainName)) {
|
||||
this.EventTask.set(ListenerMainName, new Map());
|
||||
}
|
||||
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
|
||||
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
|
||||
}
|
||||
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak);
|
||||
this.createListenerFunction(ListenerMainName);
|
||||
const EventFunc = this.createEventFunction<EventType>(serviceAndMethod);
|
||||
retEvent = await EventFunc!(...(args as any[]));
|
||||
},
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@@ -123,7 +123,7 @@ export interface HttpDownloadOptions {
|
||||
headers?: Record<string, string> | string;
|
||||
}
|
||||
|
||||
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||
async function tryDownload(options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
||||
// const chunks: Buffer[] = [];
|
||||
let url: string;
|
||||
let headers: Record<string, string> = {
|
||||
@@ -142,15 +142,26 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
|
||||
}
|
||||
}
|
||||
}
|
||||
if (useReferer && !headers['Referer']) {
|
||||
headers['Referer'] = url;
|
||||
}
|
||||
const fetchRes = await fetch(url, { headers }).catch((err) => {
|
||||
if (err.cause) {
|
||||
throw err.cause;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`);
|
||||
return fetchRes;
|
||||
}
|
||||
|
||||
const blob = await fetchRes.blob();
|
||||
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||
const useReferer = typeof options === 'string';
|
||||
let resp = await tryDownload(options);
|
||||
if (resp.status === 403 && useReferer) {
|
||||
resp = await tryDownload(options, true);
|
||||
}
|
||||
if (!resp.ok) throw new Error(`下载文件失败: ${resp.statusText}`);
|
||||
const blob = await resp.blob();
|
||||
const buffer = await blob.arrayBuffer();
|
||||
return Buffer.from(buffer);
|
||||
}
|
||||
@@ -160,8 +171,7 @@ type Uri2LocalRes = {
|
||||
errMsg: string,
|
||||
fileName: string,
|
||||
ext: string,
|
||||
path: string,
|
||||
isLocal: boolean
|
||||
path: string
|
||||
}
|
||||
|
||||
export async function checkFileV2(filePath: string) {
|
||||
@@ -194,7 +204,6 @@ export async function checkUriType(Uri: string) {
|
||||
return undefined;
|
||||
}, Uri);
|
||||
if (LocalFileRet) return LocalFileRet;
|
||||
|
||||
const OtherFileRet = await solveProblem((uri: string) => {
|
||||
//再判断是否是Http
|
||||
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
||||
@@ -206,15 +215,19 @@ export async function checkUriType(Uri: string) {
|
||||
}
|
||||
if (uri.startsWith('file://')) {
|
||||
let filePath: string;
|
||||
// await fs.copyFile(url.pathname, filePath);
|
||||
const pathname = decodeURIComponent(new URL(uri).pathname);
|
||||
if (process.platform === 'win32') {
|
||||
filePath = pathname.slice(1);
|
||||
} else {
|
||||
filePath = pathname;
|
||||
}
|
||||
|
||||
return { Uri: filePath, Type: FileUriType.Local };
|
||||
}
|
||||
if (uri.startsWith('data:')) {
|
||||
const data = uri.split(',')[1];
|
||||
if (data) return { Uri: data, Type: FileUriType.Base64 };
|
||||
}
|
||||
}, Uri);
|
||||
if (OtherFileRet) return OtherFileRet;
|
||||
|
||||
@@ -224,34 +237,42 @@ export async function checkUriType(Uri: string) {
|
||||
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> {
|
||||
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||
//解析失败
|
||||
const tempName = randomUUID();
|
||||
if (!filename) filename = randomUUID();
|
||||
//解析Http和Https协议
|
||||
|
||||
if (UriType == FileUriType.Unknown) {
|
||||
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '', isLocal: false };
|
||||
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
|
||||
}
|
||||
//解析File协议和本地文件
|
||||
if (UriType == FileUriType.Local) {
|
||||
const fileExt = path.extname(HandledUri);
|
||||
const filename = path.basename(HandledUri, fileExt);
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: HandledUri, isLocal: true };
|
||||
let filename = path.basename(HandledUri, fileExt);
|
||||
filename += fileExt;
|
||||
//复制文件到临时文件并保持后缀
|
||||
const filenameTemp = tempName + fileExt;
|
||||
const filePath = path.join(dir, filenameTemp);
|
||||
fs.copyFileSync(HandledUri, filePath);
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
}
|
||||
//接下来都要有文件名
|
||||
if (!filename) filename = randomUUID();
|
||||
//解析Http和Https协议
|
||||
|
||||
if (UriType == FileUriType.Remote) {
|
||||
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
|
||||
if (pathInfo.name) {
|
||||
filename = pathInfo.name;
|
||||
const pathlen = 200 - dir.length - pathInfo.name.length;
|
||||
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
|
||||
if (pathInfo.ext) {
|
||||
filename += pathInfo.ext;
|
||||
}
|
||||
}
|
||||
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
|
||||
const fileExt = path.extname(HandledUri);
|
||||
const filePath = path.join(dir, filename);
|
||||
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
|
||||
const filePath = path.join(dir, tempName + fileExt);
|
||||
const buffer = await httpDownload(HandledUri);
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath, isLocal: true };
|
||||
//没有文件就创建
|
||||
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
}
|
||||
//解析Base64
|
||||
if (UriType == FileUriType.Base64) {
|
||||
@@ -266,7 +287,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und
|
||||
fileExt = ext;
|
||||
filename = filename + '.' + ext;
|
||||
}
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath, isLocal: true };
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
}
|
||||
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '', isLocal: false };
|
||||
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
|
||||
}
|
||||
|
118
src/common/forward-msg-builder.ts
Normal file
118
src/common/forward-msg-builder.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { PacketMsg } from "@/core/packet/message/message";
|
||||
import * as crypto from "node:crypto";
|
||||
|
||||
interface ForwardMsgJson {
|
||||
app: string
|
||||
config: ForwardMsgJsonConfig,
|
||||
desc: string,
|
||||
extra: ForwardMsgJsonExtra,
|
||||
meta: ForwardMsgJsonMeta,
|
||||
prompt: string,
|
||||
ver: string,
|
||||
view: string
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonConfig {
|
||||
autosize: number,
|
||||
forward: number,
|
||||
round: number,
|
||||
type: string,
|
||||
width: number
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonExtra {
|
||||
filename: string,
|
||||
tsum: number,
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonMeta {
|
||||
detail: ForwardMsgJsonMetaDetail
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonMetaDetail {
|
||||
news: {
|
||||
text: string
|
||||
}[],
|
||||
resid: string,
|
||||
source: string,
|
||||
summary: string,
|
||||
uniseq: string
|
||||
}
|
||||
|
||||
interface ForwardAdaptMsg {
|
||||
senderName?: string;
|
||||
isGroupMsg?: boolean;
|
||||
msg?: ForwardAdaptMsgElement[];
|
||||
}
|
||||
|
||||
interface ForwardAdaptMsgElement {
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
export class ForwardMsgBuilder {
|
||||
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
const id = crypto.randomUUID();
|
||||
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
||||
if (!source) {
|
||||
source = isGroupMsg ? "群聊的聊天记录" :
|
||||
msg.length
|
||||
? Array.from(new Set(msg.slice(0, 4).map(m => m.senderName)))
|
||||
.join('和') + '的聊天记录'
|
||||
: '聊天记录';
|
||||
}
|
||||
if (!news) {
|
||||
news = msg.length === 0 ? [{
|
||||
text: "Nya~ This message is send from NapCat.Packet!",
|
||||
}] : msg.map(m => ({
|
||||
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
|
||||
}));
|
||||
}
|
||||
if (!summary) {
|
||||
summary = `查看${msg.length}条转发消息`;
|
||||
}
|
||||
if (!prompt) {
|
||||
prompt = "[聊天记录]";
|
||||
}
|
||||
return {
|
||||
app: "com.tencent.multimsg",
|
||||
config: {
|
||||
autosize: 1,
|
||||
forward: 1,
|
||||
round: 1,
|
||||
type: "normal",
|
||||
width: 300
|
||||
},
|
||||
desc: prompt,
|
||||
extra: {
|
||||
filename: id,
|
||||
tsum: msg.length,
|
||||
},
|
||||
meta: {
|
||||
detail: {
|
||||
news,
|
||||
resid: resId,
|
||||
source,
|
||||
summary,
|
||||
uniseq: id,
|
||||
}
|
||||
},
|
||||
prompt,
|
||||
ver: "0.0.0.5",
|
||||
view: "contact",
|
||||
};
|
||||
}
|
||||
|
||||
static fromResId(resId: string): ForwardMsgJson {
|
||||
return this.build(resId, []);
|
||||
}
|
||||
|
||||
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
return this.build(resId, packetMsg.map(msg => ({
|
||||
senderName: msg.senderName,
|
||||
isGroupMsg: msg.groupId !== undefined,
|
||||
msg: msg.msg.map(m => ({
|
||||
preview: m.valid? m.toPreview() : "[该消息类型暂不支持查看]",
|
||||
}))
|
||||
})), source, news, summary, prompt);
|
||||
}
|
||||
}
|
@@ -25,49 +25,76 @@ export async function solveAsyncProblem<T extends (...args: any[]) => Promise<an
|
||||
}
|
||||
|
||||
export class FileNapCatOneBotUUID {
|
||||
static encodeModelId(peer: Peer, modelId: string, fileId: string): string {
|
||||
return `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}`;
|
||||
static encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);//储存data的hex长度
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decodeModelId(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
modelId: string,
|
||||
fileId: string
|
||||
fileId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
if (!uuid.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
|
||||
const data = uuid.split('|');
|
||||
if (data.length !== 6) return undefined;
|
||||
const [, , chatType, peerUid, modelId, fileId] = data;
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: chatType as any,
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
modelId,
|
||||
fileId
|
||||
fileId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
|
||||
static encode(peer: Peer, msgId: string, elementId: string): string {
|
||||
return `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}`;
|
||||
static encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
//一个字节8位 一个ascii字符1字节 一个hex字符4位 表示一个ascii字符需要两个hex字符
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decode(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
msgId: string,
|
||||
elementId: string
|
||||
elementId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
if (!uuid.startsWith('NapCatOneBot|MsgFile|')) return undefined;
|
||||
const data = uuid.split('|');
|
||||
if (data.length !== 6) return undefined;
|
||||
const [, , chatType, peerUid, msgId, elementId] = data;
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|MsgFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: chatType as any,
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
msgId,
|
||||
elementId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -140,34 +167,51 @@ export function isEqual(obj1: any, obj2: any) {
|
||||
export function getDefaultQQVersionConfigInfo(): QQVersionConfigType {
|
||||
if (os.platform() === 'linux') {
|
||||
return {
|
||||
baseVersion: '3.2.12-27597',
|
||||
curVersion: '3.2.12-27597',
|
||||
baseVersion: '3.2.12.28060',
|
||||
curVersion: '3.2.12.28060',
|
||||
prevVersion: '',
|
||||
onErrorVersions: [],
|
||||
buildId: '27597',
|
||||
buildId: '27254',
|
||||
};
|
||||
}
|
||||
if (os.platform() === 'darwin') {
|
||||
return {
|
||||
baseVersion: '6.9.53.28060',
|
||||
curVersion: '6.9.53.28060',
|
||||
prevVersion: '',
|
||||
onErrorVersions: [],
|
||||
buildId: '28060',
|
||||
};
|
||||
}
|
||||
return {
|
||||
baseVersion: '9.9.15-27597',
|
||||
curVersion: '9.9.15-27597',
|
||||
baseVersion: '9.9.15-28131',
|
||||
curVersion: '9.9.15-28131',
|
||||
prevVersion: '',
|
||||
onErrorVersions: [],
|
||||
buildId: '27597',
|
||||
buildId: '28131',
|
||||
};
|
||||
}
|
||||
|
||||
export function getQQPackageInfoPath(exePath: string = ''): string {
|
||||
export function getQQPackageInfoPath(exePath: string = '', version?: string): string {
|
||||
let packagePath;
|
||||
if (os.platform() === 'darwin') {
|
||||
return path.join(path.dirname(exePath), '..', 'Resources', 'app', 'package.json');
|
||||
packagePath = path.join(path.dirname(exePath), '..', 'Resources', 'app', 'package.json');
|
||||
} else if (os.platform() === 'linux') {
|
||||
packagePath = path.join(path.dirname(exePath), './resources/app/package.json');
|
||||
} else {
|
||||
return path.join(path.dirname(exePath), 'resources', 'app', 'package.json');
|
||||
packagePath = path.join(path.dirname(exePath), './versions/' + version + '/resources/app/package.json');
|
||||
}
|
||||
//下面是老版本兼容 未来去掉
|
||||
if (!fs.existsSync(packagePath)) {
|
||||
packagePath = path.join(path.dirname(exePath), './resources/app/versions/' + version + '/package.json');
|
||||
}
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
export function getQQVersionConfigPath(exePath: string = ''): string | undefined {
|
||||
let configVersionInfoPath;
|
||||
if (os.platform() === 'win32') {
|
||||
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json');
|
||||
configVersionInfoPath = path.join(path.dirname(exePath), 'versions', 'config.json');
|
||||
} else if (os.platform() === 'darwin') {
|
||||
const userPath = os.homedir();
|
||||
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
|
||||
@@ -180,13 +224,57 @@ export function getQQVersionConfigPath(exePath: string = ''): string | undefined
|
||||
if (typeof configVersionInfoPath !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
//老版本兼容 未来去掉
|
||||
if (!fs.existsSync(configVersionInfoPath)) {
|
||||
configVersionInfoPath = path.join(path.dirname(exePath), './resources/app/versions/config.json');
|
||||
}
|
||||
if (!fs.existsSync(configVersionInfoPath)) {
|
||||
return undefined;
|
||||
}
|
||||
return configVersionInfoPath;
|
||||
}
|
||||
|
||||
export function calcQQLevel(level: QQLevel) {
|
||||
export function calcQQLevel(level?: QQLevel) {
|
||||
if (!level) return 0;
|
||||
const { crownNum, sunNum, moonNum, starNum } = level;
|
||||
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
||||
}
|
||||
|
||||
export function stringifyWithBigInt(obj: any) {
|
||||
return JSON.stringify(obj, (key, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value
|
||||
);
|
||||
}
|
||||
|
||||
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
||||
const hexSequence = "A4 09 00 00 00 35";
|
||||
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ""), "hex");
|
||||
const filePath = path.resolve(nodeMajor);
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
|
||||
let searchPosition = 0;
|
||||
while (true) {
|
||||
const index = fileContent.indexOf(sequenceBytes, searchPosition);
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const start = index + sequenceBytes.length - 1;
|
||||
const end = fileContent.indexOf(0x00, start);
|
||||
if (end === -1) {
|
||||
break;
|
||||
}
|
||||
const content = fileContent.subarray(start, end);
|
||||
if (!content.every(byte => byte === 0x00)) {
|
||||
try {
|
||||
return content.toString("utf-8");
|
||||
} catch (error) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
searchPosition = end + 1;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
@@ -139,9 +139,13 @@ export class LogWrapper {
|
||||
|
||||
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
|
||||
const isSelfSent = msg.senderUin === selfInfo.uin;
|
||||
this.log(`${
|
||||
isSelfSent ? '发送 ->' : '接收 <-'
|
||||
} ${rawMessageToText(msg)}`);
|
||||
|
||||
// Intercept grey tip
|
||||
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +159,12 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
if (msg.chatType == ChatType.KCHATTYPEC2C) {
|
||||
tokens.push(`私聊 (${msg.peerUin})`);
|
||||
} else if (msg.chatType == ChatType.KCHATTYPEGROUP) {
|
||||
tokens.push(`群聊 (群 ${msg.peerUin} 的 ${msg.senderUin})`);
|
||||
if (recursiveLevel < 1) {
|
||||
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
|
||||
}
|
||||
if (msg.senderUin !== '0') {
|
||||
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
|
||||
}
|
||||
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
|
||||
tokens.push('移动设备');
|
||||
} else /* temp */ {
|
||||
@@ -167,7 +176,8 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
function msgElementToText(element: MessageElement) {
|
||||
if (element.textElement) {
|
||||
if (element.textElement.atType === AtType.notAt) {
|
||||
return element.textElement.content;
|
||||
const originalContentLines = element.textElement.content.split('\n');
|
||||
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
||||
} else if (element.textElement.atType === AtType.atAll) {
|
||||
return `@全体成员`;
|
||||
} else if (element.textElement.atType === AtType.atUser) {
|
||||
@@ -179,17 +189,16 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
const recordMsgOrNull = msg.records.find(
|
||||
record => element.replyElement!.sourceMsgIdInRecords === record.msgId,
|
||||
);
|
||||
return `[回复消息 ${
|
||||
recordMsgOrNull &&
|
||||
recordMsgOrNull.peerUin != '284840486' // 非转发消息; 否则定位不到
|
||||
?
|
||||
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
||||
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`
|
||||
return `[回复消息 ${recordMsgOrNull &&
|
||||
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到
|
||||
?
|
||||
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
||||
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`
|
||||
}]`;
|
||||
}
|
||||
|
||||
if (element.picElement) {
|
||||
return `[图片 ${element.picElement.fileName}]`;
|
||||
return '[图片]';
|
||||
}
|
||||
|
||||
if (element.fileElement) {
|
||||
@@ -197,7 +206,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
}
|
||||
|
||||
if (element.videoElement) {
|
||||
return `[视频 ${element.videoElement.fileName}]`;
|
||||
return '[视频]';
|
||||
}
|
||||
|
||||
if (element.pttElement) {
|
||||
@@ -205,7 +214,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
}
|
||||
|
||||
if (element.arkElement) {
|
||||
return `[卡片消息 ${element.arkElement.bytesData}]`;
|
||||
return '[卡片消息]';
|
||||
}
|
||||
|
||||
if (element.faceElement) {
|
||||
@@ -213,19 +222,19 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
}
|
||||
|
||||
if (element.marketFaceElement) {
|
||||
return `[商城表情 ${element.marketFaceElement.faceName}]`;
|
||||
return element.marketFaceElement.faceName;
|
||||
}
|
||||
|
||||
if (element.markdownElement) {
|
||||
return `[Markdown ${element.markdownElement.content}]`;
|
||||
return '[Markdown 消息]';
|
||||
}
|
||||
|
||||
if (element.multiForwardMsgElement) {
|
||||
return `[转发消息]`;
|
||||
return '[转发消息]';
|
||||
}
|
||||
|
||||
if (element.elementType === ElementType.GreyTip) {
|
||||
return `[灰条消息]`; // TODO: resolve the text
|
||||
return '[灰条消息]';
|
||||
}
|
||||
|
||||
return `[未实现 (ElementType = ${element.elementType})]`;
|
||||
|
@@ -24,7 +24,9 @@ export class LRUCache<K, V> {
|
||||
} else if (this.cache.size >= this.capacity) {
|
||||
// If the cache is full, remove the least recently used key (the first one in the map)
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
this.cache.delete(firstKey);
|
||||
if (firstKey !== undefined) {
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
|
@@ -23,8 +23,10 @@ export class LimitedHashTable<K, V> {
|
||||
}
|
||||
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);
|
||||
if (oldestKey !== undefined) {
|
||||
this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
|
||||
this.keyToValue.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import fs from 'node:fs';
|
||||
import { systemPlatform } from '@/common/system';
|
||||
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath } from './helper';
|
||||
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
|
||||
import AppidTable from '@/core/external/appid.json';
|
||||
import { LogWrapper } from './log';
|
||||
import { getMajorPath } from '@/core';
|
||||
|
||||
export class QQBasicInfoWrapper {
|
||||
QQMainPath: string | undefined;
|
||||
@@ -19,14 +20,16 @@ export class QQBasicInfoWrapper {
|
||||
//基础目录获取
|
||||
this.context = context;
|
||||
this.QQMainPath = process.execPath;
|
||||
this.QQPackageInfoPath = getQQPackageInfoPath(this.QQMainPath);
|
||||
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
|
||||
|
||||
|
||||
//基础信息获取 无快更则启用默认模板填充
|
||||
this.isQuickUpdate = !!this.QQVersionConfigPath;
|
||||
this.QQVersionConfig = this.isQuickUpdate
|
||||
? JSON.parse(fs.readFileSync(this.QQVersionConfigPath!).toString())
|
||||
: getDefaultQQVersionConfigInfo();
|
||||
|
||||
this.QQPackageInfoPath = getQQPackageInfoPath(this.QQMainPath, this.QQVersionConfig?.curVersion);
|
||||
this.QQPackageInfo = JSON.parse(fs.readFileSync(this.QQPackageInfoPath).toString());
|
||||
const { appid: IQQVersionAppid, qua: IQQVersionQua } = this.getAppidV2();
|
||||
this.QQVersionAppid = IQQVersionAppid;
|
||||
@@ -51,29 +54,26 @@ export class QQBasicInfoWrapper {
|
||||
}
|
||||
|
||||
//此方法不要直接使用
|
||||
getQUAInternal() {
|
||||
switch (systemPlatform) {
|
||||
case 'linux':
|
||||
return `V1_LNX_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`;
|
||||
case 'darwin':
|
||||
return `V1_MAC_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`;
|
||||
default:
|
||||
return `V1_WIN_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`;
|
||||
}
|
||||
getQUAFallback() {
|
||||
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
|
||||
win32: `V1_WIN_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
|
||||
darwin: `V1_MAC_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
|
||||
linux: `V1_LNX_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
|
||||
};
|
||||
return platformMapping[systemPlatform] ?? (platformMapping.win32)!;
|
||||
}
|
||||
|
||||
getAppidInternal() {
|
||||
switch (systemPlatform) {
|
||||
case 'linux':
|
||||
return '537243600';
|
||||
case 'darwin':
|
||||
return '537243441';
|
||||
default:
|
||||
return '537243538';
|
||||
}
|
||||
getAppIdFallback() {
|
||||
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
|
||||
win32: '537246092',
|
||||
darwin: '537246140',
|
||||
linux: '537246140',
|
||||
};
|
||||
return platformMapping[systemPlatform] ?? '537246092';
|
||||
}
|
||||
|
||||
getAppidV2(): { appid: string; qua: string } {
|
||||
// 通过已有表 性能好
|
||||
const appidTbale = AppidTable as unknown as QQAppidTableType;
|
||||
const fullVersion = this.getFullQQVesion();
|
||||
if (fullVersion) {
|
||||
@@ -82,10 +82,25 @@ export class QQBasicInfoWrapper {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// else
|
||||
// 通过Major拉取 性能差
|
||||
try {
|
||||
let majorAppid = this.getAppidV2ByMajor(fullVersion);
|
||||
if (majorAppid) {
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`);
|
||||
return { appid: majorAppid, qua: this.getQUAFallback() };
|
||||
}
|
||||
} catch (error) {
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||
}
|
||||
// 最终兜底为老版本
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
|
||||
return { appid: this.getAppidInternal(), qua: this.getQUAInternal() };
|
||||
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
||||
}
|
||||
getAppidV2ByMajor(QQVersion: string) {
|
||||
let majorPath = getMajorPath(QQVersion);
|
||||
let appid = parseAppidFromMajor(majorPath);
|
||||
return appid;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ export class RequestUtil {
|
||||
const options = {
|
||||
hostname: option.hostname,
|
||||
port: option.port,
|
||||
path: option.href,
|
||||
path: option.pathname + option.search,
|
||||
method: method,
|
||||
headers: headers,
|
||||
};
|
||||
|
@@ -1,12 +1,8 @@
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { networkInterfaces } from 'os';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
// 缓解Win7设备兼容性问题
|
||||
let osName: string;
|
||||
// 设备ID
|
||||
let machineId: Promise<string>;
|
||||
|
||||
try {
|
||||
osName = os.hostname();
|
||||
@@ -14,54 +10,6 @@ try {
|
||||
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
|
||||
}
|
||||
|
||||
const invalidMacAddresses = new Set([
|
||||
'00:00:00:00:00:00',
|
||||
'ff:ff:ff:ff:ff:ff',
|
||||
'ac:de:48:00:11:22',
|
||||
]);
|
||||
|
||||
function validateMacAddress(candidate: string): boolean {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const tempCandidate = candidate.replace(/\-/g, ':').toLowerCase();
|
||||
return !invalidMacAddresses.has(tempCandidate);
|
||||
}
|
||||
|
||||
export async function getMachineId(): Promise<string> {
|
||||
if (!machineId) {
|
||||
machineId = (async () => {
|
||||
const id = await getMacMachineId();
|
||||
return id ?? randomUUID(); // fallback, generate a UUID
|
||||
})();
|
||||
}
|
||||
|
||||
return machineId;
|
||||
}
|
||||
|
||||
export function getMac(): string {
|
||||
const ifaces = networkInterfaces();
|
||||
for (const name in ifaces) {
|
||||
const networkInterface = ifaces[name];
|
||||
if (networkInterface) {
|
||||
for (const { mac } of networkInterface) {
|
||||
if (validateMacAddress(mac)) {
|
||||
return mac;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unable to retrieve mac address (unexpected format)');
|
||||
}
|
||||
|
||||
async function getMacMachineId(): Promise<string | undefined> {
|
||||
try {
|
||||
const crypto = await import('crypto');
|
||||
const macAddress = getMac();
|
||||
return crypto.createHash('sha256').update(macAddress, 'utf8').digest('hex');
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '2.3.0';
|
||||
export const napCatVersion = '3.4.2';
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { MsfChangeReasonType, MsfStatusType } from "../entities/adapter";
|
||||
|
||||
export class NodeIDependsAdapter {
|
||||
onMSFStatusChange(arg1: number, arg2: number) {
|
||||
// console.log(arg1, arg2);
|
||||
// if (arg1 == 2 && arg2 == 2) {
|
||||
// log("NapCat丢失网络连接,请检查网络")
|
||||
// }
|
||||
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {
|
||||
|
||||
}
|
||||
|
||||
onMSFSsoError(args: unknown) {
|
||||
|
@@ -1,63 +0,0 @@
|
||||
import {
|
||||
CacheFileListItem,
|
||||
CacheFileType,
|
||||
ChatCacheListItemBasic,
|
||||
ChatType,
|
||||
InstanceContext,
|
||||
NapCatCore,
|
||||
} from '@/core';
|
||||
|
||||
export class NTQQCacheApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async setCacheSilentScan(isSilent: boolean = true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
getCacheSessionPathList() {
|
||||
return '';
|
||||
}
|
||||
|
||||
clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
|
||||
// 参数未验证
|
||||
return this.context.session.getStorageCleanService().clearCacheDataByKeys(cacheKeys);
|
||||
}
|
||||
|
||||
addCacheScannedPaths(pathMap: object = {}) {
|
||||
return this.context.session.getStorageCleanService().addCacheScanedPaths(pathMap);
|
||||
}
|
||||
|
||||
scanCache() {
|
||||
//return (await this.context.session.getStorageCleanService().scanCache()).size;
|
||||
}
|
||||
|
||||
getHotUpdateCachePath() {
|
||||
// 未实现
|
||||
return '';
|
||||
}
|
||||
|
||||
getDesktopTmpPath() {
|
||||
// 未实现
|
||||
return '';
|
||||
}
|
||||
|
||||
getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
|
||||
return this.context.session.getStorageCleanService().getChatCacheInfo(type, pageSize, 1, pageIndex);
|
||||
}
|
||||
|
||||
getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
|
||||
// const _lastRecord = lastRecord ? lastRecord : { fileType: fileType };
|
||||
// 需要五个参数
|
||||
// return napCatCore.session.getStorageCleanService().getFileCacheInfo();
|
||||
}
|
||||
|
||||
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
|
||||
return this.context.session.getStorageCleanService().clearChatCacheInfo(chats, fileKeys);
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ import {
|
||||
Peer,
|
||||
PicElement,
|
||||
PicType,
|
||||
RawMessage,
|
||||
SendFileElement,
|
||||
SendPicElement,
|
||||
SendPttElement,
|
||||
@@ -24,16 +25,18 @@ import pathLib from 'node:path';
|
||||
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { encodeSilk } from '@/common/audio';
|
||||
import { MessageContext } from '@/onebot/api';
|
||||
|
||||
export class NTQQFileApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
rkeyManager: RkeyManager;
|
||||
packetRkey: Array<{ rkey: string; time: number; type: number; ttl: bigint }> | undefined;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey', this.context.logger);
|
||||
this.rkeyManager = new RkeyManager(['https://llob.linyuchen.net/rkey', 'http://napcat-sign.wumiao.wang:2082/rkey'], this.context.logger);
|
||||
}
|
||||
|
||||
async copyFile(filePath: string, destPath: string) {
|
||||
@@ -71,7 +74,7 @@ export class NTQQFileApi {
|
||||
file_uuid: '',
|
||||
});
|
||||
|
||||
await this.copyFile(filePath, mediaPath!);
|
||||
await this.copyFile(filePath, mediaPath);
|
||||
const fileSize = await this.getFileSize(filePath);
|
||||
return {
|
||||
md5: fileMd5,
|
||||
@@ -82,7 +85,7 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendFileElement(filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
|
||||
async createValidSendFileElement(context: MessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
|
||||
const {
|
||||
fileName: _fileName,
|
||||
path,
|
||||
@@ -91,6 +94,7 @@ export class NTQQFileApi {
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.FILE,
|
||||
elementId: '',
|
||||
@@ -103,12 +107,13 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPicElement(picPath: string, summary: string = '', subType: 0 | 1 = 0,): Promise<SendPicElement> {
|
||||
async createValidSendPicElement(context: MessageContext, picPath: string, summary: string = '', subType: 0 | 1 = 0,): Promise<SendPicElement> {
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const imageSize = await this.core.apis.FileApi.getImageSize(picPath);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.PIC,
|
||||
elementId: '',
|
||||
@@ -130,26 +135,40 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
const logger = this.core.context.logger;
|
||||
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
|
||||
thumb = pathLib.dirname(thumb);
|
||||
let videoInfo = {
|
||||
width: 1920, height: 1080,
|
||||
time: 15,
|
||||
format: 'mp4',
|
||||
size: fileSize,
|
||||
size: 0,
|
||||
filePath,
|
||||
};
|
||||
try {
|
||||
videoInfo = await getVideoInfo(path, logger);
|
||||
videoInfo = await getVideoInfo(filePath, logger);
|
||||
} catch (e) {
|
||||
logger.logError('获取视频信息失败,将使用默认值', e);
|
||||
logger.logError.bind(logger)('获取视频信息失败,将使用默认值', e);
|
||||
}
|
||||
|
||||
let fileExt = 'mp4';
|
||||
try {
|
||||
const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||
if (tempExt) fileExt = tempExt;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(logger)('获取文件类型失败', e);
|
||||
}
|
||||
const newFilePath = filePath + '.' + fileExt;
|
||||
fs.copyFileSync(filePath, newFilePath);
|
||||
context.deleteAfterSentFiles.push(newFilePath);
|
||||
filePath = newFilePath;
|
||||
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
videoInfo.size = fileSize;
|
||||
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
|
||||
thumb = pathLib.dirname(thumb);
|
||||
|
||||
const thumbPath = new Map();
|
||||
const _thumbPath = await new Promise<string | undefined>((resolve, reject) => {
|
||||
const thumbFileName = `${md5}_0.png`;
|
||||
@@ -179,11 +198,13 @@ export class NTQQFileApi {
|
||||
const thumbSize = _thumbPath ? (await fsPromises.stat(_thumbPath)).size : 0;
|
||||
thumbPath.set(0, _thumbPath);
|
||||
const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : '';
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith('.' + fileExt.toLocaleLowerCase()) ? (fileName || _fileName) : (fileName || _fileName) + '.' + fileExt;
|
||||
return {
|
||||
elementType: ElementType.VIDEO,
|
||||
elementId: '',
|
||||
videoElement: {
|
||||
fileName: fileName || _fileName,
|
||||
fileName: uploadName,
|
||||
filePath: path,
|
||||
videoMd5: md5,
|
||||
thumbMd5,
|
||||
@@ -207,7 +228,9 @@ export class NTQQFileApi {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (converted) {
|
||||
fsPromises.unlink(silkPath);
|
||||
fsPromises.unlink(silkPath).then().catch(
|
||||
(e) => this.context.logger.logError.bind(this.context.logger)('删除临时文件失败', e)
|
||||
);
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
@@ -216,7 +239,7 @@ export class NTQQFileApi {
|
||||
fileName: fileName,
|
||||
filePath: path,
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize,
|
||||
fileSize: fileSize.toString(),
|
||||
duration: duration ?? 1,
|
||||
formatType: 1,
|
||||
voiceType: 1,
|
||||
@@ -245,8 +268,54 @@ export class NTQQFileApi {
|
||||
return fileTransNotifyInfo.filePath;
|
||||
}
|
||||
|
||||
async downloadRawMsgMedia(msg: RawMessage[]) {
|
||||
const res = await Promise.all(
|
||||
msg.map(m =>
|
||||
Promise.all(
|
||||
m.elements
|
||||
.filter(element =>
|
||||
element.elementType === ElementType.PIC ||
|
||||
element.elementType === ElementType.VIDEO ||
|
||||
element.elementType === ElementType.PTT ||
|
||||
element.elementType === ElementType.FILE
|
||||
)
|
||||
.map(element =>
|
||||
this.downloadMedia(m.msgId, m.chatType, m.peerUid, element.elementId, '', '', 1000 * 60 * 2, true)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
msg.forEach((m, msgIndex) => {
|
||||
const elementResults = res[msgIndex];
|
||||
let elementIndex = 0;
|
||||
m.elements.forEach(element => {
|
||||
if (
|
||||
element.elementType === ElementType.PIC ||
|
||||
element.elementType === ElementType.VIDEO ||
|
||||
element.elementType === ElementType.PTT ||
|
||||
element.elementType === ElementType.FILE
|
||||
) {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PIC:
|
||||
element.picElement!.sourcePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
element.videoElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
element.pttElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
element.fileElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
}
|
||||
elementIndex++;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
|
||||
//logDebug('receive downloadMedia task', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force);
|
||||
// 用于下载收到的消息中的图片等
|
||||
if (sourcePath && fs.existsSync(sourcePath)) {
|
||||
if (force) {
|
||||
@@ -275,7 +344,7 @@ export class NTQQFileApi {
|
||||
filePath: thumbPath,
|
||||
}],
|
||||
() => true,
|
||||
(arg) => arg.msgId === msgId,
|
||||
(arg) => arg.msgElementId === elementId && arg.msgId === msgId,
|
||||
1,
|
||||
timeout,
|
||||
);
|
||||
@@ -298,65 +367,6 @@ export class NTQQFileApi {
|
||||
});
|
||||
}
|
||||
|
||||
async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) {
|
||||
let GroupData;
|
||||
let BuddyData;
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
GroupData =
|
||||
[{
|
||||
groupCode: peer.peerUid,
|
||||
isConf: false,
|
||||
hasModifyConfGroupFace: true,
|
||||
hasModifyConfGroupName: true,
|
||||
groupName: 'NapCat.Cached',
|
||||
remark: 'NapCat.Cached',
|
||||
}];
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
BuddyData = [{
|
||||
category_name: 'NapCat.Cached',
|
||||
peerUid: peer.peerUid,
|
||||
peerUin: peer.peerUid,
|
||||
remark: 'NapCat.Cached',
|
||||
}];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.context.session.getSearchService().addSearchHistory({
|
||||
type: 4,
|
||||
contactList: [],
|
||||
id: -1,
|
||||
groupInfos: [],
|
||||
msgs: [],
|
||||
fileInfos: [
|
||||
{
|
||||
chatType: peer.chatType,
|
||||
buddyChatInfo: BuddyData || [],
|
||||
discussChatInfo: [],
|
||||
groupChatInfo: GroupData || [],
|
||||
dataLineChatInfo: [],
|
||||
tmpChatInfo: [],
|
||||
msgId: msgId,
|
||||
msgSeq: msgSeq,
|
||||
msgTime: Math.floor(Date.now() / 1000).toString(),
|
||||
senderUid: senderUid,
|
||||
senderNick: 'NapCat.Cached',
|
||||
senderRemark: 'NapCat.Cached',
|
||||
senderCard: 'NapCat.Cached',
|
||||
elemId: elemId,
|
||||
elemType: elemType,
|
||||
fileSize: fileSize,
|
||||
filePath: '',
|
||||
fileName: fileName,
|
||||
hits: [{
|
||||
start: 12,
|
||||
end: 14,
|
||||
}],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
|
||||
const randomResultId = 100000 + Math.floor(Math.random() * 10000);
|
||||
let searchId = 0;
|
||||
@@ -398,30 +408,65 @@ export class NTQQFileApi {
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
const url: string = element.originImageUrl!; // 没有域名
|
||||
const url: string = element.originImageUrl ?? '';
|
||||
const md5HexStr = element.md5HexStr;
|
||||
const fileMd5 = element.md5HexStr;
|
||||
|
||||
if (url) {
|
||||
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);//临时解析拼接
|
||||
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
|
||||
const urlRkey = parsedUrl.searchParams.get('rkey');
|
||||
const imageAppid = parsedUrl.searchParams.get('appid');
|
||||
const isNTFlavoredPic = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||
if (isNTFlavoredPic) {
|
||||
let rkey = parsedUrl.searchParams.get('rkey');
|
||||
if (rkey) {
|
||||
return IMAGE_HTTP_HOST_NT + url;
|
||||
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||
const imageFileId = parsedUrl.searchParams.get('fileid');
|
||||
|
||||
const rkeyData = {
|
||||
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
||||
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
||||
online_rkey: false
|
||||
};
|
||||
try {
|
||||
if (this.core.apis.PacketApi.available) {
|
||||
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
if (rkey_expired_private || rkey_expired_group) {
|
||||
this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket();
|
||||
}
|
||||
if (this.packetRkey && this.packetRkey.length > 0) {
|
||||
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
|
||||
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
|
||||
rkeyData.online_rkey = true;
|
||||
}
|
||||
}
|
||||
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;
|
||||
} catch (error: any) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
|
||||
}
|
||||
} else if (fileMd5 || md5HexStr) {
|
||||
// 没有url,需要自己拼接
|
||||
|
||||
if (!rkeyData.online_rkey) {
|
||||
try {
|
||||
const tempRkeyData = await this.rkeyManager.getRkey();
|
||||
rkeyData.group_rkey = tempRkeyData.group_rkey;
|
||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
||||
}
|
||||
}
|
||||
if (isNTV2 && urlRkey) {
|
||||
return IMAGE_HTTP_HOST_NT + urlRkey;
|
||||
} else if (isNTV2 && rkeyData.online_rkey) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST_NT + url + `&rkey=${rkey}`;
|
||||
} else if (isNTV2 && imageFileId) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
}
|
||||
|
||||
}
|
||||
//到这里说明可能是旧客户端
|
||||
if (fileMd5 || md5HexStr) {
|
||||
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
|
||||
}
|
||||
|
||||
this.context.logger.logDebug('图片url获取失败', element);
|
||||
return '';
|
||||
}
|
||||
|
@@ -6,16 +6,13 @@ export class NTQQFriendApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
// friends: Map<string, Friend> = new Map<string, FriendV2>();
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
// if (!this.context.basicInfoWrapper.requireMinNTQQBuild('26702')) {
|
||||
// this.getFriends(true);
|
||||
// }
|
||||
}
|
||||
|
||||
async setBuddyRemark(uid: string, remark: string) {
|
||||
return this.context.session.getBuddyService().setBuddyRemark({ uid, remark });
|
||||
}
|
||||
async getBuddyV2SimpleInfoMap(refresh = false) {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
||||
@@ -37,7 +34,13 @@ export class NTQQFriendApi {
|
||||
data.forEach((value) => retMap.set(value.uin!, value.uid!));
|
||||
return retMap;
|
||||
}
|
||||
|
||||
async delBuudy(uid: string, tempBlock = false, tempBothDel = false) {
|
||||
return this.context.session.getBuddyService().delBuddy({
|
||||
friendUid: uid,
|
||||
tempBlock: tempBlock,
|
||||
tempBothDel: tempBothDel
|
||||
});
|
||||
}
|
||||
async getBuddyV2ExWithCate(refresh = false) {
|
||||
const categoryMap: Map<string, any> = new Map();
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
|
@@ -9,8 +9,9 @@ import {
|
||||
MemberExtSourceType,
|
||||
NapCatCore,
|
||||
} from '@/core';
|
||||
import { isNumeric } from '@/common/helper';
|
||||
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
||||
import { LimitedHashTable } from '@/common/message-unique';
|
||||
import { NTEventWrapper } from '@/common/event';
|
||||
|
||||
export class NTQQGroupApi {
|
||||
context: InstanceContext;
|
||||
@@ -19,11 +20,12 @@ export class NTQQGroupApi {
|
||||
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
|
||||
groups: Group[] = [];
|
||||
essenceLRU = new LimitedHashTable<number, string>(1000);
|
||||
session: any;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.initCache().then().catch(context.logger.logError);
|
||||
this.initCache().then().catch(context.logger.logError.bind(context.logger));
|
||||
}
|
||||
|
||||
async initCache() {
|
||||
@@ -32,6 +34,15 @@ export class NTQQGroupApi {
|
||||
this.groupCache.set(group.groupCode, group);
|
||||
}
|
||||
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
|
||||
// process.pid 调试点
|
||||
}
|
||||
|
||||
async getCoreAndBaseInfo(uids: string[]) {
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
uids,
|
||||
);
|
||||
}
|
||||
|
||||
async fetchGroupEssenceList(groupCode: string) {
|
||||
@@ -42,9 +53,13 @@ export class NTQQGroupApi {
|
||||
pageLimit: 300,
|
||||
}, pskey);
|
||||
}
|
||||
|
||||
async clearGroupNotifiesUnreadCount(unk: boolean) {
|
||||
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(unk);
|
||||
async getGroupShutUpMemberList(groupCode: string) {
|
||||
let data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', 1, 1000, (group_id) => group_id === groupCode);
|
||||
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||
return (await data)[1];
|
||||
}
|
||||
async clearGroupNotifiesUnreadCount(uk: boolean) {
|
||||
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
|
||||
}
|
||||
|
||||
async setGroupAvatar(gc: string, filePath: string) {
|
||||
@@ -60,9 +75,9 @@ export class NTQQGroupApi {
|
||||
return groupList;
|
||||
}
|
||||
|
||||
async getGroupExtFE0Info(GroupCode: string[], forced = true) {
|
||||
async getGroupExtFE0Info(groupCode: string[], forced = true) {
|
||||
return this.context.session.getGroupService().getGroupExt0xEF0Info(
|
||||
GroupCode,
|
||||
groupCode,
|
||||
[],
|
||||
{
|
||||
bindGuildId: 1,
|
||||
@@ -120,8 +135,8 @@ export class NTQQGroupApi {
|
||||
return group;
|
||||
}
|
||||
|
||||
async getGroupMemberAll(GroupCode: string, forced = false) {
|
||||
return this.context.session.getGroupService().getAllMemberList(GroupCode, forced);
|
||||
async getGroupMemberAll(groupCode: string, forced = false) {
|
||||
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
|
||||
}
|
||||
|
||||
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
||||
@@ -130,7 +145,7 @@ export class NTQQGroupApi {
|
||||
let members = this.groupMemberCache.get(groupCodeStr);
|
||||
if (!members) {
|
||||
try {
|
||||
members = await this.getGroupMembersV2(groupCodeStr);
|
||||
members = await this.getGroupMembers(groupCodeStr);
|
||||
// 更新群成员列表
|
||||
this.groupMemberCache.set(groupCodeStr, members);
|
||||
} catch (e) {
|
||||
@@ -151,13 +166,14 @@ export class NTQQGroupApi {
|
||||
|
||||
let member = getMember();
|
||||
if (!member) {
|
||||
members = await this.getGroupMembersV2(groupCodeStr);
|
||||
members = await this.getGroupMembers(groupCodeStr);
|
||||
member = getMember();
|
||||
}
|
||||
return member;
|
||||
}
|
||||
async getGroupRecommendContactArkJson(GroupCode: string) {
|
||||
return this.context.session.getGroupService().getGroupRecommendContactArkJson(GroupCode);
|
||||
|
||||
async getGroupRecommendContactArkJson(groupCode: string) {
|
||||
return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode);
|
||||
}
|
||||
|
||||
async CreatGroupFileFolder(groupCode: string, folderName: string) {
|
||||
@@ -173,7 +189,6 @@ export class NTQQGroupApi {
|
||||
}
|
||||
|
||||
async addGroupEssence(GroupCode: string, msgId: string) {
|
||||
// 需要 ob11msgId -> msgId + (peer) -> msgSeq + msgRandom
|
||||
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||
chatType: 2,
|
||||
guildId: '',
|
||||
@@ -184,7 +199,6 @@ export class NTQQGroupApi {
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
||||
};
|
||||
// GetMsgByShortID(shortID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
|
||||
return this.context.session.getGroupService().addGroupEssence(param);
|
||||
}
|
||||
|
||||
@@ -202,7 +216,6 @@ export class NTQQGroupApi {
|
||||
groupCode: GroupCode,
|
||||
needDeleteLocalMsg: needDeleteLocalMsg,
|
||||
};
|
||||
//应该是直接返回不需要Listener的 未经测试 需测试再发布
|
||||
return this.context.session.getGroupService().quitGroupV2(param);
|
||||
}
|
||||
|
||||
@@ -265,36 +278,116 @@ export class NTQQGroupApi {
|
||||
return member;
|
||||
}
|
||||
|
||||
async getGroupMembersV2(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
||||
const groupService = this.context.session.getGroupService();
|
||||
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
|
||||
const listener = this.core.eventWrapper.registerListen(
|
||||
'NodeIKernelGroupListener/onMemberListChange',
|
||||
async searchGroup(groupCode: string) {
|
||||
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelSearchService/searchGroup',
|
||||
'NodeIKernelSearchListener/onSearchGroupResult',
|
||||
[{
|
||||
keyWords: groupCode,
|
||||
groupNum: 25,
|
||||
exactSearch: false,
|
||||
penetrate: ''
|
||||
}],
|
||||
(ret) => ret.result === 0,
|
||||
(params) => !!params.groupInfos.find(g => g.groupCode === groupCode),
|
||||
1,
|
||||
500,
|
||||
(params) => params.sceneId === sceneId,
|
||||
5000
|
||||
);
|
||||
try {
|
||||
const [membersFromFunc, membersFromListener] = await Promise.allSettled([
|
||||
groupService.getNextMemberList(sceneId, undefined, num),
|
||||
listener,
|
||||
]);
|
||||
if (membersFromFunc.status === 'fulfilled' && membersFromListener.status === 'fulfilled') {
|
||||
return new Map([
|
||||
...membersFromFunc.value.result.infos,
|
||||
...membersFromListener.value[0].infos,
|
||||
]);
|
||||
}
|
||||
if (membersFromFunc.status === 'fulfilled') {
|
||||
return membersFromFunc.value.result.infos;
|
||||
}
|
||||
if (membersFromListener.status === 'fulfilled') {
|
||||
return membersFromListener.value[0].infos;
|
||||
}
|
||||
throw new Error('获取群成员列表失败');
|
||||
} finally {
|
||||
groupService.destroyMemberListScene(sceneId);
|
||||
return ret.groupInfos.find(g => g.groupCode === groupCode);
|
||||
}
|
||||
|
||||
async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) {
|
||||
const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => {
|
||||
return eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getMemberInfo',
|
||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||
[GroupCode, [uid], forced],
|
||||
(ret) => ret.result === 0,
|
||||
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
|
||||
1,
|
||||
forced ? 2500 : 250
|
||||
);
|
||||
}, this.core.eventWrapper, GroupCode, uid, forced);
|
||||
if (data && data[3] instanceof Map && data[3].has(uid)) {
|
||||
return data[3].get(uid);
|
||||
}
|
||||
if (retry > 0) {
|
||||
const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined;
|
||||
if (trydata) return trydata;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async tryGetGroupMembersV2(modeListener = false, groupQQ: string, num = 30, timeout = 100): Promise<{
|
||||
infos: Map<string, GroupMember>;
|
||||
finish: boolean;
|
||||
hasNext: boolean | undefined;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 0, timeout, (params) => params.sceneId === sceneId)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
let resMode2;
|
||||
if (modeListener) {
|
||||
const ret = (await once)?.[0];
|
||||
if (ret) {
|
||||
resMode2 = ret;
|
||||
}
|
||||
}
|
||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||
return {
|
||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||
finish: result.result.finish,
|
||||
hasNext: resMode2?.hasNext,
|
||||
};
|
||||
}
|
||||
|
||||
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
|
||||
infos: Map<string, GroupMember>;
|
||||
finish: boolean;
|
||||
hasNext: boolean | undefined;
|
||||
listenerMode: boolean;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 0, timeout, (params) => params.sceneId === sceneId)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
let resMode2;
|
||||
if (result.result.finish && result.result.infos.size === 0) {
|
||||
const ret = (await once)?.[0];
|
||||
if (ret) {
|
||||
resMode2 = ret;
|
||||
}
|
||||
}
|
||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||
//console.log('GetGroupMembersV3 len :', result.result.infos.size, resMode2?.infos.size, groupQQ);
|
||||
return {
|
||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||
finish: result.result.finish,
|
||||
hasNext: resMode2?.hasNext,
|
||||
listenerMode: resMode2?.hasNext !== undefined ? true : false
|
||||
};
|
||||
}
|
||||
|
||||
async getGroupMembersV2(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
||||
//console.log('getGroupMembers -->', groupQQ);
|
||||
let res = await this.GetGroupMembersV3(groupQQ, num);
|
||||
let ret = res.infos;
|
||||
if (res.infos.size === 0 && !res.listenerMode) {
|
||||
res = await this.GetGroupMembersV3(groupQQ, num);
|
||||
ret = res.infos;
|
||||
}
|
||||
if (res.infos.size === 0) {
|
||||
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
|
||||
}
|
||||
//console.log("<---------------")
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
||||
@@ -304,8 +397,7 @@ export class NTQQGroupApi {
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
|
||||
this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`); //, Array.from(result.result.infos.values()));
|
||||
this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`);
|
||||
return result.result.infos;
|
||||
}
|
||||
|
||||
@@ -336,12 +428,12 @@ export class NTQQGroupApi {
|
||||
return this.context.session.getGroupService().operateSysNotify(
|
||||
false,
|
||||
{
|
||||
'operateType': operateType, // 2 拒绝
|
||||
'targetMsg': {
|
||||
'seq': seq, // 通知序列号
|
||||
'type': type,
|
||||
'groupCode': groupCode,
|
||||
'postscript': reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
|
||||
operateType: operateType, // 2 拒绝
|
||||
targetMsg: {
|
||||
seq: seq, // 通知序列号
|
||||
type: type,
|
||||
groupCode: groupCode,
|
||||
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -393,11 +485,10 @@ export class NTQQGroupApi {
|
||||
}
|
||||
|
||||
async getGroupRemainAtTimes(GroupCode: string) {
|
||||
this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
|
||||
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
|
||||
}
|
||||
|
||||
async getMemberExtInfo(groupCode: string, uin: string) {
|
||||
// 仅NTQQ 9.9.11 24568测试 容易炸开谨慎使用
|
||||
return this.context.session.getGroupService().getMemberExtInfo(
|
||||
{
|
||||
groupCode: groupCode,
|
||||
|
@@ -5,5 +5,4 @@ export * from './msg';
|
||||
export * from './user';
|
||||
export * from './webapi';
|
||||
export * from './sign';
|
||||
export * from './system';
|
||||
export * from './cache';
|
||||
export * from './system';
|
@@ -3,6 +3,9 @@ import { InstanceContext, NapCatCore } from '@/core';
|
||||
import { GeneralCallResult } from '@/core/services/common';
|
||||
|
||||
export class NTQQMsgApi {
|
||||
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
|
||||
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
|
||||
}
|
||||
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
|
||||
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
|
||||
// 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
|
||||
@@ -19,27 +22,18 @@ export class NTQQMsgApi {
|
||||
return this.context.session.getMsgService().getAioFirstViewLatestMsgs(peer, MsgCount);
|
||||
}
|
||||
|
||||
async getLatestDbMsgs(peer: Peer, MsgCount: number) {
|
||||
return this.context.session.getMsgService().getLatestDbMsgs(peer, MsgCount);
|
||||
}
|
||||
|
||||
async FetchLongMsg(peer: Peer, msgId: string) {
|
||||
return this.context.session.getMsgService().fetchLongMsg(peer, msgId);
|
||||
}
|
||||
|
||||
async sendShowInputStatusReq(peer: Peer, eventType: number) {
|
||||
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
|
||||
}
|
||||
|
||||
async getSourceOfReplyMsgV2(peer: Peer, clientSeq: string, time: string) {
|
||||
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
|
||||
}
|
||||
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
|
||||
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged M likiowa
|
||||
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
||||
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
|
||||
}
|
||||
|
||||
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
|
||||
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
|
||||
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
|
||||
// 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
|
||||
emojiId = emojiId.toString();
|
||||
return this.context.session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set);
|
||||
}
|
||||
@@ -93,6 +87,18 @@ export class NTQQMsgApi {
|
||||
pageLimit: 1,
|
||||
});
|
||||
}
|
||||
async queryMsgsWithFilterExWithSeqV3(peer: Peer, msgSeq: string, SendersUid: string[]) {
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||
chatInfo: peer,
|
||||
filterMsgType: [],
|
||||
filterSendersUid: SendersUid,
|
||||
filterMsgToTime: '0',
|
||||
filterMsgFromTime: '0',
|
||||
isReverseOrder: false,
|
||||
isIncludeCurrent: true,
|
||||
pageLimit: 1,
|
||||
});
|
||||
}
|
||||
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||
chatInfo: peer,
|
||||
@@ -105,16 +111,9 @@ export class NTQQMsgApi {
|
||||
pageLimit: 1,
|
||||
});
|
||||
}
|
||||
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
|
||||
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
|
||||
}
|
||||
async getMsgBySeqList(peer: Peer, msgSeqList: string[]) {
|
||||
//坏的
|
||||
return await this.context.session.getMsgService().getMsgsBySeqList(peer, msgSeqList);
|
||||
}
|
||||
async getMsgBySeqExFirstMsg(peer: Peer, rootMsgId: string, replyMsgId: string) {
|
||||
let reply = await this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, rootMsgId, replyMsgId);
|
||||
console.log(reply);
|
||||
// 客户端还在用别慌
|
||||
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
|
||||
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
|
||||
}
|
||||
async getMsgExBySeq(peer: Peer, msgSeq: string) {
|
||||
const DateNow = Math.floor(Date.now() / 1000);
|
||||
@@ -157,11 +156,16 @@ export class NTQQMsgApi {
|
||||
return this.context.session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder);
|
||||
}
|
||||
|
||||
async recallMsg(peer: Peer, msgIds: string[]) {
|
||||
await this.context.session.getMsgService().recallMsg({
|
||||
chatType: peer.chatType,
|
||||
peerUid: peer.peerUid,
|
||||
}, msgIds);
|
||||
async recallMsg(peer: Peer, msgId: string) {
|
||||
await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelMsgService/recallMsg',
|
||||
'NodeIKernelMsgListener/onMsgInfoListUpdate',
|
||||
[peer, [msgId]],
|
||||
() => true,
|
||||
(updatedList) => updatedList.find(m => m.msgId === msgId && m.recallTime !== '0') !== undefined,
|
||||
1,
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
async PrepareTempChat(toUserUid: string, GroupCode: string, nickname: string) {
|
||||
@@ -195,7 +199,7 @@ export class NTQQMsgApi {
|
||||
await this.PrepareTempChat(peer.peerUid, peer.guildId, member.nick);
|
||||
}
|
||||
}
|
||||
const msgId = await this.generateMsgUniqueId(peer.chatType, await this.getServerTime());
|
||||
const msgId = await this.generateMsgUniqueId(peer.chatType);
|
||||
peer.guildId = msgId;
|
||||
const [, msgList] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelMsgService/sendMsg',
|
||||
@@ -221,12 +225,8 @@ export class NTQQMsgApi {
|
||||
return msgList.find(msgRecord => msgRecord.guildId === msgId);
|
||||
}
|
||||
|
||||
async generateMsgUniqueId(chatType: number, time: string) {
|
||||
return this.context.session.getMsgService().generateMsgUniqueId(chatType, time);
|
||||
}
|
||||
|
||||
async getServerTime() {
|
||||
return this.context.session.getMSFService().getServerTime();
|
||||
async generateMsgUniqueId(chatType: number) {
|
||||
return this.context.session.getMsgService().generateMsgUniqueId(chatType, this.context.session.getMSFService().getServerTime());
|
||||
}
|
||||
|
||||
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
|
||||
|
197
src/core/apis/packet.ts
Normal file
197
src/core/apis/packet.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import * as os from 'os';
|
||||
import { ChatType, InstanceContext, NapCatCore } from '..';
|
||||
import offset from '@/core/external/offset.json';
|
||||
import { PacketClient, RecvPacketData } from '@/core/packet/client';
|
||||
import { PacketSession } from "@/core/packet/session";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||
import { NapProtoMsg } from '@/core/packet/proto/NapProto';
|
||||
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
|
||||
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
|
||||
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { SendLongMsgResp } from "@/core/packet/proto/message/action";
|
||||
import { PacketMsg } from "@/core/packet/message/message";
|
||||
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/message/element";
|
||||
import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp";
|
||||
import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
||||
|
||||
|
||||
interface OffsetType {
|
||||
[key: string]: {
|
||||
recv: string;
|
||||
send: string;
|
||||
};
|
||||
}
|
||||
|
||||
const typedOffset: OffsetType = offset;
|
||||
|
||||
export class NTQQPacketApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
logger: LogWrapper;
|
||||
serverUrl: string | undefined;
|
||||
qqVersion: string | undefined;
|
||||
packetSession: PacketSession | undefined;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.logger = core.context.logger;
|
||||
this.packetSession = undefined;
|
||||
const config = this.core.configLoader.configData;
|
||||
if (config && config.packetServer && config.packetServer.length > 0) {
|
||||
const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086';
|
||||
this.InitSendPacket(serverUrl, this.context.basicInfoWrapper.getFullQQVesion())
|
||||
.then()
|
||||
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
|
||||
} else {
|
||||
this.core.context.logger.logWarn('PacketServer未配置,NapCat.Packet将不会加载!');
|
||||
}
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
return this.packetSession?.client.available ?? false;
|
||||
}
|
||||
|
||||
async InitSendPacket(serverUrl: string, qqversion: string) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.qqVersion = qqversion;
|
||||
const offsetTable: OffsetType = offset;
|
||||
const table = offsetTable[qqversion + '-' + os.arch()];
|
||||
if (!table) {
|
||||
this.logger.logError('PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch());
|
||||
return false;
|
||||
}
|
||||
const url = 'ws://' + this.serverUrl + '/ws';
|
||||
this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core));
|
||||
const cb = () => {
|
||||
if (this.packetSession && this.packetSession.client) {
|
||||
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
|
||||
}
|
||||
};
|
||||
await this.packetSession.client.connect(cb);
|
||||
return true;
|
||||
}
|
||||
|
||||
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
|
||||
return this.packetSession!.client.sendPacket(cmd, data, rsp);
|
||||
}
|
||||
|
||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
||||
}
|
||||
|
||||
async sendPokePacket(peer: number, group?: number) {
|
||||
const data = this.packetSession?.packer.packPokePacket(peer, group);
|
||||
await this.sendOidbPacket(data!, false);
|
||||
}
|
||||
|
||||
async sendRkeyPacket() {
|
||||
const packet = this.packetSession?.packer.packRkeyPacket();
|
||||
const ret = await this.sendOidbPacket(packet!, true);
|
||||
if (!ret?.hex_data) return [];
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
|
||||
return retData.data.rkeyList;
|
||||
}
|
||||
async sendGroupSignPacket(groupCode: string) {
|
||||
const packet = this.packetSession?.packer.packGroupSignReq(this.core.selfInfo.uin, groupCode);
|
||||
await this.sendOidbPacket(packet!, true);
|
||||
}
|
||||
async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
|
||||
let status = 0;
|
||||
try {
|
||||
const packet = this.packetSession?.packer.packStatusPacket(uin);
|
||||
const ret = await this.sendOidbPacket(packet!, true);
|
||||
const data = Buffer.from(ret.hex_data, 'hex');
|
||||
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
|
||||
// ext & 0xff00 + ext >> 16 & 0xff
|
||||
const extBigInt = BigInt(ext); // 转换为 BigInt
|
||||
if (extBigInt <= 10n) {
|
||||
return { status: Number(extBigInt) * 10, ext_status: 0 };
|
||||
}
|
||||
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); // 使用 BigInt 操作符
|
||||
return { status: 10, ext_status: status };
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
|
||||
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
|
||||
await this.sendOidbPacket(data!, true);
|
||||
}
|
||||
|
||||
// TODO: can simplify this
|
||||
async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
|
||||
const reqList = [];
|
||||
for (const m of msg) {
|
||||
for (const e of m.msg) {
|
||||
if (e instanceof PacketMsgPicElement) {
|
||||
reqList.push(this.packetSession?.highwaySession.uploadImage({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
if (e instanceof PacketMsgVideoElement) {
|
||||
reqList.push(this.packetSession?.highwaySession.uploadVideo({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
if (e instanceof PacketMsgPttElement) {
|
||||
reqList.push(this.packetSession?.highwaySession.uploadPtt({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
if (e instanceof PacketMsgFileElement) {
|
||||
reqList.push(this.packetSession?.highwaySession.uploadFile({
|
||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
||||
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
|
||||
}, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
const res = await Promise.allSettled(reqList);
|
||||
this.logger.log(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}个`);
|
||||
res.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
this.logger.logError(`上传第${index + 1}个资源失败:${result.reason}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
|
||||
await this.uploadResources(msg, groupUin);
|
||||
const data = await this.packetSession?.packer.packUploadForwardMsg(this.core.selfInfo.uid, msg, groupUin);
|
||||
const ret = await this.sendPacket('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', data!, true);
|
||||
this.logger.logDebug('sendUploadForwardMsg', ret);
|
||||
const resp = new NapProtoMsg(SendLongMsgResp).decode(Buffer.from(ret.hex_data, 'hex'));
|
||||
return resp.result.resId;
|
||||
}
|
||||
|
||||
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
|
||||
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
|
||||
const ret = await this.sendOidbPacket(data!, true);
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
||||
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
|
||||
if (resp.download.retCode !== 0) {
|
||||
throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`);
|
||||
}
|
||||
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
|
||||
}
|
||||
|
||||
async sendMiniAppShareInfoReq(param: MiniAppReqParams) {
|
||||
const data = this.packetSession?.packer.packMiniAppAdaptShareInfo(param);
|
||||
const ret = await this.sendPacket("LightAppSvc.mini_app_share.AdaptShareInfo", data!, true);
|
||||
const body = new NapProtoMsg(MiniAppAdaptShareInfoResp).decode(Buffer.from(ret.hex_data, 'hex'));
|
||||
return JSON.parse(body.content.jsonContent) as MiniAppRawData;
|
||||
}
|
||||
}
|
@@ -214,11 +214,6 @@ export class NTQQMusicSignApi {
|
||||
//console.log(MusicReal);
|
||||
return { ...MusicReal.data, mid: signedMid };
|
||||
}
|
||||
|
||||
async CreateMusicThirdWay1(id: string = '', mid: string = '') {
|
||||
|
||||
}
|
||||
|
||||
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
|
||||
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
|
||||
|
||||
|
@@ -22,7 +22,7 @@ export class NTQQSystemApi {
|
||||
}
|
||||
|
||||
async getOnlineDev() {
|
||||
return this.context.session.getMsgService().getOnLineDev();
|
||||
this.context.session.getMsgService().getOnLineDev();
|
||||
}
|
||||
|
||||
async getArkJsonCollection(cid: string) {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import type { ModifyProfileParams, User } from '@/core/entities';
|
||||
import { ModifyProfileParams, User, UserDetailSource } from '@/core/entities';
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import { ProfileBizType, UserDetailSource } from '@/core/services';
|
||||
import { InstanceContext, NapCatCore } from '..';
|
||||
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
|
||||
import { solveAsyncProblem } from '@/common/helper';
|
||||
|
||||
export class NTQQUserApi {
|
||||
@@ -12,8 +11,14 @@ export class NTQQUserApi {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async getProfileLike(uid: string) {
|
||||
//self_tind格式
|
||||
async createUidFromTinyId(tinyId: string) {
|
||||
return this.context.session.getMsgService().createUidFromTinyId(this.core.selfInfo.uin, tinyId);
|
||||
}
|
||||
async getStatusByUid(uid: string) {
|
||||
return this.context.session.getProfileService().getStatus(uid);
|
||||
}
|
||||
async getProfileLike(uid: string, start: number, count: number) {
|
||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||
friendUids: [uid],
|
||||
basic: 1,
|
||||
@@ -21,11 +26,22 @@ export class NTQQUserApi {
|
||||
favorite: 0,
|
||||
userProfile: 1,
|
||||
type: 2,
|
||||
start: start,
|
||||
limit: count,
|
||||
});
|
||||
}
|
||||
async fetchOtherProfileLike(uid: string) {
|
||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||
friendUids: [uid],
|
||||
basic: 1,
|
||||
vote: 1,
|
||||
favorite: 0,
|
||||
userProfile: 0,
|
||||
type: 1,
|
||||
start: 0,
|
||||
limit: 20,
|
||||
});
|
||||
}
|
||||
|
||||
async setLongNick(longNick: string) {
|
||||
return this.context.session.getProfileService().setLongNick(longNick);
|
||||
}
|
||||
@@ -52,8 +68,7 @@ export class NTQQUserApi {
|
||||
}
|
||||
|
||||
async setQQAvatar(filePath: string) {
|
||||
type setQQAvatarRet = { result: number, errMsg: string };
|
||||
const ret = await this.context.session.getProfileService().setHeader(filePath) as setQQAvatarRet;
|
||||
const ret = await this.context.session.getProfileService().setHeader(filePath);
|
||||
return { result: ret?.result, errMsg: ret?.errMsg };
|
||||
}
|
||||
|
||||
@@ -104,12 +119,20 @@ export class NTQQUserApi {
|
||||
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
|
||||
}
|
||||
|
||||
//需要异常处理
|
||||
async getCookies(domain: string) {
|
||||
const ClientKeyData = await this.forceFetchClientKey();
|
||||
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + this.core.selfInfo.uin +
|
||||
'&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + this.core.selfInfo.uin + '%2Finfocenter&keyindex=19%27';
|
||||
return await RequestUtil.HttpsGetCookies(requestUrl);
|
||||
const data = await RequestUtil.HttpsGetCookies(requestUrl);
|
||||
if (!data.p_skey || data.p_skey.length == 0) {
|
||||
try {
|
||||
const pskey = (await this.getPSkey([domain])).domainPskeyMap.get(domain);
|
||||
if (pskey) data.p_skey = pskey;
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async getPSkey(domainList: string[]) {
|
||||
@@ -160,7 +183,7 @@ export class NTQQUserApi {
|
||||
if (uid) return uid;
|
||||
uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
|
||||
if (uid) return uid;
|
||||
const unverifiedUid = (await this.getUserDetailInfoByUinV2(Uin)).detail.uid;//从QQ Native 特殊转换
|
||||
const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换
|
||||
if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid;
|
||||
//if (uid) return uid;
|
||||
return uid;
|
||||
@@ -196,7 +219,7 @@ export class NTQQUserApi {
|
||||
return await this.context.session.getRecentContactService().getRecentContactList();
|
||||
}
|
||||
|
||||
async getUserDetailInfoByUinV2(Uin: string) {
|
||||
async getUserDetailInfoByUin(Uin: string) {
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getUserDetailInfoByUin',
|
||||
Uin
|
||||
|
@@ -157,7 +157,7 @@ export class NTQQWebApi {
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
|
||||
try {
|
||||
let settings = JSON.stringify({
|
||||
const settings = JSON.stringify({
|
||||
is_show_edit_card: is_show_edit_card,
|
||||
tip_window_type: tip_window_type,
|
||||
confirm_required: confirm_required
|
||||
@@ -167,7 +167,7 @@ export class NTQQWebApi {
|
||||
imgWidth: imgWidth.toString(),
|
||||
imgHeight: imgHeight.toString(),
|
||||
};
|
||||
let ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
|
||||
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
|
||||
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
|
||||
bkn: this.getBknFromCookie(cookieObject),
|
||||
qid: GroupCode,
|
||||
@@ -264,7 +264,7 @@ export class NTQQWebApi {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError('获取龙王信息失败');
|
||||
this.context.logger.logError.bind(this.context.logger)('获取龙王信息失败');
|
||||
}
|
||||
}
|
||||
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
||||
@@ -280,7 +280,7 @@ export class NTQQWebApi {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError('获取群聊之火失败');
|
||||
this.context.logger.logError.bind(this.context.logger)('获取群聊之火失败');
|
||||
}
|
||||
}
|
||||
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
||||
@@ -296,7 +296,7 @@ export class NTQQWebApi {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError('获取群聊炽焰失败');
|
||||
this.context.logger.logError.bind(this.context.logger)('获取群聊炽焰失败');
|
||||
}
|
||||
}
|
||||
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
||||
@@ -312,7 +312,7 @@ export class NTQQWebApi {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError('获取快乐源泉失败');
|
||||
this.context.logger.logError.bind(this.context.logger)('获取快乐源泉失败');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,4 +338,12 @@ export class NTQQWebApi {
|
||||
}
|
||||
return (hash & 0x7FFFFFFF).toString();
|
||||
}
|
||||
public getBknFromSKey(sKey: string) {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < sKey.length; i++) {
|
||||
const code = sKey.charCodeAt(i);
|
||||
hash = hash + (hash << 5) + code;
|
||||
}
|
||||
return (hash & 0x7FFFFFFF).toString();
|
||||
}
|
||||
}
|
||||
|
11
src/core/entities/adapter.ts
Normal file
11
src/core/entities/adapter.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum MsfStatusType {
|
||||
KUNKNOWN,
|
||||
KDISCONNECTED,
|
||||
KCONNECTED
|
||||
}
|
||||
export enum MsfChangeReasonType {
|
||||
KUNKNOWN,
|
||||
KUSERLOGININ,
|
||||
KUSERLOGINOUT,
|
||||
KAUTO
|
||||
}
|
12
src/core/entities/contact.ts
Normal file
12
src/core/entities/contact.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export interface FSABRecentContactParams {
|
||||
anchorPointContact: {
|
||||
contactId: string;
|
||||
sortField: string;
|
||||
pos: number;
|
||||
};
|
||||
relativeMoveCount: number;
|
||||
listType: number;
|
||||
count: number;
|
||||
fetchOld: boolean;
|
||||
}
|
@@ -117,6 +117,7 @@ export enum GroupMemberRole {
|
||||
}
|
||||
|
||||
export interface GroupMember {
|
||||
memberRealLevel: string | undefined;
|
||||
memberSpecialTitle?: string;
|
||||
avatarPath: string;
|
||||
cardName: string;
|
||||
|
@@ -27,94 +27,70 @@ export interface GetFileListParam {
|
||||
|
||||
export enum ElementType {
|
||||
UNKNOWN = 0,
|
||||
|
||||
TEXT = 1,
|
||||
|
||||
PIC = 2,
|
||||
|
||||
FILE = 3,
|
||||
|
||||
PTT = 4,
|
||||
|
||||
VIDEO = 5,
|
||||
|
||||
FACE = 6,
|
||||
|
||||
REPLY = 7,
|
||||
|
||||
GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等
|
||||
WALLET = 9,
|
||||
|
||||
/**
|
||||
* “小灰条”,包括拍一拍 (Poke)、撤回提示等
|
||||
*/
|
||||
GreyTip = 8,
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
|
||||
|
||||
type ElementBase<
|
||||
K extends keyof ElementFullBase,
|
||||
S extends Partial<{ [P in K]: keyof NonNullable<ElementFullBase[P]> | Array<keyof NonNullable<ElementFullBase[P]>> }> = object
|
||||
> = {
|
||||
[P in K]:
|
||||
S[P] extends Array<infer U>
|
||||
? Pick<NonNullable<ElementFullBase[P]>, U & keyof NonNullable<ElementFullBase[P]>>
|
||||
: S[P] extends keyof NonNullable<ElementFullBase[P]>
|
||||
? Pick<NonNullable<ElementFullBase[P]>, S[P]>
|
||||
: NonNullable<ElementFullBase[P]>;
|
||||
};
|
||||
|
||||
export interface SendElementBase<ET extends ElementType> {
|
||||
elementType: ET;
|
||||
elementId: string;
|
||||
extBufForUI?: string;
|
||||
}
|
||||
|
||||
export interface ActionBarElement {
|
||||
rows: InlineKeyboardRow[];
|
||||
botAppid: string;
|
||||
}
|
||||
|
||||
export interface SendActionBarElement {
|
||||
elementType: ElementType.ACTIONBAR;
|
||||
elementId: string;
|
||||
actionBarElement: ActionBarElement;
|
||||
}
|
||||
|
||||
export interface RecommendedMsgElement {
|
||||
rows: InlineKeyboardRow[];
|
||||
botAppid: string;
|
||||
}
|
||||
|
||||
export interface SendRecommendedMsgElement {
|
||||
elementType: ElementType.RECOMMENDEDMSG;
|
||||
elementId: string;
|
||||
recommendedMsgElement: RecommendedMsgElement;
|
||||
}
|
||||
export type SendRecommendedMsgElement = SendElementBase<ElementType.RECOMMENDEDMSG> & ElementBase<'recommendedMsgElement'>;
|
||||
|
||||
export interface InlineKeyboardButton {
|
||||
id: string;
|
||||
@@ -171,11 +147,7 @@ export enum NTMsgType {
|
||||
KMSGTYPEWALLET = 10
|
||||
}
|
||||
|
||||
export interface SendTaskTopMsgElement {
|
||||
elementType: ElementType.TASKTOPMSG;
|
||||
elementId: string;
|
||||
taskTopMsgElement: TaskTopMsgElement;
|
||||
}
|
||||
export type SendTaskTopMsgElement = SendElementBase<ElementType.TASKTOPMSG> & ElementBase<'taskTopMsgElement'>;
|
||||
|
||||
export interface TofuRecordElement {
|
||||
type: number;
|
||||
@@ -194,11 +166,7 @@ export interface TofuRecordElement {
|
||||
onscreennotify: boolean;
|
||||
}
|
||||
|
||||
export interface SendTofuRecordElement {
|
||||
elementType: ElementType.TOFURECORD;
|
||||
elementId: string;
|
||||
tofuRecordElement: TofuRecordElement;
|
||||
}
|
||||
export type SendTofuRecordElement = SendElementBase<ElementType.TOFURECORD> & ElementBase<'tofuRecordElement'>;
|
||||
|
||||
export interface FaceBubbleElement {
|
||||
faceCount: number;
|
||||
@@ -216,12 +184,7 @@ export interface FaceBubbleElement {
|
||||
};
|
||||
}
|
||||
|
||||
export interface SendFaceBubbleElement {
|
||||
elementType: ElementType.FACEBUBBLE;
|
||||
elementId: string;
|
||||
faceBubbleElement: FaceBubbleElement;
|
||||
|
||||
}
|
||||
export type SendFaceBubbleElement = SendElementBase<ElementType.FACEBUBBLE> & ElementBase<'faceBubbleElement'>;
|
||||
|
||||
export interface AvRecordElement {
|
||||
type: number;
|
||||
@@ -232,11 +195,7 @@ export interface AvRecordElement {
|
||||
extraType: number;
|
||||
}
|
||||
|
||||
export interface SendavRecordElement {
|
||||
elementType: ElementType.AVRECORD;
|
||||
elementId: string;
|
||||
avRecordElement: AvRecordElement;
|
||||
}
|
||||
export type SendAvRecordElement = SendElementBase<ElementType.AVRECORD> & ElementBase<'avRecordElement'>;
|
||||
|
||||
export interface YoloUserInfo {
|
||||
uid: string;
|
||||
@@ -245,24 +204,13 @@ export interface YoloUserInfo {
|
||||
bizId: string;
|
||||
}
|
||||
|
||||
export interface SendInlineKeyboardElement {
|
||||
elementType: ElementType.INLINEKEYBOARD;
|
||||
elementId: string;
|
||||
inlineKeyboardElement: {
|
||||
rows: number;
|
||||
botAppid: string;
|
||||
};
|
||||
|
||||
}
|
||||
export type SendInlineKeyboardElement = SendElementBase<ElementType.INLINEKEYBOARD> & ElementBase<'inlineKeyboardElement'>;
|
||||
|
||||
export interface YoloGameResultElement {
|
||||
UserInfo: YoloUserInfo[];
|
||||
}
|
||||
|
||||
export interface SendYoloGameResultElement {
|
||||
elementType: ElementType.YOLOGAMERESULT;
|
||||
yoloGameResultElement: YoloGameResultElement;
|
||||
}
|
||||
export type SendYoloGameResultElement = SendElementBase<ElementType.YOLOGAMERESULT> & ElementBase<'yoloGameResultElement'>;
|
||||
|
||||
export interface GiphyElement {
|
||||
id: string;
|
||||
@@ -271,17 +219,9 @@ export interface GiphyElement {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface SendGiphyElement {
|
||||
elementType: ElementType.GIPHY;
|
||||
elementId: string;
|
||||
giphyElement: GiphyElement;
|
||||
}
|
||||
export type SendGiphyElement = SendElementBase<ElementType.GIPHY> & ElementBase<'giphyElement'>;
|
||||
|
||||
export interface SendWalletElement {
|
||||
elementType: ElementType.UNKNOWN;//不做 设置位置
|
||||
elementId: string;
|
||||
walletElement: Record<string, never>;
|
||||
}
|
||||
export type SendWalletElement = SendElementBase<ElementType.UNKNOWN> & ElementBase<'walletElement'>;
|
||||
|
||||
export interface CalendarElement {
|
||||
summary: string;
|
||||
@@ -291,49 +231,16 @@ export interface CalendarElement {
|
||||
schema: string;
|
||||
}
|
||||
|
||||
export interface SendCalendarElement {
|
||||
elementType: ElementType.CALENDAR;
|
||||
elementId: string;
|
||||
calendarElement: CalendarElement;
|
||||
}
|
||||
export type SendCalendarElement = SendElementBase<ElementType.CALENDAR> & ElementBase<'calendarElement'>;
|
||||
|
||||
export interface SendliveGiftElement {
|
||||
elementType: ElementType.LIVEGIFT;
|
||||
elementId: string;
|
||||
liveGiftElement: Record<string, never>;
|
||||
}
|
||||
export type SendLiveGiftElement = SendElementBase<ElementType.LIVEGIFT> & ElementBase<'liveGiftElement'>;
|
||||
|
||||
export interface SendTextElement {
|
||||
elementType: ElementType.TEXT;
|
||||
elementId: string;
|
||||
textElement: {
|
||||
content: string;
|
||||
atType: number;
|
||||
atUid: string;
|
||||
atTinyId: string;
|
||||
atNtUid: string;
|
||||
};
|
||||
}
|
||||
export type SendTextElement = SendElementBase<ElementType.TEXT> & ElementBase<'textElement'>;
|
||||
|
||||
export interface SendPttElement {
|
||||
elementType: ElementType.PTT;
|
||||
elementId: string;
|
||||
pttElement: {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
md5HexStr: string;
|
||||
fileSize: number;
|
||||
duration: number; // 单位是秒
|
||||
formatType: number;
|
||||
voiceType: number;
|
||||
voiceChangeType: number;
|
||||
canConvert2Text: boolean;
|
||||
waveAmplitudes: number[];
|
||||
fileSubId: string;
|
||||
playState: number;
|
||||
autoConvertText: number;
|
||||
};
|
||||
}
|
||||
export type SendPttElement = SendElementBase<ElementType.PTT> & ElementBase<'pttElement', {
|
||||
pttElement: ['fileName', 'filePath', 'md5HexStr', 'fileSize', 'duration', 'formatType', 'voiceType',
|
||||
'voiceChangeType', 'canConvert2Text', 'waveAmplitudes', 'fileSubId', 'playState', 'autoConvertText']
|
||||
}>;
|
||||
|
||||
export enum PicType {
|
||||
gif = 2000,
|
||||
@@ -359,11 +266,7 @@ export enum NTMsgAtType {
|
||||
ATTYPEUNKNOWN = 0
|
||||
}
|
||||
|
||||
export interface SendPicElement {
|
||||
elementType: ElementType.PIC;
|
||||
elementId: string;
|
||||
picElement: PicElement;
|
||||
}
|
||||
export type SendPicElement = SendElementBase<ElementType.PIC> & ElementBase<'picElement'>;
|
||||
|
||||
export interface ReplyElement {
|
||||
sourceMsgIdInRecords?: string;
|
||||
@@ -372,55 +275,30 @@ export interface ReplyElement {
|
||||
senderUin: string;
|
||||
senderUidStr?: string;
|
||||
replyMsgTime?: string;
|
||||
replyMsgClientSeq?: string;
|
||||
}
|
||||
|
||||
export interface SendReplyElement {
|
||||
elementType: ElementType.REPLY;
|
||||
elementId: string;
|
||||
replyElement: ReplyElement;
|
||||
}
|
||||
export type SendReplyElement = SendElementBase<ElementType.REPLY> & ElementBase<'replyElement'>;
|
||||
|
||||
export interface SendFaceElement {
|
||||
elementType: ElementType.FACE;
|
||||
elementId: string;
|
||||
faceElement: FaceElement;
|
||||
}
|
||||
export type SendFaceElement = SendElementBase<ElementType.FACE> & ElementBase<'faceElement'>;
|
||||
|
||||
export interface SendMarketFaceElement {
|
||||
elementType: ElementType.MFACE;
|
||||
marketFaceElement: MarketFaceElement;
|
||||
}
|
||||
export type SendMarketFaceElement = SendElementBase<ElementType.MFACE> & ElementBase<'marketFaceElement'>;
|
||||
|
||||
export interface SendstructLongMsgElement {
|
||||
elementType: ElementType.STRUCTLONGMSG;
|
||||
elementId: string;
|
||||
structLongMsgElement: StructLongMsgElement;
|
||||
}
|
||||
export type SendStructLongMsgElement = SendElementBase<ElementType.STRUCTLONGMSG> & ElementBase<'structLongMsgElement'>;
|
||||
|
||||
export interface StructLongMsgElement {
|
||||
xmlContent: string;
|
||||
resId: string;
|
||||
}
|
||||
|
||||
export interface SendactionBarElement {
|
||||
elementType: ElementType.ACTIONBAR;
|
||||
elementId: string;
|
||||
actionBarElement: {
|
||||
rows: number;
|
||||
botAppid: string;
|
||||
};
|
||||
}
|
||||
export type SendActionBarElement = SendElementBase<ElementType.ACTIONBAR> & ElementBase<'actionBarElement'>;
|
||||
|
||||
export interface ShareLocationElement {
|
||||
text: string;
|
||||
ext: string;
|
||||
}
|
||||
|
||||
export interface SendShareLocationElement {
|
||||
elementType: ElementType.SHARELOCATION;
|
||||
elementId: string;
|
||||
shareLocationElement?: ShareLocationElement;
|
||||
}
|
||||
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
|
||||
|
||||
export interface FileElement {
|
||||
fileMd5?: string;
|
||||
@@ -440,29 +318,13 @@ export interface FileElement {
|
||||
fileBizId?: number;
|
||||
}
|
||||
|
||||
export interface SendFileElement {
|
||||
elementType: ElementType.FILE;
|
||||
elementId: string;
|
||||
fileElement: FileElement;
|
||||
}
|
||||
export type SendFileElement = SendElementBase<ElementType.FILE> & ElementBase<'fileElement'>;
|
||||
|
||||
export interface SendVideoElement {
|
||||
elementType: ElementType.VIDEO;
|
||||
elementId: string;
|
||||
videoElement: VideoElement;
|
||||
}
|
||||
export type SendVideoElement = SendElementBase<ElementType.VIDEO> & ElementBase<'videoElement'>;
|
||||
|
||||
export interface SendArkElement {
|
||||
elementType: ElementType.ARK;
|
||||
elementId: string;
|
||||
arkElement: ArkElement;
|
||||
}
|
||||
export type SendArkElement = SendElementBase<ElementType.ARK> & ElementBase<'arkElement'>;
|
||||
|
||||
export interface SendMarkdownElement {
|
||||
elementType: ElementType.MARKDOWN;
|
||||
elementId: string;
|
||||
markdownElement: MarkdownElement;
|
||||
}
|
||||
export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & ElementBase<'markdownElement'>;
|
||||
|
||||
export type SendMessageElement = SendTextElement | SendPttElement |
|
||||
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
|
||||
@@ -479,7 +341,7 @@ export interface TextElement {
|
||||
export interface MessageElement {
|
||||
elementType: ElementType,
|
||||
elementId: string,
|
||||
extBufForUI: string,//"0x",
|
||||
extBufForUI?: string, //"0x",
|
||||
textElement?: TextElement;
|
||||
faceElement?: FaceElement,
|
||||
marketFaceElement?: MarketFaceElement,
|
||||
@@ -508,7 +370,6 @@ export interface MessageElement {
|
||||
taskTopMsgElement?: TaskTopMsgElement,
|
||||
recommendedMsgElement?: RecommendedMsgElement,
|
||||
actionBarElement?: ActionBarElement
|
||||
|
||||
}
|
||||
|
||||
export enum AtType {
|
||||
@@ -516,6 +377,12 @@ export enum AtType {
|
||||
atAll = 1,
|
||||
atUser = 2
|
||||
}
|
||||
export enum MsgSourceType {
|
||||
K_DOWN_SOURCETYPE_AIOINNER = 1,
|
||||
K_DOWN_SOURCETYPE_BIGSCREEN = 2,
|
||||
K_DOWN_SOURCETYPE_HISTORY = 3,
|
||||
K_DOWN_SOURCETYPE_UNKNOWN = 0
|
||||
}
|
||||
|
||||
// 来自Android分析
|
||||
export enum ChatType {
|
||||
@@ -571,7 +438,7 @@ export interface PttElement {
|
||||
fileSize: string; // "4261"
|
||||
fileSubId: string; // "0"
|
||||
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
|
||||
formatType: string; // 1
|
||||
formatType: number; // 1
|
||||
invalidState: number; // 0
|
||||
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
|
||||
playState: number; // 0
|
||||
@@ -582,6 +449,7 @@ export interface PttElement {
|
||||
voiceChangeType: number; // 0
|
||||
voiceType: number; // 0
|
||||
waveAmplitudes: number[];
|
||||
autoConvertText: number;
|
||||
}
|
||||
|
||||
export interface ArkElement {
|
||||
@@ -659,7 +527,8 @@ export interface GrayTipElement {
|
||||
export enum FaceType {
|
||||
normal = 1, // 小黄脸
|
||||
normal2 = 2, // 新小黄脸, 从faceIndex 222开始?
|
||||
dice = 3 // 骰子
|
||||
dice = 3, // 骰子
|
||||
poke = 5 // 拍一拍
|
||||
}
|
||||
|
||||
export enum FaceIndex {
|
||||
@@ -786,7 +655,8 @@ export interface InlineKeyboardElementRowButton {
|
||||
export interface InlineKeyboardElement {
|
||||
rows: [{
|
||||
buttons: InlineKeyboardElementRowButton[]
|
||||
}];
|
||||
}],
|
||||
botAppid: string;
|
||||
}
|
||||
|
||||
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
|
||||
@@ -872,6 +742,8 @@ export interface RawMessage {
|
||||
/**
|
||||
* 扩展字段,与 Ob11 msg ID 有关
|
||||
*/
|
||||
|
||||
|
||||
id?: number;
|
||||
|
||||
guildId: string;
|
||||
@@ -908,11 +780,26 @@ export interface RawMessage {
|
||||
*/
|
||||
peerUin: string;
|
||||
|
||||
/**
|
||||
* 好友备注(如果是好友消息)
|
||||
*/
|
||||
remark?: string;
|
||||
|
||||
/**
|
||||
* 群名(如果是群消息)
|
||||
*/
|
||||
peerName: string;
|
||||
|
||||
/**
|
||||
* 发送者昵称(如果是好友消息)
|
||||
*/
|
||||
sendNickName: string;
|
||||
|
||||
/**
|
||||
* 发送者好友备注(如果是群消息并且有发送者好友)
|
||||
*/
|
||||
sendRemarkName: string;
|
||||
|
||||
/**
|
||||
* 发送者群名片(如果是群消息)
|
||||
*/
|
||||
@@ -933,4 +820,46 @@ export interface RawMessage {
|
||||
records: RawMessage[];
|
||||
|
||||
elements: MessageElement[];
|
||||
|
||||
sourceType: MsgSourceType;
|
||||
}
|
||||
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 MsgReqType {
|
||||
peer: Peer,
|
||||
byType: number,
|
||||
msgId: string,
|
||||
msgSeq: string,
|
||||
msgTime: string,
|
||||
clientSeq: string,
|
||||
cnt: number,
|
||||
queryOrder: boolean,
|
||||
includeSelf: boolean,
|
||||
includeDeleteMsg: boolean,
|
||||
extraCnt: number
|
||||
}
|
||||
//getMsgsIncludeSelf Peer必须 byType 1
|
||||
//getMsgsWithMsgTimeAndClientSeqForC2C Peer必须 byType 3
|
||||
|
@@ -43,6 +43,50 @@ export enum GroupInviteType {
|
||||
BYGROUPMEMBER,
|
||||
BYDISCUSSMEMBER
|
||||
}
|
||||
export interface ShutUpGroupHonor {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export interface ShutUpGroupMember {
|
||||
uid: string;
|
||||
qid: string;
|
||||
uin: string;
|
||||
nick: string;
|
||||
remark: string;
|
||||
cardType: number;
|
||||
cardName: string;
|
||||
role: number;
|
||||
avatarPath: string;
|
||||
shutUpTime: number;
|
||||
isDelete: boolean;
|
||||
isSpecialConcerned: boolean;
|
||||
isSpecialShield: boolean;
|
||||
isRobot: boolean;
|
||||
groupHonor: ShutUpGroupHonor;
|
||||
memberRealLevel: number;
|
||||
memberLevel: number;
|
||||
globalGroupLevel: number;
|
||||
globalGroupPoint: number;
|
||||
memberTitleId: number;
|
||||
memberSpecialTitle: string;
|
||||
specialTitleExpireTime: string;
|
||||
userShowFlag: number;
|
||||
userShowFlagNew: number;
|
||||
richFlag: number;
|
||||
mssVipType: number;
|
||||
bigClubLevel: number;
|
||||
bigClubFlag: number;
|
||||
autoRemark: string;
|
||||
creditLevel: number;
|
||||
joinTime: number;
|
||||
lastSpeakTime: number;
|
||||
memberFlag: number;
|
||||
memberFlagExt: number;
|
||||
memberMobileFlag: number;
|
||||
memberFlagExt2: number;
|
||||
isSpecialShielded: boolean;
|
||||
cardNameId: number;
|
||||
}
|
||||
|
||||
export interface GroupNotify {
|
||||
seq: string; // 通知序列号
|
||||
|
@@ -1,14 +1,15 @@
|
||||
export interface IdMusicSignPostData {
|
||||
type: 'qq' | '163',
|
||||
type: 'qq' | '163' | 'kugou' | 'migu' | 'kuwo',
|
||||
id: string | number,
|
||||
}
|
||||
|
||||
export interface CustomMusicSignPostData {
|
||||
type: 'custom',
|
||||
type: 'qq' | '163' | 'kugou' | 'migu' | 'kuwo' | 'custom',
|
||||
id: undefined,
|
||||
url: string,
|
||||
audio: string,
|
||||
title: string,
|
||||
image?: string,
|
||||
audio?: string,
|
||||
title?: string,
|
||||
image: string,
|
||||
singer?: string
|
||||
}
|
||||
|
||||
|
@@ -153,7 +153,10 @@ interface CommonExt {
|
||||
labels: any[];
|
||||
qqLevel: QQLevel;
|
||||
}
|
||||
|
||||
export enum BuddyListReqType {
|
||||
KNOMAL,
|
||||
KLETTER
|
||||
}
|
||||
interface Pic {
|
||||
picId: string;
|
||||
picTime: number;
|
||||
@@ -213,7 +216,7 @@ export interface BuddyProfileLikeReq {
|
||||
userProfile: number;
|
||||
type: number;
|
||||
start: number;
|
||||
limit: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface QQLevel {
|
||||
@@ -285,16 +288,16 @@ export interface User {
|
||||
export interface SelfInfo extends User {
|
||||
online?: boolean;
|
||||
}
|
||||
export type Friend = User;
|
||||
|
||||
export interface Friend extends User {
|
||||
}
|
||||
// 本来是 Friend extends User 现在用不到
|
||||
|
||||
export enum BizKey {
|
||||
KPRIVILEGEICON,
|
||||
KPHOTOWALL
|
||||
}
|
||||
|
||||
export interface UserDetailInfoByUinV2 {
|
||||
export interface UserDetailInfoByUin {
|
||||
result: number,
|
||||
errMsg: string,
|
||||
detail: {
|
||||
@@ -305,62 +308,15 @@ export interface UserDetailInfoByUinV2 {
|
||||
photoWall: null
|
||||
}
|
||||
}
|
||||
|
||||
export interface UserDetailInfoByUin {
|
||||
result: number,
|
||||
errMsg: string,
|
||||
info: {
|
||||
uid: string,//这个没办法用
|
||||
qid: string,
|
||||
uin: string,
|
||||
nick: string,
|
||||
remark: string,
|
||||
longNick: string,
|
||||
avatarUrl: string,
|
||||
birthday_year: number,
|
||||
birthday_month: number,
|
||||
birthday_day: number,
|
||||
sex: number,//0
|
||||
topTime: string,
|
||||
constellation: number,
|
||||
shengXiao: number,
|
||||
kBloodType: number,
|
||||
homeTown: string,
|
||||
makeFriendCareer: number,
|
||||
pos: string,
|
||||
eMail: string,
|
||||
phoneNum: string,
|
||||
college: string,
|
||||
country: string,
|
||||
province: string,
|
||||
city: string,
|
||||
postCode: string,
|
||||
address: string,
|
||||
isBlock: boolean,
|
||||
isSpecialCareOpen: boolean,
|
||||
isSpecialCareZone: boolean,
|
||||
ringId: string,
|
||||
regTime: number,
|
||||
interest: string,
|
||||
termType: number,
|
||||
labels: any[],
|
||||
qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number },
|
||||
isHideQQLevel: number,
|
||||
privilegeIcon: { jumpUrl: string, openIconList: any[], closeIconList: any[] },
|
||||
isHidePrivilegeIcon: number,
|
||||
photoWall: { picList: any[] },
|
||||
vipFlag: boolean,
|
||||
yearVipFlag: boolean,
|
||||
svipFlag: boolean,
|
||||
vipLevel: number,
|
||||
status: number,
|
||||
qidianMasterFlag: number,
|
||||
qidianCrewFlag: number,
|
||||
qidianCrewFlag2: number,
|
||||
extStatus: number,
|
||||
recommendImgFlag: number,
|
||||
disableEmojiShortCuts: number,
|
||||
pendantId: string,
|
||||
vipNameColorId: string
|
||||
}
|
||||
export enum UserDetailSource {
|
||||
KDB,
|
||||
KSERVER
|
||||
}
|
||||
|
||||
export enum ProfileBizType {
|
||||
KALL,
|
||||
KBASEEXTEND,
|
||||
KVAS,
|
||||
KQZONE,
|
||||
KOTHER
|
||||
}
|
72
src/core/external/appid.json
vendored
72
src/core/external/appid.json
vendored
@@ -1,14 +1,66 @@
|
||||
{
|
||||
"3.2.12-27597": {
|
||||
"appid": 537243600,
|
||||
"qua": "V1_LNX_NQ_3.2.12_27597_GW_B"
|
||||
"9.9.15-28060": {
|
||||
"appid": 537246092,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28060_GW_B"
|
||||
},
|
||||
"9.9.15-27597": {
|
||||
"appid": 537243441,
|
||||
"qua": "V1_WIN_NQ_9.9.15_27597_GW_B"
|
||||
"9.9.15-28131": {
|
||||
"appid": 537246092,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28131_GW_B"
|
||||
},
|
||||
"6.9.53-27597": {
|
||||
"appid": 537243538,
|
||||
"qua": "V1_MAC_NQ_6.9.53_27597_GW_B"
|
||||
"3.2.12-28060": {
|
||||
"appid": 537246140,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28060_GW_B"
|
||||
},
|
||||
"3.2.12-28131": {
|
||||
"appid": 537246140,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28131_GW_B"
|
||||
},
|
||||
"6.9.55-28131": {
|
||||
"appid": 537246115,
|
||||
"qua": "V1_MAC_NQ_6.9.55_28131_GW_B"
|
||||
},
|
||||
"9.9.15-28327": {
|
||||
"appid": 537249321,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28327_GW_B"
|
||||
},
|
||||
"3.2.12-28327": {
|
||||
"appid": 537249393,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28327_GW_B"
|
||||
},
|
||||
"9.9.15-28418": {
|
||||
"appid": 537249321,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28418_GW_B"
|
||||
},
|
||||
"3.2.12-28418": {
|
||||
"appid": 537249393,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28418_GW_B"
|
||||
},
|
||||
"6.9.56-28418": {
|
||||
"appid": 537249367,
|
||||
"qua": "V1_MAC_NQ_6.9.56_28418_GW_B"
|
||||
},
|
||||
"9.9.15-28498": {
|
||||
"appid": 537249321,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28498_GW_B"
|
||||
},
|
||||
"3.2.13-28788": {
|
||||
"appid": 537249787,
|
||||
"qua": "V1_LNX_NQ_3.2.13_28788_GW_B"
|
||||
},
|
||||
"9.9.16-28788": {
|
||||
"appid": 537249739,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
||||
},
|
||||
"9.9.16-28971":{
|
||||
"appid": 537249775,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
|
||||
},
|
||||
"3.2.13-28971": {
|
||||
"appid": 537249848,
|
||||
"qua": "V1_LNX_NQ_3.2.13_28971_GW_B"
|
||||
},
|
||||
"6.9.58-28971": {
|
||||
"appid": 537249826,
|
||||
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
|
||||
}
|
||||
}
|
||||
}
|
5
src/core/external/napcat.json
vendored
5
src/core/external/napcat.json
vendored
@@ -2,5 +2,6 @@
|
||||
"fileLog": true,
|
||||
"consoleLog": true,
|
||||
"fileLogLevel": "debug",
|
||||
"consoleLogLevel": "info"
|
||||
}
|
||||
"consoleLogLevel": "info",
|
||||
"packetServer": ""
|
||||
}
|
42
src/core/external/offset.json
vendored
Normal file
42
src/core/external/offset.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"3.2.12-28418-x64": {
|
||||
"recv": "A0723E0",
|
||||
"send": "A06EAE0"
|
||||
},
|
||||
"9.9.15-28418-x64": {
|
||||
"recv": "37A9004",
|
||||
"send": "37A4BD0"
|
||||
},
|
||||
"9.9.15-28498-x64": {
|
||||
"recv": "37A9004",
|
||||
"send": "37A4BD0"
|
||||
},
|
||||
"9.9.16-28788-x64": {
|
||||
"send": "38076D0",
|
||||
"recv": "380BB04"
|
||||
},
|
||||
"3.2.13-28788-x64": {
|
||||
"send": "A0CEC20",
|
||||
"recv": "A0D2520"
|
||||
},
|
||||
"3.2.13-28788-arm64": {
|
||||
"send": "6E91018",
|
||||
"recv": "6E94850"
|
||||
},
|
||||
"9.9.16-28971-x64": {
|
||||
"send": "38079F0",
|
||||
"recv": "380BE24"
|
||||
},
|
||||
"3.2.13-28971-x64": {
|
||||
"send": "A0CEF60",
|
||||
"recv": "A0D2860"
|
||||
},
|
||||
"3.2.12-28971-arm64": {
|
||||
"send": "6E91318",
|
||||
"recv": "6E94B50"
|
||||
},
|
||||
"6.9.56-28418-arm64": {
|
||||
"send": "4471360",
|
||||
"recv": "4473BCC"
|
||||
}
|
||||
}
|
18
src/core/external/proto/ProfileLikeTip.proto
vendored
Normal file
18
src/core/external/proto/ProfileLikeTip.proto
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
syntax = "proto3";
|
||||
package SysMessage;
|
||||
|
||||
message likeDetail {
|
||||
string txt = 1;
|
||||
int64 uin = 3;
|
||||
string nickname = 5;
|
||||
}
|
||||
|
||||
message likeMsg {
|
||||
int32 times = 1;
|
||||
int32 time = 2;
|
||||
likeDetail detail = 3;
|
||||
}
|
||||
|
||||
message profileLikeTip {
|
||||
likeMsg msg = 14;
|
||||
}
|
@@ -8,7 +8,7 @@ interface ServerRkeyData {
|
||||
}
|
||||
|
||||
export class RkeyManager {
|
||||
serverUrl: string = '';
|
||||
serverUrl: string[] = [];
|
||||
logger: LogWrapper;
|
||||
private rkeyData: ServerRkeyData = {
|
||||
group_rkey: '',
|
||||
@@ -16,7 +16,7 @@ export class RkeyManager {
|
||||
expired_time: 0,
|
||||
};
|
||||
|
||||
constructor(serverUrl: string, logger: LogWrapper) {
|
||||
constructor(serverUrl: string[], logger: LogWrapper) {
|
||||
this.logger = logger;
|
||||
this.serverUrl = serverUrl;
|
||||
}
|
||||
@@ -26,7 +26,8 @@ export class RkeyManager {
|
||||
try {
|
||||
await this.refreshRkey();
|
||||
} catch (e) {
|
||||
this.logger.logError('获取rkey失败', e);
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
//this.logger.logError.bind(this.logger)('获取rkey失败', e);
|
||||
}
|
||||
}
|
||||
return this.rkeyData;
|
||||
@@ -40,6 +41,22 @@ export class RkeyManager {
|
||||
|
||||
async refreshRkey(): Promise<any> {
|
||||
//刷新rkey
|
||||
this.rkeyData = await RequestUtil.HttpGetJson<ServerRkeyData>(this.serverUrl, 'GET');
|
||||
for (const url of this.serverUrl) {
|
||||
try {
|
||||
const temp = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
|
||||
this.rkeyData = {
|
||||
group_rkey: temp.group_rkey.slice(6),
|
||||
private_rkey: temp.private_rkey.slice(6),
|
||||
expired_time: temp.expired_time
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.logError.bind(this.logger)(`[Rkey] Get Rkey ${url} Error `, e);
|
||||
//是否为最后一个url
|
||||
if (url === this.serverUrl[this.serverUrl.length - 1]) {
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -22,14 +22,14 @@ import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
|
||||
import { NapCatPathWrapper } from '@/common/path';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { getMachineId, hostname, systemName, systemVersion } from '@/common/system';
|
||||
import { hostname, systemName, systemVersion } from '@/common/system';
|
||||
import { NTEventWrapper } from '@/common/event';
|
||||
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/entities';
|
||||
import { NapCatConfigLoader } from '@/core/helper/config';
|
||||
import os from 'node:os';
|
||||
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||
|
||||
import { NTQQPacketApi } from './apis/packet';
|
||||
export * from './wrapper';
|
||||
export * from './entities';
|
||||
export * from './services';
|
||||
@@ -45,18 +45,43 @@ export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
|
||||
let appPath;
|
||||
if (os.platform() === 'darwin') {
|
||||
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
||||
} else {
|
||||
} else if (os.platform() === 'linux') {
|
||||
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
||||
} else {
|
||||
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
||||
}
|
||||
let wrapperNodePath = path.resolve(appPath, 'wrapper.node');
|
||||
if (!fs.existsSync(wrapperNodePath)) {
|
||||
wrapperNodePath = path.join(appPath, `versions/${QQVersion}/wrapper.node`);
|
||||
wrapperNodePath = path.join(appPath, `./resources/app/wrapper.node`);
|
||||
}
|
||||
//老版本兼容 未来去掉
|
||||
if (!fs.existsSync(wrapperNodePath)) {
|
||||
wrapperNodePath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
|
||||
}
|
||||
const nativemodule: any = { exports: {} };
|
||||
process.dlopen(nativemodule, wrapperNodePath);
|
||||
return nativemodule.exports;
|
||||
}
|
||||
|
||||
export function getMajorPath(QQVersion: string): string {
|
||||
// major.node
|
||||
let appPath;
|
||||
if (os.platform() === 'darwin') {
|
||||
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
||||
} else if (os.platform() === 'linux') {
|
||||
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
||||
} else {
|
||||
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
||||
}
|
||||
let majorPath = path.resolve(appPath, 'major.node');
|
||||
if (!fs.existsSync(majorPath)) {
|
||||
majorPath = path.join(appPath, `./resources/app/major.node`);
|
||||
}
|
||||
//老版本兼容 未来去掉
|
||||
if (!fs.existsSync(majorPath)) {
|
||||
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
|
||||
}
|
||||
return majorPath;
|
||||
}
|
||||
export class NapCatCore {
|
||||
readonly context: InstanceContext;
|
||||
readonly apis: StableNTApiWrapper;
|
||||
@@ -75,17 +100,18 @@ export class NapCatCore {
|
||||
this.context = context;
|
||||
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
|
||||
this.eventWrapper = new NTEventWrapper(context.session);
|
||||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
|
||||
this.apis = {
|
||||
FileApi: new NTQQFileApi(this.context, this),
|
||||
SystemApi: new NTQQSystemApi(this.context, this),
|
||||
CollectionApi: new NTQQCollectionApi(this.context, this),
|
||||
PacketApi: new NTQQPacketApi(this.context, this),
|
||||
WebApi: new NTQQWebApi(this.context, this),
|
||||
FriendApi: new NTQQFriendApi(this.context, this),
|
||||
MsgApi: new NTQQMsgApi(this.context, this),
|
||||
UserApi: new NTQQUserApi(this.context, this),
|
||||
GroupApi: new NTQQGroupApi(this.context, this),
|
||||
};
|
||||
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
|
||||
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
|
||||
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
|
||||
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
|
||||
@@ -93,7 +119,8 @@ export class NapCatCore {
|
||||
if (!fs.existsSync(this.NapCatTempPath)) {
|
||||
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
||||
}
|
||||
this.initNapCatCoreListeners().then().catch(this.context.logger.logError);
|
||||
|
||||
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||
|
||||
this.context.logger.setFileLogEnabled(
|
||||
this.configLoader.configData.fileLog,
|
||||
@@ -121,7 +148,7 @@ export class NapCatCore {
|
||||
const msgListener = new NodeIKernelMsgListener();
|
||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||
// 下线通知
|
||||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||||
this.context.logger.logError.bind(this.context.logger)('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||||
this.selfInfo.online = false;
|
||||
};
|
||||
msgListener.onRecvMsg = (msgs) => {
|
||||
@@ -132,7 +159,7 @@ export class NapCatCore {
|
||||
};
|
||||
//await sleep(2500);
|
||||
this.context.session.getMsgService().addKernelMsgListener(
|
||||
proxiedListenerOf(msgListener, this.context.logger) as any,
|
||||
proxiedListenerOf(msgListener, this.context.logger),
|
||||
);
|
||||
|
||||
const profileListener = new NodeIKernelProfileListener();
|
||||
@@ -145,8 +172,9 @@ export class NapCatCore {
|
||||
if (Info.status == 20) {
|
||||
this.selfInfo.online = false;
|
||||
this.context.logger.log("账号状态变更为离线");
|
||||
} else {
|
||||
this.selfInfo.online = true;
|
||||
}
|
||||
this.selfInfo.online = true;
|
||||
};
|
||||
this.context.session.getProfileService().addKernelProfileListener(
|
||||
proxiedListenerOf(profileListener, this.context.logger),
|
||||
@@ -227,7 +255,7 @@ export class NapCatCore {
|
||||
}
|
||||
};
|
||||
this.context.session.getGroupService().addKernelGroupListener(
|
||||
proxiedListenerOf(groupListener, this.context.logger) as any,
|
||||
proxiedListenerOf(groupListener, this.context.logger),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,22 +268,34 @@ export class NapCatCore {
|
||||
}
|
||||
}
|
||||
|
||||
export async function genSessionConfig(QQVersionAppid: string, QQVersion: string, selfUin: string, selfUid: string, account_path: string): Promise<WrapperSessionInitConfig> {
|
||||
export async function genSessionConfig(
|
||||
guid: string,
|
||||
QQVersionAppid: string,
|
||||
QQVersion: string,
|
||||
selfUin: string,
|
||||
selfUid: string,
|
||||
account_path: string
|
||||
): Promise<WrapperSessionInitConfig> {
|
||||
const downloadPath = path.join(account_path, 'NapCat', 'temp');
|
||||
fs.mkdirSync(downloadPath, { recursive: true });
|
||||
const guid: string = await getMachineId();//26702 支持JS获取guid值 在LoginService中获取 TODO mlikiow a
|
||||
const platformMapping: Partial<Record<NodeJS.Platform, PlatformType>> = {
|
||||
win32: PlatformType.KWINDOWS,
|
||||
darwin: PlatformType.KMAC,
|
||||
linux: PlatformType.KLINUX,
|
||||
};
|
||||
const systemPlatform = platformMapping[os.platform()] ?? PlatformType.KWINDOWS;
|
||||
return {
|
||||
selfUin,
|
||||
selfUid,
|
||||
desktopPathConfig: {
|
||||
account_path, // 可以通过NodeQQNTWrapperUtil().getNTUserDataInfoConfig()获取
|
||||
},
|
||||
clientVer: QQVersion, // 9.9.8-22355
|
||||
clientVer: QQVersion,
|
||||
a2: '',
|
||||
d2: '',
|
||||
d2Key: '',
|
||||
machineId: '',
|
||||
platform: PlatformType.KWINDOWS, // 3是Windows?
|
||||
platform: systemPlatform, // 3是Windows?
|
||||
platVer: systemVersion, // 系统版本号, 应该可以固定
|
||||
appid: QQVersionAppid,
|
||||
rdeliveryConfig: {
|
||||
@@ -263,7 +303,7 @@ export async function genSessionConfig(QQVersionAppid: string, QQVersion: string
|
||||
systemId: 0,
|
||||
appId: '',
|
||||
logicEnvironment: '',
|
||||
platform: PlatformType.KWINDOWS,
|
||||
platform: systemPlatform,
|
||||
language: '',
|
||||
sdkVersion: '',
|
||||
userId: '',
|
||||
@@ -303,6 +343,7 @@ export interface InstanceContext {
|
||||
export interface StableNTApiWrapper {
|
||||
FileApi: NTQQFileApi,
|
||||
SystemApi: NTQQSystemApi,
|
||||
PacketApi: NTQQPacketApi,
|
||||
CollectionApi: NTQQCollectionApi,
|
||||
WebApi: NTQQWebApi,
|
||||
FriendApi: NTQQFriendApi,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/core/entities';
|
||||
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/entities';
|
||||
|
||||
export class NodeIKernelGroupListener {
|
||||
onGroupListInited(listEmpty: boolean): void { }
|
||||
@@ -71,7 +71,8 @@ export class NodeIKernelGroupListener {
|
||||
sceneId: string,
|
||||
ids: string[],
|
||||
infos: Map<string, GroupMember>, // uid -> GroupMember
|
||||
finish: boolean,
|
||||
hasPrev: boolean,
|
||||
hasNext: boolean,
|
||||
hasRobot: boolean
|
||||
}) {
|
||||
}
|
||||
@@ -79,6 +80,6 @@ export class NodeIKernelGroupListener {
|
||||
onSearchMemberChange(...args: unknown[]) {
|
||||
}
|
||||
|
||||
onShutUpMemberListChanged(...args: unknown[]) {
|
||||
onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>) {
|
||||
}
|
||||
}
|
97
src/core/listeners/NodeIKernelSearchListener.ts
Normal file
97
src/core/listeners/NodeIKernelSearchListener.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { ChatType } from '@/core';
|
||||
export interface SearchGroupInfo {
|
||||
groupCode: string;
|
||||
ownerUid: string;
|
||||
groupFlag: number;
|
||||
groupFlagExt: number;
|
||||
maxMemberNum: number;
|
||||
memberNum: number;
|
||||
groupOption: number;
|
||||
classExt: number;
|
||||
groupName: string;
|
||||
fingerMemo: string;
|
||||
groupQuestion: string;
|
||||
certType: number;
|
||||
shutUpAllTimestamp: number;
|
||||
shutUpMeTimestamp: number;
|
||||
groupTypeFlag: number;
|
||||
privilegeFlag: number;
|
||||
groupSecLevel: number;
|
||||
groupFlagExt3: number;
|
||||
isConfGroup: number;
|
||||
isModifyConfGroupFace: number;
|
||||
isModifyConfGroupName: number;
|
||||
noFigerOpenFlag: number;
|
||||
noCodeFingerOpenFlag: number;
|
||||
groupFlagExt4: number;
|
||||
groupMemo: string;
|
||||
cmdUinMsgSeq: number;
|
||||
cmdUinJoinTime: number;
|
||||
cmdUinUinFlag: number;
|
||||
cmdUinMsgMask: number;
|
||||
groupSecLevelInfo: number;
|
||||
cmdUinPrivilege: number;
|
||||
cmdUinFlagEx2: number;
|
||||
appealDeadline: number;
|
||||
remarkName: string;
|
||||
isTop: boolean;
|
||||
richFingerMemo: string;
|
||||
groupAnswer: string;
|
||||
joinGroupAuth: string;
|
||||
isAllowModifyConfGroupName: number;
|
||||
}
|
||||
|
||||
export interface GroupInfo {
|
||||
groupCode: string;
|
||||
searchGroupInfo: SearchGroupInfo;
|
||||
privilege: number;
|
||||
}
|
||||
|
||||
export interface GroupSearchResult {
|
||||
keyWord: string;
|
||||
errorCode: number;
|
||||
groupInfos: GroupInfo[];
|
||||
penetrate: string;
|
||||
isEnd: boolean;
|
||||
nextPos: number;
|
||||
}
|
||||
export interface NodeIKernelSearchListener {
|
||||
|
||||
onSearchGroupResult(params: GroupSearchResult): void;
|
||||
|
||||
onSearchFileKeywordsResult(params: {
|
||||
searchId: string,
|
||||
hasMore: boolean,
|
||||
resultItems: {
|
||||
chatType: ChatType,
|
||||
buddyChatInfo: any[],
|
||||
discussChatInfo: any[],
|
||||
groupChatInfo: {
|
||||
groupCode: string,
|
||||
isConf: boolean,
|
||||
hasModifyConfGroupFace: boolean,
|
||||
hasModifyConfGroupName: boolean,
|
||||
groupName: string,
|
||||
remark: string
|
||||
}[],
|
||||
dataLineChatInfo: any[],
|
||||
tmpChatInfo: any[],
|
||||
msgId: string,
|
||||
msgSeq: string,
|
||||
msgTime: string,
|
||||
senderUid: string,
|
||||
senderNick: string,
|
||||
senderRemark: string,
|
||||
senderCard: string,
|
||||
elemId: string,
|
||||
elemType: number,
|
||||
fileSize: string,
|
||||
filePath: string,
|
||||
fileName: string,
|
||||
hits: {
|
||||
start: number,
|
||||
end: number
|
||||
}[]
|
||||
}[]
|
||||
}): void;
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
import { ChatType } from '@/core';
|
||||
|
||||
export interface NodeIKernelSearchListener_Polyfill {
|
||||
onSearchFileKeywordsResult(params: {
|
||||
searchId: string,
|
||||
hasMore: boolean,
|
||||
resultItems: {
|
||||
chatType: ChatType,
|
||||
buddyChatInfo: any[],
|
||||
discussChatInfo: any[],
|
||||
groupChatInfo: {
|
||||
groupCode: string,
|
||||
isConf: boolean,
|
||||
hasModifyConfGroupFace: boolean,
|
||||
hasModifyConfGroupName: boolean,
|
||||
groupName: string,
|
||||
remark: string
|
||||
}[],
|
||||
dataLineChatInfo: any[],
|
||||
tmpChatInfo: any[],
|
||||
msgId: string,
|
||||
msgSeq: string,
|
||||
msgTime: string,
|
||||
senderUid: string,
|
||||
senderNick: string,
|
||||
senderRemark: string,
|
||||
senderCard: string,
|
||||
elemId: string,
|
||||
elemType: number,
|
||||
fileSize: string,
|
||||
filePath: string,
|
||||
fileName: string,
|
||||
hits: {
|
||||
start: number,
|
||||
end: number
|
||||
}[]
|
||||
}[]
|
||||
}): void;
|
||||
}
|
5
src/core/listeners/NodeIO3MiscListener.ts
Normal file
5
src/core/listeners/NodeIO3MiscListener.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class NodeIO3MiscListener {
|
||||
getOnAmgomDataPiece(...arg: unknown[]) {
|
||||
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@ export * from './NodeIKernelProfileListener';
|
||||
export * from './NodeIKernelTicketListener';
|
||||
export * from './NodeIKernelStorageCleanListener';
|
||||
export * from './NodeIKernelFileAssistantListener';
|
||||
export * from './NodeIKernelSearchListener_Polyfill';
|
||||
export * from './NodeIKernelSearchListener';
|
||||
|
||||
import type {
|
||||
NodeIKernelBuddyListener,
|
||||
@@ -19,11 +19,11 @@ import type {
|
||||
NodeIKernelMsgListener,
|
||||
NodeIKernelProfileListener,
|
||||
NodeIKernelRobotListener,
|
||||
NodeIKernelSearchListener_Polyfill,
|
||||
NodeIKernelSessionListener,
|
||||
NodeIKernelStorageCleanListener,
|
||||
NodeIKernelTicketListener,
|
||||
} from '.';
|
||||
import { NodeIKernelSearchListener } from './NodeIKernelSearchListener';
|
||||
|
||||
export type ListenerNamingMapping = {
|
||||
NodeIKernelSessionListener: NodeIKernelSessionListener;
|
||||
@@ -36,5 +36,5 @@ export type ListenerNamingMapping = {
|
||||
NodeIKernelTicketListener: NodeIKernelTicketListener;
|
||||
NodeIKernelStorageCleanListener: NodeIKernelStorageCleanListener;
|
||||
NodeIKernelFileAssistantListener: NodeIKernelFileAssistantListener;
|
||||
NodeIKernelSearchListener: NodeIKernelSearchListener_Polyfill;
|
||||
NodeIKernelSearchListener: NodeIKernelSearchListener;
|
||||
};
|
||||
|
183
src/core/packet/client.ts
Normal file
183
src/core/packet/client.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import WebSocket, { Data } from "ws";
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
trace_id_md5?: string,
|
||||
data: RecvPacketData
|
||||
}
|
||||
|
||||
export interface RecvPacketData {
|
||||
seq: number
|
||||
cmd: string
|
||||
hex_data: string
|
||||
}
|
||||
|
||||
export class PacketClient {
|
||||
private websocket: WebSocket | undefined;
|
||||
private isConnected: boolean = false;
|
||||
private reconnectAttempts: number = 0;
|
||||
private readonly maxReconnectAttempts: number = 60;//现在暂时不可配置
|
||||
private readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
||||
private readonly clientUrl: string = '';
|
||||
readonly napCatCore: NapCatCore;
|
||||
private readonly logger: LogWrapper;
|
||||
|
||||
constructor(url: string, core: NapCatCore) {
|
||||
this.clientUrl = url;
|
||||
this.napCatCore = core;
|
||||
this.logger = core.context.logger;
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
return this.isConnected && this.websocket !== undefined;
|
||||
}
|
||||
|
||||
private randText(len: number) {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < len; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
connect(cb: any): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
//this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`);
|
||||
this.websocket = new WebSocket(this.clientUrl);
|
||||
this.websocket.on('error', (err) => { }/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/);
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.logger.log.bind(this.logger)(`[Core] [Packet Server] 已连接到 ${this.clientUrl}`);
|
||||
cb();
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
//this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
|
||||
reject(new Error(`${error.message}`));
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
// const message = JSON.parse(event.data.toString());
|
||||
// console.log("Received message:", message);
|
||||
this.handleMessage(event.data).then().catch();
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
this.isConnected = false;
|
||||
//this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`);
|
||||
this.attemptReconnect(cb);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private attemptReconnect(cb: any): void {
|
||||
try {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
this.connect(cb).catch((error) => {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 尝试重连失败:${error.message}`);
|
||||
});
|
||||
}, 5000 * this.reconnectAttempts);
|
||||
} else {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] ${this.clientUrl} 已达到最大重连次数!`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 重连时出错: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
|
||||
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
|
||||
}
|
||||
|
||||
async init(pid: number, recv: string, send: string): Promise<void> {
|
||||
if (!this.isConnected || !this.websocket) {
|
||||
throw new Error("WebSocket is not connected");
|
||||
}
|
||||
const initMessage = {
|
||||
action: 'init',
|
||||
pid: pid,
|
||||
recv: recv,
|
||||
send: send
|
||||
};
|
||||
this.websocket.send(JSON.stringify(initMessage));
|
||||
}
|
||||
|
||||
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
|
||||
}): Promise<RecvPacketData> {
|
||||
return new Promise<RecvPacketData>((resolve, reject) => {
|
||||
if (!this.isConnected || !this.websocket) {
|
||||
throw new Error("WebSocket is not connected");
|
||||
}
|
||||
const commandMessage = {
|
||||
action: 'send',
|
||||
cmd: cmd,
|
||||
data: data,
|
||||
trace_id: trace_id
|
||||
};
|
||||
this.websocket.send(JSON.stringify(commandMessage));
|
||||
if (rsp) {
|
||||
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve(json);
|
||||
});
|
||||
}
|
||||
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
|
||||
sendcb(json);
|
||||
if (!rsp) {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve(json);
|
||||
}
|
||||
});
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleMessage(message: Data): Promise<void> {
|
||||
try {
|
||||
const json: RecvPacket = JSON.parse(message.toString());
|
||||
const trace_id_md5 = json.trace_id_md5;
|
||||
const action = json?.type ?? 'init';
|
||||
const event = this.cb.get(trace_id_md5 + action);
|
||||
if (event) {
|
||||
await event(json.data);
|
||||
}
|
||||
//console.log("Received message:", json);
|
||||
} catch (error) {
|
||||
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
|
||||
// wtfk tx
|
||||
// 校验失败和异常 可能返回undefined
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.available) {
|
||||
this.logger.logError('NapCat.Packet 未初始化!');
|
||||
return undefined;
|
||||
}
|
||||
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
||||
const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2);
|
||||
this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
|
||||
// await sleep(10);
|
||||
await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
|
||||
}).then((res) => resolve(res)).catch((e: Error) => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
||||
}
|
||||
}
|
79
src/core/packet/entities/miniApp.ts
Normal file
79
src/core/packet/entities/miniApp.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export interface MiniAppReqCustomParams {
|
||||
title: string;
|
||||
desc: string;
|
||||
picUrl: string;
|
||||
jumpUrl: string;
|
||||
}
|
||||
|
||||
export interface MiniAppReqTemplateParams {
|
||||
sdkId: string;
|
||||
appId: string;
|
||||
scene: number;
|
||||
iconUrl: string;
|
||||
templateType: number;
|
||||
businessType: number;
|
||||
verType: number;
|
||||
shareType: number;
|
||||
versionId: string;
|
||||
withShareTicket: number;
|
||||
}
|
||||
|
||||
export interface MiniAppReqParams extends MiniAppReqCustomParams, MiniAppReqTemplateParams {}
|
||||
|
||||
export interface MiniAppData {
|
||||
ver: string;
|
||||
prompt: string;
|
||||
config: Config;
|
||||
app: string;
|
||||
view: string;
|
||||
meta: MetaData;
|
||||
miniappShareOrigin: number;
|
||||
miniappOpenRefer: string;
|
||||
}
|
||||
|
||||
export interface MiniAppRawData {
|
||||
appName: string;
|
||||
appView: string;
|
||||
ver: string;
|
||||
desc: string;
|
||||
prompt: string;
|
||||
metaData: MetaData;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
interface Config {
|
||||
type: string;
|
||||
width: number;
|
||||
height: number;
|
||||
forward: number;
|
||||
autoSize: number;
|
||||
ctime: number;
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface Host {
|
||||
uin: number;
|
||||
nick: string;
|
||||
}
|
||||
|
||||
interface Detail {
|
||||
appid: string;
|
||||
appType: number;
|
||||
title: string;
|
||||
desc: string;
|
||||
icon: string;
|
||||
preview: string;
|
||||
url: string;
|
||||
scene: number;
|
||||
host: Host;
|
||||
shareTemplateId: string;
|
||||
shareTemplateData: Record<string, unknown>;
|
||||
showLittleTail: string;
|
||||
gamePoints: string;
|
||||
gamePointsUrl: string;
|
||||
shareOrigin: number;
|
||||
}
|
||||
|
||||
interface MetaData {
|
||||
detail_1: Detail;
|
||||
}
|
94
src/core/packet/helper/miniAppHelper.ts
Normal file
94
src/core/packet/helper/miniAppHelper.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
MiniAppData,
|
||||
MiniAppReqParams,
|
||||
MiniAppRawData,
|
||||
MiniAppReqCustomParams,
|
||||
MiniAppReqTemplateParams
|
||||
} from "@/core/packet/entities/miniApp";
|
||||
|
||||
type MiniAppTemplateNameList = "bili" | "weibo";
|
||||
|
||||
export abstract class MiniAppInfo {
|
||||
static sdkId: string = "V1_PC_MINISDK_99.99.99_1_APP_A";
|
||||
template: MiniAppReqTemplateParams;
|
||||
|
||||
private static appMap = new Map<MiniAppTemplateNameList, MiniAppInfo>();
|
||||
|
||||
protected constructor(template: MiniAppReqTemplateParams) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
static get(name: MiniAppTemplateNameList): MiniAppInfo | undefined {
|
||||
return this.appMap.get(name);
|
||||
}
|
||||
|
||||
static Bili = new class extends MiniAppInfo {
|
||||
constructor() {
|
||||
super({
|
||||
sdkId: MiniAppInfo.sdkId,
|
||||
appId: "1109937557",
|
||||
scene: 1,
|
||||
templateType: 1,
|
||||
businessType: 0,
|
||||
verType: 3,
|
||||
shareType: 0,
|
||||
versionId: "cfc5f7b05b44b5956502edaecf9d2240",
|
||||
withShareTicket: 0,
|
||||
iconUrl: "https://miniapp.gtimg.cn/public/appicon/51f90239b78a2e4994c11215f4c4ba15_200.jpg"
|
||||
});
|
||||
MiniAppInfo.appMap.set("bili", this);
|
||||
}
|
||||
};
|
||||
|
||||
static WeiBo = new class extends MiniAppInfo {
|
||||
constructor() {
|
||||
super({
|
||||
sdkId: MiniAppInfo.sdkId,
|
||||
appId: "1109224783",
|
||||
scene: 1,
|
||||
templateType: 1,
|
||||
businessType: 0,
|
||||
verType: 3,
|
||||
shareType: 0,
|
||||
versionId: "e482a3cc4e574d9b772e96ba6eec9ba2",
|
||||
withShareTicket: 0,
|
||||
iconUrl: "https://miniapp.gtimg.cn/public/appicon/35bbb44dc68e65194cfacfb206b8f1f7_200.jpg"
|
||||
});
|
||||
MiniAppInfo.appMap.set("weibo", this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class MiniAppInfoHelper {
|
||||
static generateReq(custom: MiniAppReqCustomParams, template: MiniAppReqTemplateParams): MiniAppReqParams {
|
||||
return {
|
||||
...custom,
|
||||
...template
|
||||
};
|
||||
}
|
||||
|
||||
static RawToSend(rawData: MiniAppRawData): MiniAppData {
|
||||
return {
|
||||
ver: rawData.ver,
|
||||
prompt: rawData.prompt,
|
||||
config: rawData.config,
|
||||
app: rawData.appName,
|
||||
view: rawData.appView,
|
||||
meta: rawData.metaData,
|
||||
miniappShareOrigin: 3,
|
||||
miniappOpenRefer: "10002",
|
||||
};
|
||||
}
|
||||
|
||||
static SendToRaw(data: MiniAppData): MiniAppRawData {
|
||||
return {
|
||||
appName: data.app,
|
||||
appView: data.view,
|
||||
ver: data.ver,
|
||||
desc: data.meta.detail_1.desc,
|
||||
prompt: data.prompt,
|
||||
metaData: data.meta,
|
||||
config: data.config,
|
||||
};
|
||||
}
|
||||
}
|
72
src/core/packet/highway/client.ts
Normal file
72
src/core/packet/highway/client.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as stream from 'node:stream';
|
||||
import { ReadStream } from "node:fs";
|
||||
import { PacketHighwaySig } from "@/core/packet/highway/session";
|
||||
import { HighwayHttpUploader, HighwayTcpUploader } from "@/core/packet/highway/uploader";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
|
||||
export interface PacketHighwayTrans {
|
||||
uin: string;
|
||||
cmd: number;
|
||||
command: string;
|
||||
data: stream.Readable;
|
||||
sum: Uint8Array;
|
||||
size: number;
|
||||
ticket: Uint8Array;
|
||||
loginSig?: Uint8Array;
|
||||
ext: Uint8Array;
|
||||
encrypt: boolean;
|
||||
timeout?: number;
|
||||
server: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export class PacketHighwayClient {
|
||||
sig: PacketHighwaySig;
|
||||
server: string = 'htdata3.qq.com';
|
||||
port: number = 80;
|
||||
logger: LogWrapper;
|
||||
|
||||
constructor(sig: PacketHighwaySig, logger: LogWrapper, server: string = 'htdata3.qq.com', port: number = 80) {
|
||||
this.sig = sig;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
changeServer(server: string, port: number) {
|
||||
this.server = server;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 1200): PacketHighwayTrans {
|
||||
return {
|
||||
uin: this.sig.uin,
|
||||
cmd: cmd,
|
||||
command: 'PicUp.DataUp',
|
||||
data: data,
|
||||
sum: md5,
|
||||
size: fileSize,
|
||||
ticket: this.sig.sigSession!,
|
||||
ext: extendInfo,
|
||||
encrypt: false,
|
||||
timeout: timeout,
|
||||
server: this.server,
|
||||
port: this.port,
|
||||
} as PacketHighwayTrans;
|
||||
}
|
||||
|
||||
async upload(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array): Promise<void> {
|
||||
const trans = this.buildDataUpTrans(cmd, data, fileSize, md5, extendInfo);
|
||||
try {
|
||||
const tcpUploader = new HighwayTcpUploader(trans, this.logger);
|
||||
await tcpUploader.upload();
|
||||
} catch (e) {
|
||||
this.logger.logError(`[Highway] upload failed: ${e}, fallback to http upload`);
|
||||
try {
|
||||
const httpUploader = new HighwayHttpUploader(trans, this.logger);
|
||||
await httpUploader.upload();
|
||||
} catch (e) {
|
||||
this.logger.logError(`[Highway] http upload failed: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
src/core/packet/highway/frame.ts
Normal file
23
src/core/packet/highway/frame.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import assert from "node:assert";
|
||||
|
||||
export class Frame{
|
||||
static pack(head: Buffer, body: Buffer): Buffer {
|
||||
const totalLength = 9 + head.length + body.length + 1;
|
||||
const buffer = Buffer.allocUnsafe(totalLength);
|
||||
buffer[0] = 0x28;
|
||||
buffer.writeUInt32BE(head.length, 1);
|
||||
buffer.writeUInt32BE(body.length, 5);
|
||||
head.copy(buffer, 9);
|
||||
body.copy(buffer, 9 + head.length);
|
||||
buffer[totalLength - 1] = 0x29;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static unpack(frame: Buffer): [Buffer, Buffer] {
|
||||
assert(frame[0] === 0x28 && frame[frame.length - 1] === 0x29, 'Invalid frame!');
|
||||
const headLen = frame.readUInt32BE(1);
|
||||
const bodyLen = frame.readUInt32BE(5);
|
||||
// assert(frame.length === 9 + headLen + bodyLen + 1, `Frame ${frame.toString('hex')} length does not match head and body lengths!`);
|
||||
return [frame.subarray(9, 9 + headLen), frame.subarray(9 + headLen, 9 + headLen + bodyLen)];
|
||||
}
|
||||
}
|
570
src/core/packet/highway/session.ts
Normal file
570
src/core/packet/highway/session.ts
Normal file
@@ -0,0 +1,570 @@
|
||||
import * as fs from "node:fs";
|
||||
import { ChatType, Peer } from "@/core";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketClient } from "@/core/packet/client";
|
||||
import { PacketPacker } from "@/core/packet/packer";
|
||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
|
||||
import { PacketHighwayClient } from "@/core/packet/highway/client";
|
||||
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
||||
import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase";
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/message/element";
|
||||
import { FileUploadExt, NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway";
|
||||
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
|
||||
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
|
||||
export const BlockSize = 1024 * 1024;
|
||||
|
||||
interface HighwayServerAddr {
|
||||
ip: string
|
||||
port: number
|
||||
}
|
||||
|
||||
export interface PacketHighwaySig {
|
||||
uin: string;
|
||||
uid: string;
|
||||
sigSession: Uint8Array | null
|
||||
sessionKey: Uint8Array | null
|
||||
serverAddr: HighwayServerAddr[]
|
||||
}
|
||||
|
||||
export class PacketHighwaySession {
|
||||
protected packetClient: PacketClient;
|
||||
protected packetHighwayClient: PacketHighwayClient;
|
||||
protected sig: PacketHighwaySig;
|
||||
protected logger: LogWrapper;
|
||||
protected packer: PacketPacker;
|
||||
private cachedPrepareReq: Promise<void> | null = null;
|
||||
|
||||
constructor(logger: LogWrapper, client: PacketClient, packer: PacketPacker) {
|
||||
this.packetClient = client;
|
||||
this.logger = logger;
|
||||
this.sig = {
|
||||
uin: this.packetClient.napCatCore.selfInfo.uin,
|
||||
uid: this.packetClient.napCatCore.selfInfo.uid,
|
||||
sigSession: null,
|
||||
sessionKey: null,
|
||||
serverAddr: [],
|
||||
};
|
||||
this.packer = packer;
|
||||
this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger);
|
||||
}
|
||||
|
||||
private async checkAvailable() {
|
||||
if (!this.packetClient.available) {
|
||||
throw new Error('packetServer不可用,请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置');
|
||||
}
|
||||
if (this.sig.sigSession === null || this.sig.sessionKey === null) {
|
||||
this.logger.logWarn('[Highway] sigSession or sessionKey not available!');
|
||||
if (this.cachedPrepareReq === null) {
|
||||
this.cachedPrepareReq = this.prepareUpload().finally(() => {
|
||||
this.cachedPrepareReq = null;
|
||||
});
|
||||
}
|
||||
await this.cachedPrepareReq;
|
||||
}
|
||||
}
|
||||
|
||||
private async prepareUpload(): Promise<void> {
|
||||
const packet = this.packer.packHttp0x6ff_501();
|
||||
const req = await this.packetClient.sendPacket('HttpConn.0x6ff_501', packet, true);
|
||||
const rsp = new NapProtoMsg(HttpConn0x6ff_501Response).decode(
|
||||
Buffer.from(req.hex_data, 'hex')
|
||||
);
|
||||
this.sig.sigSession = rsp.httpConn.sigSession;
|
||||
this.sig.sessionKey = rsp.httpConn.sessionKey;
|
||||
for (const info of rsp.httpConn.serverInfos) {
|
||||
if (info.serviceType !== 1) continue;
|
||||
for (const addr of info.serverAddrs) {
|
||||
this.logger.logDebug(`[Highway PrepareUpload] server addr add: ${int32ip2str(addr.ip)}:${addr.port}`);
|
||||
this.sig.serverAddr.push({
|
||||
ip: int32ip2str(addr.ip),
|
||||
port: addr.port
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> {
|
||||
await this.checkAvailable();
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
await this.uploadGroupImageReq(+peer.peerUid, img);
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
await this.uploadC2CImageReq(peer.peerUid, img);
|
||||
} else {
|
||||
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise<void> {
|
||||
await this.checkAvailable();
|
||||
if (+(video.fileSize ?? 0) > 1024 * 1024 * 100) {
|
||||
throw new Error(`[Highway] 视频文件过大: ${(+(video.fileSize ?? 0) / (1024 * 1024)).toFixed(2)} MB > 100 MB,请使用文件上传!`);
|
||||
}
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
await this.uploadGroupVideoReq(+peer.peerUid, video);
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
await this.uploadC2CVideoReq(peer.peerUid, video);
|
||||
} else {
|
||||
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadPtt(peer: Peer, ptt: PacketMsgPttElement): Promise<void> {
|
||||
await this.checkAvailable();
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
await this.uploadGroupPttReq(+peer.peerUid, ptt);
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
await this.uploadC2CPttReq(peer.peerUid, ptt);
|
||||
} else {
|
||||
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(peer: Peer, file: PacketMsgFileElement): Promise<void> {
|
||||
await this.checkAvailable();
|
||||
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
||||
await this.uploadGroupFileReq(+peer.peerUid, file);
|
||||
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
||||
await this.uploadC2CFileReq(peer.peerUid, file);
|
||||
} else {
|
||||
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise<void> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const preReq = await this.packer.packUploadGroupImgReq(groupUin, img);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: ukey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1004,
|
||||
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
||||
img.size,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
img.msgInfo = preRespData.upload.msgInfo;
|
||||
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
|
||||
}
|
||||
|
||||
private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise<void> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const preReq = await this.packer.packUploadC2CImgReq(peerUid, img);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: ukey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1003,
|
||||
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
||||
img.size,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
img.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
|
||||
private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<void> {
|
||||
if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty");
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const preReq = await this.packer.packUploadGroupVideoReq(groupUin, video);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: ukey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: await calculateSha1StreamBytes(video.filePath!)
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1005,
|
||||
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
|
||||
+video.fileSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
const subFile = preRespData.upload.subFileInfos[0];
|
||||
if (subFile.uKey && subFile.uKey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: subFile.uKey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1006,
|
||||
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
|
||||
+video.thumbSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
|
||||
}
|
||||
video.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
|
||||
private async uploadC2CVideoReq(peerUid: string, video: PacketMsgVideoElement): Promise<void> {
|
||||
if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty");
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const preReq = await this.packer.packUploadC2CVideoReq(peerUid, video);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: ukey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: await calculateSha1StreamBytes(video.filePath!)
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1001,
|
||||
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
|
||||
+video.fileSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
const subFile = preRespData.upload.subFileInfos[0];
|
||||
if (subFile.uKey && subFile.uKey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: subFile.uKey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1002,
|
||||
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
|
||||
+video.thumbSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
|
||||
}
|
||||
video.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
|
||||
private async uploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise<void> {
|
||||
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
||||
const preReq = await this.packer.packUploadGroupPttReq(groupUin, ptt);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: ukey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1008,
|
||||
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
|
||||
ptt.fileSize,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadGroupPttReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
ptt.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
|
||||
private async uploadC2CPttReq(peerUid: string, ptt: PacketMsgPttElement): Promise<void> {
|
||||
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
||||
const preReq = await this.packer.packUploadC2CPttReq(peerUid, ptt);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
this.logger.logDebug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
||||
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
|
||||
fileUuid: index.fileUuid,
|
||||
uKey: ukey,
|
||||
network: {
|
||||
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
|
||||
},
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
1007,
|
||||
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
|
||||
ptt.fileSize,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadC2CPttReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
}
|
||||
ptt.msgInfo = preRespData.upload.msgInfo;
|
||||
}
|
||||
|
||||
private async uploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise<void> {
|
||||
file.isGroupFile = true;
|
||||
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
||||
file.fileSha1 = await calculateSha1(file.filePath);
|
||||
const preReq = await this.packer.packUploadGroupFileReq(groupUin, file);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(preResp.body);
|
||||
if (!preRespData?.upload?.boolFileExist) {
|
||||
this.logger.logDebug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
|
||||
const ext = new NapProtoMsg(FileUploadExt).encode({
|
||||
unknown1: 100,
|
||||
unknown2: 1,
|
||||
entry: {
|
||||
busiBuff: {
|
||||
senderUin: BigInt(this.sig.uin),
|
||||
receiverUin: BigInt(groupUin),
|
||||
groupCode: BigInt(groupUin),
|
||||
},
|
||||
fileEntry: {
|
||||
fileSize: BigInt(file.fileSize),
|
||||
md5: file.fileMd5,
|
||||
md5S2: file.fileMd5,
|
||||
checkKey: preRespData.upload.checkKey,
|
||||
fileId: preRespData.upload.fileId,
|
||||
uploadKey: preRespData.upload.fileKey,
|
||||
},
|
||||
clientInfo: {
|
||||
clientType: 3,
|
||||
appId: "100",
|
||||
terminalType: 3,
|
||||
clientVer: "1.1.1",
|
||||
unknown: 4
|
||||
},
|
||||
fileNameInfo: {
|
||||
fileName: file.fileName
|
||||
},
|
||||
host: {
|
||||
hosts: [
|
||||
{
|
||||
url: {
|
||||
host: preRespData.upload.uploadIp,
|
||||
unknown: 1,
|
||||
},
|
||||
port: preRespData.upload.uploadPort,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
unknown200: 0,
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
71,
|
||||
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
|
||||
file.fileSize,
|
||||
file.fileMd5,
|
||||
ext
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadGroupFileReq file exist, don't need upload!`);
|
||||
}
|
||||
file.fileUuid = preRespData.upload.fileId;
|
||||
}
|
||||
|
||||
private async uploadC2CFileReq(peerUid: string, file: PacketMsgFileElement): Promise<void> {
|
||||
file.isGroupFile = false;
|
||||
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
||||
file.fileSha1 = await calculateSha1(file.filePath);
|
||||
const preReq = await this.packer.packUploadC2CFileReq(this.sig.uid, peerUid, file);
|
||||
const preRespRaw = await this.packetClient.sendOidbPacket( preReq, true);
|
||||
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
|
||||
Buffer.from(preRespRaw.hex_data, 'hex')
|
||||
);
|
||||
const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0XE37Response).decode(preResp.body);
|
||||
if (!preRespData.upload?.boolFileExist) {
|
||||
this.logger.logDebug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
|
||||
const ext = new NapProtoMsg(FileUploadExt).encode({
|
||||
unknown1: 100,
|
||||
unknown2: 1,
|
||||
entry: {
|
||||
busiBuff: {
|
||||
senderUin: BigInt(this.sig.uin),
|
||||
},
|
||||
fileEntry: {
|
||||
fileSize: BigInt(file.fileSize),
|
||||
md5: file.fileMd5,
|
||||
md5S2: file.fileMd5,
|
||||
checkKey: file.fileSha1,
|
||||
fileId: preRespData.upload?.uuid,
|
||||
uploadKey: preRespData.upload?.mediaPlatformUploadKey,
|
||||
},
|
||||
clientInfo: {
|
||||
clientType: 3,
|
||||
appId: "100",
|
||||
terminalType: 3,
|
||||
clientVer: "1.1.1",
|
||||
unknown: 4
|
||||
},
|
||||
fileNameInfo: {
|
||||
fileName: file.fileName
|
||||
},
|
||||
host: {
|
||||
hosts: [
|
||||
{
|
||||
url: {
|
||||
host: preRespData.upload?.uploadIp,
|
||||
unknown: 1,
|
||||
},
|
||||
port: preRespData.upload?.uploadPort,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
unknown200: 1,
|
||||
unknown3: 0
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
95,
|
||||
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
|
||||
file.fileSize,
|
||||
file.fileMd5,
|
||||
ext
|
||||
);
|
||||
}
|
||||
file.fileUuid = preRespData.upload?.uuid;
|
||||
file.fileHash = preRespData.upload?.fileAddon;
|
||||
const FetchExistFileReq = this.packer.packOfflineFileDownloadReq(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
|
||||
const resp = await this.packetClient.sendOidbPacket(FetchExistFileReq, true);
|
||||
const oidb_resp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(resp.hex_data, 'hex'));
|
||||
file._e37_800_rsp = new NapProtoMsg(OidbSvcTrpcTcp0XE37_800Response).decode(oidb_resp.body);
|
||||
file._private_send_uid = this.sig.uid;
|
||||
file._private_recv_uid = peerUid;
|
||||
}
|
||||
}
|
215
src/core/packet/highway/uploader.ts
Normal file
215
src/core/packet/highway/uploader.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import * as net from "node:net";
|
||||
import * as crypto from "node:crypto";
|
||||
import * as http from "node:http";
|
||||
import * as stream from "node:stream";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import * as tea from "@/core/packet/utils/crypto/tea";
|
||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import { ReqDataHighwayHead, RespDataHighwayHead } from "@/core/packet/proto/highway/highway";
|
||||
import { BlockSize } from "@/core/packet/highway/session";
|
||||
import { PacketHighwayTrans } from "@/core/packet/highway/client";
|
||||
import { Frame } from "@/core/packet/highway/frame";
|
||||
|
||||
abstract class HighwayUploader {
|
||||
readonly trans: PacketHighwayTrans;
|
||||
readonly logger: LogWrapper;
|
||||
|
||||
constructor(trans: PacketHighwayTrans, logger: LogWrapper) {
|
||||
this.trans = trans;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private encryptTransExt(key: Uint8Array) {
|
||||
if (!this.trans.encrypt) return;
|
||||
this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key));
|
||||
}
|
||||
|
||||
protected timeout(): Promise<void> {
|
||||
return new Promise<void>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`[Highway] timeout after ${this.trans.timeout}s`));
|
||||
}, (this.trans.timeout ?? Infinity) * 1000
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array {
|
||||
return new NapProtoMsg(ReqDataHighwayHead).encode({
|
||||
msgBaseHead: {
|
||||
version: 1,
|
||||
uin: this.trans.uin,
|
||||
command: "PicUp.DataUp",
|
||||
seq: 0,
|
||||
retryTimes: 0,
|
||||
appId: 1600001604,
|
||||
dataFlag: 16,
|
||||
commandId: this.trans.cmd,
|
||||
},
|
||||
msgSegHead: {
|
||||
serviceId: 0,
|
||||
filesize: BigInt(this.trans.size),
|
||||
dataOffset: BigInt(offset),
|
||||
dataLength: bodyLength,
|
||||
serviceTicket: this.trans.ticket,
|
||||
md5: bodyMd5,
|
||||
fileMd5: this.trans.sum,
|
||||
cacheAddr: 0,
|
||||
cachePort: 0,
|
||||
},
|
||||
bytesReqExtendInfo: this.trans.ext,
|
||||
timestamp: BigInt(0),
|
||||
msgLoginSigHead: {
|
||||
uint32LoginSigType: 8,
|
||||
appId: 1600001604,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
abstract upload(): Promise<void>;
|
||||
}
|
||||
|
||||
class HighwayTcpUploaderTransform extends stream.Transform {
|
||||
uploader: HighwayTcpUploader;
|
||||
offset: number;
|
||||
|
||||
constructor(uploader: HighwayTcpUploader) {
|
||||
super();
|
||||
this.uploader = uploader;
|
||||
this.offset = 0;
|
||||
}
|
||||
|
||||
_transform(data: Buffer, _: BufferEncoding, callback: stream.TransformCallback) {
|
||||
let chunkOffset = 0;
|
||||
while (chunkOffset < data.length) {
|
||||
const chunkSize = Math.min(BlockSize, data.length - chunkOffset);
|
||||
const chunk = data.subarray(chunkOffset, chunkOffset + chunkSize);
|
||||
const chunkMd5 = crypto.createHash('md5').update(chunk).digest();
|
||||
const head = this.uploader.buildPicUpHead(this.offset, chunk.length, chunkMd5);
|
||||
chunkOffset += chunk.length;
|
||||
this.offset += chunk.length;
|
||||
this.push(Frame.pack(Buffer.from(head), chunk));
|
||||
}
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
export class HighwayTcpUploader extends HighwayUploader {
|
||||
async upload(): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const upload = new Promise<void>((resolve, reject) => {
|
||||
const highwayTransForm = new HighwayTcpUploaderTransform(this);
|
||||
const socket = net.connect(this.trans.port, this.trans.server, () => {
|
||||
this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false });
|
||||
});
|
||||
const handleRspHeader = (header: Buffer) => {
|
||||
const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header);
|
||||
if (rsp.errorCode !== 0) {
|
||||
socket.end();
|
||||
reject(new Error(`[Highway] tcpUpload failed (code=${rsp.errorCode})`));
|
||||
}
|
||||
const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2);
|
||||
this.logger.logDebug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`);
|
||||
if (Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength) >= Number(rsp.msgSegHead?.filesize)) {
|
||||
this.logger.logDebug('[Highway] tcpUpload finished.');
|
||||
socket.end();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
socket.on('data', (chunk: Buffer) => {
|
||||
if (signal.aborted) {
|
||||
socket.end();
|
||||
reject(new Error('Upload aborted due to timeout'));
|
||||
}
|
||||
const [head, _] = Frame.unpack(chunk);
|
||||
handleRspHeader(head);
|
||||
});
|
||||
socket.on('close', () => {
|
||||
this.logger.logDebug('[Highway] tcpUpload socket closed.');
|
||||
resolve();
|
||||
});
|
||||
socket.on('error', (err) => {
|
||||
socket.end();
|
||||
reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`));
|
||||
});
|
||||
this.trans.data.on('error', (err) => {
|
||||
socket.end();
|
||||
reject(new Error(`[Highway] tcpUpload readable error: ${err}`));
|
||||
});
|
||||
});
|
||||
const timeout = this.timeout().catch((err) => {
|
||||
controller.abort();
|
||||
throw new Error(err.message);
|
||||
});
|
||||
await Promise.race([upload, timeout]);
|
||||
}
|
||||
}
|
||||
|
||||
export class HighwayHttpUploader extends HighwayUploader {
|
||||
async upload(): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
const upload = (async () => {
|
||||
let offset = 0;
|
||||
for await (const chunk of this.trans.data) {
|
||||
if (signal.aborted) {
|
||||
throw new Error('Upload aborted due to timeout');
|
||||
}
|
||||
const block = chunk as Buffer;
|
||||
try {
|
||||
await this.uploadBlock(block, offset);
|
||||
} catch (err) {
|
||||
throw new Error(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`);
|
||||
}
|
||||
offset += block.length;
|
||||
}
|
||||
})();
|
||||
const timeout = this.timeout().catch((err) => {
|
||||
controller.abort();
|
||||
throw new Error(err.message);
|
||||
});
|
||||
await Promise.race([upload, timeout]);
|
||||
}
|
||||
|
||||
private async uploadBlock(block: Buffer, offset: number): Promise<void> {
|
||||
const chunkMD5 = crypto.createHash('md5').update(block).digest();
|
||||
const payload = this.buildPicUpHead(offset, block.length, chunkMD5);
|
||||
const frame = Frame.pack(Buffer.from(payload), block);
|
||||
const resp = await this.httpPostHighwayContent(frame, `http://${this.trans.server}:${this.trans.port}/cgi-bin/httpconn?htcmd=0x6FF0087&uin=${this.trans.uin}`);
|
||||
const [head, body] = Frame.unpack(resp);
|
||||
const headData = new NapProtoMsg(RespDataHighwayHead).decode(head);
|
||||
this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`);
|
||||
if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
|
||||
}
|
||||
|
||||
private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const options: http.RequestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Connection': 'keep-alive',
|
||||
'Accept-Encoding': 'identity',
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)',
|
||||
'Content-Length': frame.length.toString(),
|
||||
},
|
||||
};
|
||||
const req = http.request(serverURL, options, (res) => {
|
||||
const data: Buffer[] = [];
|
||||
res.on('data', (chunk) => {
|
||||
data.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve(Buffer.concat(data));
|
||||
});
|
||||
});
|
||||
req.write(frame);
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
20
src/core/packet/highway/utils.ts
Normal file
20
src/core/packet/highway/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto";
|
||||
import { IPv4 } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
||||
import { NTHighwayIPv4 } from "@/core/packet/proto/highway/highway";
|
||||
|
||||
export const int32ip2str = (ip: number) => {
|
||||
ip = ip & 0xffffffff;
|
||||
return [ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, ((ip & 0xff000000) >> 24) & 0xff].join('.');
|
||||
};
|
||||
|
||||
export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType<typeof IPv4>[]): NapProtoEncodeStructType<typeof NTHighwayIPv4>[] =>{
|
||||
return ipv4s.map((ip) => {
|
||||
return {
|
||||
domain: {
|
||||
isEnable: true,
|
||||
ip: int32ip2str(ip.outIP!),
|
||||
},
|
||||
port: ip.outPort!
|
||||
} as NapProtoEncodeStructType<typeof NTHighwayIPv4>;
|
||||
});
|
||||
};
|
74
src/core/packet/message/builder.ts
Normal file
74
src/core/packet/message/builder.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as crypto from "crypto";
|
||||
import { PushMsgBody } from "@/core/packet/proto/message/message";
|
||||
import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
|
||||
import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/element";
|
||||
import { SendTextElement } from "@/core";
|
||||
|
||||
export class PacketMsgBuilder {
|
||||
private logger: LogWrapper;
|
||||
|
||||
constructor(logger: LogWrapper) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
protected static failBackText = new PacketMsgTextElement(
|
||||
{
|
||||
textElement: { content: "[该消息类型暂不支持查看]" }!
|
||||
} as SendTextElement
|
||||
);
|
||||
|
||||
buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
|
||||
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
|
||||
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`;
|
||||
const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => {
|
||||
return acc !== undefined ? acc : msg.buildContent();
|
||||
}, undefined);
|
||||
const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []);
|
||||
if (!msgContent && !msgElement.length) {
|
||||
this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement!`);
|
||||
msgElement.push(PacketMsgBuilder.failBackText.buildElement());
|
||||
}
|
||||
return {
|
||||
responseHead: {
|
||||
fromUid: "",
|
||||
fromUin: node.senderUin,
|
||||
toUid: node.groupId ? undefined : selfUid,
|
||||
forward: node.groupId ? undefined : {
|
||||
friendName: node.senderName,
|
||||
},
|
||||
grp: node.groupId ? {
|
||||
groupUin: node.groupId,
|
||||
memberName: node.senderName,
|
||||
unknown5: 2
|
||||
} : undefined,
|
||||
},
|
||||
contentHead: {
|
||||
type: node.groupId ? 82 : 9,
|
||||
subType: node.groupId ? undefined : 4,
|
||||
divSeq: node.groupId ? undefined : 4,
|
||||
msgId: crypto.randomBytes(4).readUInt32LE(0),
|
||||
sequence: crypto.randomBytes(4).readUInt32LE(0),
|
||||
timeStamp: +node.time.toString().substring(0, 10),
|
||||
field7: BigInt(1),
|
||||
field8: 0,
|
||||
field9: 0,
|
||||
forward: {
|
||||
field1: 0,
|
||||
field2: 0,
|
||||
field3: node.groupId ? 0 : 2,
|
||||
unknownBase64: avatar,
|
||||
avatar: avatar
|
||||
}
|
||||
},
|
||||
body: {
|
||||
richText: {
|
||||
elems: msgElement
|
||||
},
|
||||
msgContent: msgContent,
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
163
src/core/packet/message/converter.ts
Normal file
163
src/core/packet/message/converter.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
Peer,
|
||||
ChatType,
|
||||
ElementType,
|
||||
MessageElement,
|
||||
RawMessage,
|
||||
SendArkElement,
|
||||
SendFaceElement,
|
||||
SendFileElement,
|
||||
SendMarkdownElement,
|
||||
SendMarketFaceElement,
|
||||
SendPicElement,
|
||||
SendPttElement,
|
||||
SendReplyElement,
|
||||
SendStructLongMsgElement,
|
||||
SendTextElement,
|
||||
SendVideoElement
|
||||
} from "@/core";
|
||||
import {
|
||||
IPacketMsgElement,
|
||||
PacketMsgAtElement,
|
||||
PacketMsgFaceElement,
|
||||
PacketMsgFileElement,
|
||||
PacketMsgLightAppElement,
|
||||
PacketMsgMarkDownElement,
|
||||
PacketMsgMarkFaceElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgReplyElement,
|
||||
PacketMsgTextElement,
|
||||
PacketMsgVideoElement,
|
||||
PacketMultiMsgElement
|
||||
} from "@/core/packet/message/element";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
|
||||
const SupportedElementTypes = [
|
||||
ElementType.TEXT,
|
||||
ElementType.PIC,
|
||||
ElementType.REPLY,
|
||||
ElementType.FACE,
|
||||
ElementType.MFACE,
|
||||
ElementType.VIDEO,
|
||||
ElementType.FILE,
|
||||
ElementType.PTT,
|
||||
ElementType.ARK,
|
||||
ElementType.MARKDOWN,
|
||||
ElementType.STRUCTLONGMSG
|
||||
];
|
||||
|
||||
type SendMessageTypeElementMap = {
|
||||
[ElementType.TEXT]: SendTextElement,
|
||||
[ElementType.PIC]: SendPicElement,
|
||||
[ElementType.FILE]: SendFileElement,
|
||||
[ElementType.PTT]: SendPttElement,
|
||||
[ElementType.VIDEO]: SendVideoElement,
|
||||
[ElementType.FACE]: SendFaceElement,
|
||||
[ElementType.REPLY]: SendReplyElement,
|
||||
[ElementType.ARK]: SendArkElement,
|
||||
[ElementType.MFACE]: SendMarketFaceElement,
|
||||
[ElementType.STRUCTLONGMSG]: SendStructLongMsgElement,
|
||||
[ElementType.MARKDOWN]: SendMarkdownElement,
|
||||
};
|
||||
|
||||
type ElementToPacketMsgConverters = {
|
||||
[K in keyof SendMessageTypeElementMap]: (
|
||||
sendElement: MessageElement
|
||||
) => IPacketMsgElement<SendMessageTypeElementMap[K]>;
|
||||
}
|
||||
|
||||
export type rawMsgWithSendMsg = {
|
||||
senderUin: number;
|
||||
senderUid?: string;
|
||||
senderName: string;
|
||||
groupId?: number;
|
||||
time: number;
|
||||
msg: PacketSendMsgElement[]
|
||||
}
|
||||
|
||||
export class PacketMsgConverter {
|
||||
private logger: LogWrapper;
|
||||
|
||||
constructor(logger: LogWrapper) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters {
|
||||
return SupportedElementTypes.includes(type);
|
||||
}
|
||||
|
||||
rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg {
|
||||
return {
|
||||
senderUid: msg.senderUid ?? '',
|
||||
senderUin: msg.senderUin,
|
||||
senderName: msg.senderName,
|
||||
groupId: msg.groupId,
|
||||
time: msg.time,
|
||||
msg: msg.msg.map((element) => {
|
||||
if (!this.isValidElementType(element.elementType)) return null;
|
||||
return this.rawToPacketMsgConverters[element.elementType](element as MessageElement);
|
||||
}).filter((e) => e !== null)
|
||||
};
|
||||
}
|
||||
|
||||
rawMsgToPacketMsg(msg: RawMessage, ctxPeer: Peer): PacketMsg {
|
||||
return {
|
||||
seq: +msg.msgSeq,
|
||||
groupId: ctxPeer.chatType === ChatType.KCHATTYPEGROUP ? +msg.peerUid : undefined,
|
||||
senderUid: msg.senderUid,
|
||||
senderUin: +msg.senderUin,
|
||||
senderName: msg.sendMemberName && msg.sendMemberName !== ''
|
||||
? msg.sendMemberName
|
||||
: msg.sendNickName && msg.sendNickName !== ''
|
||||
? msg.sendNickName
|
||||
: "QQ用户",
|
||||
time: +msg.msgTime,
|
||||
msg: msg.elements.map((element) => {
|
||||
if (!this.isValidElementType(element.elementType)) return null;
|
||||
return this.rawToPacketMsgConverters[element.elementType](element);
|
||||
}).filter((e) => e !== null)
|
||||
};
|
||||
}
|
||||
|
||||
private rawToPacketMsgConverters: ElementToPacketMsgConverters = {
|
||||
[ElementType.TEXT]: (element) => {
|
||||
if (element.textElement?.atType) {
|
||||
return new PacketMsgAtElement(element as SendTextElement);
|
||||
}
|
||||
return new PacketMsgTextElement(element as SendTextElement);
|
||||
},
|
||||
[ElementType.PIC]: (element) => {
|
||||
return new PacketMsgPicElement(element as SendPicElement);
|
||||
},
|
||||
[ElementType.REPLY]: (element) => {
|
||||
return new PacketMsgReplyElement(element as SendReplyElement);
|
||||
},
|
||||
[ElementType.FACE]: (element) => {
|
||||
return new PacketMsgFaceElement(element as SendFaceElement);
|
||||
},
|
||||
[ElementType.MFACE]: (element) => {
|
||||
return new PacketMsgMarkFaceElement(element as SendMarketFaceElement);
|
||||
},
|
||||
[ElementType.VIDEO]: (element) => {
|
||||
return new PacketMsgVideoElement(element as SendVideoElement);
|
||||
},
|
||||
[ElementType.FILE]: (element) => {
|
||||
return new PacketMsgFileElement(element as SendFileElement);
|
||||
},
|
||||
[ElementType.PTT]: (element) => {
|
||||
return new PacketMsgPttElement(element as SendPttElement);
|
||||
},
|
||||
[ElementType.ARK]: (element) => {
|
||||
return new PacketMsgLightAppElement(element as SendArkElement);
|
||||
},
|
||||
[ElementType.MARKDOWN]: (element) => {
|
||||
return new PacketMsgMarkDownElement(element as SendMarkdownElement);
|
||||
},
|
||||
// TODO: check this logic, move it in arkElement?
|
||||
[ElementType.STRUCTLONGMSG]: (element) => {
|
||||
return new PacketMultiMsgElement(element as SendStructLongMsgElement);
|
||||
}
|
||||
};
|
||||
}
|
534
src/core/packet/message/element.ts
Normal file
534
src/core/packet/message/element.ts
Normal file
@@ -0,0 +1,534 @@
|
||||
import * as zlib from "node:zlib";
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import {
|
||||
CustomFace,
|
||||
Elem,
|
||||
MarkdownData,
|
||||
MentionExtra,
|
||||
NotOnlineImage,
|
||||
QBigFaceExtra,
|
||||
QSmallFaceExtra
|
||||
} from "@/core/packet/proto/message/element";
|
||||
import {
|
||||
AtType,
|
||||
PicType,
|
||||
SendArkElement,
|
||||
SendFaceElement,
|
||||
SendFileElement,
|
||||
SendMarkdownElement,
|
||||
SendMarketFaceElement,
|
||||
SendPicElement,
|
||||
SendPttElement,
|
||||
SendReplyElement,
|
||||
SendStructLongMsgElement,
|
||||
SendTextElement,
|
||||
SendVideoElement
|
||||
} from "@/core";
|
||||
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
|
||||
import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
|
||||
import { FileExtra, GroupFileExtra } from "@/core/packet/proto/message/component";
|
||||
import { OidbSvcTrpcTcp0XE37_800Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
|
||||
// raw <-> packet
|
||||
// TODO: SendStructLongMsgElement
|
||||
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
||||
protected constructor(rawElement: T) {
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
buildContent(): Uint8Array | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return '[暂不支持该消息类型喵~]';
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgTextElement extends IPacketMsgElement<SendTextElement> {
|
||||
text: string;
|
||||
|
||||
constructor(element: SendTextElement) {
|
||||
super(element);
|
||||
this.text = element.textElement.content;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
text: {
|
||||
str: this.text
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgAtElement extends PacketMsgTextElement {
|
||||
targetUid: string;
|
||||
atAll: boolean;
|
||||
|
||||
constructor(element: SendTextElement) {
|
||||
super(element);
|
||||
this.targetUid = element.textElement.atNtUid;
|
||||
this.atAll = element.textElement.atType === AtType.atAll;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
text: {
|
||||
str: this.text,
|
||||
pbReserve: new NapProtoMsg(MentionExtra).encode({
|
||||
type: this.atAll ? 1 : 2,
|
||||
uin: 0,
|
||||
field5: 0,
|
||||
uid: this.targetUid,
|
||||
}
|
||||
)
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
messageId: bigint;
|
||||
messageSeq: number;
|
||||
messageClientSeq: number;
|
||||
targetUin: number;
|
||||
targetUid: string;
|
||||
time: number;
|
||||
elems: PacketMsg[];
|
||||
|
||||
constructor(element: SendReplyElement) {
|
||||
super(element);
|
||||
this.messageId = BigInt(element.replyElement.replayMsgId ?? 0);
|
||||
this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0);
|
||||
this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0);
|
||||
this.targetUin = +(element.replyElement.senderUin ?? 0);
|
||||
this.targetUid = element.replyElement.senderUidStr ?? '';
|
||||
this.time = +(element.replyElement.replyMsgTime ?? 0);
|
||||
this.elems = []; // TODO: in replyElement.sourceMsgTextElems
|
||||
}
|
||||
|
||||
get isGroupReply(): boolean {
|
||||
return this.messageClientSeq !== 0;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
srcMsg: {
|
||||
origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq],
|
||||
senderUin: BigInt(this.targetUin),
|
||||
time: this.time,
|
||||
elems: [], // TODO: in replyElement.sourceMsgTextElems
|
||||
pbReserve: {
|
||||
messageId: this.messageId,
|
||||
},
|
||||
toUin: BigInt(0),
|
||||
}
|
||||
}, {
|
||||
text: this.isGroupReply ? {
|
||||
str: 'nya~',
|
||||
pbReserve: new NapProtoMsg(MentionExtra).encode({
|
||||
type: this.targetUin === 0 ? 1 : 2,
|
||||
uin: 0,
|
||||
field5: 0,
|
||||
uid: String(this.targetUid),
|
||||
}),
|
||||
} : undefined,
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[回复消息]";
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
||||
faceId: number;
|
||||
isLargeFace: boolean;
|
||||
|
||||
constructor(element: SendFaceElement) {
|
||||
super(element);
|
||||
this.faceId = element.faceElement.faceIndex;
|
||||
this.isLargeFace = element.faceElement.faceType === 3;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (this.isLargeFace) {
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 37,
|
||||
pbElem: new NapProtoMsg(QBigFaceExtra).encode({
|
||||
aniStickerPackId: "1",
|
||||
aniStickerId: "8",
|
||||
faceId: this.faceId,
|
||||
field4: 1,
|
||||
field6: "",
|
||||
preview: "",
|
||||
field9: 1
|
||||
}),
|
||||
businessType: 1
|
||||
}
|
||||
}];
|
||||
} else if (this.faceId < 260) {
|
||||
return [{
|
||||
face: {
|
||||
index: this.faceId
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 33,
|
||||
pbElem: new NapProtoMsg(QSmallFaceExtra).encode({
|
||||
faceId: this.faceId,
|
||||
preview: "",
|
||||
preview2: ""
|
||||
}),
|
||||
businessType: 1
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[表情]";
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceElement> {
|
||||
emojiName: string;
|
||||
emojiId: string;
|
||||
emojiPackageId: number;
|
||||
emojiKey: string;
|
||||
|
||||
constructor(element: SendMarketFaceElement) {
|
||||
super(element);
|
||||
this.emojiName = element.marketFaceElement.faceName;
|
||||
this.emojiId = element.marketFaceElement.emojiId;
|
||||
this.emojiPackageId = element.marketFaceElement.emojiPackageId;
|
||||
this.emojiKey = element.marketFaceElement.key;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
marketFace: {
|
||||
faceName: this.emojiName,
|
||||
itemType: 6,
|
||||
faceInfo: 1,
|
||||
faceId: Buffer.from(this.emojiId, 'hex'),
|
||||
tabId: this.emojiPackageId,
|
||||
subType: 3,
|
||||
key: this.emojiKey,
|
||||
imageWidth: 300,
|
||||
imageHeight: 300,
|
||||
pbReserve: {
|
||||
field8: 1
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return `[${this.emojiName}]`;
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
md5: string;
|
||||
width: number;
|
||||
height: number;
|
||||
picType: PicType;
|
||||
sha1: string | null = null;
|
||||
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
|
||||
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
|
||||
c2cPicExt: NapProtoEncodeStructType<typeof NotOnlineImage> | null = null;
|
||||
|
||||
constructor(element: SendPicElement) {
|
||||
super(element);
|
||||
this.path = element.picElement.sourcePath;
|
||||
this.name = element.picElement.fileName;
|
||||
this.size = +element.picElement.fileSize;
|
||||
this.md5 = element.picElement.md5HexStr ?? '';
|
||||
this.width = element.picElement.picWidth;
|
||||
this.height = element.picElement.picHeight;
|
||||
this.picType = element.picElement.picType;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return !!this.msgInfo;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (!this.msgInfo) return [];
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 48,
|
||||
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
|
||||
businessType: 10,
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[图片]";
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
|
||||
fileSize?: string;
|
||||
filePath?: string;
|
||||
thumbSize?: number;
|
||||
thumbPath?: string;
|
||||
fileMd5?: string;
|
||||
fileSha1?: string;
|
||||
thumbMd5?: string;
|
||||
thumbSha1?: string;
|
||||
thumbWidth?: number;
|
||||
thumbHeight?: number;
|
||||
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
|
||||
|
||||
constructor(element: SendVideoElement) {
|
||||
super(element);
|
||||
this.fileSize = element.videoElement.fileSize;
|
||||
this.filePath = element.videoElement.filePath;
|
||||
this.thumbSize = element.videoElement.thumbSize;
|
||||
this.thumbPath = element.videoElement.thumbPath?.get(0);
|
||||
this.fileMd5 = element.videoElement.videoMd5;
|
||||
this.thumbMd5 = element.videoElement.thumbMd5;
|
||||
this.thumbWidth = element.videoElement.thumbWidth;
|
||||
this.thumbHeight = element.videoElement.thumbHeight;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return !!this.msgInfo;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (!this.msgInfo) return [];
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 48,
|
||||
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
|
||||
businessType: 21,
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[视频]";
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
|
||||
filePath: string;
|
||||
fileSize: number;
|
||||
fileMd5: string;
|
||||
fileSha1?: string;
|
||||
fileDuration: number;
|
||||
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
|
||||
|
||||
constructor(element: SendPttElement) {
|
||||
super(element);
|
||||
this.filePath = element.pttElement.filePath;
|
||||
this.fileSize = +element.pttElement.fileSize; // TODO: cc
|
||||
this.fileMd5 = element.pttElement.md5HexStr;
|
||||
this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [];
|
||||
// if (!this.msgInfo) return [];
|
||||
// return [{
|
||||
// commonElem: {
|
||||
// serviceType: 48,
|
||||
// pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
|
||||
// businessType: 22,
|
||||
// }
|
||||
// }];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[语音]";
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
fileSize: number;
|
||||
fileSha1?: Uint8Array;
|
||||
fileMd5?: Uint8Array;
|
||||
fileUuid?: string;
|
||||
fileHash?: string;
|
||||
isGroupFile?: boolean;
|
||||
_private_send_uid?: string;
|
||||
_private_recv_uid?: string;
|
||||
_e37_800_rsp?: NapProtoEncodeStructType<typeof OidbSvcTrpcTcp0XE37_800Response>;
|
||||
|
||||
constructor(element: SendFileElement) {
|
||||
super(element);
|
||||
this.fileName = element.fileElement.fileName;
|
||||
this.filePath = element.fileElement.filePath;
|
||||
this.fileSize = +element.fileElement.fileSize;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
return this.isGroupFile || Boolean(this._e37_800_rsp);
|
||||
}
|
||||
|
||||
buildContent(): Uint8Array | undefined {
|
||||
if (this.isGroupFile || !this._e37_800_rsp) return undefined;
|
||||
return new NapProtoMsg(FileExtra).encode({
|
||||
file: {
|
||||
fileType: 0,
|
||||
fileUuid: this.fileUuid,
|
||||
fileMd5: this.fileMd5,
|
||||
fileName: this.fileName,
|
||||
fileSize: BigInt(this.fileSize),
|
||||
subcmd: 1,
|
||||
dangerEvel: 0,
|
||||
expireTime: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
|
||||
fileHash: this.fileHash,
|
||||
},
|
||||
field6: {
|
||||
field2: {
|
||||
field1: this._e37_800_rsp?.body?.field30?.field110,
|
||||
fileUuid: this.fileUuid,
|
||||
fileName: this.fileName,
|
||||
field6: this._e37_800_rsp?.body?.field30?.field3,
|
||||
field7: this._e37_800_rsp?.body?.field30?.field101,
|
||||
field8: this._e37_800_rsp?.body?.field30?.field100,
|
||||
timestamp1: this._e37_800_rsp?.body?.field30?.timestamp1,
|
||||
fileHash: this.fileHash,
|
||||
selfUid: this._private_send_uid,
|
||||
destUid: this._private_recv_uid,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
if (!this.isGroupFile) return [];
|
||||
const lb = Buffer.alloc(2);
|
||||
const transElemVal = new NapProtoMsg(GroupFileExtra).encode({
|
||||
field1: 6,
|
||||
fileName: this.fileName,
|
||||
inner: {
|
||||
info: {
|
||||
busId: 102,
|
||||
fileId: this.fileUuid,
|
||||
fileSize: BigInt(this.fileSize),
|
||||
fileName: this.fileName,
|
||||
fileSha: this.fileSha1,
|
||||
extInfoString: "",
|
||||
fileMd5: this.fileMd5,
|
||||
}
|
||||
}
|
||||
});
|
||||
lb.writeUInt16BE(transElemVal.length);
|
||||
return [{
|
||||
transElem: {
|
||||
elemType: 24,
|
||||
elemValue: Buffer.concat([Buffer.from([0x01]), lb, transElemVal]) // TLV
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return `[文件]${this.fileName}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgLightAppElement extends IPacketMsgElement<SendArkElement> {
|
||||
payload: string;
|
||||
|
||||
constructor(element: SendArkElement) {
|
||||
super(element);
|
||||
this.payload = element.arkElement.bytesData;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
lightAppElem: {
|
||||
data: Buffer.concat([
|
||||
Buffer.from([0x01]),
|
||||
zlib.deflateSync(Buffer.from(this.payload, 'utf-8'))
|
||||
])
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[卡片消息]";
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMsgMarkDownElement extends IPacketMsgElement<SendMarkdownElement> {
|
||||
content: string;
|
||||
|
||||
constructor(element: SendMarkdownElement) {
|
||||
super(element);
|
||||
this.content = element.markdownElement.content;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
commonElem: {
|
||||
serviceType: 45,
|
||||
pbElem: new NapProtoMsg(MarkdownData).encode({
|
||||
content: this.content
|
||||
}),
|
||||
businessType: 1
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return `[Markdown消息 ${this.content}]`;
|
||||
}
|
||||
}
|
||||
|
||||
export class PacketMultiMsgElement extends IPacketMsgElement<SendStructLongMsgElement> {
|
||||
resid: string;
|
||||
message: PacketMsg[];
|
||||
|
||||
constructor(rawElement: SendStructLongMsgElement, message?: PacketMsg[]) {
|
||||
super(rawElement);
|
||||
this.resid = rawElement.structLongMsgElement.resId;
|
||||
this.message = message ?? [];
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
return [{
|
||||
lightAppElem: {
|
||||
data: Buffer.concat([
|
||||
Buffer.from([0x01]),
|
||||
zlib.deflateSync(Buffer.from(JSON.stringify(ForwardMsgBuilder.fromPacketMsg(this.resid, this.message)), 'utf-8'))
|
||||
])
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[聊天记录]";
|
||||
}
|
||||
}
|
15
src/core/packet/message/message.ts
Normal file
15
src/core/packet/message/message.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IPacketMsgElement } from "@/core/packet/message/element";
|
||||
import { SendMessageElement, SendStructLongMsgElement } from "@/core";
|
||||
|
||||
export type PacketSendMsgElement = SendMessageElement | SendStructLongMsgElement
|
||||
|
||||
export interface PacketMsg {
|
||||
seq?: number;
|
||||
clientSeq?: number;
|
||||
groupId?: number;
|
||||
senderUid: string;
|
||||
senderUin: number;
|
||||
senderName: string;
|
||||
time: number;
|
||||
msg: IPacketMsgElement<PacketSendMsgElement>[]
|
||||
}
|
747
src/core/packet/packer.ts
Normal file
747
src/core/packet/packer.ts
Normal file
@@ -0,0 +1,747 @@
|
||||
import * as zlib from "node:zlib";
|
||||
import * as crypto from "node:crypto";
|
||||
import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
|
||||
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202";
|
||||
import { OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body } from "@/core/packet/proto/oidb/Oidb.0x8FC_2";
|
||||
import { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.0XFE1_2";
|
||||
import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1";
|
||||
import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
|
||||
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
|
||||
import { PacketMsgBuilder } from "@/core/packet/message/builder";
|
||||
import {
|
||||
PacketMsgFileElement,
|
||||
PacketMsgPicElement,
|
||||
PacketMsgPttElement,
|
||||
PacketMsgVideoElement
|
||||
} from "@/core/packet/message/element";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketMsg } from "@/core/packet/message/message";
|
||||
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
|
||||
import { PacketMsgConverter } from "@/core/packet/message/converter";
|
||||
import { PacketClient } from "@/core/packet/client";
|
||||
import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700";
|
||||
import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
|
||||
import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
|
||||
import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
||||
|
||||
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
||||
|
||||
export interface OidbPacket {
|
||||
cmd: string;
|
||||
data: PacketHexStr
|
||||
}
|
||||
|
||||
export class PacketPacker {
|
||||
readonly logger: LogWrapper;
|
||||
readonly client: PacketClient;
|
||||
readonly packetBuilder: PacketMsgBuilder;
|
||||
readonly packetConverter: PacketMsgConverter;
|
||||
|
||||
constructor(logger: LogWrapper, client: PacketClient) {
|
||||
this.logger = logger;
|
||||
this.client = client;
|
||||
this.packetBuilder = new PacketMsgBuilder(logger);
|
||||
this.packetConverter = new PacketMsgConverter(logger);
|
||||
}
|
||||
|
||||
private packetPacket(byteArray: Uint8Array): PacketHexStr {
|
||||
return Buffer.from(byteArray).toString('hex') as PacketHexStr;
|
||||
}
|
||||
|
||||
packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
|
||||
const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({
|
||||
command: cmd,
|
||||
subCommand: subCmd,
|
||||
body: body,
|
||||
isReserved: isUid ? 1 : 0
|
||||
});
|
||||
return {
|
||||
cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`,
|
||||
data: this.packetPacket(data)
|
||||
};
|
||||
}
|
||||
|
||||
packPokePacket(peer: number, group?: number): OidbPacket {
|
||||
const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({
|
||||
uin: peer,
|
||||
groupUin: group,
|
||||
friendUin: group ?? peer,
|
||||
ext: 0
|
||||
});
|
||||
return this.packOidbPacket(0xed3, 1, oidb_0xed3);
|
||||
}
|
||||
|
||||
packRkeyPacket(): OidbPacket {
|
||||
const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 1,
|
||||
command: 202
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 1,
|
||||
sceneType: 0
|
||||
},
|
||||
client: {
|
||||
agentType: 2
|
||||
}
|
||||
},
|
||||
downloadRKeyReq: {
|
||||
key: [10, 20, 2]
|
||||
},
|
||||
});
|
||||
return this.packOidbPacket(0x9067, 202, oidb_0x9067_202);
|
||||
}
|
||||
|
||||
packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket {
|
||||
const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({
|
||||
targetUid: uid,
|
||||
specialTitle: tittle,
|
||||
expiredTime: -1,
|
||||
uinName: tittle
|
||||
});
|
||||
const oidb_0x8FC_2 = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2).encode({
|
||||
groupUin: +groupCode,
|
||||
body: oidb_0x8FC_2_body
|
||||
});
|
||||
return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false);
|
||||
}
|
||||
|
||||
packStatusPacket(uin: number): OidbPacket {
|
||||
const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({
|
||||
uin: uin,
|
||||
key: [{ key: 27372 }]
|
||||
});
|
||||
return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2);
|
||||
}
|
||||
|
||||
async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> {
|
||||
const msgBody = this.packetBuilder.buildFakeMsg(selfUid, msg);
|
||||
const longMsgResultData = new NapProtoMsg(LongMsgResult).encode(
|
||||
{
|
||||
action: {
|
||||
actionCommand: "MultiMsg",
|
||||
actionData: {
|
||||
msgBody: msgBody
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
|
||||
const req = new NapProtoMsg(SendLongMsgReq).encode(
|
||||
{
|
||||
info: {
|
||||
type: groupUin === 0 ? 1 : 3,
|
||||
uid: {
|
||||
uid: groupUin === 0 ? selfUid : groupUin.toString(),
|
||||
},
|
||||
groupUin: groupUin,
|
||||
payload: payload
|
||||
},
|
||||
settings: {
|
||||
field1: 4, field2: 1, field3: 7, field4: 0
|
||||
}
|
||||
}
|
||||
);
|
||||
// this.logger.logDebug("packUploadForwardMsg REQ!!!", req);
|
||||
return this.packetPacket(req);
|
||||
}
|
||||
|
||||
// highway part
|
||||
packHttp0x6ff_501(): PacketHexStr {
|
||||
return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({
|
||||
httpConn: {
|
||||
field1: 0,
|
||||
field2: 0,
|
||||
field3: 16,
|
||||
field4: 1,
|
||||
field6: 3,
|
||||
serviceTypes: [1, 5, 10, 21],
|
||||
// tgt: "", // TODO: do we really need tgt? seems not
|
||||
field9: 2,
|
||||
field10: 9,
|
||||
field11: 8,
|
||||
ver: "1.0.1"
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<OidbPacket> {
|
||||
const req = new NapProtoMsg(NTV2RichMediaReq).encode(
|
||||
{
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 1,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 1,
|
||||
sceneType: 2,
|
||||
group: {
|
||||
groupUin: groupUin
|
||||
},
|
||||
},
|
||||
client: {
|
||||
agentType: 2
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: +img.size,
|
||||
fileHash: img.md5,
|
||||
fileSha1: img.sha1!,
|
||||
fileName: img.name,
|
||||
type: {
|
||||
type: 1,
|
||||
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
|
||||
videoFormat: 0,
|
||||
voiceFormat: 0,
|
||||
},
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
time: 0,
|
||||
original: 1
|
||||
},
|
||||
subFileType: 0,
|
||||
}
|
||||
],
|
||||
tryFastUploadCompleted: true,
|
||||
srvSendMsg: false,
|
||||
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
|
||||
compatQMsgSceneType: 2,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
|
||||
textSummary: "Nya~", // TODO:
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
},
|
||||
ptt: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
bytesReserve: Buffer.alloc(0),
|
||||
bytesGeneralFlags: Buffer.alloc(0),
|
||||
}
|
||||
},
|
||||
clientSeq: 0,
|
||||
noNeedCompatMsg: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
return this.packOidbPacket(0x11c4, 100, req, true, false);
|
||||
}
|
||||
|
||||
async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise<OidbPacket> {
|
||||
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 1,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 1,
|
||||
sceneType: 1,
|
||||
c2C: {
|
||||
accountType: 2,
|
||||
targetUid: peerUin
|
||||
},
|
||||
},
|
||||
client: {
|
||||
agentType: 2,
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: +img.size,
|
||||
fileHash: img.md5,
|
||||
fileSha1: img.sha1!,
|
||||
fileName: img.name,
|
||||
type: {
|
||||
type: 1,
|
||||
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
|
||||
videoFormat: 0,
|
||||
voiceFormat: 0,
|
||||
},
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
time: 0,
|
||||
original: 1
|
||||
},
|
||||
subFileType: 0,
|
||||
}
|
||||
],
|
||||
tryFastUploadCompleted: true,
|
||||
srvSendMsg: false,
|
||||
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
|
||||
compatQMsgSceneType: 1,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
|
||||
textSummary: "Nya~", // TODO:
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
},
|
||||
ptt: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
bytesReserve: Buffer.alloc(0),
|
||||
bytesGeneralFlags: Buffer.alloc(0),
|
||||
}
|
||||
},
|
||||
clientSeq: 0,
|
||||
noNeedCompatMsg: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
return this.packOidbPacket(0x11c5, 100, req, true, false);
|
||||
}
|
||||
|
||||
async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<OidbPacket> {
|
||||
if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
|
||||
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 3,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 2,
|
||||
sceneType: 2,
|
||||
group: {
|
||||
groupUin: groupUin
|
||||
},
|
||||
},
|
||||
client: {
|
||||
agentType: 2
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: +video.fileSize,
|
||||
fileHash: video.fileMd5,
|
||||
fileSha1: video.fileSha1,
|
||||
fileName: "nya.mp4",
|
||||
type: {
|
||||
type: 2,
|
||||
picFormat: 0,
|
||||
videoFormat: 0,
|
||||
voiceFormat: 0
|
||||
},
|
||||
height: 0,
|
||||
width: 0,
|
||||
time: 0,
|
||||
original: 0
|
||||
},
|
||||
subFileType: 0
|
||||
}, {
|
||||
fileInfo: {
|
||||
fileSize: +video.thumbSize,
|
||||
fileHash: video.thumbMd5,
|
||||
fileSha1: video.thumbSha1,
|
||||
fileName: "nya.jpg",
|
||||
type: {
|
||||
type: 1,
|
||||
picFormat: 0,
|
||||
videoFormat: 0,
|
||||
voiceFormat: 0
|
||||
},
|
||||
height: video.thumbHeight,
|
||||
width: video.thumbWidth,
|
||||
time: 0,
|
||||
original: 0
|
||||
},
|
||||
subFileType: 100
|
||||
}
|
||||
],
|
||||
tryFastUploadCompleted: true,
|
||||
srvSendMsg: false,
|
||||
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
|
||||
compatQMsgSceneType: 2,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bizType: 0,
|
||||
textSummary: "Nya~",
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]),
|
||||
},
|
||||
ptt: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
bytesReserve: Buffer.alloc(0),
|
||||
bytesGeneralFlags: Buffer.alloc(0),
|
||||
}
|
||||
},
|
||||
clientSeq: 0,
|
||||
noNeedCompatMsg: false
|
||||
}
|
||||
});
|
||||
return this.packOidbPacket(0x11EA, 100, req, true, false);
|
||||
}
|
||||
|
||||
async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise<OidbPacket> {
|
||||
if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
|
||||
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 3,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 2,
|
||||
sceneType: 1,
|
||||
c2C: {
|
||||
accountType: 2,
|
||||
targetUid: peerUin
|
||||
}
|
||||
},
|
||||
client: {
|
||||
agentType: 2
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: +video.fileSize,
|
||||
fileHash: video.fileMd5,
|
||||
fileSha1: video.fileSha1,
|
||||
fileName: "nya.mp4",
|
||||
type: {
|
||||
type: 2,
|
||||
picFormat: 0,
|
||||
videoFormat: 0,
|
||||
voiceFormat: 0
|
||||
},
|
||||
height: 0,
|
||||
width: 0,
|
||||
time: 0,
|
||||
original: 0
|
||||
},
|
||||
subFileType: 0
|
||||
}, {
|
||||
fileInfo: {
|
||||
fileSize: +video.thumbSize,
|
||||
fileHash: video.thumbMd5,
|
||||
fileSha1: video.thumbSha1,
|
||||
fileName: "nya.jpg",
|
||||
type: {
|
||||
type: 1,
|
||||
picFormat: 0,
|
||||
videoFormat: 0,
|
||||
voiceFormat: 0
|
||||
},
|
||||
height: video.thumbHeight,
|
||||
width: video.thumbWidth,
|
||||
time: 0,
|
||||
original: 0
|
||||
},
|
||||
subFileType: 100
|
||||
}
|
||||
],
|
||||
tryFastUploadCompleted: true,
|
||||
srvSendMsg: false,
|
||||
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
|
||||
compatQMsgSceneType: 2,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bizType: 0,
|
||||
textSummary: "Nya~",
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]),
|
||||
},
|
||||
ptt: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
bytesReserve: Buffer.alloc(0),
|
||||
bytesGeneralFlags: Buffer.alloc(0),
|
||||
}
|
||||
},
|
||||
clientSeq: 0,
|
||||
noNeedCompatMsg: false
|
||||
}
|
||||
});
|
||||
return this.packOidbPacket(0x11E9, 100, req, true, false);
|
||||
}
|
||||
|
||||
async packUploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise<OidbPacket> {
|
||||
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 1,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 3,
|
||||
sceneType: 2,
|
||||
group: {
|
||||
groupUin: groupUin
|
||||
}
|
||||
},
|
||||
client: {
|
||||
agentType: 2
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: ptt.fileSize,
|
||||
fileHash: ptt.fileMd5,
|
||||
fileSha1: ptt.fileSha1,
|
||||
fileName: `${ptt.fileMd5}.amr`,
|
||||
type: {
|
||||
type: 3,
|
||||
picFormat: 0,
|
||||
videoFormat: 0,
|
||||
voiceFormat: 1
|
||||
},
|
||||
height: 0,
|
||||
width: 0,
|
||||
time: ptt.fileDuration,
|
||||
original: 0
|
||||
},
|
||||
subFileType: 0
|
||||
}
|
||||
],
|
||||
tryFastUploadCompleted: true,
|
||||
srvSendMsg: false,
|
||||
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
|
||||
compatQMsgSceneType: 2,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
textSummary: "Nya~",
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
},
|
||||
ptt: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
bytesReserve: Buffer.from([0x08, 0x00, 0x38, 0x00]),
|
||||
bytesGeneralFlags: Buffer.from([0x9a, 0x01, 0x07, 0xaa, 0x03, 0x04, 0x08, 0x08, 0x12, 0x00]),
|
||||
}
|
||||
},
|
||||
clientSeq: 0,
|
||||
noNeedCompatMsg: false
|
||||
}
|
||||
});
|
||||
return this.packOidbPacket(0x126E, 100, req, true, false);
|
||||
}
|
||||
|
||||
async packUploadC2CPttReq(peerUin: string, ptt: PacketMsgPttElement): Promise<OidbPacket> {
|
||||
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 4,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 3,
|
||||
sceneType: 1,
|
||||
c2C: {
|
||||
accountType: 2,
|
||||
targetUid: peerUin
|
||||
}
|
||||
},
|
||||
client: {
|
||||
agentType: 2
|
||||
}
|
||||
},
|
||||
upload: {
|
||||
uploadInfo: [
|
||||
{
|
||||
fileInfo: {
|
||||
fileSize: ptt.fileSize,
|
||||
fileHash: ptt.fileMd5,
|
||||
fileSha1: ptt.fileSha1,
|
||||
fileName: `${ptt.fileMd5}.amr`,
|
||||
type: {
|
||||
type: 3,
|
||||
picFormat: 0,
|
||||
videoFormat: 0,
|
||||
voiceFormat: 1
|
||||
},
|
||||
height: 0,
|
||||
width: 0,
|
||||
time: ptt.fileDuration,
|
||||
original: 0
|
||||
},
|
||||
subFileType: 0
|
||||
}
|
||||
],
|
||||
tryFastUploadCompleted: true,
|
||||
srvSendMsg: false,
|
||||
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
|
||||
compatQMsgSceneType: 1,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
textSummary: "Nya~",
|
||||
},
|
||||
ptt: {
|
||||
bytesReserve: Buffer.from([0x08, 0x00, 0x38, 0x00]),
|
||||
bytesGeneralFlags: Buffer.from([0x9a, 0x01, 0x0b, 0xaa, 0x03, 0x08, 0x08, 0x04, 0x12, 0x04, 0x00, 0x00, 0x00, 0x00]),
|
||||
}
|
||||
},
|
||||
clientSeq: 0,
|
||||
noNeedCompatMsg: false
|
||||
}
|
||||
});
|
||||
return this.packOidbPacket(0x126D, 100, req, true, false);
|
||||
}
|
||||
|
||||
async packUploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise<OidbPacket> {
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({
|
||||
file: {
|
||||
groupUin: groupUin,
|
||||
appId: 4,
|
||||
busId: 102,
|
||||
entrance: 6,
|
||||
targetDirectory: '/', // TODO:
|
||||
fileName: file.fileName,
|
||||
localDirectory: `/${file.fileName}`,
|
||||
fileSize: BigInt(file.fileSize),
|
||||
fileMd5: file.fileMd5,
|
||||
fileSha1: file.fileSha1,
|
||||
fileSha3: Buffer.alloc(0),
|
||||
field15: true
|
||||
}
|
||||
});
|
||||
return this.packOidbPacket(0x6D6, 0, body, true, false);
|
||||
}
|
||||
|
||||
async packUploadC2CFileReq(selfUid: string, peerUid: string, file: PacketMsgFileElement): Promise<OidbPacket> {
|
||||
const body = new NapProtoMsg(OidbSvcTrpcTcp0XE37_1700).encode({
|
||||
command: 1700,
|
||||
seq: 0,
|
||||
upload: {
|
||||
senderUid: selfUid,
|
||||
receiverUid: peerUid,
|
||||
fileSize: file.fileSize,
|
||||
fileName: file.fileName,
|
||||
md510MCheckSum: await computeMd5AndLengthWithLimit(file.filePath, 10 * 1024 * 1024),
|
||||
sha1CheckSum: file.fileSha1,
|
||||
localPath: "/",
|
||||
md5CheckSum: file.fileMd5,
|
||||
sha3CheckSum: Buffer.alloc(0)
|
||||
},
|
||||
businessId: 3,
|
||||
clientType: 1,
|
||||
flagSupportMediaPlatform: 1
|
||||
});
|
||||
return this.packOidbPacket(0xE37, 1700, body, false, false);
|
||||
}
|
||||
|
||||
packOfflineFileDownloadReq(fileUUID: string, fileHash: string, senderUid: string, receiverUid: string): OidbPacket {
|
||||
return this.packOidbPacket(0xE37, 800, new NapProtoMsg(OidbSvcTrpcTcp0XE37_800).encode({
|
||||
subCommand: 800,
|
||||
field2: 0,
|
||||
body: {
|
||||
senderUid: senderUid,
|
||||
receiverUid: receiverUid,
|
||||
fileUuid: fileUUID,
|
||||
fileHash: fileHash,
|
||||
},
|
||||
field101: 3,
|
||||
field102: 1,
|
||||
field200: 1,
|
||||
}), false, false);
|
||||
}
|
||||
|
||||
packGroupFileDownloadReq(groupUin: number, fileUUID: string): OidbPacket {
|
||||
return this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({
|
||||
download: {
|
||||
groupUin: groupUin,
|
||||
appId: 7,
|
||||
busId: 102,
|
||||
fileId: fileUUID
|
||||
}
|
||||
}), true, false
|
||||
);
|
||||
}
|
||||
|
||||
packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr {
|
||||
return this.packetPacket(
|
||||
new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({
|
||||
subCommand: 1200,
|
||||
field2: 1,
|
||||
body: {
|
||||
receiverUid: selfUid,
|
||||
fileUuid: fileUUID,
|
||||
type: 2,
|
||||
fileHash: fileHash,
|
||||
t2: 0
|
||||
},
|
||||
field101: 3,
|
||||
field102: 103,
|
||||
field200: 1,
|
||||
field99999: Buffer.from([0xc0, 0x85, 0x2c, 0x01])
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
packGroupSignReq(uin: string, groupCode: string): OidbPacket {
|
||||
return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode(
|
||||
{
|
||||
body: {
|
||||
uin: uin,
|
||||
groupUin: groupCode,
|
||||
version: "9.0.90"
|
||||
}
|
||||
}
|
||||
), false, false);
|
||||
}
|
||||
|
||||
packMiniAppAdaptShareInfo(req: MiniAppReqParams): PacketHexStr {
|
||||
return this.packetPacket(
|
||||
new NapProtoMsg(MiniAppAdaptShareInfoReq).encode(
|
||||
{
|
||||
appId: req.sdkId,
|
||||
body: {
|
||||
extInfo: {
|
||||
field2: Buffer.alloc(0)
|
||||
},
|
||||
appid: req.appId,
|
||||
title: req.title,
|
||||
desc: req.desc,
|
||||
time: BigInt(Date.now()),
|
||||
scene: req.scene,
|
||||
templateType: req.templateType,
|
||||
businessType: req.businessType,
|
||||
picUrl: req.picUrl,
|
||||
vidUrl: "",
|
||||
jumpUrl: req.jumpUrl,
|
||||
iconUrl: req.iconUrl,
|
||||
verType: req.verType,
|
||||
shareType: req.shareType,
|
||||
versionId: req.versionId,
|
||||
withShareTicket: req.withShareTicket,
|
||||
webURL: "",
|
||||
appidRich: Buffer.alloc(0),
|
||||
template: {
|
||||
templateId: "",
|
||||
templateData: ""
|
||||
},
|
||||
field20: ""
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
139
src/core/packet/proto/NapProto.ts
Normal file
139
src/core/packet/proto/NapProto.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { MessageType, PartialMessage, RepeatType, ScalarType } from '@protobuf-ts/runtime';
|
||||
import { PartialFieldInfo } from "@protobuf-ts/runtime/build/types/reflection-info";
|
||||
|
||||
type LowerCamelCase<S extends string> = CamelCaseHelper<S, false, true>;
|
||||
|
||||
type CamelCaseHelper<
|
||||
S extends string,
|
||||
CapNext extends boolean,
|
||||
IsFirstChar extends boolean
|
||||
> = S extends `${infer F}${infer R}`
|
||||
? F extends '_'
|
||||
? CamelCaseHelper<R, true, false>
|
||||
: F extends `${number}`
|
||||
? `${F}${CamelCaseHelper<R, true, false>}`
|
||||
: CapNext extends true
|
||||
? `${Uppercase<F>}${CamelCaseHelper<R, false, false>}`
|
||||
: IsFirstChar extends true
|
||||
? `${Lowercase<F>}${CamelCaseHelper<R, false, false>}`
|
||||
: `${F}${CamelCaseHelper<R, false, false>}`
|
||||
: '';
|
||||
|
||||
type ScalarTypeToTsType<T extends ScalarType> =
|
||||
T extends ScalarType.DOUBLE | ScalarType.FLOAT | ScalarType.INT32 | ScalarType.FIXED32 | ScalarType.UINT32 | ScalarType.SFIXED32 | ScalarType.SINT32 ? number :
|
||||
T extends ScalarType.INT64 | ScalarType.UINT64 | ScalarType.FIXED64 | ScalarType.SFIXED64 | ScalarType.SINT64 ? bigint :
|
||||
T extends ScalarType.BOOL ? boolean :
|
||||
T extends ScalarType.STRING ? string :
|
||||
T extends ScalarType.BYTES ? Uint8Array :
|
||||
never;
|
||||
|
||||
interface BaseProtoFieldType<T, O extends boolean, R extends O extends true ? false : boolean> {
|
||||
kind: 'scalar' | 'message';
|
||||
no: number;
|
||||
type: T;
|
||||
optional: O;
|
||||
repeat: R;
|
||||
}
|
||||
|
||||
interface ScalarProtoFieldType<T extends ScalarType, O extends boolean, R extends O extends true ? false : boolean> extends BaseProtoFieldType<T, O, R> {
|
||||
kind: 'scalar';
|
||||
}
|
||||
|
||||
interface MessageProtoFieldType<T extends () => ProtoMessageType, O extends boolean, R extends O extends true ? false : boolean> extends BaseProtoFieldType<T, O, R> {
|
||||
kind: 'message';
|
||||
}
|
||||
|
||||
type ProtoFieldType =
|
||||
| ScalarProtoFieldType<ScalarType, boolean, boolean>
|
||||
| MessageProtoFieldType<() => ProtoMessageType, boolean, boolean>;
|
||||
|
||||
type ProtoMessageType = {
|
||||
[key: string]: ProtoFieldType;
|
||||
};
|
||||
|
||||
export function ProtoField<T extends ScalarType, O extends boolean = false, R extends O extends true ? false : boolean = false>(no: number, type: T, optional?: O, repeat?: R): ScalarProtoFieldType<T, O, R>;
|
||||
export function ProtoField<T extends () => ProtoMessageType, O extends boolean = false, R extends O extends true ? false : boolean = false>(no: number, type: T, optional?: O, repeat?: R): MessageProtoFieldType<T, O, R>;
|
||||
export function ProtoField(no: number, type: ScalarType | (() => ProtoMessageType), optional?: boolean, repeat?: boolean): ProtoFieldType {
|
||||
if (typeof type === 'function') {
|
||||
return { kind: 'message', no: no, type: type, optional: optional ?? false, repeat: repeat ?? false };
|
||||
} else {
|
||||
return { kind: 'scalar', no: no, type: type, optional: optional ?? false, repeat: repeat ?? false };
|
||||
}
|
||||
}
|
||||
|
||||
type ProtoFieldReturnType<T, E extends boolean> = NonNullable<T> extends ScalarProtoFieldType<infer S, infer O, infer R>
|
||||
? ScalarTypeToTsType<S>
|
||||
: T extends NonNullable<MessageProtoFieldType<infer S, infer O, infer R>>
|
||||
? NonNullable<NapProtoStructType<ReturnType<S>, E>>
|
||||
: never;
|
||||
|
||||
type RequiredFieldsBaseType<T, E extends boolean> = {
|
||||
[K in keyof T as T[K] extends { optional: true } ? never : LowerCamelCase<K & string>]:
|
||||
T[K] extends { repeat: true }
|
||||
? ProtoFieldReturnType<T[K], E>[]
|
||||
: ProtoFieldReturnType<T[K], E>
|
||||
}
|
||||
|
||||
type OptionalFieldsBaseType<T, E extends boolean> = {
|
||||
[K in keyof T as T[K] extends { optional: true } ? LowerCamelCase<K & string> : never]?:
|
||||
T[K] extends { repeat: true }
|
||||
? ProtoFieldReturnType<T[K], E>[]
|
||||
: ProtoFieldReturnType<T[K], E>
|
||||
}
|
||||
|
||||
type RequiredFieldsType<T, E extends boolean> = E extends true ? Partial<RequiredFieldsBaseType<T, E>> : RequiredFieldsBaseType<T, E>;
|
||||
|
||||
type OptionalFieldsType<T, E extends boolean> = E extends true ? Partial<OptionalFieldsBaseType<T, E>> : OptionalFieldsBaseType<T, E>;
|
||||
|
||||
type NapProtoStructType<T, E extends boolean> = RequiredFieldsType<T, E> & OptionalFieldsType<T, E>;
|
||||
|
||||
export type NapProtoEncodeStructType<T> = NapProtoStructType<T, true>;
|
||||
|
||||
export type NapProtoDecodeStructType<T> = NapProtoStructType<T, false>;
|
||||
|
||||
const NapProtoMsgCache = new Map<ProtoMessageType, MessageType<NapProtoStructType<ProtoMessageType, boolean>>>();
|
||||
|
||||
export class NapProtoMsg<T extends ProtoMessageType> {
|
||||
private readonly _msg: T;
|
||||
private readonly _field: PartialFieldInfo[];
|
||||
private readonly _proto_msg: MessageType<NapProtoStructType<T, boolean>>;
|
||||
|
||||
constructor(fields: T) {
|
||||
this._msg = fields;
|
||||
this._field = Object.keys(fields).map(key => {
|
||||
const field = fields[key];
|
||||
if (field.kind === 'scalar') {
|
||||
const repeatType = field.repeat
|
||||
? [ScalarType.STRING, ScalarType.BYTES].includes(field.type)
|
||||
? RepeatType.UNPACKED
|
||||
: RepeatType.PACKED
|
||||
: RepeatType.NO;
|
||||
return {
|
||||
no: field.no,
|
||||
name: key,
|
||||
kind: 'scalar',
|
||||
T: field.type,
|
||||
opt: field.optional,
|
||||
repeat: repeatType,
|
||||
};
|
||||
} else if (field.kind === 'message') {
|
||||
return {
|
||||
no: field.no,
|
||||
name: key,
|
||||
kind: 'message',
|
||||
repeat: field.repeat ? RepeatType.PACKED : RepeatType.NO,
|
||||
T: () => new NapProtoMsg(field.type())._proto_msg,
|
||||
};
|
||||
}
|
||||
}) as PartialFieldInfo[];
|
||||
this._proto_msg = new MessageType<NapProtoStructType<T, boolean>>('nya', this._field);
|
||||
}
|
||||
|
||||
encode(data: NapProtoEncodeStructType<T>): Uint8Array {
|
||||
return this._proto_msg.toBinary(this._proto_msg.create(data as PartialMessage<NapProtoEncodeStructType<T>>));
|
||||
}
|
||||
|
||||
decode(data: Uint8Array): NapProtoDecodeStructType<T> {
|
||||
return this._proto_msg.fromBinary(data) as NapProtoDecodeStructType<T>;
|
||||
}
|
||||
}
|
114
src/core/packet/proto/action/action.ts
Normal file
114
src/core/packet/proto/action/action.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/proto/message/message";
|
||||
|
||||
export const FaceRoamRequest = {
|
||||
comm: ProtoField(1, () => PlatInfo, true),
|
||||
selfUin: ProtoField(2, ScalarType.UINT32),
|
||||
subCmd: ProtoField(3, ScalarType.UINT32),
|
||||
field6: ProtoField(6, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const PlatInfo = {
|
||||
imPlat: ProtoField(1, ScalarType.UINT32),
|
||||
osVersion: ProtoField(2, ScalarType.STRING, true),
|
||||
qVersion: ProtoField(3, ScalarType.STRING, true),
|
||||
};
|
||||
|
||||
export const FaceRoamResponse = {
|
||||
retCode: ProtoField(1, ScalarType.UINT32),
|
||||
errMsg: ProtoField(2, ScalarType.STRING),
|
||||
subCmd: ProtoField(3, ScalarType.UINT32),
|
||||
userInfo: ProtoField(6, () => FaceRoamUserInfo),
|
||||
};
|
||||
|
||||
export const FaceRoamUserInfo = {
|
||||
fileName: ProtoField(1, ScalarType.STRING, false, true),
|
||||
deleteFile: ProtoField(2, ScalarType.STRING, false, true),
|
||||
bid: ProtoField(3, ScalarType.STRING),
|
||||
maxRoamSize: ProtoField(4, ScalarType.UINT32),
|
||||
emojiType: ProtoField(5, ScalarType.UINT32, false, true),
|
||||
};
|
||||
|
||||
export const SendMessageRequest = {
|
||||
state: ProtoField(1, ScalarType.INT32),
|
||||
sizeCache: ProtoField(2, ScalarType.INT32),
|
||||
unknownFields: ProtoField(3, ScalarType.BYTES),
|
||||
routingHead: ProtoField(4, () => RoutingHead),
|
||||
contentHead: ProtoField(5, () => ContentHead),
|
||||
messageBody: ProtoField(6, () => MessageBody),
|
||||
msgSeq: ProtoField(7, ScalarType.INT32),
|
||||
msgRand: ProtoField(8, ScalarType.INT32),
|
||||
syncCookie: ProtoField(9, ScalarType.BYTES),
|
||||
msgVia: ProtoField(10, ScalarType.INT32),
|
||||
dataStatist: ProtoField(11, ScalarType.INT32),
|
||||
messageControl: ProtoField(12, () => MessageControl),
|
||||
multiSendSeq: ProtoField(13, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const SendMessageResponse = {
|
||||
result: ProtoField(1, ScalarType.INT32),
|
||||
errMsg: ProtoField(2, ScalarType.STRING, true),
|
||||
timestamp1: ProtoField(3, ScalarType.UINT32),
|
||||
field10: ProtoField(10, ScalarType.UINT32),
|
||||
groupSequence: ProtoField(11, ScalarType.UINT32, true),
|
||||
timestamp2: ProtoField(12, ScalarType.UINT32),
|
||||
privateSequence: ProtoField(14, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const SetStatus = {
|
||||
status: ProtoField(1, ScalarType.UINT32),
|
||||
extStatus: ProtoField(2, ScalarType.UINT32),
|
||||
batteryStatus: ProtoField(3, ScalarType.UINT32),
|
||||
customExt: ProtoField(4, () => SetStatusCustomExt, true),
|
||||
};
|
||||
|
||||
export const SetStatusCustomExt = {
|
||||
faceId: ProtoField(1, ScalarType.UINT32),
|
||||
text: ProtoField(2, ScalarType.STRING, true),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const SetStatusResponse = {
|
||||
message: ProtoField(2, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const HttpConn = {
|
||||
field1: ProtoField(1, ScalarType.INT32),
|
||||
field2: ProtoField(2, ScalarType.INT32),
|
||||
field3: ProtoField(3, ScalarType.INT32),
|
||||
field4: ProtoField(4, ScalarType.INT32),
|
||||
tgt: ProtoField(5, ScalarType.STRING),
|
||||
field6: ProtoField(6, ScalarType.INT32),
|
||||
serviceTypes: ProtoField(7, ScalarType.INT32, false, true),
|
||||
field9: ProtoField(9, ScalarType.INT32),
|
||||
field10: ProtoField(10, ScalarType.INT32),
|
||||
field11: ProtoField(11, ScalarType.INT32),
|
||||
ver: ProtoField(15, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const HttpConn0x6ff_501 = {
|
||||
httpConn: ProtoField(0x501, () => HttpConn),
|
||||
};
|
||||
|
||||
export const HttpConn0x6ff_501Response = {
|
||||
httpConn: ProtoField(0x501, () => HttpConnResponse),
|
||||
};
|
||||
|
||||
export const HttpConnResponse = {
|
||||
sigSession: ProtoField(1, ScalarType.BYTES),
|
||||
sessionKey: ProtoField(2, ScalarType.BYTES),
|
||||
serverInfos: ProtoField(3, () => ServerInfo, false, true),
|
||||
};
|
||||
|
||||
export const ServerAddr = {
|
||||
type: ProtoField(1, ScalarType.UINT32),
|
||||
ip: ProtoField(2, ScalarType.FIXED32),
|
||||
port: ProtoField(3, ScalarType.UINT32),
|
||||
area: ProtoField(4, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const ServerInfo = {
|
||||
serviceType: ProtoField(1, ScalarType.UINT32),
|
||||
serverAddrs: ProtoField(2, () => ServerAddr, false, true),
|
||||
};
|
49
src/core/packet/proto/action/miniAppAdaptShareInfo.ts
Normal file
49
src/core/packet/proto/action/miniAppAdaptShareInfo.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
|
||||
export const MiniAppAdaptShareInfoReq = {
|
||||
appId: ProtoField(2, ScalarType.STRING),
|
||||
body: ProtoField(4, () => MiniAppAdaptShareInfoReqBody),
|
||||
};
|
||||
|
||||
export const MiniAppAdaptShareInfoReqBody = {
|
||||
extInfo: ProtoField(1, () => ExtInfo),
|
||||
appid: ProtoField(2, ScalarType.STRING),
|
||||
title: ProtoField(3, ScalarType.STRING),
|
||||
desc: ProtoField(4, ScalarType.STRING),
|
||||
time: ProtoField(5, ScalarType.UINT64),
|
||||
scene: ProtoField(6, ScalarType.UINT32),
|
||||
templateType: ProtoField(7, ScalarType.UINT32),
|
||||
businessType: ProtoField(8, ScalarType.UINT32),
|
||||
picUrl: ProtoField(9, ScalarType.STRING),
|
||||
vidUrl: ProtoField(10, ScalarType.STRING),
|
||||
jumpUrl: ProtoField(11, ScalarType.STRING),
|
||||
iconUrl: ProtoField(12, ScalarType.STRING),
|
||||
verType: ProtoField(13, ScalarType.UINT32),
|
||||
shareType: ProtoField(14, ScalarType.UINT32),
|
||||
versionId: ProtoField(15, ScalarType.STRING),
|
||||
withShareTicket: ProtoField(16, ScalarType.UINT32),
|
||||
webURL: ProtoField(17, ScalarType.STRING),
|
||||
appidRich: ProtoField(18, ScalarType.BYTES),
|
||||
template: ProtoField(19, () => Template),
|
||||
field20: ProtoField(20, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const ExtInfo = {
|
||||
field2: ProtoField(2, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const Template = {
|
||||
templateId: ProtoField(1, ScalarType.STRING),
|
||||
templateData: ProtoField(2, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const MiniAppAdaptShareInfoResp = {
|
||||
field2: ProtoField(2, ScalarType.UINT32),
|
||||
field3: ProtoField(3, ScalarType.STRING),
|
||||
content: ProtoField(4, () => MiniAppAdaptShareInfoRespContent),
|
||||
};
|
||||
|
||||
export const MiniAppAdaptShareInfoRespContent = {
|
||||
jsonContent: ProtoField(2, ScalarType.STRING),
|
||||
};
|
155
src/core/packet/proto/highway/highway.ts
Normal file
155
src/core/packet/proto/highway/highway.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
import { MsgInfo, MsgInfoBody } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||
|
||||
export const DataHighwayHead = {
|
||||
version: ProtoField(1, ScalarType.UINT32),
|
||||
uin: ProtoField(2, ScalarType.STRING, true),
|
||||
command: ProtoField(3, ScalarType.STRING, true),
|
||||
seq: ProtoField(4, ScalarType.UINT32, true),
|
||||
retryTimes: ProtoField(5, ScalarType.UINT32, true),
|
||||
appId: ProtoField(6, ScalarType.UINT32),
|
||||
dataFlag: ProtoField(7, ScalarType.UINT32),
|
||||
commandId: ProtoField(8, ScalarType.UINT32),
|
||||
buildVer: ProtoField(9, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const FileUploadExt = {
|
||||
unknown1: ProtoField(1, ScalarType.INT32),
|
||||
unknown2: ProtoField(2, ScalarType.INT32),
|
||||
unknown3: ProtoField(3, ScalarType.INT32),
|
||||
entry: ProtoField(100, () => FileUploadEntry),
|
||||
unknown200: ProtoField(200, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const FileUploadEntry = {
|
||||
busiBuff: ProtoField(100, () => ExcitingBusiInfo),
|
||||
fileEntry: ProtoField(200, () => ExcitingFileEntry),
|
||||
clientInfo: ProtoField(300, () => ExcitingClientInfo),
|
||||
fileNameInfo: ProtoField(400, () => ExcitingFileNameInfo),
|
||||
host: ProtoField(500, () => ExcitingHostConfig),
|
||||
};
|
||||
|
||||
export const ExcitingBusiInfo = {
|
||||
busId: ProtoField(1, ScalarType.INT32),
|
||||
senderUin: ProtoField(100, ScalarType.UINT64),
|
||||
receiverUin: ProtoField(200, ScalarType.UINT64),
|
||||
groupCode: ProtoField(400, ScalarType.UINT64),
|
||||
};
|
||||
|
||||
export const ExcitingFileEntry = {
|
||||
fileSize: ProtoField(100, ScalarType.UINT64),
|
||||
md5: ProtoField(200, ScalarType.BYTES),
|
||||
checkKey: ProtoField(300, ScalarType.BYTES),
|
||||
md5S2: ProtoField(400, ScalarType.BYTES),
|
||||
fileId: ProtoField(600, ScalarType.STRING),
|
||||
uploadKey: ProtoField(700, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const ExcitingClientInfo = {
|
||||
clientType: ProtoField(100, ScalarType.INT32),
|
||||
appId: ProtoField(200, ScalarType.STRING),
|
||||
terminalType: ProtoField(300, ScalarType.INT32),
|
||||
clientVer: ProtoField(400, ScalarType.STRING),
|
||||
unknown: ProtoField(600, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const ExcitingFileNameInfo = {
|
||||
fileName: ProtoField(100, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const ExcitingHostConfig = {
|
||||
hosts: ProtoField(200, () => ExcitingHostInfo, false, true),
|
||||
};
|
||||
|
||||
export const ExcitingHostInfo = {
|
||||
url: ProtoField(1, () => ExcitingUrlInfo),
|
||||
port: ProtoField(2, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const ExcitingUrlInfo = {
|
||||
unknown: ProtoField(1, ScalarType.INT32),
|
||||
host: ProtoField(2, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const LoginSigHead = {
|
||||
uint32LoginSigType: ProtoField(1, ScalarType.UINT32),
|
||||
bytesLoginSig: ProtoField(2, ScalarType.BYTES),
|
||||
appId: ProtoField(3, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const NTV2RichMediaHighwayExt = {
|
||||
fileUuid: ProtoField(1, ScalarType.STRING),
|
||||
uKey: ProtoField(2, ScalarType.STRING),
|
||||
network: ProtoField(5, () => NTHighwayNetwork),
|
||||
msgInfoBody: ProtoField(6, () => MsgInfoBody, false, true),
|
||||
blockSize: ProtoField(10, ScalarType.UINT32),
|
||||
hash: ProtoField(11, () => NTHighwayHash),
|
||||
};
|
||||
|
||||
export const NTHighwayHash = {
|
||||
fileSha1: ProtoField(1, ScalarType.BYTES, false, true),
|
||||
};
|
||||
|
||||
export const NTHighwayNetwork = {
|
||||
ipv4s: ProtoField(1, () => NTHighwayIPv4, false, true),
|
||||
};
|
||||
|
||||
export const NTHighwayIPv4 = {
|
||||
domain: ProtoField(1, () => NTHighwayDomain),
|
||||
port: ProtoField(2, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const NTHighwayDomain = {
|
||||
isEnable: ProtoField(1, ScalarType.BOOL),
|
||||
ip: ProtoField(2, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const ReqDataHighwayHead = {
|
||||
msgBaseHead: ProtoField(1, () => DataHighwayHead, true),
|
||||
msgSegHead: ProtoField(2, () => SegHead, true),
|
||||
bytesReqExtendInfo: ProtoField(3, ScalarType.BYTES, true),
|
||||
timestamp: ProtoField(4, ScalarType.UINT64),
|
||||
msgLoginSigHead: ProtoField(5, () => LoginSigHead, true),
|
||||
};
|
||||
|
||||
export const RespDataHighwayHead = {
|
||||
msgBaseHead: ProtoField(1, () => DataHighwayHead, true),
|
||||
msgSegHead: ProtoField(2, () => SegHead, true),
|
||||
errorCode: ProtoField(3, ScalarType.UINT32),
|
||||
allowRetry: ProtoField(4, ScalarType.UINT32),
|
||||
cacheCost: ProtoField(5, ScalarType.UINT32),
|
||||
htCost: ProtoField(6, ScalarType.UINT32),
|
||||
bytesRspExtendInfo: ProtoField(7, ScalarType.BYTES, true),
|
||||
timestamp: ProtoField(8, ScalarType.UINT64),
|
||||
range: ProtoField(9, ScalarType.UINT64),
|
||||
isReset: ProtoField(10, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const SegHead = {
|
||||
serviceId: ProtoField(1, ScalarType.UINT32, true),
|
||||
filesize: ProtoField(2, ScalarType.UINT64),
|
||||
dataOffset: ProtoField(3, ScalarType.UINT64, true),
|
||||
dataLength: ProtoField(4, ScalarType.UINT32),
|
||||
retCode: ProtoField(5, ScalarType.UINT32, true),
|
||||
serviceTicket: ProtoField(6, ScalarType.BYTES),
|
||||
flag: ProtoField(7, ScalarType.UINT32, true),
|
||||
md5: ProtoField(8, ScalarType.BYTES),
|
||||
fileMd5: ProtoField(9, ScalarType.BYTES),
|
||||
cacheAddr: ProtoField(10, ScalarType.UINT32, true),
|
||||
queryTimes: ProtoField(11, ScalarType.UINT32),
|
||||
updateCacheIp: ProtoField(12, ScalarType.UINT32),
|
||||
cachePort: ProtoField(13, ScalarType.UINT32, true),
|
||||
};
|
||||
|
||||
export const GroupAvatarExtra = {
|
||||
type: ProtoField(1, ScalarType.UINT32),
|
||||
groupUin: ProtoField(2, ScalarType.UINT32),
|
||||
field3: ProtoField(3, () => GroupAvatarExtraField3),
|
||||
field5: ProtoField(5, ScalarType.UINT32),
|
||||
field6: ProtoField(6, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const GroupAvatarExtraField3 = {
|
||||
field1: ProtoField(1, ScalarType.UINT32),
|
||||
};
|
117
src/core/packet/proto/message/action.ts
Normal file
117
src/core/packet/proto/message/action.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
import { PushMsgBody } from "@/core/packet/proto/message/message";
|
||||
|
||||
export const LongMsgResult = {
|
||||
action: ProtoField(2, () => LongMsgAction)
|
||||
};
|
||||
|
||||
export const LongMsgAction = {
|
||||
actionCommand: ProtoField(1, ScalarType.STRING),
|
||||
actionData: ProtoField(2, () => LongMsgContent)
|
||||
};
|
||||
|
||||
export const LongMsgContent = {
|
||||
msgBody: ProtoField(1, () => PushMsgBody, false, true)
|
||||
};
|
||||
|
||||
export const RecvLongMsgReq = {
|
||||
info: ProtoField(1, () => RecvLongMsgInfo, true),
|
||||
settings: ProtoField(15, () => LongMsgSettings, true)
|
||||
};
|
||||
|
||||
export const RecvLongMsgInfo = {
|
||||
uid: ProtoField(1, () => LongMsgUid, true),
|
||||
resId: ProtoField(2, ScalarType.STRING, true),
|
||||
acquire: ProtoField(3, ScalarType.BOOL)
|
||||
};
|
||||
|
||||
export const LongMsgUid = {
|
||||
uid: ProtoField(2, ScalarType.STRING, true)
|
||||
};
|
||||
|
||||
export const LongMsgSettings = {
|
||||
field1: ProtoField(1, ScalarType.UINT32),
|
||||
field2: ProtoField(2, ScalarType.UINT32),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
field4: ProtoField(4, ScalarType.UINT32)
|
||||
};
|
||||
|
||||
export const RecvLongMsgResp = {
|
||||
result: ProtoField(1, () => RecvLongMsgResult),
|
||||
settings: ProtoField(15, () => LongMsgSettings)
|
||||
};
|
||||
|
||||
export const RecvLongMsgResult = {
|
||||
resId: ProtoField(3, ScalarType.STRING),
|
||||
payload: ProtoField(4, ScalarType.BYTES)
|
||||
};
|
||||
|
||||
export const SendLongMsgReq = {
|
||||
info: ProtoField(2, () => SendLongMsgInfo),
|
||||
settings: ProtoField(15, () => LongMsgSettings)
|
||||
};
|
||||
|
||||
export const SendLongMsgInfo = {
|
||||
type: ProtoField(1, ScalarType.UINT32),
|
||||
uid: ProtoField(2, () => LongMsgUid, true),
|
||||
groupUin: ProtoField(3, ScalarType.UINT32, true),
|
||||
payload: ProtoField(4, ScalarType.BYTES, true)
|
||||
};
|
||||
|
||||
export const SendLongMsgResp = {
|
||||
result: ProtoField(2, () => SendLongMsgResult),
|
||||
settings: ProtoField(15, () => LongMsgSettings)
|
||||
};
|
||||
|
||||
export const SendLongMsgResult = {
|
||||
resId: ProtoField(3, ScalarType.STRING)
|
||||
};
|
||||
|
||||
export const SsoGetGroupMsg = {
|
||||
info: ProtoField(1, () => SsoGetGroupMsgInfo),
|
||||
direction: ProtoField(2, ScalarType.BOOL)
|
||||
};
|
||||
|
||||
export const SsoGetGroupMsgInfo = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
startSequence: ProtoField(2, ScalarType.UINT32),
|
||||
endSequence: ProtoField(3, ScalarType.UINT32)
|
||||
};
|
||||
|
||||
export const SsoGetGroupMsgResponse = {
|
||||
body: ProtoField(3, () => SsoGetGroupMsgResponseBody)
|
||||
};
|
||||
|
||||
export const SsoGetGroupMsgResponseBody = {
|
||||
groupUin: ProtoField(3, ScalarType.UINT32),
|
||||
startSequence: ProtoField(4, ScalarType.UINT32),
|
||||
endSequence: ProtoField(5, ScalarType.UINT32),
|
||||
messages: ProtoField(6, () => PushMsgBody, false, true)
|
||||
};
|
||||
|
||||
export const SsoGetRoamMsg = {
|
||||
friendUid: ProtoField(1, ScalarType.STRING, true),
|
||||
time: ProtoField(2, ScalarType.UINT32),
|
||||
random: ProtoField(3, ScalarType.UINT32),
|
||||
count: ProtoField(4, ScalarType.UINT32),
|
||||
direction: ProtoField(5, ScalarType.BOOL)
|
||||
};
|
||||
|
||||
export const SsoGetRoamMsgResponse = {
|
||||
friendUid: ProtoField(3, ScalarType.STRING),
|
||||
timestamp: ProtoField(5, ScalarType.UINT32),
|
||||
random: ProtoField(6, ScalarType.UINT32),
|
||||
messages: ProtoField(7, () => PushMsgBody, false, true)
|
||||
};
|
||||
|
||||
export const SsoGetC2cMsg = {
|
||||
friendUid: ProtoField(2, ScalarType.STRING, true),
|
||||
startSequence: ProtoField(3, ScalarType.UINT32),
|
||||
endSequence: ProtoField(4, ScalarType.UINT32)
|
||||
};
|
||||
|
||||
export const SsoGetC2cMsgResponse = {
|
||||
friendUid: ProtoField(4, ScalarType.STRING),
|
||||
messages: ProtoField(7, () => PushMsgBody, false, true)
|
||||
};
|
11
src/core/packet/proto/message/c2c.ts
Normal file
11
src/core/packet/proto/message/c2c.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
|
||||
export const C2C = {
|
||||
uin: ProtoField(1, ScalarType.UINT32, true),
|
||||
uid: ProtoField(2, ScalarType.STRING, true),
|
||||
field3: ProtoField(3, ScalarType.UINT32, true),
|
||||
sig: ProtoField(4, ScalarType.UINT32, true),
|
||||
receiverUin: ProtoField(5, ScalarType.UINT32, true),
|
||||
receiverUid: ProtoField(6, ScalarType.STRING, true),
|
||||
};
|
166
src/core/packet/proto/message/component.ts
Normal file
166
src/core/packet/proto/message/component.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
import { Elem } from "@/core/packet/proto/message/element";
|
||||
|
||||
export const Attr = {
|
||||
codePage: ProtoField(1, ScalarType.INT32),
|
||||
time: ProtoField(2, ScalarType.INT32),
|
||||
random: ProtoField(3, ScalarType.INT32),
|
||||
color: ProtoField(4, ScalarType.INT32),
|
||||
size: ProtoField(5, ScalarType.INT32),
|
||||
effect: ProtoField(6, ScalarType.INT32),
|
||||
charSet: ProtoField(7, ScalarType.INT32),
|
||||
pitchAndFamily: ProtoField(8, ScalarType.INT32),
|
||||
fontName: ProtoField(9, ScalarType.STRING),
|
||||
reserveData: ProtoField(10, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const NotOnlineFile = {
|
||||
fileType: ProtoField(1, ScalarType.INT32, true),
|
||||
sig: ProtoField(2, ScalarType.BYTES, true),
|
||||
fileUuid: ProtoField(3, ScalarType.STRING, true),
|
||||
fileMd5: ProtoField(4, ScalarType.BYTES, true),
|
||||
fileName: ProtoField(5, ScalarType.STRING, true),
|
||||
fileSize: ProtoField(6, ScalarType.INT64, true),
|
||||
note: ProtoField(7, ScalarType.BYTES, true),
|
||||
reserved: ProtoField(8, ScalarType.INT32, true),
|
||||
subcmd: ProtoField(9, ScalarType.INT32, true),
|
||||
microCloud: ProtoField(10, ScalarType.INT32, true),
|
||||
bytesFileUrls: ProtoField(11, ScalarType.BYTES, false, true),
|
||||
downloadFlag: ProtoField(12, ScalarType.INT32, true),
|
||||
dangerEvel: ProtoField(50, ScalarType.INT32, true),
|
||||
lifeTime: ProtoField(51, ScalarType.INT32, true),
|
||||
uploadTime: ProtoField(52, ScalarType.INT32, true),
|
||||
absFileType: ProtoField(53, ScalarType.INT32, true),
|
||||
clientType: ProtoField(54, ScalarType.INT32, true),
|
||||
expireTime: ProtoField(55, ScalarType.INT32, true),
|
||||
pbReserve: ProtoField(56, ScalarType.BYTES, true),
|
||||
fileHash: ProtoField(57, ScalarType.STRING, true),
|
||||
};
|
||||
|
||||
export const Ptt = {
|
||||
fileType: ProtoField(1, ScalarType.INT32),
|
||||
srcUin: ProtoField(2, ScalarType.UINT64),
|
||||
fileUuid: ProtoField(3, ScalarType.STRING),
|
||||
fileMd5: ProtoField(4, ScalarType.BYTES),
|
||||
fileName: ProtoField(5, ScalarType.STRING),
|
||||
fileSize: ProtoField(6, ScalarType.INT32),
|
||||
reserve: ProtoField(7, ScalarType.BYTES),
|
||||
fileId: ProtoField(8, ScalarType.INT32),
|
||||
serverIp: ProtoField(9, ScalarType.INT32),
|
||||
serverPort: ProtoField(10, ScalarType.INT32),
|
||||
boolValid: ProtoField(11, ScalarType.BOOL),
|
||||
signature: ProtoField(12, ScalarType.BYTES),
|
||||
shortcut: ProtoField(13, ScalarType.BYTES),
|
||||
fileKey: ProtoField(14, ScalarType.BYTES),
|
||||
magicPttIndex: ProtoField(15, ScalarType.INT32),
|
||||
voiceSwitch: ProtoField(16, ScalarType.INT32),
|
||||
pttUrl: ProtoField(17, ScalarType.BYTES),
|
||||
groupFileKey: ProtoField(18, ScalarType.STRING),
|
||||
time: ProtoField(19, ScalarType.INT32),
|
||||
downPara: ProtoField(20, ScalarType.BYTES),
|
||||
format: ProtoField(29, ScalarType.INT32),
|
||||
pbReserve: ProtoField(30, ScalarType.BYTES),
|
||||
bytesPttUrls: ProtoField(31, ScalarType.BYTES, false, true),
|
||||
downloadFlag: ProtoField(32, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const RichText = {
|
||||
attr: ProtoField(1, () => Attr, true),
|
||||
elems: ProtoField(2, () => Elem, false, true),
|
||||
notOnlineFile: ProtoField(3, () => NotOnlineFile, true),
|
||||
ptt: ProtoField(4, () => Ptt, true),
|
||||
};
|
||||
|
||||
export const ButtonExtra = {
|
||||
data: ProtoField(1, () => KeyboardData),
|
||||
};
|
||||
|
||||
export const KeyboardData = {
|
||||
rows: ProtoField(1, () => Row, false, true),
|
||||
};
|
||||
|
||||
export const Row = {
|
||||
buttons: ProtoField(1, () => Button, false, true),
|
||||
};
|
||||
|
||||
export const Button = {
|
||||
id: ProtoField(1, ScalarType.STRING),
|
||||
renderData: ProtoField(2, () => RenderData),
|
||||
action: ProtoField(3, () => Action),
|
||||
};
|
||||
|
||||
export const RenderData = {
|
||||
label: ProtoField(1, ScalarType.STRING),
|
||||
visitedLabel: ProtoField(2, ScalarType.STRING),
|
||||
style: ProtoField(3, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const Action = {
|
||||
type: ProtoField(1, ScalarType.INT32),
|
||||
permission: ProtoField(2, () => Permission),
|
||||
unsupportTips: ProtoField(4, ScalarType.STRING),
|
||||
data: ProtoField(5, ScalarType.STRING),
|
||||
reply: ProtoField(7, ScalarType.BOOL),
|
||||
enter: ProtoField(8, ScalarType.BOOL),
|
||||
};
|
||||
|
||||
export const Permission = {
|
||||
type: ProtoField(1, ScalarType.INT32),
|
||||
specifyRoleIds: ProtoField(2, ScalarType.STRING, false, true),
|
||||
specifyUserIds: ProtoField(3, ScalarType.STRING, false, true),
|
||||
};
|
||||
|
||||
export const FileExtra = {
|
||||
file: ProtoField(1, () => NotOnlineFile),
|
||||
field6: ProtoField(6, () => PrivateFileExtra),
|
||||
};
|
||||
|
||||
export const PrivateFileExtra = {
|
||||
field2: ProtoField(2, () => PrivateFileExtraField2),
|
||||
};
|
||||
|
||||
export const PrivateFileExtraField2 = {
|
||||
field1: ProtoField(1, ScalarType.UINT32),
|
||||
fileUuid: ProtoField(4, ScalarType.STRING),
|
||||
fileName: ProtoField(5, ScalarType.STRING),
|
||||
field6: ProtoField(6, ScalarType.UINT32),
|
||||
field7: ProtoField(7, ScalarType.BYTES),
|
||||
field8: ProtoField(8, ScalarType.BYTES),
|
||||
timestamp1: ProtoField(9, ScalarType.UINT32),
|
||||
fileHash: ProtoField(14, ScalarType.STRING),
|
||||
selfUid: ProtoField(15, ScalarType.STRING),
|
||||
destUid: ProtoField(16, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const GroupFileExtra = {
|
||||
field1: ProtoField(1, ScalarType.UINT32),
|
||||
fileName: ProtoField(2, ScalarType.STRING),
|
||||
display: ProtoField(3, ScalarType.STRING),
|
||||
inner: ProtoField(7, () => GroupFileExtraInner),
|
||||
};
|
||||
|
||||
export const GroupFileExtraInner = {
|
||||
info: ProtoField(2, () => GroupFileExtraInfo),
|
||||
};
|
||||
|
||||
export const GroupFileExtraInfo = {
|
||||
busId: ProtoField(1, ScalarType.UINT32),
|
||||
fileId: ProtoField(2, ScalarType.STRING),
|
||||
fileSize: ProtoField(3, ScalarType.UINT64),
|
||||
fileName: ProtoField(4, ScalarType.STRING),
|
||||
field5: ProtoField(5, ScalarType.UINT32),
|
||||
fileSha: ProtoField(6, ScalarType.BYTES),
|
||||
extInfoString: ProtoField(7, ScalarType.STRING),
|
||||
fileMd5: ProtoField(8, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const ImageExtraUrl = {
|
||||
origUrl: ProtoField(30, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const PokeExtra = {
|
||||
type: ProtoField(1, ScalarType.UINT32),
|
||||
field7: ProtoField(7, ScalarType.UINT32),
|
||||
field8: ProtoField(8, ScalarType.UINT32),
|
||||
};
|
361
src/core/packet/proto/message/element.ts
Normal file
361
src/core/packet/proto/message/element.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
|
||||
export const Elem = {
|
||||
text: ProtoField(1, () => Text, true),
|
||||
face: ProtoField(2, () => Face, true),
|
||||
onlineImage: ProtoField(3, () => OnlineImage, true),
|
||||
notOnlineImage: ProtoField(4, () => NotOnlineImage, true),
|
||||
transElem: ProtoField(5, () => TransElem, true),
|
||||
marketFace: ProtoField(6, () => MarketFace, true),
|
||||
customFace: ProtoField(8, () => CustomFace, true),
|
||||
elemFlags2: ProtoField(9, () => ElemFlags2, true),
|
||||
richMsg: ProtoField(12, () => RichMsg, true),
|
||||
groupFile: ProtoField(13, () => GroupFile, true),
|
||||
extraInfo: ProtoField(16, () => ExtraInfo, true),
|
||||
videoFile: ProtoField(19, () => VideoFile, true),
|
||||
anonymousGroupMessage: ProtoField(21, () => AnonymousGroupMessage, true),
|
||||
customElem: ProtoField(31, () => CustomElem, true),
|
||||
generalFlags: ProtoField(37, () => GeneralFlags, true),
|
||||
srcMsg: ProtoField(45, () => SrcMsg, true),
|
||||
lightAppElem: ProtoField(51, () => LightAppElem, true),
|
||||
commonElem: ProtoField(53, () => CommonElem, true),
|
||||
};
|
||||
|
||||
export const Text = {
|
||||
str: ProtoField(1, ScalarType.STRING, true),
|
||||
lint: ProtoField(2, ScalarType.STRING, true),
|
||||
attr6Buf: ProtoField(3, ScalarType.BYTES, true),
|
||||
attr7Buf: ProtoField(4, ScalarType.BYTES, true),
|
||||
buf: ProtoField(11, ScalarType.BYTES, true),
|
||||
pbReserve: ProtoField(12, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const Face = {
|
||||
index: ProtoField(1, ScalarType.INT32, true),
|
||||
old: ProtoField(2, ScalarType.BYTES, true),
|
||||
buf: ProtoField(11, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const OnlineImage = {
|
||||
guid: ProtoField(1, ScalarType.BYTES),
|
||||
filePath: ProtoField(2, ScalarType.BYTES),
|
||||
oldVerSendFile: ProtoField(3, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const NotOnlineImage = {
|
||||
filePath: ProtoField(1, ScalarType.STRING),
|
||||
fileLen: ProtoField(2, ScalarType.UINT32),
|
||||
downloadPath: ProtoField(3, ScalarType.STRING),
|
||||
oldVerSendFile: ProtoField(4, ScalarType.BYTES),
|
||||
imgType: ProtoField(5, ScalarType.INT32),
|
||||
previewsImage: ProtoField(6, ScalarType.BYTES),
|
||||
picMd5: ProtoField(7, ScalarType.BYTES),
|
||||
picHeight: ProtoField(8, ScalarType.UINT32),
|
||||
picWidth: ProtoField(9, ScalarType.UINT32),
|
||||
resId: ProtoField(10, ScalarType.STRING),
|
||||
flag: ProtoField(11, ScalarType.BYTES),
|
||||
thumbUrl: ProtoField(12, ScalarType.STRING),
|
||||
original: ProtoField(13, ScalarType.INT32),
|
||||
bigUrl: ProtoField(14, ScalarType.STRING),
|
||||
origUrl: ProtoField(15, ScalarType.STRING),
|
||||
bizType: ProtoField(16, ScalarType.INT32),
|
||||
result: ProtoField(17, ScalarType.INT32),
|
||||
index: ProtoField(18, ScalarType.INT32),
|
||||
opFaceBuf: ProtoField(19, ScalarType.BYTES),
|
||||
oldPicMd5: ProtoField(20, ScalarType.BOOL),
|
||||
thumbWidth: ProtoField(21, ScalarType.INT32),
|
||||
thumbHeight: ProtoField(22, ScalarType.INT32),
|
||||
fileId: ProtoField(23, ScalarType.INT32),
|
||||
showLen: ProtoField(24, ScalarType.UINT32),
|
||||
downloadLen: ProtoField(25, ScalarType.UINT32),
|
||||
x400Url: ProtoField(26, ScalarType.STRING),
|
||||
x400Width: ProtoField(27, ScalarType.INT32),
|
||||
x400Height: ProtoField(28, ScalarType.INT32),
|
||||
pbRes: ProtoField(29, () => NotOnlineImage_PbReserve),
|
||||
};
|
||||
|
||||
export const NotOnlineImage_PbReserve = {
|
||||
subType: ProtoField(1, ScalarType.INT32),
|
||||
field3: ProtoField(3, ScalarType.INT32),
|
||||
field4: ProtoField(4, ScalarType.INT32),
|
||||
summary: ProtoField(8, ScalarType.STRING),
|
||||
field10: ProtoField(10, ScalarType.INT32),
|
||||
field20: ProtoField(20, () => NotOnlineImage_PbReserve2),
|
||||
url: ProtoField(30, ScalarType.STRING),
|
||||
md5Str: ProtoField(31, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const NotOnlineImage_PbReserve2 = {
|
||||
field1: ProtoField(1, ScalarType.INT32),
|
||||
field2: ProtoField(2, ScalarType.STRING),
|
||||
field3: ProtoField(3, ScalarType.INT32),
|
||||
field4: ProtoField(4, ScalarType.INT32),
|
||||
field5: ProtoField(5, ScalarType.INT32),
|
||||
field7: ProtoField(7, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const TransElem = {
|
||||
elemType: ProtoField(1, ScalarType.INT32),
|
||||
elemValue: ProtoField(2, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const MarketFace = {
|
||||
faceName: ProtoField(1, ScalarType.STRING),
|
||||
itemType: ProtoField(2, ScalarType.INT32),
|
||||
faceInfo: ProtoField(3, ScalarType.INT32),
|
||||
faceId: ProtoField(4, ScalarType.BYTES),
|
||||
tabId: ProtoField(5, ScalarType.INT32),
|
||||
subType: ProtoField(6, ScalarType.INT32),
|
||||
key: ProtoField(7, ScalarType.STRING),
|
||||
param: ProtoField(8, ScalarType.BYTES),
|
||||
mediaType: ProtoField(9, ScalarType.INT32),
|
||||
imageWidth: ProtoField(10, ScalarType.INT32),
|
||||
imageHeight: ProtoField(11, ScalarType.INT32),
|
||||
mobileparam: ProtoField(12, ScalarType.BYTES),
|
||||
pbReserve: ProtoField(13, () => MarketFacePbRes),
|
||||
};
|
||||
|
||||
export const MarketFacePbRes = {
|
||||
field8: ProtoField(8, ScalarType.INT32)
|
||||
};
|
||||
|
||||
export const CustomFace = {
|
||||
guid: ProtoField(1, ScalarType.BYTES),
|
||||
filePath: ProtoField(2, ScalarType.STRING),
|
||||
shortcut: ProtoField(3, ScalarType.STRING),
|
||||
buffer: ProtoField(4, ScalarType.BYTES),
|
||||
flag: ProtoField(5, ScalarType.BYTES),
|
||||
oldData: ProtoField(6, ScalarType.BYTES, true),
|
||||
fileId: ProtoField(7, ScalarType.UINT32),
|
||||
serverIp: ProtoField(8, ScalarType.INT32, true),
|
||||
serverPort: ProtoField(9, ScalarType.INT32, true),
|
||||
fileType: ProtoField(10, ScalarType.INT32),
|
||||
signature: ProtoField(11, ScalarType.BYTES),
|
||||
useful: ProtoField(12, ScalarType.INT32),
|
||||
md5: ProtoField(13, ScalarType.BYTES),
|
||||
thumbUrl: ProtoField(14, ScalarType.STRING),
|
||||
bigUrl: ProtoField(15, ScalarType.STRING),
|
||||
origUrl: ProtoField(16, ScalarType.STRING),
|
||||
bizType: ProtoField(17, ScalarType.INT32),
|
||||
repeatIndex: ProtoField(18, ScalarType.INT32),
|
||||
repeatImage: ProtoField(19, ScalarType.INT32),
|
||||
imageType: ProtoField(20, ScalarType.INT32),
|
||||
index: ProtoField(21, ScalarType.INT32),
|
||||
width: ProtoField(22, ScalarType.INT32),
|
||||
height: ProtoField(23, ScalarType.INT32),
|
||||
source: ProtoField(24, ScalarType.INT32),
|
||||
size: ProtoField(25, ScalarType.UINT32),
|
||||
origin: ProtoField(26, ScalarType.INT32),
|
||||
thumbWidth: ProtoField(27, ScalarType.INT32, true),
|
||||
thumbHeight: ProtoField(28, ScalarType.INT32, true),
|
||||
showLen: ProtoField(29, ScalarType.INT32),
|
||||
downloadLen: ProtoField(30, ScalarType.INT32),
|
||||
x400Url: ProtoField(31, ScalarType.STRING, true),
|
||||
x400Width: ProtoField(32, ScalarType.INT32),
|
||||
x400Height: ProtoField(33, ScalarType.INT32),
|
||||
pbRes: ProtoField(34, () => CustomFace_PbReserve, true),
|
||||
};
|
||||
|
||||
export const CustomFace_PbReserve = {
|
||||
subType: ProtoField(1, ScalarType.INT32),
|
||||
summary: ProtoField(9, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const ElemFlags2 = {
|
||||
colorTextId: ProtoField(1, ScalarType.UINT32),
|
||||
msgId: ProtoField(2, ScalarType.UINT64),
|
||||
whisperSessionId: ProtoField(3, ScalarType.UINT32),
|
||||
pttChangeBit: ProtoField(4, ScalarType.UINT32),
|
||||
vipStatus: ProtoField(5, ScalarType.UINT32),
|
||||
compatibleId: ProtoField(6, ScalarType.UINT32),
|
||||
insts: ProtoField(7, () => Instance, false, true),
|
||||
msgRptCnt: ProtoField(8, ScalarType.UINT32),
|
||||
srcInst: ProtoField(9, () => Instance),
|
||||
longtitude: ProtoField(10, ScalarType.UINT32),
|
||||
latitude: ProtoField(11, ScalarType.UINT32),
|
||||
customFont: ProtoField(12, ScalarType.UINT32),
|
||||
pcSupportDef: ProtoField(13, () => PcSupportDef),
|
||||
crmFlags: ProtoField(14, ScalarType.UINT32, true),
|
||||
};
|
||||
|
||||
export const PcSupportDef = {
|
||||
pcPtlBegin: ProtoField(1, ScalarType.UINT32),
|
||||
pcPtlEnd: ProtoField(2, ScalarType.UINT32),
|
||||
macPtlBegin: ProtoField(3, ScalarType.UINT32),
|
||||
macPtlEnd: ProtoField(4, ScalarType.UINT32),
|
||||
ptlsSupport: ProtoField(5, ScalarType.INT32, false, true),
|
||||
ptlsNotSupport: ProtoField(6, ScalarType.UINT32, false, true),
|
||||
};
|
||||
|
||||
export const Instance = {
|
||||
appId: ProtoField(1, ScalarType.UINT32),
|
||||
instId: ProtoField(2, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const RichMsg = {
|
||||
template1: ProtoField(1, ScalarType.BYTES, true),
|
||||
serviceId: ProtoField(2, ScalarType.INT32, true),
|
||||
msgResId: ProtoField(3, ScalarType.BYTES, true),
|
||||
rand: ProtoField(4, ScalarType.INT32, true),
|
||||
seq: ProtoField(5, ScalarType.UINT32, true),
|
||||
};
|
||||
|
||||
export const GroupFile = {
|
||||
filename: ProtoField(1, ScalarType.BYTES),
|
||||
fileSize: ProtoField(2, ScalarType.UINT64),
|
||||
fileId: ProtoField(3, ScalarType.BYTES),
|
||||
batchId: ProtoField(4, ScalarType.BYTES),
|
||||
fileKey: ProtoField(5, ScalarType.BYTES),
|
||||
mark: ProtoField(6, ScalarType.BYTES),
|
||||
sequence: ProtoField(7, ScalarType.UINT64),
|
||||
batchItemId: ProtoField(8, ScalarType.BYTES),
|
||||
feedMsgTime: ProtoField(9, ScalarType.INT32),
|
||||
pbReserve: ProtoField(10, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const ExtraInfo = {
|
||||
nick: ProtoField(1, ScalarType.BYTES),
|
||||
groupCard: ProtoField(2, ScalarType.BYTES),
|
||||
level: ProtoField(3, ScalarType.INT32),
|
||||
flags: ProtoField(4, ScalarType.INT32),
|
||||
groupMask: ProtoField(5, ScalarType.INT32),
|
||||
msgTailId: ProtoField(6, ScalarType.INT32),
|
||||
senderTitle: ProtoField(7, ScalarType.BYTES),
|
||||
apnsTips: ProtoField(8, ScalarType.BYTES),
|
||||
uin: ProtoField(9, ScalarType.UINT64),
|
||||
msgStateFlag: ProtoField(10, ScalarType.INT32),
|
||||
apnsSoundType: ProtoField(11, ScalarType.INT32),
|
||||
newGroupFlag: ProtoField(12, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const VideoFile = {
|
||||
fileUuid: ProtoField(1, ScalarType.STRING),
|
||||
fileMd5: ProtoField(2, ScalarType.BYTES),
|
||||
fileName: ProtoField(3, ScalarType.STRING),
|
||||
fileFormat: ProtoField(4, ScalarType.INT32),
|
||||
fileTime: ProtoField(5, ScalarType.INT32),
|
||||
fileSize: ProtoField(6, ScalarType.INT32),
|
||||
thumbWidth: ProtoField(7, ScalarType.INT32),
|
||||
thumbHeight: ProtoField(8, ScalarType.INT32),
|
||||
thumbFileMd5: ProtoField(9, ScalarType.BYTES),
|
||||
source: ProtoField(10, ScalarType.BYTES),
|
||||
thumbFileSize: ProtoField(11, ScalarType.INT32),
|
||||
busiType: ProtoField(12, ScalarType.INT32),
|
||||
fromChatType: ProtoField(13, ScalarType.INT32),
|
||||
toChatType: ProtoField(14, ScalarType.INT32),
|
||||
boolSupportProgressive: ProtoField(15, ScalarType.BOOL),
|
||||
fileWidth: ProtoField(16, ScalarType.INT32),
|
||||
fileHeight: ProtoField(17, ScalarType.INT32),
|
||||
subBusiType: ProtoField(18, ScalarType.INT32),
|
||||
videoAttr: ProtoField(19, ScalarType.INT32),
|
||||
bytesThumbFileUrls: ProtoField(20, ScalarType.BYTES, false, true),
|
||||
bytesVideoFileUrls: ProtoField(21, ScalarType.BYTES, false, true),
|
||||
thumbDownloadFlag: ProtoField(22, ScalarType.INT32),
|
||||
videoDownloadFlag: ProtoField(23, ScalarType.INT32),
|
||||
pbReserve: ProtoField(24, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const AnonymousGroupMessage = {
|
||||
flags: ProtoField(1, ScalarType.INT32),
|
||||
anonId: ProtoField(2, ScalarType.BYTES),
|
||||
anonNick: ProtoField(3, ScalarType.BYTES),
|
||||
headPortrait: ProtoField(4, ScalarType.INT32),
|
||||
expireTime: ProtoField(5, ScalarType.INT32),
|
||||
bubbleId: ProtoField(6, ScalarType.INT32),
|
||||
rankColor: ProtoField(7, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const CustomElem = {
|
||||
desc: ProtoField(1, ScalarType.BYTES),
|
||||
data: ProtoField(2, ScalarType.BYTES),
|
||||
enumType: ProtoField(3, ScalarType.INT32),
|
||||
ext: ProtoField(4, ScalarType.BYTES),
|
||||
sound: ProtoField(5, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const GeneralFlags = {
|
||||
bubbleDiyTextId: ProtoField(1, ScalarType.INT32),
|
||||
groupFlagNew: ProtoField(2, ScalarType.INT32),
|
||||
uin: ProtoField(3, ScalarType.UINT64),
|
||||
rpId: ProtoField(4, ScalarType.BYTES),
|
||||
prpFold: ProtoField(5, ScalarType.INT32),
|
||||
longTextFlag: ProtoField(6, ScalarType.INT32),
|
||||
longTextResId: ProtoField(7, ScalarType.STRING, true),
|
||||
groupType: ProtoField(8, ScalarType.INT32),
|
||||
toUinFlag: ProtoField(9, ScalarType.INT32),
|
||||
glamourLevel: ProtoField(10, ScalarType.INT32),
|
||||
memberLevel: ProtoField(11, ScalarType.INT32),
|
||||
groupRankSeq: ProtoField(12, ScalarType.UINT64),
|
||||
olympicTorch: ProtoField(13, ScalarType.INT32),
|
||||
babyqGuideMsgCookie: ProtoField(14, ScalarType.BYTES),
|
||||
uin32ExpertFlag: ProtoField(15, ScalarType.INT32),
|
||||
bubbleSubId: ProtoField(16, ScalarType.INT32),
|
||||
pendantId: ProtoField(17, ScalarType.UINT64),
|
||||
rpIndex: ProtoField(18, ScalarType.BYTES),
|
||||
pbReserve: ProtoField(19, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const SrcMsg = {
|
||||
origSeqs: ProtoField(1, ScalarType.UINT32, false, true),
|
||||
senderUin: ProtoField(2, ScalarType.UINT64),
|
||||
time: ProtoField(3, ScalarType.INT32, true),
|
||||
flag: ProtoField(4, ScalarType.INT32, true),
|
||||
elems: ProtoField(5, () => Elem, false, true),
|
||||
type: ProtoField(6, ScalarType.INT32, true),
|
||||
richMsg: ProtoField(7, ScalarType.BYTES, true),
|
||||
pbReserve: ProtoField(8, () => SrcMsgPbRes, true),
|
||||
sourceMsg: ProtoField(9, ScalarType.BYTES, true),
|
||||
toUin: ProtoField(10, ScalarType.UINT64, true),
|
||||
troopName: ProtoField(11, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const SrcMsgPbRes = {
|
||||
messageId: ProtoField(3, ScalarType.UINT64),
|
||||
senderUid: ProtoField(6, ScalarType.STRING, true),
|
||||
receiverUid: ProtoField(7, ScalarType.STRING, true),
|
||||
friendSeq: ProtoField(8, ScalarType.UINT32, true),
|
||||
};
|
||||
|
||||
export const LightAppElem = {
|
||||
data: ProtoField(1, ScalarType.BYTES),
|
||||
msgResid: ProtoField(2, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const CommonElem = {
|
||||
serviceType: ProtoField(1, ScalarType.INT32),
|
||||
pbElem: ProtoField(2, ScalarType.BYTES),
|
||||
businessType: ProtoField(3, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const FaceExtra = {
|
||||
faceId: ProtoField(1, ScalarType.INT32, true),
|
||||
};
|
||||
|
||||
export const MentionExtra = {
|
||||
type: ProtoField(3, ScalarType.INT32, true),
|
||||
uin: ProtoField(4, ScalarType.UINT32, true),
|
||||
field5: ProtoField(5, ScalarType.INT32, true),
|
||||
uid: ProtoField(9, ScalarType.STRING, true),
|
||||
};
|
||||
|
||||
export const QBigFaceExtra = {
|
||||
AniStickerPackId: ProtoField(1, ScalarType.STRING, true),
|
||||
AniStickerId: ProtoField(2, ScalarType.STRING, true),
|
||||
faceId: ProtoField(3, ScalarType.INT32, true),
|
||||
Field4: ProtoField(4, ScalarType.INT32, true),
|
||||
AniStickerType: ProtoField(5, ScalarType.INT32, true),
|
||||
field6: ProtoField(6, ScalarType.STRING, true),
|
||||
preview: ProtoField(7, ScalarType.STRING, true),
|
||||
field9: ProtoField(9, ScalarType.INT32, true),
|
||||
};
|
||||
|
||||
export const QSmallFaceExtra = {
|
||||
faceId: ProtoField(1, ScalarType.UINT32),
|
||||
preview: ProtoField(2, ScalarType.STRING),
|
||||
preview2: ProtoField(3, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const MarkdownData = {
|
||||
content: ProtoField(1, ScalarType.STRING)
|
||||
};
|
19
src/core/packet/proto/message/group.ts
Normal file
19
src/core/packet/proto/message/group.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
|
||||
export const GroupRecallMsg = {
|
||||
type: ProtoField(1, ScalarType.UINT32),
|
||||
groupUin: ProtoField(2, ScalarType.UINT32),
|
||||
field3: ProtoField(3, () => GroupRecallMsgField3),
|
||||
field4: ProtoField(4, () => GroupRecallMsgField4),
|
||||
};
|
||||
|
||||
export const GroupRecallMsgField3 = {
|
||||
sequence: ProtoField(1, ScalarType.UINT32),
|
||||
random: ProtoField(2, ScalarType.UINT32),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const GroupRecallMsgField4 = {
|
||||
field1: ProtoField(1, ScalarType.UINT32),
|
||||
};
|
75
src/core/packet/proto/message/message.ts
Normal file
75
src/core/packet/proto/message/message.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
import { ForwardHead, Grp, GrpTmp, ResponseForward, ResponseGrp, Trans0X211, WPATmp } from "@/core/packet/proto/message/routing";
|
||||
import { RichText } from "@/core/packet/proto/message/component";
|
||||
import { C2C } from "@/core/packet/proto/message/c2c";
|
||||
|
||||
export const ContentHead = {
|
||||
type: ProtoField(1, ScalarType.UINT32),
|
||||
subType: ProtoField(2, ScalarType.UINT32, true),
|
||||
divSeq: ProtoField(3, ScalarType.UINT32, true),
|
||||
msgId: ProtoField(4, ScalarType.UINT32, true),
|
||||
sequence: ProtoField(5, ScalarType.UINT32, true),
|
||||
timeStamp: ProtoField(6, ScalarType.UINT32, true),
|
||||
field7: ProtoField(7, ScalarType.UINT64, true),
|
||||
field8: ProtoField(8, ScalarType.UINT32, true),
|
||||
field9: ProtoField(9, ScalarType.UINT32, true),
|
||||
newId: ProtoField(12, ScalarType.UINT64, true),
|
||||
forward: ProtoField(15, () => ForwardHead, true),
|
||||
};
|
||||
|
||||
export const MessageBody = {
|
||||
richText: ProtoField(1, () => RichText, true),
|
||||
msgContent: ProtoField(2, ScalarType.BYTES, true),
|
||||
msgEncryptContent: ProtoField(3, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const Message = {
|
||||
routingHead: ProtoField(1, () => RoutingHead, true),
|
||||
contentHead: ProtoField(2, () => ContentHead, true),
|
||||
body: ProtoField(3, () => MessageBody, true),
|
||||
clientSequence: ProtoField(4, ScalarType.UINT32, true),
|
||||
random: ProtoField(5, ScalarType.UINT32, true),
|
||||
syncCookie: ProtoField(6, ScalarType.BYTES, true),
|
||||
via: ProtoField(8, ScalarType.UINT32, true),
|
||||
dataStatist: ProtoField(9, ScalarType.UINT32, true),
|
||||
ctrl: ProtoField(12, () => MessageControl, true),
|
||||
multiSendSeq: ProtoField(14, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const MessageControl = {
|
||||
msgFlag: ProtoField(1, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const PushMsg = {
|
||||
message: ProtoField(1, () => PushMsgBody),
|
||||
status: ProtoField(3, ScalarType.INT32, true),
|
||||
pingFlag: ProtoField(5, ScalarType.INT32, true),
|
||||
generalFlag: ProtoField(9, ScalarType.INT32, true),
|
||||
};
|
||||
|
||||
export const PushMsgBody = {
|
||||
responseHead: ProtoField(1, () => ResponseHead),
|
||||
contentHead: ProtoField(2, () => ContentHead),
|
||||
body: ProtoField(3, () => MessageBody, true),
|
||||
};
|
||||
|
||||
export const ResponseHead = {
|
||||
fromUin: ProtoField(1, ScalarType.UINT32),
|
||||
fromUid: ProtoField(2, ScalarType.STRING, true),
|
||||
type: ProtoField(3, ScalarType.UINT32),
|
||||
sigMap: ProtoField(4, ScalarType.UINT32),
|
||||
toUin: ProtoField(5, ScalarType.UINT32),
|
||||
toUid: ProtoField(6, ScalarType.STRING, true),
|
||||
forward: ProtoField(7, () => ResponseForward, true),
|
||||
grp: ProtoField(8, () => ResponseGrp, true),
|
||||
};
|
||||
|
||||
export const RoutingHead = {
|
||||
c2c: ProtoField(1, () => C2C, true),
|
||||
grp: ProtoField(2, () => Grp, true),
|
||||
grpTmp: ProtoField(3, () => GrpTmp, true),
|
||||
wpaTmp: ProtoField(6, () => WPATmp, true),
|
||||
trans0X211: ProtoField(15, () => Trans0X211, true),
|
||||
};
|
||||
|
22
src/core/packet/proto/message/notify.ts
Normal file
22
src/core/packet/proto/message/notify.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
|
||||
export const FriendRecall = {
|
||||
info: ProtoField(1, () => FriendRecallInfo),
|
||||
instId: ProtoField(2, ScalarType.UINT32),
|
||||
appId: ProtoField(3, ScalarType.UINT32),
|
||||
longMessageFlag: ProtoField(4, ScalarType.UINT32),
|
||||
reserved: ProtoField(5, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const FriendRecallInfo = {
|
||||
fromUid: ProtoField(1, ScalarType.STRING),
|
||||
toUid: ProtoField(2, ScalarType.STRING),
|
||||
sequence: ProtoField(3, ScalarType.UINT32),
|
||||
newId: ProtoField(4, ScalarType.UINT64),
|
||||
time: ProtoField(5, ScalarType.UINT32),
|
||||
random: ProtoField(6, ScalarType.UINT32),
|
||||
pkgNum: ProtoField(7, ScalarType.UINT32),
|
||||
pkgIndex: ProtoField(8, ScalarType.UINT32),
|
||||
divSeq: ProtoField(9, ScalarType.UINT32),
|
||||
};
|
41
src/core/packet/proto/message/routing.ts
Normal file
41
src/core/packet/proto/message/routing.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
|
||||
export const ForwardHead = {
|
||||
field1: ProtoField(1, ScalarType.UINT32, true),
|
||||
field2: ProtoField(2, ScalarType.UINT32, true),
|
||||
field3: ProtoField(3, ScalarType.UINT32, true),
|
||||
unknownBase64: ProtoField(5, ScalarType.STRING, true),
|
||||
avatar: ProtoField(6, ScalarType.STRING, true),
|
||||
};
|
||||
|
||||
export const Grp = {
|
||||
groupCode: ProtoField(1, ScalarType.UINT32, true),
|
||||
};
|
||||
|
||||
export const GrpTmp = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32, true),
|
||||
toUin: ProtoField(2, ScalarType.UINT32, true),
|
||||
};
|
||||
|
||||
export const ResponseForward = {
|
||||
friendName: ProtoField(6, ScalarType.STRING, true),
|
||||
};
|
||||
|
||||
export const ResponseGrp = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
memberName: ProtoField(4, ScalarType.STRING),
|
||||
unknown5: ProtoField(5, ScalarType.UINT32),
|
||||
groupName: ProtoField(7, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const Trans0X211 = {
|
||||
toUin: ProtoField(1, ScalarType.UINT64, true),
|
||||
ccCmd: ProtoField(2, ScalarType.UINT32, true),
|
||||
uid: ProtoField(8, ScalarType.STRING, true),
|
||||
};
|
||||
|
||||
export const WPATmp = {
|
||||
toUin: ProtoField(1, ScalarType.UINT64),
|
||||
sig: ProtoField(2, ScalarType.BYTES),
|
||||
};
|
62
src/core/packet/proto/oidb/Oidb.0XE37_800.ts
Normal file
62
src/core/packet/proto/oidb/Oidb.0XE37_800.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { ScalarType } from "@protobuf-ts/runtime";
|
||||
import { ProtoField } from "../NapProto";
|
||||
import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
|
||||
|
||||
export const OidbSvcTrpcTcp0XE37_800 = {
|
||||
subCommand: ProtoField(1, ScalarType.UINT32),
|
||||
field2: ProtoField(2, ScalarType.INT32),
|
||||
body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800Body, true),
|
||||
field101: ProtoField(101, ScalarType.INT32),
|
||||
field102: ProtoField(102, ScalarType.INT32),
|
||||
field200: ProtoField(200, ScalarType.INT32)
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0XE37_800Body = {
|
||||
senderUid: ProtoField(10, ScalarType.STRING, true),
|
||||
receiverUid: ProtoField(20, ScalarType.STRING, true),
|
||||
fileUuid: ProtoField(30, ScalarType.STRING, true),
|
||||
fileHash: ProtoField(40, ScalarType.STRING, true)
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0XE37Response = {
|
||||
command: ProtoField(1, ScalarType.UINT32),
|
||||
seq: ProtoField(2, ScalarType.INT32),
|
||||
upload: ProtoField(19, () => ApplyUploadRespV3, true),
|
||||
businessId: ProtoField(101, ScalarType.INT32),
|
||||
clientType: ProtoField(102, ScalarType.INT32),
|
||||
flagSupportMediaPlatform: ProtoField(200, ScalarType.INT32)
|
||||
};
|
||||
|
||||
export const ApplyUploadRespV3 = {
|
||||
retCode: ProtoField(10, ScalarType.INT32),
|
||||
retMsg: ProtoField(20, ScalarType.STRING, true),
|
||||
totalSpace: ProtoField(30, ScalarType.INT64),
|
||||
usedSpace: ProtoField(40, ScalarType.INT64),
|
||||
uploadedSize: ProtoField(50, ScalarType.INT64),
|
||||
uploadIp: ProtoField(60, ScalarType.STRING, true),
|
||||
uploadDomain: ProtoField(70, ScalarType.STRING, true),
|
||||
uploadPort: ProtoField(80, ScalarType.UINT32),
|
||||
uuid: ProtoField(90, ScalarType.STRING, true),
|
||||
uploadKey: ProtoField(100, ScalarType.BYTES, true),
|
||||
boolFileExist: ProtoField(110, ScalarType.BOOL),
|
||||
packSize: ProtoField(120, ScalarType.INT32),
|
||||
uploadIpList: ProtoField(130, ScalarType.STRING, false, true), // repeated
|
||||
uploadHttpsPort: ProtoField(140, ScalarType.INT32),
|
||||
uploadHttpsDomain: ProtoField(150, ScalarType.STRING, true),
|
||||
uploadDns: ProtoField(160, ScalarType.STRING, true),
|
||||
uploadLanip: ProtoField(170, ScalarType.STRING, true),
|
||||
fileAddon: ProtoField(200, ScalarType.STRING, true),
|
||||
mediaPlatformUploadKey: ProtoField(220, ScalarType.BYTES, true)
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0XE37_800Response = {
|
||||
command: ProtoField(1, ScalarType.UINT32, true),
|
||||
subCommand: ProtoField(2, ScalarType.UINT32, true),
|
||||
body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800ResponseBody, true),
|
||||
field50: ProtoField(50, ScalarType.UINT32, true),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0XE37_800ResponseBody = {
|
||||
field10: ProtoField(10, ScalarType.UINT32, true),
|
||||
field30: ProtoField(30, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true),
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user