Compare commits

...

213 Commits

Author SHA1 Message Date
手瓜一十雪
d672680c4c feat: 复活吧我的arm64 2024-11-05 21:17:07 +08:00
手瓜一十雪
b89f2805e7 feat: linux.x64'packet 2024-11-05 21:12:19 +08:00
手瓜一十雪
78b4aa9295 feat: linux x64 support 2024-11-05 21:11:41 +08:00
手瓜一十雪
0a06637e78 style: lint 2024-11-05 20:59:18 +08:00
手瓜一十雪
13afa2c7ab fix: 去掉开发日志 2024-11-05 20:54:39 +08:00
手瓜一十雪
51d34d17cc feat: 去掉无用日志 2024-11-05 20:52:36 +08:00
手瓜一十雪
18a99341d5 Merge pull request #494 from NapNeko/dependabot/npm_and_yarn/vite-tsconfig-paths-5.1.0
chore(deps-dev): bump vite-tsconfig-paths from 4.3.2 to 5.1.0
2024-11-05 20:50:59 +08:00
手瓜一十雪
f01c8f0110 Merge pull request #493 from NapNeko/multi-packet
refactor: automatically select the optimal packet backend
2024-11-05 20:50:36 +08:00
手瓜一十雪
d8070eee2a fix: LL 2024-11-05 20:49:16 +08:00
手瓜一十雪
8519b7f4df update: LiteLoader 2024-11-05 20:48:28 +08:00
手瓜一十雪
591ab1b1df feat: 去掉开发输出 2024-11-05 20:40:25 +08:00
手瓜一十雪
393815b11e fix 2024-11-05 20:33:55 +08:00
dependabot[bot]
341a397bc4 chore(deps-dev): bump vite-tsconfig-paths from 4.3.2 to 5.1.0
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 4.3.2 to 5.1.0.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v4.3.2...v5.1.0)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 08:39:26 +00:00
pk5ls20
e46d274a75 feat: add new NapCat config key: packetBackend
- Acceptable values: `native`, `frida`, `auto`, `disable`
- Default value is set to `auto`
2024-11-05 14:45:02 +08:00
pk5ls20
ad6f21980c refactor: auto judge client 2024-11-05 14:24:54 +08:00
pk5ls20
017b8b7f15 chore: better log 2024-11-05 13:52:56 +08:00
pk5ls20
9b448b17e6 refactor: NapProto -> https://github.com/NapNeko/NapProto 2024-11-05 12:47:28 +08:00
手瓜一十雪
f9996a9987 fix: 日志乱飞版本 2024-11-05 11:24:36 +08:00
手瓜一十雪
000ef55273 fix: 一点小问题 2024-11-05 10:25:41 +08:00
手瓜一十雪
e1ac0f02b4 fix: 搞炸了 让我思考下 2024-11-05 10:22:52 +08:00
手瓜一十雪
b9297e3f1d fix 2024-11-05 10:18:11 +08:00
手瓜一十雪
34d0669ca8 fix 2024-11-05 10:16:06 +08:00
手瓜一十雪
25e42720cf fix: 开始初步调试 2024-11-05 10:14:00 +08:00
手瓜一十雪
f7c1951191 fix: 一些异常类型 2024-11-05 10:07:56 +08:00
pk5ls20
479b971b0c refactor: automatically select the optimal packet backend 2024-11-04 23:52:52 +08:00
手瓜一十雪
347ba5f354 feat: 初步封装 NativePacketClient 2024-11-04 21:09:11 +08:00
pk5ls20
81dbb9d980 perf: use cache in NapProto 2024-11-04 14:42:16 +08:00
pk5ls20
c4e1a3ab04 fix: SendGroupAiRecord wrong return id 2024-11-04 14:41:16 +08:00
pk5ls20
90ec774a21 feat: better mface toPreview 2024-11-03 17:29:50 +08:00
手瓜一十雪
db7a27e624 style: lint 2024-11-03 12:13:56 +08:00
Mlikiowa
f7d965eda2 release: v3.4.7 2024-11-03 01:50:09 +00:00
手瓜一十雪
74ca2e2e16 Merge pull request #485 from clansty/revert/get_forward_msg
revert: 还原 ob11 风格 get_forward_msg
2024-11-03 09:48:07 +08:00
Clansty
8ab550f2f5 revert: 还原 ob11 风格 get_forward_msg 2024-11-03 09:44:35 +08:00
pk5ls20
018aca4db2 fix: type hint 2024-11-03 02:45:58 +08:00
Mlikiowa
d4327166c1 release: v3.4.6 2024-11-02 05:20:34 +00:00
手瓜一十雪
fa25d2e779 feat: arm64 29271 2024-11-02 13:19:13 +08:00
手瓜一十雪
3ce1c3f0ec feat: support 3.2.13-29271-x64 2024-11-02 10:58:36 +08:00
手瓜一十雪
96dff5141e feat: packet 29271 2024-11-02 10:46:15 +08:00
手瓜一十雪
78d85d9965 feat: 29271 2024-11-02 10:02:39 +08:00
手瓜一十雪
37ec455b02 Merge pull request #484 from NapNeko/feat/ai-voice
feat: ai voice
2024-11-02 08:15:05 +08:00
pk5ls20
6ab82739a6 feat: ai voice 2024-11-02 01:51:57 +08:00
手瓜一十雪
a36917e7c0 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-01 12:13:24 +08:00
手瓜一十雪
21f3428b36 feat: support 6.9.58-28971 2024-11-01 12:13:04 +08:00
Mlikiowa
f8a487db25 release: v3.4.5 2024-10-31 12:30:25 +00:00
手瓜一十雪
73a859be04 fix: report self 2024-10-31 20:30:02 +08:00
手瓜一十雪
63bcee01a1 fix: report self 2024-10-31 20:27:17 +08:00
Mlikiowa
85b4966ba8 release: v3.4.4 2024-10-31 11:32:42 +00:00
手瓜一十雪
36c2c567b7 fix: #444 2024-10-31 19:32:10 +08:00
Mlikiowa
7b1ac224f6 release: v3.4.3 2024-10-31 10:18:21 +00:00
手瓜一十雪
34d9f04f15 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-31 18:17:53 +08:00
手瓜一十雪
be5da7cc6f fix: 正向ws异常推送事件问题 2024-10-31 18:17:41 +08:00
Mlikiowa
8d32ccb5d4 release: v3.4.2 2024-10-31 10:03:19 +00:00
手瓜一十雪
6acceb884c fix: #473 2024-10-31 18:00:55 +08:00
Hao Guan
4c834fd640 chore: Major获取Appid添加提示 (#480) 2024-10-31 16:10:27 +08:00
Mlikiowa
301278c7a9 release: v3.4.1 2024-10-31 00:36:50 +00:00
凌莞~(=^▽^=)
42ee83c54f feat: GetStrangerInfo 加回以前的完整信息 (#479) 2024-10-31 07:25:13 +08:00
Mlikiowa
e631f69621 release: v3.4.0 2024-10-30 13:11:06 +00:00
Nepenthe
ce8760a39a 修复<get_record>接口 (#478) 2024-10-30 21:09:32 +08:00
Mlikiowa
ff952956de release: v3.3.27 2024-10-30 07:28:17 +00:00
手瓜一十雪
28f3ff4971 fix: reply msg 大坐牢 #452 #477 2024-10-30 15:27:26 +08:00
手瓜一十雪
19e728c3cb feat: 提升全平台兼容性 2024-10-30 13:50:47 +08:00
Mlikiowa
269773ed6b release: v3.3.26 2024-10-30 01:23:01 +00:00
Hao Guan
e0d32417e1 chore: AppID for macOS 6.9.58-28971 (#476) 2024-10-30 09:20:32 +08:00
pk5ls20
9fa6083bed refactor: kill any (#475)
* refactor: kill any stage 1

* refactor: kill any stage 2

* refactor: kill any stage 3
2024-10-30 09:10:30 +08:00
手瓜一十雪
4d2fccdfb4 style: lint 2024-10-29 18:48:20 +08:00
Mlikiowa
c1c4bdfe94 release: v3.3.25 2024-10-29 10:42:12 +00:00
手瓜一十雪
8a0e9e8b61 release: v3.3.25 2024-10-29 18:41:39 +08:00
手瓜一十雪
1190e14171 docs: 调整文档优先级 2024-10-29 14:25:36 +08:00
Mlikiowa
00292b177a release: v3.3.22 2024-10-29 02:56:37 +00:00
手瓜一十雪
88de57f984 Merge pull request #472 from pohgxz/main
完善<set_input_status>接口
2024-10-29 10:53:29 +08:00
手瓜一十雪
61ddf38892 fix: Error 2024-10-29 10:52:50 +08:00
Nepenthe
52b3540ec3 修改<get_profile_like>接口 2024-10-29 07:51:16 +08:00
Nepenthe
5f831958c3 完善<set_input_status>接口 2024-10-28 23:21:49 +08:00
手瓜一十雪
c3d4698af3 try fix: error 2024-10-28 21:34:13 +08:00
Mlikiowa
bd6e83217d release: v3.3.21 2024-10-28 04:05:30 +00:00
pk5ls20
50ec49d9a2 feat: GetMiniAppArk 2024-10-28 10:12:24 +08:00
pk5ls20
dc3a089070 chore: rename msg to message in packet module 2024-10-28 07:59:24 +08:00
Mlikiowa
530e380178 release: v3.3.20 2024-10-27 14:46:24 +00:00
手瓜一十雪
10e4387add fix: script 2024-10-27 22:45:51 +08:00
手瓜一十雪
e925bc3aa8 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 22:44:04 +08:00
手瓜一十雪
427b3a7560 release: v3.3.18 2024-10-27 22:43:55 +08:00
Version
c8da950725 chore:version change 2024-10-27 14:42:23 +00:00
手瓜一十雪
743c5b8196 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 22:41:56 +08:00
手瓜一十雪
5e62abea57 fix: version 控制 2024-10-27 22:41:46 +08:00
Version
6bfc545582 chore:version change 2024-10-27 14:37:13 +00:00
手瓜一十雪
411108a2d2 fix: version check 2024-10-27 22:36:48 +08:00
Version
308a6fa9e4 chore:version change 2024-10-27 14:33:40 +00:00
Version
2dc7b785d0 chore:version change 2024-10-27 14:33:19 +00:00
手瓜一十雪
0e69e9e839 fix: checkVersion 2024-10-27 22:32:52 +08:00
手瓜一十雪
b83229b5da feat: 自动化版本发布控制 2024-10-27 22:30:01 +08:00
手瓜一十雪
6f053f5f7d feat: 我补药要手动release啦! 2024-10-27 22:20:11 +08:00
手瓜一十雪
c3dc53eaaf release: v3.3.12 2024-10-27 22:14:17 +08:00
手瓜一十雪
ffdc34cfe2 Merge pull request #470 from pohgxz/main
修复<get_group_at_all_remain>接口总是返回<Error: atInfo not found>
2024-10-27 22:10:55 +08:00
手瓜一十雪
4825a0e341 fix: type Error 2024-10-27 22:07:11 +08:00
Nepenthe
95a00d7f35 修复<get_group_at_all_remain>接口总是返回<Error: atInfo not found> 2024-10-27 22:04:48 +08:00
手瓜一十雪
d885bab426 feat: 类型修复 2024-10-27 22:03:22 +08:00
手瓜一十雪
e2a6a0bc02 release: v3.2.12 2024-10-27 20:56:27 +08:00
手瓜一十雪
ff7d8609ce fix: #452 修复seq搜索的老问题 可能修好了 2024-10-27 20:52:09 +08:00
手瓜一十雪
7507b90e03 fix: #458 2024-10-27 20:38:02 +08:00
手瓜一十雪
2b226a4b27 release: v3.1.11 2024-10-27 19:29:42 +08:00
手瓜一十雪
8b0232c4fe fix: error 2024-10-27 11:17:01 +08:00
手瓜一十雪
0728ee9ad6 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 11:09:00 +08:00
手瓜一十雪
8c6f04d0bc feat: #469 回收连接(未测试) 2024-10-27 11:08:48 +08:00
pk5ls20
c67fad789e fix: compatibility bigint 2024-10-27 10:50:48 +08:00
手瓜一十雪
4072339d70 release: v3.1.10 2024-10-27 10:03:07 +08:00
手瓜一十雪
3a244f5804 style: lint 2024-10-27 10:02:42 +08:00
pk5ls20
f12cf59137 feat: enhance compatibility of upload_forward_msg with go-cqhttp 2024-10-27 09:59:38 +08:00
手瓜一十雪
c76f556a11 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 09:44:26 +08:00
手瓜一十雪
e0f3d07b98 release: 3.1.9 2024-10-27 09:44:14 +08:00
手瓜一十雪
378d85dc67 Merge pull request #468 from NapNeko/refactor/msg-element
refactor: core msg entity & packet msg converter & resolve #455
2024-10-27 09:40:41 +08:00
pk5ls20
875e91fc0e chore: simplify logic 2024-10-27 09:37:17 +08:00
pk5ls20
15f7cd9814 feat: better fake forwardMsg logic & display 2024-10-27 09:33:20 +08:00
pk5ls20
1eb5cd6237 fix: downloadRawMsgMedia edge case 2024-10-27 09:04:24 +08:00
pk5ls20
ad2f843c8f fix: downloadRawMsgMedia 2024-10-27 07:31:32 +08:00
pk5ls20
8e550e216e chore: i18n for packet log messages 2024-10-27 07:04:53 +08:00
pk5ls20
9f07b07c82 feat: support node id in fake forward (with auto download richMedia in reference message) 2024-10-27 06:50:59 +08:00
pk5ls20
0be6effc32 feat: support node id in fake forward (broken impl) 2024-10-27 05:19:53 +08:00
pk5ls20
7ab6a10fc9 refactor & fix: refactor msg entity & adjust some wrong definition 2024-10-27 04:16:15 +08:00
手瓜一十雪
fb09af0e64 release: v3.1.8 2024-10-26 21:25:03 +08:00
手瓜一十雪
0d99d30b2d feat: version hint 2024-10-26 21:24:24 +08:00
手瓜一十雪
0000ec8b5b fix: fetchFavEmojiList 2024-10-26 21:15:11 +08:00
手瓜一十雪
0085bd8a1f fix: q-gate 2024-10-26 20:46:38 +08:00
手瓜一十雪
617139dfa4 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-26 20:36:59 +08:00
手瓜一十雪
4eb4a612d0 fix: type 2024-10-26 20:25:34 +08:00
pk5ls20
cda5e784f6 fix: payload basic check in GetPacketStatusDepends 2024-10-26 19:51:43 +08:00
手瓜一十雪
d93a280ab3 fix: 进一步getNextMemberList 2024-10-26 18:40:21 +08:00
手瓜一十雪
f7e2b3a4a7 feat: new function 2024-10-26 18:10:08 +08:00
手瓜一十雪
39d9c8fa74 release: v3.1.7 2024-10-26 16:26:30 +08:00
手瓜一十雪
8823895a03 Merge pull request #466 from cnxysoft/upmain
perf: 群成员拉取
2024-10-26 16:18:17 +08:00
手瓜一十雪
b44a9e696c Merge branch 'main' into pr/466 2024-10-26 15:47:13 +08:00
手瓜一十雪
cf28a3dc17 fix: ai solve 2024-10-26 10:49:18 +08:00
手瓜一十雪
7416e6caf6 feat: GoCQHTTPDeleteFriend 2024-10-26 10:36:41 +08:00
手瓜一十雪
90f6896f3c feat: GoCQ兼容性提高 2024-10-26 10:22:04 +08:00
Alen
eebcd0700d Merge branch 'main' into upmain 2024-10-26 07:22:25 +08:00
Alen
133eee0c66 perf: 群成员拉取
getgroupmemberlist启用no_cache
2024-10-26 07:20:40 +08:00
pk5ls20
640fb75f74 feat: support for customizing the timestamp of fake forwardMsg 2024-10-26 04:06:42 +08:00
Alen
51dcc1add6 Merge branch 'main' into upmain 2024-10-25 23:58:18 +08:00
Alen
730c928f91 Merge pull request #465 from cnxysoft/upmain
refactor: 群成员列表获取
2024-10-25 23:49:27 +08:00
Alen
c3b7e111b9 style: 2024-10-25 22:26:18 +08:00
pk5ls20
1874e48925 Merge pull request #464 from clansty/feat/nested-forward
feat: 嵌套合并转发消息
2024-10-25 22:13:32 +08:00
pk5ls20
e7a082c91c feat: better recursive parsing with depth limits 2024-10-25 22:10:24 +08:00
Alen
5d4f45407e fix: 群成员拉取 2024-10-25 21:50:19 +08:00
Clansty
17c37ec32f feat: 嵌套合并转发消息 2024-10-25 19:37:04 +08:00
手瓜一十雪
b5f8140c79 feat: v3 Logo 2024-10-25 19:31:44 +08:00
手瓜一十雪
63f746c237 style: lint 2024-10-25 18:09:41 +08:00
手瓜一十雪
dac6709f27 feat: 6.9.56-28418-mac 2024-10-25 17:57:28 +08:00
手瓜一十雪
470c8d0b29 release: v3.1.6 2024-10-25 17:44:30 +08:00
Wesley F. Young
b0d35e803b update: bump express version to 5.0.0 (Why use beta.2?) 2024-10-25 09:59:02 +08:00
pk5ls20
a71475be8b feat: allow pass string user_id in handleForwardedNodesPacket 2024-10-25 09:17:16 +08:00
pk5ls20
b9f2cc5142 feat: reject >100MB video highway upload 2024-10-25 08:59:56 +08:00
pk5ls20
2d46e55b9b feat: better highway upload log 2024-10-25 08:54:18 +08:00
pk5ls20
684e254996 feat: make PacketMsgPttElement invalid 2024-10-25 08:33:26 +08:00
手瓜一十雪
a2f7903960 Merge pull request #460 from NapNeko/feat/packet-more
feat: support more element in proto
2024-10-25 08:26:52 +08:00
pk5ls20
c0c757d6bd Merge branch 'main' into feat/packet-more 2024-10-25 08:17:28 +08:00
pk5ls20
da0fad743d feat: maybe more stable fake forwardMsg 2024-10-25 08:09:17 +08:00
手瓜一十雪
80b10d6025 Merge pull request #463 from clansty/feat/custom-forward-display
feat: 自定义合并转发外显信息
2024-10-25 08:05:41 +08:00
pk5ls20
a27c2a69c4 feat: maybe more stable fake forwardMsg 2024-10-25 07:27:35 +08:00
pk5ls20
9ed2a2fd19 refactor: simplify oidb packet pack & send 2024-10-25 06:48:01 +08:00
pk5ls20
aa9d96718c refactor: outer calculation 2024-10-25 05:54:46 +08:00
pk5ls20
aa67a2b71c chore: cv多了( 2024-10-25 05:17:01 +08:00
pk5ls20
d3405edd42 refactor: packet highway & etc, kill some todo 2024-10-25 05:11:10 +08:00
Clansty
3612098d62 feat: 自定义合并转发外显信息 2024-10-25 02:42:50 +08:00
Alen
2f08b72d69 fix: 群成员拉取 2024-10-24 23:00:38 +08:00
手瓜一十雪
ab66904c1a feat: 3.2.13-28971-arm64 2024-10-24 21:57:54 +08:00
手瓜一十雪
55542a3dbe feat: 28971 Linux 2024-10-24 20:40:58 +08:00
手瓜一十雪
8569a45114 release: v3.1.5 2024-10-24 20:16:12 +08:00
手瓜一十雪
c790311fc3 release: v3.1.5 2024-10-24 20:11:39 +08:00
手瓜一十雪
3c45c8bd80 feat: 28971 2024-10-24 20:11:07 +08:00
手瓜一十雪
d5b7b3ae31 feat: ntappid 2024-10-24 17:55:33 +08:00
手瓜一十雪
43e73a5f24 doc: big Logo 2024-10-24 17:03:23 +08:00
手瓜一十雪
698947ed97 Merge branch 'main' into feat/packet-more 2024-10-24 14:00:17 +08:00
手瓜一十雪
f3d967ae07 release: 3.1.4 2024-10-24 13:39:05 +08:00
手瓜一十雪
dbe72fa07e feat: SetGroupSign 2024-10-24 13:38:22 +08:00
pk5ls20
801a97d85b chore: remove useless log 2024-10-24 04:58:53 +08:00
pk5ls20
9f8f938c47 feat: build & upload file 2024-10-24 04:53:41 +08:00
Wesley F. Young
8fe37d1c1e chore: reformat package.json 2024-10-23 17:54:12 +08:00
pk5ls20
5cca8457e7 chore: 有笨蛋 2024-10-23 16:33:05 +08:00
pk5ls20
e9332e7646 feat: add ptt msg pack & upload 2024-10-23 16:12:31 +08:00
手瓜一十雪
31365505d8 Merge pull request #461 from huankong233/main
优化 contact 支持群聊和私聊
2024-10-23 09:09:07 +08:00
huankong233
b3fbe9e34a 优化 contact 支持群聊和私聊 2024-10-23 09:05:45 +08:00
pk5ls20
4082b651c5 feat & fix: add video msg pack & upload, fix some bugs in uploading c2c elements 2024-10-23 06:14:48 +08:00
Alen
0081000ef0 Merge branch 'main' into upmain 2024-10-23 01:08:56 +08:00
Alen
ad4d6a1070 refactor: 群成员获取 2024-10-23 01:07:52 +08:00
手瓜一十雪
5190b26399 Merge pull request #457 from huankong233/main
删除一些过时的接口
2024-10-22 17:57:04 +08:00
手瓜一十雪
29a8db96f4 fix 2024-10-22 17:56:51 +08:00
huankong233
1a4c2cabfd 删除一些过时的接口 2024-10-22 16:45:52 +08:00
手瓜一十雪
ef9189055c release: 3.1.3 2024-10-22 12:43:54 +08:00
手瓜一十雪
5cc3719125 fix: rkey 2024-10-22 12:42:24 +08:00
手瓜一十雪
5d46f41348 fix: dep 2024-10-22 12:14:43 +08:00
手瓜一十雪
3c2c1963f4 release: 3.1.2 2024-10-22 12:11:02 +08:00
手瓜一十雪
4896ca9279 fix 2024-10-22 11:37:01 +08:00
手瓜一十雪
f0afba6cd9 fix: GetOnlineClient 2024-10-22 11:34:28 +08:00
手瓜一十雪
bd717c298a fix: get_online_clients 2024-10-22 11:17:39 +08:00
手瓜一十雪
baaa8a70dc release: 3.1.1 2024-10-22 11:08:24 +08:00
手瓜一十雪
6d561c6e6f Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-22 11:04:43 +08:00
手瓜一十雪
e6b6947d49 feat: 标准化凭据获取 2024-10-22 11:04:28 +08:00
手瓜一十雪
52e99a2175 Merge pull request #454 from huankong233/main
简单修复一些小问题
2024-10-22 10:17:08 +08:00
手瓜一十雪
052d17a46f fix: getfile 2024-10-22 10:15:16 +08:00
huankong233
1aa1f4c212 优化 getFile 处理逻辑 2024-10-22 09:48:55 +08:00
huankong233
c3a48e3344 修复标记好友/群聊信息已读逻辑 2024-10-21 19:51:17 +08:00
手瓜一十雪
1d5483dc28 Merge pull request #451 from huankong233/main
对接口顺序和文档同步
2024-10-21 16:47:46 +08:00
huankong233
54277fa0df CleanCache 未实现 2024-10-21 16:09:27 +08:00
huankong233
ab04bd262f 对接口顺序和文档同步 2024-10-21 15:49:57 +08:00
手瓜一十雪
fb23087b65 release: 3.1.0 2024-10-21 14:35:57 +08:00
手瓜一十雪
846fee7ac8 fix: error import 2024-10-21 14:33:12 +08:00
手瓜一十雪
977eacc679 try: fix arm64 2024-10-21 14:12:10 +08:00
手瓜一十雪
dacfefe644 style: lint 2024-10-21 10:17:31 +08:00
pk5ls20
345e941e11 chore: remove unnecessary comments 2024-10-21 04:10:53 +08:00
pk5ls20
6cb7d45464 feat & refactor: decouple the forwardMsg construction logic and implement the OB11 element conversion for the forward node. 2024-10-21 04:05:02 +08:00
pk5ls20
e7222653fa release: 3.0.6 2024-10-20 23:54:21 +08:00
pk5ls20
014f0758f5 chore: 部分回滚 https://github.com/NapNeko/NapCatQQ/commit/bb72d70b 2024-10-20 23:52:36 +08:00
pk5ls20
0e8b416f6d Merge pull request #448 from pk5ls20/feat/friend-poke
feat: add `friend_poke` OneBot11 API
2024-10-20 23:18:26 +08:00
pk5ls20
09a60a2204 feat: add friend_poke OneBot11 API 2024-10-20 23:09:38 +08:00
131 changed files with 4325 additions and 1523 deletions

View File

@@ -127,6 +127,10 @@ jobs:
zip -q -r NapCat.Framework.Windows.Once.zip * zip -q -r NapCat.Framework.Windows.Once.zip *
cd .. cd ..
mv ./NapCat.Framework.Windows.Once/NapCat.Framework.Windows.Once.zip ./ 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 - name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
@@ -143,4 +147,8 @@ jobs:
NapCat.Framework.zip NapCat.Framework.zip
NapCat.Shell.zip NapCat.Shell.zip
NapCat.Framework.Windows.Once.zip NapCat.Framework.Windows.Once.zip
napcat.packet.arm64
napcat.packet.exe
napcat.packet.linux
napcat.packet.production.py
draft: true draft: true

View File

@@ -1,5 +1,7 @@
<div align="center"> <div align="center">
<img src="https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto" alt="NapCatQQ" width="640" height="320" />
![Logo](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto)
</div> </div>
--- ---
@@ -23,7 +25,6 @@ NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
**首次使用**请务必查看如下文档看使用教程 **首次使用**请务必查看如下文档看使用教程
### 文档地址 ### 文档地址
[Github.IO](https://napneko.github.io/)
[Cloudflare.Worker](https://doc.napneko.icu/) [Cloudflare.Worker](https://doc.napneko.icu/)
@@ -31,6 +32,7 @@ NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
[Cloudflare.Pages](https://napneko.pages.dev/) [Cloudflare.Pages](https://napneko.pages.dev/)
[Github.IO](https://napneko.github.io/)
## 回家旅途 ## 回家旅途
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS) [QQ Group](https://qm.qq.com/q/VfjAq5HIMS)

Binary file not shown.

BIN
external/packet/napcat.packet.arm64 vendored Normal file

Binary file not shown.

BIN
external/packet/napcat.packet.exe vendored Normal file

Binary file not shown.

BIN
external/packet/napcat.packet.linux vendored Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 335 KiB

View File

@@ -4,7 +4,7 @@
"name": "NapCatQQ", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "3.0.5", "version": "3.4.7",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {

View File

@@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "3.0.5", "version": "3.4.7",
"scripts": { "scripts": {
"build:framework": "vite build --mode framework", "build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell", "build:shell": "vite build --mode shell",
@@ -13,6 +13,8 @@
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.24.7", "@babel/preset-typescript": "^7.24.7",
"@log4js-node/log4js-api": "^1.0.2", "@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.2",
"@protobuf-ts/runtime": "^2.9.4",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
@@ -23,30 +25,29 @@
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^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": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.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", "typescript": "^5.3.3",
"vite": "^5.2.6", "vite": "^5.2.6",
"vite-plugin-cp": "^4.0.8", "vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^5.1.0"
"@protobuf-ts/runtime": "^2.9.4",
"ajv": "^8.13.0",
"fast-xml-parser": "^4.3.6",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"async-mutex": "^0.5.0",
"file-type": "^19.0.0",
"json-schema-to-ts": "^3.1.0",
"image-size": "^1.1.1",
"cors": "^2.8.5"
}, },
"dependencies": { "dependencies": {
"qrcode-terminal": "^0.12.0", "express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"express": "^5.0.0-beta.2",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
} }
} }

View File

@@ -4,16 +4,27 @@ const process = require("process");
console.log("[NapCat] [CheckVersion] 开始检测当前仓库版本..."); console.log("[NapCat] [CheckVersion] 开始检测当前仓库版本...");
try { try {
const packageJson = require("../package.json"); const packageJson = require("../package.json");
const manifsetJson = require("../manifest.json");
const currentVersion = packageJson.version; const currentVersion = packageJson.version;
const targetVersion = process.env.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] currentVersion:", currentVersion, "targetVersion:", targetVersion);
console.log("[NapCat] [CheckVersion] manifestCurrentVersion:", manifestCurrentVersion, "manifestTargetVersion:", manifestTargetVersion);
// 验证 targetVersion 格式 // 验证 targetVersion 格式
if (!targetVersion || typeof targetVersion !== 'string') { if (!targetVersion || typeof targetVersion !== 'string') {
console.log("[NapCat] [CheckVersion] 目标版本格式不正确或未设置!"); console.log("[NapCat] [CheckVersion] 目标版本格式不正确或未设置!");
return; return;
} }
// 验证 manifestTargetVersion 格式
if (!manifestTargetVersion || typeof manifestTargetVersion !== 'string') {
console.log("[NapCat] [CheckVersion] manifest目标版本格式不正确或未设置");
return;
}
// 写入脚本文件的统一函数 // 写入脚本文件的统一函数
const writeScriptToFile = (content) => { const writeScriptToFile = (content) => {
@@ -21,7 +32,7 @@ try {
console.log("[NapCat] [CheckVersion] checkVersion.sh 文件已更新。"); console.log("[NapCat] [CheckVersion] checkVersion.sh 文件已更新。");
}; };
if (currentVersion === targetVersion) { if (currentVersion === targetVersion && manifestCurrentVersion === manifestTargetVersion) {
// 不需要更新版本,写入一个简单的脚本 // 不需要更新版本,写入一个简单的脚本
const simpleScript = "#!/bin/bash\necho \"CheckVersion Is Done\""; const simpleScript = "#!/bin/bash\necho \"CheckVersion Is Done\"";
writeScriptToFile(simpleScript); writeScriptToFile(simpleScript);
@@ -29,11 +40,14 @@ try {
// 更新版本构建安全的sed命令 // 更新版本构建安全的sed命令
const safeScriptContent = ` const safeScriptContent = `
#!/bin/bash #!/bin/bash
git config --global user.email "bot@test.wumiao.wang" git config --global user.email "nanaeonn@outlook.com"
git config --global user.name "Version" git config --global user.name "Mlikiowa"
sed -i "s/\\\"version\\\": \\\"${currentVersion}\\\"/\\\"version\\\": \\\"${targetVersion}\\\"/g" package.json 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 add .
git commit -m "chore:version change" git commit -m "release: v${targetVersion}"
git push -u origin main`; git push -u origin main`;
writeScriptToFile(safeScriptContent); writeScriptToFile(safeScriptContent);
} }

View File

@@ -9,6 +9,15 @@ interface InternalMapKey {
checker: ((...args: any[]) => boolean) | undefined; 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 type ListenerClassBase = Record<string, string>;
export class NTEventWrapper { export class NTEventWrapper {
@@ -43,10 +52,8 @@ export class NTEventWrapper {
createEventFunction< createEventFunction<
Service extends keyof ServiceNamingMapping, Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>, ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
// eslint-disable-next-line T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
// @ts-ignore
T extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
>(eventName: `${Service}/${ServiceMethod}`): T | undefined { >(eventName: `${Service}/${ServiceMethod}`): T | undefined {
const eventNameArr = eventName.split('/'); const eventNameArr = eventName.split('/');
type eventType = { type eventType = {
@@ -98,10 +105,8 @@ export class NTEventWrapper {
async callNoListenerEvent< async callNoListenerEvent<
Service extends keyof ServiceNamingMapping, Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>, ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
// eslint-disable-next-line EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
// @ts-ignore
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
>( >(
serviceAndMethod: `${Service}/${ServiceMethod}`, serviceAndMethod: `${Service}/${ServiceMethod}`,
...args: Parameters<EventType> ...args: Parameters<EventType>
@@ -111,10 +116,8 @@ export class NTEventWrapper {
async registerListen< async registerListen<
Listener extends keyof ListenerNamingMapping, Listener extends keyof ListenerNamingMapping,
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>, ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
// eslint-disable-next-line ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
// @ts-ignore
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod],
>( >(
listenerAndMethod: `${Listener}/${ListenerMethod}`, listenerAndMethod: `${Listener}/${ListenerMethod}`,
waitTimes = 1, waitTimes = 1,
@@ -164,15 +167,11 @@ export class NTEventWrapper {
async callNormalEventV2< async callNormalEventV2<
Service extends keyof ServiceNamingMapping, Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>, ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
Listener extends keyof ListenerNamingMapping, Listener extends keyof ListenerNamingMapping,
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>, ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
// eslint-disable-next-line EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
// @ts-ignore ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
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}`, serviceAndMethod: `${Service}/${ServiceMethod}`,
listenerAndMethod: `${Listener}/${ListenerMethod}`, listenerAndMethod: `${Listener}/${ListenerMethod}`,

View 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);
}
}

View File

@@ -52,7 +52,7 @@ export class FileNapCatOneBotUUID {
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data; const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
return { return {
peer: { peer: {
chatType: chatType as any, chatType: +chatType,
peerUid: peerUid, peerUid: peerUid,
}, },
modelId, modelId,
@@ -89,7 +89,7 @@ export class FileNapCatOneBotUUID {
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data; const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
return { return {
peer: { peer: {
chatType: chatType as any, chatType: +chatType,
peerUid: peerUid, peerUid: peerUid,
}, },
msgId, msgId,
@@ -239,3 +239,42 @@ export function calcQQLevel(level?: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level; const { crownNum, sunNum, moonNum, starNum } = level;
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum; return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
} }
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;
}

View File

@@ -1,6 +1,6 @@
export class LRUCache<K, V> { export class LRUCache<K, V> {
private capacity: number; private capacity: number;
private cache: Map<K, V>; public cache: Map<K, V>;
constructor(capacity: number) { constructor(capacity: number) {
this.capacity = capacity; this.capacity = capacity;

View File

@@ -23,10 +23,10 @@ export class LimitedHashTable<K, V> {
} }
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) { while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value; const oldestKey = this.keyToValue.keys().next().value;
// @ts-ignore if (oldestKey !== undefined) {
this.valueToKey.delete(this.keyToValue.get(oldestKey)!); this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
// @ts-ignore this.keyToValue.delete(oldestKey);
this.keyToValue.delete(oldestKey); }
} }
} }

View File

@@ -1,8 +1,9 @@
import fs from 'node:fs'; import fs from 'node:fs';
import { systemPlatform } from '@/common/system'; 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 AppidTable from '@/core/external/appid.json';
import { LogWrapper } from './log'; import { LogWrapper } from './log';
import { getMajorPath } from '@/core';
export class QQBasicInfoWrapper { export class QQBasicInfoWrapper {
QQMainPath: string | undefined; QQMainPath: string | undefined;
@@ -72,6 +73,7 @@ export class QQBasicInfoWrapper {
} }
getAppidV2(): { appid: string; qua: string } { getAppidV2(): { appid: string; qua: string } {
// 通过已有表 性能好
const appidTbale = AppidTable as unknown as QQAppidTableType; const appidTbale = AppidTable as unknown as QQAppidTableType;
const fullVersion = this.getFullQQVesion(); const fullVersion = this.getFullQQVesion();
if (fullVersion) { if (fullVersion) {
@@ -80,10 +82,25 @@ export class QQBasicInfoWrapper {
return data; return data;
} }
} }
// 通过Major拉取 性能差
// else try {
const 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版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,); this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() }; return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
} }
getAppidV2ByMajor(QQVersion: string) {
const majorPath = getMajorPath(QQVersion);
const appid = parseAppidFromMajor(majorPath);
return appid;
}
} }

View File

@@ -1 +1 @@
export const napCatVersion = '3.0.5'; export const napCatVersion = '3.4.7';

View File

@@ -6,6 +6,7 @@ import {
Peer, Peer,
PicElement, PicElement,
PicType, PicType,
RawMessage,
SendFileElement, SendFileElement,
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
@@ -30,7 +31,7 @@ export class NTQQFileApi {
context: InstanceContext; context: InstanceContext;
core: NapCatCore; core: NapCatCore;
rkeyManager: RkeyManager; rkeyManager: RkeyManager;
packetRkey: Array<{ rkey: string; time: number; type: number; }> | undefined; packetRkey: Array<{ rkey: string; time: number; type: number; ttl: bigint }> | undefined;
constructor(context: InstanceContext, core: NapCatCore) { constructor(context: InstanceContext, core: NapCatCore) {
this.context = context; this.context = context;
@@ -238,7 +239,7 @@ export class NTQQFileApi {
fileName: fileName, fileName: fileName,
filePath: path, filePath: path,
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, fileSize: fileSize.toString(),
duration: duration ?? 1, duration: duration ?? 1,
formatType: 1, formatType: 1,
voiceType: 1, voiceType: 1,
@@ -267,6 +268,53 @@ export class NTQQFileApi {
return fileTransNotifyInfo.filePath; 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) { async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
@@ -296,7 +344,7 @@ export class NTQQFileApi {
filePath: thumbPath, filePath: thumbPath,
}], }],
() => true, () => true,
(arg) => arg.msgId === msgId, (arg) => arg.msgElementId === elementId && arg.msgId === msgId,
1, 1,
timeout, timeout,
); );
@@ -378,10 +426,12 @@ export class NTQQFileApi {
}; };
try { try {
if (this.core.apis.PacketApi.available) { if (this.core.apis.PacketApi.available) {
if ((!this.packetRkey || this.packetRkey[0].time > Date.now() / 1000)) { 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(); this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket();
} }
if (this.packetRkey.length > 0) { if (this.packetRkey && this.packetRkey.length > 0) {
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6); rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6); rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
rkeyData.online_rkey = true; rkeyData.online_rkey = true;

View File

@@ -34,7 +34,13 @@ export class NTQQFriendApi {
data.forEach((value) => retMap.set(value.uin!, value.uid!)); data.forEach((value) => retMap.set(value.uin!, value.uid!));
return retMap; 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) { async getBuddyV2ExWithCate(refresh = false) {
const categoryMap: Map<string, any> = new Map(); const categoryMap: Map<string, any> = new Map();
const buddyService = this.context.session.getBuddyService(); const buddyService = this.context.session.getBuddyService();

View File

@@ -54,7 +54,9 @@ export class NTQQGroupApi {
}, pskey); }, pskey);
} }
async getGroupShutUpMemberList(groupCode: string) { async getGroupShutUpMemberList(groupCode: string) {
return this.context.session.getGroupService().getGroupShutUpMemberList(groupCode); const 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) { async clearGroupNotifiesUnreadCount(uk: boolean) {
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk); return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
@@ -316,18 +318,76 @@ export class NTQQGroupApi {
return undefined; return undefined;
} }
async getGroupMembersV2(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { async tryGetGroupMembersV2(modeListener = false, groupQQ: string, num = 30, timeout = 100): Promise<{
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); infos: Map<string, GroupMember>;
let once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 1, 2000, (params) => params.sceneId === sceneId) finish: boolean;
.catch(); hasNext: boolean | undefined;
const result = await this.context.session.getGroupService().getNextMemberList(sceneId!, undefined, num); }> {
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) { if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg); throw new Error('获取群成员列表出错,' + result.errMsg);
} }
if (result.result.infos.size === 0) { let resMode2;
return (await once)[0].infos; if (modeListener) {
const ret = (await once)?.[0];
if (ret) {
resMode2 = ret;
}
} }
return result.result.infos; 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>> { async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
@@ -425,7 +485,7 @@ export class NTQQGroupApi {
} }
async getGroupRemainAtTimes(GroupCode: string) { async getGroupRemainAtTimes(GroupCode: string) {
this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode); return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
} }
async getMemberExtInfo(groupCode: string, uin: string) { async getMemberExtInfo(groupCode: string, uin: string) {

View File

@@ -3,6 +3,9 @@ import { InstanceContext, NapCatCore } from '@/core';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
export class NTQQMsgApi { 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//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType // 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
@@ -22,7 +25,9 @@ export class NTQQMsgApi {
async sendShowInputStatusReq(peer: Peer, eventType: number) { async sendShowInputStatusReq(peer: Peer, eventType: number) {
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid); 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) { async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa //注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count); return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
@@ -82,6 +87,18 @@ export class NTQQMsgApi {
pageLimit: 1, 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) { async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, { return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer, chatInfo: peer,
@@ -94,9 +111,9 @@ export class NTQQMsgApi {
pageLimit: 1, pageLimit: 1,
}); });
} }
//@deprecated // 客户端还在用别慌
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z); return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
} }
async getMsgExBySeq(peer: Peer, msgSeq: string) { async getMsgExBySeq(peer: Peer, msgSeq: string) {
const DateNow = Math.floor(Date.now() / 1000); const DateNow = Math.floor(Date.now() / 1000);

View File

@@ -1,19 +1,31 @@
import * as crypto from 'crypto';
import * as os from 'os'; import * as os from 'os';
import { ChatType, InstanceContext, NapCatCore } from '..'; import { ChatType, InstanceContext, NapCatCore } from '..';
import offset from '@/core/external/offset.json'; import offset from '@/core/external/offset.json';
import { PacketClient, RecvPacketData } from '@/core/packet/client';
import { PacketSession } from "@/core/packet/session"; import { PacketSession } from "@/core/packet/session";
import { PacketHexStr } from "@/core/packet/packer"; import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { NapProtoMsg } from '@/core/packet/proto/NapProto'; import { NapProtoMsg, NapProtoEncodeStructType, NapProtoDecodeStructType } from "@napneko/nap-proto-core";
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202'; import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase'; import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2'; import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
import { LogWrapper } from "@/common/log"; import { LogWrapper } from "@/common/log";
import { SendLongMsgResp } from "@/core/packet/proto/message/action"; import { SendLongMsgResp } from "@/core/packet/proto/message/action";
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg } from "@/core/packet/message/message";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { PacketMsgPicElement } from "@/core/packet/msg/element"; import {
import { c } from 'vite/dist/node/types.d-aGj9QkWt'; 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";
import { AIVoiceChatType, AIVoiceItemList } from "@/core/packet/entities/aiChat";
import { OidbSvcTrpcTcp0X929B_0Resp, OidbSvcTrpcTcp0X929D_0Resp } from "@/core/packet/proto/oidb/Oidb.0x929";
import { IndexNode, MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
import { RecvPacketData } from "@/core/packet/client/client";
interface OffsetType { interface OffsetType {
[key: string]: { [key: string]: {
@@ -27,8 +39,7 @@ const typedOffset: OffsetType = offset;
export class NTQQPacketApi { export class NTQQPacketApi {
context: InstanceContext; context: InstanceContext;
core: NapCatCore; core: NapCatCore;
logger: LogWrapper logger: LogWrapper;
serverUrl: string | undefined;
qqVersion: string | undefined; qqVersion: string | undefined;
packetSession: PacketSession | undefined; packetSession: PacketSession | undefined;
@@ -37,34 +48,32 @@ export class NTQQPacketApi {
this.core = core; this.core = core;
this.logger = core.context.logger; this.logger = core.context.logger;
this.packetSession = undefined; this.packetSession = undefined;
const config = this.core.configLoader.configData; this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
if (config && config.packetServer && config.packetServer.length > 0) { .then()
const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086'; .catch(this.core.context.logger.logError.bind(this.core.context.logger));
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 is not set, will not init NapCat.Packet!');
}
} }
get available(): boolean { get available(): boolean {
return this.packetSession?.client.available ?? false; return this.packetSession?.client.available ?? false;
} }
async InitSendPacket(serverUrl: string, qqversion: string) { async InitSendPacket(qqversion: string) {
this.serverUrl = serverUrl;
this.qqVersion = qqversion; this.qqVersion = qqversion;
const offsetTable: OffsetType = offset; const table = typedOffset[qqversion + '-' + os.arch()];
const table = offsetTable[qqversion + '-' + os.arch()]; if (!table) {
if (!table) return false; this.logger.logError('[Core] [Packet] PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch());
const url = 'ws://' + this.serverUrl + '/ws'; return false;
this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core)); }
if (this.core.configLoader.configData.packetBackend === 'disable') {
this.logger.logWarn('[Core] [Packet] 已禁用Packet后端NapCat.Packet将不会加载');
return false;
}
this.packetSession = new PacketSession(this.core);
const cb = () => { const cb = () => {
if (this.packetSession && this.packetSession.client) { if (this.packetSession && this.packetSession.client) {
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger)); this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
} }
} };
await this.packetSession.client.connect(cb); await this.packetSession.client.connect(cb);
return true; return true;
} }
@@ -73,25 +82,32 @@ export class NTQQPacketApi {
return this.packetSession!.client.sendPacket(cmd, data, rsp); return this.packetSession!.client.sendPacket(cmd, data, rsp);
} }
async sendPokePacket(group: number, peer: number) { async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
const data = this.packetSession?.packer.packPokePacket(group, peer); return this.sendPacket(pkt.cmd, pkt.data, rsp);
await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false); }
async sendPokePacket(peer: number, group?: number) {
const data = this.packetSession?.packer.packPokePacket(peer, group);
await this.sendOidbPacket(data!, false);
} }
async sendRkeyPacket() { async sendRkeyPacket() {
const packet = this.packetSession?.packer.packRkeyPacket(); const packet = this.packetSession?.packer.packRkeyPacket();
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', packet!, true); const ret = await this.sendOidbPacket(packet!, true);
if (!ret?.hex_data) return []; if (!ret?.hex_data) return [];
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body); const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
return retData.data.rkeyList; 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> { async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
let status = 0; let status = 0;
try { try {
const packet = this.packetSession?.packer.packStatusPacket(uin); const packet = this.packetSession?.packer.packStatusPacket(uin);
const ret = await this.sendPacket('OidbSvcTrpcTcp.0xfe1_2', packet!, true); const ret = await this.sendOidbPacket(packet!, true);
const data = Buffer.from(ret.hex_data, 'hex'); const data = Buffer.from(ret.hex_data, 'hex');
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value; const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
// ext & 0xff00 + ext >> 16 & 0xff // ext & 0xff00 + ext >> 16 & 0xff
@@ -108,22 +124,47 @@ export class NTQQPacketApi {
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) { async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle); const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true); await this.sendOidbPacket(data!, true);
} }
private async uploadResources(msg: PacketMsg[], groupUin: number = 0) { // TODO: can simplify this
const reqList = [] async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
const reqList = [];
for (const m of msg) { for (const m of msg) {
for (const e of m.msg) { for (const e of m.msg) {
if (e instanceof PacketMsgPicElement) { if (e instanceof PacketMsgPicElement) {
reqList.push(this.packetSession?.highwaySession.uploadImage({ reqList.push(this.packetSession?.highwaySession.uploadImage({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid 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)); }, e));
} }
} }
} }
return Promise.all(reqList); 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) { async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
@@ -137,12 +178,63 @@ export class NTQQPacketApi {
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) { async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID); const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x6d6_2', data!, true); const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body); const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
if (resp.download.retCode !== 0) { if (resp.download.retCode !== 0) {
throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`); throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`);
} }
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=` return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
}
async sendGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const data = this.packetSession?.packer.packGroupPttFileDownloadReq(groupUin, node);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(NTV2RichMediaResp).decode(body);
const info = resp.download.info;
return `https://${info.domain}${info.urlPath}${resp.download.rKeyParam}`;
}
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;
}
async sendFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType) : Promise<AIVoiceItemList[] | null> {
const data = this.packetSession?.packer.packFetchAiVoiceListReq(groupUin, chatType);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929D_0Resp).decode(body);
if (!resp.content) return null;
return resp.content.map((item) => {
return {
category: item.category,
voices: item.voices
};
});
}
async sendAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
let reqTime = 0;
const reqMaxTime = 30;
const sessionId = crypto.randomBytes(4).readUInt32BE(0);
while (true) {
if (reqTime >= reqMaxTime) {
throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`);
}
reqTime++;
const data = this.packetSession?.packer.packAiVoiceChatReq(groupUin, voiceId, text, chatType, sessionId);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBase).decode(Buffer.from(ret.hex_data, 'hex'));
if (body.errorCode) {
throw new Error(`sendAiVoiceChatReq retCode: ${body.errorCode} error: ${body.errorMsg}`);
}
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929B_0Resp).decode(body.body);
if (!resp.msgInfo) continue;
return resp.msgInfo;
}
} }
} }

View File

@@ -18,7 +18,7 @@ export class NTQQUserApi {
async getStatusByUid(uid: string) { async getStatusByUid(uid: string) {
return this.context.session.getProfileService().getStatus(uid); return this.context.session.getProfileService().getStatus(uid);
} }
async getProfileLike(uid: string) { async getProfileLike(uid: string, start: number, count: number) {
return this.context.session.getProfileLikeService().getBuddyProfileLike({ return this.context.session.getProfileLikeService().getBuddyProfileLike({
friendUids: [uid], friendUids: [uid],
basic: 1, basic: 1,
@@ -26,8 +26,8 @@ export class NTQQUserApi {
favorite: 0, favorite: 0,
userProfile: 1, userProfile: 1,
type: 2, type: 2,
start: 0, start: start,
limit: 20, limit: count,
}); });
} }
async fetchOtherProfileLike(uid: string) { async fetchOtherProfileLike(uid: string) {

View File

@@ -338,4 +338,12 @@ export class NTQQWebApi {
} }
return (hash & 0x7FFFFFFF).toString(); 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();
}
} }

View File

@@ -27,94 +27,70 @@ export interface GetFileListParam {
export enum ElementType { export enum ElementType {
UNKNOWN = 0, UNKNOWN = 0,
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
PTT = 4, PTT = 4,
VIDEO = 5, VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等
WALLET = 9, WALLET = 9,
/**
* “小灰条”,包括拍一拍 (Poke)、撤回提示等
*/
GreyTip = 8,
ARK = 10, ARK = 10,
MFACE = 11, MFACE = 11,
LIVEGIFT = 12, LIVEGIFT = 12,
STRUCTLONGMSG = 13, STRUCTLONGMSG = 13,
MARKDOWN = 14, MARKDOWN = 14,
GIPHY = 15, GIPHY = 15,
MULTIFORWARD = 16, MULTIFORWARD = 16,
INLINEKEYBOARD = 17, INLINEKEYBOARD = 17,
INTEXTGIFT = 18, INTEXTGIFT = 18,
CALENDAR = 19, CALENDAR = 19,
YOLOGAMERESULT = 20, YOLOGAMERESULT = 20,
AVRECORD = 21, AVRECORD = 21,
FEED = 22, FEED = 22,
TOFURECORD = 23, TOFURECORD = 23,
ACEBUBBLE = 24, ACEBUBBLE = 24,
ACTIVITY = 25, ACTIVITY = 25,
TOFU = 26, TOFU = 26,
FACEBUBBLE = 27, FACEBUBBLE = 27,
SHARELOCATION = 28, SHARELOCATION = 28,
TASKTOPMSG = 29, TASKTOPMSG = 29,
RECOMMENDEDMSG = 43, RECOMMENDEDMSG = 43,
ACTIONBAR = 44 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 { export interface ActionBarElement {
rows: InlineKeyboardRow[]; rows: InlineKeyboardRow[];
botAppid: string; botAppid: string;
} }
export interface SendActionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: ActionBarElement;
}
export interface RecommendedMsgElement { export interface RecommendedMsgElement {
rows: InlineKeyboardRow[]; rows: InlineKeyboardRow[];
botAppid: string; botAppid: string;
} }
export interface SendRecommendedMsgElement { export type SendRecommendedMsgElement = SendElementBase<ElementType.RECOMMENDEDMSG> & ElementBase<'recommendedMsgElement'>;
elementType: ElementType.RECOMMENDEDMSG;
elementId: string;
recommendedMsgElement: RecommendedMsgElement;
}
export interface InlineKeyboardButton { export interface InlineKeyboardButton {
id: string; id: string;
@@ -171,11 +147,7 @@ export enum NTMsgType {
KMSGTYPEWALLET = 10 KMSGTYPEWALLET = 10
} }
export interface SendTaskTopMsgElement { export type SendTaskTopMsgElement = SendElementBase<ElementType.TASKTOPMSG> & ElementBase<'taskTopMsgElement'>;
elementType: ElementType.TASKTOPMSG;
elementId: string;
taskTopMsgElement: TaskTopMsgElement;
}
export interface TofuRecordElement { export interface TofuRecordElement {
type: number; type: number;
@@ -194,11 +166,7 @@ export interface TofuRecordElement {
onscreennotify: boolean; onscreennotify: boolean;
} }
export interface SendTofuRecordElement { export type SendTofuRecordElement = SendElementBase<ElementType.TOFURECORD> & ElementBase<'tofuRecordElement'>;
elementType: ElementType.TOFURECORD;
elementId: string;
tofuRecordElement: TofuRecordElement;
}
export interface FaceBubbleElement { export interface FaceBubbleElement {
faceCount: number; faceCount: number;
@@ -216,12 +184,7 @@ export interface FaceBubbleElement {
}; };
} }
export interface SendFaceBubbleElement { export type SendFaceBubbleElement = SendElementBase<ElementType.FACEBUBBLE> & ElementBase<'faceBubbleElement'>;
elementType: ElementType.FACEBUBBLE;
elementId: string;
faceBubbleElement: FaceBubbleElement;
}
export interface AvRecordElement { export interface AvRecordElement {
type: number; type: number;
@@ -232,11 +195,7 @@ export interface AvRecordElement {
extraType: number; extraType: number;
} }
export interface SendavRecordElement { export type SendAvRecordElement = SendElementBase<ElementType.AVRECORD> & ElementBase<'avRecordElement'>;
elementType: ElementType.AVRECORD;
elementId: string;
avRecordElement: AvRecordElement;
}
export interface YoloUserInfo { export interface YoloUserInfo {
uid: string; uid: string;
@@ -245,24 +204,13 @@ export interface YoloUserInfo {
bizId: string; bizId: string;
} }
export interface SendInlineKeyboardElement { export type SendInlineKeyboardElement = SendElementBase<ElementType.INLINEKEYBOARD> & ElementBase<'inlineKeyboardElement'>;
elementType: ElementType.INLINEKEYBOARD;
elementId: string;
inlineKeyboardElement: {
rows: number;
botAppid: string;
};
}
export interface YoloGameResultElement { export interface YoloGameResultElement {
UserInfo: YoloUserInfo[]; UserInfo: YoloUserInfo[];
} }
export interface SendYoloGameResultElement { export type SendYoloGameResultElement = SendElementBase<ElementType.YOLOGAMERESULT> & ElementBase<'yoloGameResultElement'>;
elementType: ElementType.YOLOGAMERESULT;
yoloGameResultElement: YoloGameResultElement;
}
export interface GiphyElement { export interface GiphyElement {
id: string; id: string;
@@ -271,17 +219,9 @@ export interface GiphyElement {
height: number; height: number;
} }
export interface SendGiphyElement { export type SendGiphyElement = SendElementBase<ElementType.GIPHY> & ElementBase<'giphyElement'>;
elementType: ElementType.GIPHY;
elementId: string;
giphyElement: GiphyElement;
}
export interface SendWalletElement { export type SendWalletElement = SendElementBase<ElementType.UNKNOWN> & ElementBase<'walletElement'>;
elementType: ElementType.UNKNOWN;//不做 设置位置
elementId: string;
walletElement: Record<string, never>;
}
export interface CalendarElement { export interface CalendarElement {
summary: string; summary: string;
@@ -291,49 +231,16 @@ export interface CalendarElement {
schema: string; schema: string;
} }
export interface SendCalendarElement { export type SendCalendarElement = SendElementBase<ElementType.CALENDAR> & ElementBase<'calendarElement'>;
elementType: ElementType.CALENDAR;
elementId: string;
calendarElement: CalendarElement;
}
export interface SendliveGiftElement { export type SendLiveGiftElement = SendElementBase<ElementType.LIVEGIFT> & ElementBase<'liveGiftElement'>;
elementType: ElementType.LIVEGIFT;
elementId: string;
liveGiftElement: Record<string, never>;
}
export interface SendTextElement { export type SendTextElement = SendElementBase<ElementType.TEXT> & ElementBase<'textElement'>;
elementType: ElementType.TEXT;
elementId: string;
textElement: {
content: string;
atType: number;
atUid: string;
atTinyId: string;
atNtUid: string;
};
}
export interface SendPttElement { export type SendPttElement = SendElementBase<ElementType.PTT> & ElementBase<'pttElement', {
elementType: ElementType.PTT; pttElement: ['fileName', 'filePath', 'md5HexStr', 'fileSize', 'duration', 'formatType', 'voiceType',
elementId: string; 'voiceChangeType', 'canConvert2Text', 'waveAmplitudes', 'fileSubId', 'playState', 'autoConvertText']
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 enum PicType { export enum PicType {
gif = 2000, gif = 2000,
@@ -359,11 +266,7 @@ export enum NTMsgAtType {
ATTYPEUNKNOWN = 0 ATTYPEUNKNOWN = 0
} }
export interface SendPicElement { export type SendPicElement = SendElementBase<ElementType.PIC> & ElementBase<'picElement'>;
elementType: ElementType.PIC;
elementId: string;
picElement: PicElement;
}
export interface ReplyElement { export interface ReplyElement {
sourceMsgIdInRecords?: string; sourceMsgIdInRecords?: string;
@@ -375,53 +278,27 @@ export interface ReplyElement {
replyMsgClientSeq?: string; replyMsgClientSeq?: string;
} }
export interface SendReplyElement { export type SendReplyElement = SendElementBase<ElementType.REPLY> & ElementBase<'replyElement'>;
elementType: ElementType.REPLY;
elementId: string;
replyElement: ReplyElement;
}
export interface SendFaceElement { export type SendFaceElement = SendElementBase<ElementType.FACE> & ElementBase<'faceElement'>;
elementType: ElementType.FACE;
elementId: string;
faceElement: FaceElement;
}
export interface SendMarketFaceElement { export type SendMarketFaceElement = SendElementBase<ElementType.MFACE> & ElementBase<'marketFaceElement'>;
elementType: ElementType.MFACE;
marketFaceElement: MarketFaceElement;
}
export interface SendStructLongMsgElement { export type SendStructLongMsgElement = SendElementBase<ElementType.STRUCTLONGMSG> & ElementBase<'structLongMsgElement'>;
elementType: ElementType.STRUCTLONGMSG;
elementId: string;
structLongMsgElement: StructLongMsgElement;
}
export interface StructLongMsgElement { export interface StructLongMsgElement {
xmlContent: string; xmlContent: string;
resId: string; resId: string;
} }
export interface SendactionBarElement { export type SendActionBarElement = SendElementBase<ElementType.ACTIONBAR> & ElementBase<'actionBarElement'>;
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: number;
botAppid: string;
};
}
export interface ShareLocationElement { export interface ShareLocationElement {
text: string; text: string;
ext: string; ext: string;
} }
export interface SendShareLocationElement { export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
elementType: ElementType.SHARELOCATION;
elementId: string;
shareLocationElement?: ShareLocationElement;
}
export interface FileElement { export interface FileElement {
fileMd5?: string; fileMd5?: string;
@@ -441,29 +318,13 @@ export interface FileElement {
fileBizId?: number; fileBizId?: number;
} }
export interface SendFileElement { export type SendFileElement = SendElementBase<ElementType.FILE> & ElementBase<'fileElement'>;
elementType: ElementType.FILE;
elementId: string;
fileElement: FileElement;
}
export interface SendVideoElement { export type SendVideoElement = SendElementBase<ElementType.VIDEO> & ElementBase<'videoElement'>;
elementType: ElementType.VIDEO;
elementId: string;
videoElement: VideoElement;
}
export interface SendArkElement { export type SendArkElement = SendElementBase<ElementType.ARK> & ElementBase<'arkElement'>;
elementType: ElementType.ARK;
elementId: string;
arkElement: ArkElement;
}
export interface SendMarkdownElement { export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & ElementBase<'markdownElement'>;
elementType: ElementType.MARKDOWN;
elementId: string;
markdownElement: MarkdownElement;
}
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement | SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
@@ -480,7 +341,7 @@ export interface TextElement {
export interface MessageElement { export interface MessageElement {
elementType: ElementType, elementType: ElementType,
elementId: string, elementId: string,
extBufForUI: string,//"0x", extBufForUI?: string, //"0x",
textElement?: TextElement; textElement?: TextElement;
faceElement?: FaceElement, faceElement?: FaceElement,
marketFaceElement?: MarketFaceElement, marketFaceElement?: MarketFaceElement,
@@ -509,7 +370,6 @@ export interface MessageElement {
taskTopMsgElement?: TaskTopMsgElement, taskTopMsgElement?: TaskTopMsgElement,
recommendedMsgElement?: RecommendedMsgElement, recommendedMsgElement?: RecommendedMsgElement,
actionBarElement?: ActionBarElement actionBarElement?: ActionBarElement
} }
export enum AtType { export enum AtType {
@@ -578,7 +438,7 @@ export interface PttElement {
fileSize: string; // "4261" fileSize: string; // "4261"
fileSubId: string; // "0" fileSubId: string; // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV" fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1 formatType: number; // 1
invalidState: number; // 0 invalidState: number; // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6" md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0 playState: number; // 0
@@ -589,6 +449,7 @@ export interface PttElement {
voiceChangeType: number; // 0 voiceChangeType: number; // 0
voiceType: number; // 0 voiceType: number; // 0
waveAmplitudes: number[]; waveAmplitudes: number[];
autoConvertText: number;
} }
export interface ArkElement { export interface ArkElement {
@@ -794,7 +655,8 @@ export interface InlineKeyboardElementRowButton {
export interface InlineKeyboardElement { export interface InlineKeyboardElement {
rows: [{ rows: [{
buttons: InlineKeyboardElementRowButton[] buttons: InlineKeyboardElementRowButton[]
}]; }],
botAppid: string;
} }
export interface TipAioOpGrayTipElement { // 这是什么提示来着? export interface TipAioOpGrayTipElement { // 这是什么提示来着?
@@ -960,6 +822,8 @@ export interface RawMessage {
elements: MessageElement[]; elements: MessageElement[];
sourceType: MsgSourceType; sourceType: MsgSourceType;
isOnlineMsg: boolean;
} }
export interface QueryMsgsParams { export interface QueryMsgsParams {
chatInfo: Peer; chatInfo: Peer;

View File

@@ -43,6 +43,50 @@ export enum GroupInviteType {
BYGROUPMEMBER, BYGROUPMEMBER,
BYDISCUSSMEMBER 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 { export interface GroupNotify {
seq: string; // 通知序列号 seq: string; // 通知序列号

View File

@@ -50,5 +50,29 @@
"9.9.16-28788": { "9.9.16-28788": {
"appid": 537249739, "appid": 537249739,
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B" "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"
},
"9.9.16-29271": {
"appid": 537249813,
"qua": "V1_WIN_NQ_9.9.16_29271_GW_B"
},
"3.2.13-29271": {
"appid": 537249913,
"qua": "V1_LNX_NQ_3.2.13_29271_GW_B"
},
"6.9.59-29271": {
"appid": 537249863,
"qua": "V1_MAC_NQ_6.9.59_29271_GW_B"
} }
} }

View File

@@ -3,5 +3,6 @@
"consoleLog": true, "consoleLog": true,
"fileLogLevel": "debug", "fileLogLevel": "debug",
"consoleLogLevel": "info", "consoleLogLevel": "info",
"packetBackend": "auto",
"packetServer": "" "packetServer": ""
} }

View File

@@ -1,4 +1,8 @@
{ {
"6.9.56-28418-arm64": {
"send": "4471360",
"recv": "4473BCC"
},
"3.2.12-28418-x64": { "3.2.12-28418-x64": {
"recv": "A0723E0", "recv": "A0723E0",
"send": "A06EAE0" "send": "A06EAE0"
@@ -18,5 +22,37 @@
"3.2.13-28788-x64": { "3.2.13-28788-x64": {
"send": "A0CEC20", "send": "A0CEC20",
"recv": "A0D2520" "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.58-28971-arm64": {
"send": "449ACA0",
"recv": "449D50C"
},
"9.9.16-29271-x64": {
"send": "3833510",
"recv": "3837944"
},
"3.2.13-29271-x64": {
"send": "A11E680",
"recv": "A121F80"
},
"3.2.13-29271-arm64": {
"send": "6ECA098",
"recv": "6ECD8D0"
} }
} }

View File

@@ -62,7 +62,26 @@ export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
process.dlopen(nativemodule, wrapperNodePath); process.dlopen(nativemodule, wrapperNodePath);
return nativemodule.exports; 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 { export class NapCatCore {
readonly context: InstanceContext; readonly context: InstanceContext;
readonly apis: StableNTApiWrapper; readonly apis: StableNTApiWrapper;
@@ -100,7 +119,7 @@ export class NapCatCore {
if (!fs.existsSync(this.NapCatTempPath)) { if (!fs.existsSync(this.NapCatTempPath)) {
fs.mkdirSync(this.NapCatTempPath, { recursive: true }); fs.mkdirSync(this.NapCatTempPath, { recursive: true });
} }
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger)); this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
this.context.logger.setFileLogEnabled( this.context.logger.setFileLogEnabled(
@@ -140,7 +159,7 @@ export class NapCatCore {
}; };
//await sleep(2500); //await sleep(2500);
this.context.session.getMsgService().addKernelMsgListener( this.context.session.getMsgService().addKernelMsgListener(
proxiedListenerOf(msgListener, this.context.logger) as any, proxiedListenerOf(msgListener, this.context.logger),
); );
const profileListener = new NodeIKernelProfileListener(); const profileListener = new NodeIKernelProfileListener();
@@ -236,7 +255,7 @@ export class NapCatCore {
} }
}; };
this.context.session.getGroupService().addKernelGroupListener( this.context.session.getGroupService().addKernelGroupListener(
proxiedListenerOf(groupListener, this.context.logger) as any, proxiedListenerOf(groupListener, this.context.logger),
); );
} }
@@ -276,7 +295,7 @@ export async function genSessionConfig(
d2: '', d2: '',
d2Key: '', d2Key: '',
machineId: '', machineId: '',
platform: systemPlatform, // 3是Windows? platform: systemPlatform, // 3是Windows?
platVer: systemVersion, // 系统版本号, 应该可以固定 platVer: systemVersion, // 系统版本号, 应该可以固定
appid: QQVersionAppid, appid: QQVersionAppid,
rdeliveryConfig: { rdeliveryConfig: {

View File

@@ -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 { export class NodeIKernelGroupListener {
onGroupListInited(listEmpty: boolean): void { } onGroupListInited(listEmpty: boolean): void { }
@@ -71,7 +71,8 @@ export class NodeIKernelGroupListener {
sceneId: string, sceneId: string,
ids: string[], ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean, hasPrev: boolean,
hasNext: boolean,
hasRobot: boolean hasRobot: boolean
}) { }) {
} }
@@ -79,6 +80,6 @@ export class NodeIKernelGroupListener {
onSearchMemberChange(...args: unknown[]) { onSearchMemberChange(...args: unknown[]) {
} }
onShutUpMemberListChanged(...args: unknown[]) { onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>) {
} }
} }

View File

@@ -1,180 +0,0 @@
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 { PacketHexStr } from "@/core/packet/packer";
import { sleep } from "@/common/helper";
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] Connected to ${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] Reconnecting attempt failed,${error.message}`);
});
}, 5000 * this.reconnectAttempts);
} else {
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] Max reconnect attempts reached. ${this.clientUrl}`);
}
} catch (error: any) {
this.logger.logError.bind(this.logger)(`Error attempting to reconnect: ${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 is not init');
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));
});
}
}

View File

@@ -0,0 +1,101 @@
import { LRUCache } from "@/common/lru-cache";
import { NapCatCore } from "@/core";
import { LogWrapper } from "@/common/log";
import crypto, { createHash } from "crypto";
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { NapCatConfig } from "@/core/helper/config";
export interface RecvPacket {
type: string, // 仅recv
trace_id_md5?: string,
data: RecvPacketData
}
export interface RecvPacketData {
seq: number
cmd: string
hex_data: string
}
export abstract class PacketClient {
readonly napCatCore: NapCatCore;
protected readonly logger: LogWrapper;
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
protected isAvailable: boolean = false;
protected config: NapCatConfig;
protected constructor(core: NapCatCore) {
this.napCatCore = core;
this.logger = core.context.logger;
this.config = core.configLoader.configData;
}
private randText(len: number): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
get available(): boolean {
return this.isAvailable;
}
abstract check(core: NapCatCore): boolean;
abstract init(pid: number, recv: string, send: string): Promise<void>;
abstract connect(cb: () => void): Promise<void>;
abstract sendCommandImpl(cmd: string, data: string, trace_id: string): void;
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);
}
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 (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);
}
});
this.sendCommandImpl(cmd, data, trace_id);
const timeoutHandle = setTimeout(() => {
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
}, timeout);
});
}
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
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 () => {
//console.log('sendPacket:', cmd, data, trace_id);
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);
}
}

View File

@@ -0,0 +1,80 @@
import crypto, { createHash } from "crypto";
import { NapCatCore } from "@/core";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import { PacketClient } from "@/core/packet/client/client";
import { constants } from "node:os";
import { LRUCache } from "@/common/lru-cache";
//0 send 1recv
export interface NativePacketExportType {
InitHook?: (recv: string, send: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean;
SendPacket?: (cmd: string, data: string, trace_id: string) => void;
}
export class NativePacketClient extends PacketClient {
private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64'];
private MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
private sendEvent = new LRUCache<number, string>(500);//seq->trace_id
constructor(core: NapCatCore) {
super(core);
}
get available(): boolean {
return this.isAvailable;
}
check(): boolean {
const platform = process.platform + '.' + process.arch;
if (!this.supportedPlatforms.includes(platform)) {
this.logger.logWarn(`[Core] [Packet:Native] 不支持的平台: ${platform}`);
return false;
}
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node');
if (!fs.existsSync(moehoo_path)) {
this.logger.logWarn(`[Core] [Packet:Native] 缺失运行时文件: ${moehoo_path}`);
return false;
}
return true;
}
async init(pid: number, recv: string, send: string): Promise<void> {
const platform = process.platform + '.' + process.arch;
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node');
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, uin: string, cmd: string, seq: number, hex_data: string) => {
const trace_id = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex');
if (type === 0 && this.cb.get(trace_id + 'recv')) {
//此时为send 提取seq
this.sendEvent.put(seq, trace_id);
}
if (type === 1 && this.sendEvent.get(seq)) {
//此时为recv 调用callback
const trace_id = this.sendEvent.get(seq);
const callback = this.cb.get(trace_id + 'recv');
console.log('callback:', callback, trace_id);
callback?.({ seq, cmd, hex_data });
}
// const callback = this.cb.get(createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex') + (type === 0 ? 'send' : 'recv'));
// if (callback) {
// callback({ seq, cmd, hex_data });
// } else {
// this.logger.logError(`Callback not found for hex_data: ${hex_data}`);
// }
//console.log('type:', type, 'cmd:', cmd, 'trace_id:', trace_id);
});
this.isAvailable = true;
}
sendCommandImpl(cmd: string, data: string, trace_id: string): void {
const trace_id_md5 = createHash('md5').update(trace_id).digest('hex');
//console.log('sendCommandImpl:', cmd, data, trace_id_md5);
this.MoeHooExport.exports.SendPacket?.(cmd, data, trace_id_md5);
this.cb.get(trace_id_md5 + 'send')?.({ seq: 0, cmd, hex_data: '' });
}
connect(cb: () => void): Promise<void> {
cb();
return Promise.resolve();
}
}

View File

@@ -0,0 +1,112 @@
import { Data, WebSocket } from "ws";
import { NapCatCore } from "@/core";
import { PacketClient, RecvPacket } from "@/core/packet/client/client";
export class wsPacketClient extends PacketClient {
private websocket: WebSocket | undefined;
private reconnectAttempts: number = 0;
private readonly maxReconnectAttempts: number = 60; // 现在暂时不可配置
private readonly clientUrl: string | null = null;
private clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`;
constructor(core: NapCatCore) {
super(core);
this.clientUrl = this.config.packetServer ? this.clientUrlWrap( this.config.packetServer) : null;
}
check(): boolean {
if (!this.clientUrl) {
this.logger.logWarn(`[Core] [Packet:Server] 未配置服务器地址`);
return false;
}
return true;
}
connect(cb: () => void): 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.isAvailable = 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.isAvailable = 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}`);
}
}
async init(pid: number, recv: string, send: string): Promise<void> {
if (!this.isAvailable || !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));
}
sendCommandImpl(cmd: string, data: string, trace_id: string) : void {
const commandMessage = {
action: 'send',
cmd: cmd,
data: data,
trace_id: trace_id
};
this.websocket!.send(JSON.stringify(commandMessage));
}
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}`);
}
}
}

View File

@@ -0,0 +1,16 @@
export enum AIVoiceChatType {
Unknown = 0,
Sound = 1,
Sing = 2
}
export interface AIVoiceItem {
voiceId: string;
voiceDisplayName: string;
voiceExampleUrl: string;
}
export interface AIVoiceItemList {
category: string;
voices: AIVoiceItem[];
}

View 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;
}

View 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,
};
}
}

View File

@@ -1,8 +1,8 @@
import * as stream from 'node:stream'; import * as stream from 'node:stream';
import {ReadStream} from "node:fs"; import { ReadStream } from "node:fs";
import {PacketHighwaySig} from "@/core/packet/highway/session"; import { PacketHighwaySig } from "@/core/packet/highway/session";
import {HighwayHttpUploader, HighwayTcpUploader} from "@/core/packet/highway/uploader"; import { HighwayHttpUploader, HighwayTcpUploader } from "@/core/packet/highway/uploader";
import {LogWrapper} from "@/common/log"; import { LogWrapper } from "@/common/log";
export interface PacketHighwayTrans { export interface PacketHighwayTrans {
uin: string; uin: string;
@@ -36,7 +36,7 @@ export class PacketHighwayClient {
this.port = port; this.port = port;
} }
private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 3600): PacketHighwayTrans { private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 1200): PacketHighwayTrans {
return { return {
uin: this.sig.uin, uin: this.sig.uin,
cmd: cmd, cmd: cmd,

View File

@@ -1,16 +1,24 @@
import * as fs from "node:fs"; import * as fs from "node:fs";
import {ChatType, Peer} from "@/core"; import { ChatType, Peer } from "@/core";
import {LogWrapper} from "@/common/log"; import { LogWrapper } from "@/common/log";
import {PacketClient} from "@/core/packet/client"; import { PacketPacker } from "@/core/packet/packer";
import {PacketPacker} from "@/core/packet/packer"; import { NapProtoMsg } from "@napneko/nap-proto-core";
import {NapProtoMsg} from "@/core/packet/proto/NapProto"; import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
import {HttpConn0x6ff_501Response} from "@/core/packet/proto/action/action"; import { PacketHighwayClient } from "@/core/packet/highway/client";
import {PacketHighwayClient} from "@/core/packet/highway/client"; import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
import {NTV2RichMediaResp} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase";
import {OidbSvcTrpcTcpBaseRsp} from "@/core/packet/proto/oidb/OidbBase"; import {
import {PacketMsgPicElement} from "@/core/packet/msg/element"; PacketMsgFileElement,
import {NTV2RichMediaHighwayExt} from "@/core/packet/proto/highway/highway"; PacketMsgPicElement,
import {int32ip2str, oidbIpv4s2HighwayIpv4s} from "@/core/packet/highway/utils"; 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";
import { PacketClient } from "@/core/packet/client/client";
export const BlockSize = 1024 * 1024; export const BlockSize = 1024 * 1024;
@@ -21,6 +29,7 @@ interface HighwayServerAddr {
export interface PacketHighwaySig { export interface PacketHighwaySig {
uin: string; uin: string;
uid: string;
sigSession: Uint8Array | null sigSession: Uint8Array | null
sessionKey: Uint8Array | null sessionKey: Uint8Array | null
serverAddr: HighwayServerAddr[] serverAddr: HighwayServerAddr[]
@@ -39,18 +48,18 @@ export class PacketHighwaySession {
this.logger = logger; this.logger = logger;
this.sig = { this.sig = {
uin: this.packetClient.napCatCore.selfInfo.uin, uin: this.packetClient.napCatCore.selfInfo.uin,
uid: this.packetClient.napCatCore.selfInfo.uid,
sigSession: null, sigSession: null,
sessionKey: null, sessionKey: null,
serverAddr: [], serverAddr: [],
} };
this.packer = packer; this.packer = packer;
this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger); this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger);
} }
private async checkAvailable() { private async checkAvailable() {
if (!this.packetClient.available) { if (!this.packetClient.available) {
this.logger.logError('[Highway] packetServer not available!'); throw new Error('packetBackend不可用请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置');
throw new Error('packetServer不可用请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置');
} }
if (this.sig.sigSession === null || this.sig.sessionKey === null) { if (this.sig.sigSession === null || this.sig.sessionKey === null) {
this.logger.logWarn('[Highway] sigSession or sessionKey not available!'); this.logger.logWarn('[Highway] sigSession or sessionKey not available!');
@@ -69,8 +78,8 @@ export class PacketHighwaySession {
const rsp = new NapProtoMsg(HttpConn0x6ff_501Response).decode( const rsp = new NapProtoMsg(HttpConn0x6ff_501Response).decode(
Buffer.from(req.hex_data, 'hex') Buffer.from(req.hex_data, 'hex')
); );
this.sig.sigSession = rsp.httpConn.sigSession this.sig.sigSession = rsp.httpConn.sigSession;
this.sig.sessionKey = rsp.httpConn.sessionKey this.sig.sessionKey = rsp.httpConn.sessionKey;
for (const info of rsp.httpConn.serverInfos) { for (const info of rsp.httpConn.serverInfos) {
if (info.serviceType !== 1) continue; if (info.serviceType !== 1) continue;
for (const addr of info.serverAddrs) { for (const addr of info.serverAddrs) {
@@ -78,7 +87,7 @@ export class PacketHighwaySession {
this.sig.serverAddr.push({ this.sig.serverAddr.push({
ip: int32ip2str(addr.ip), ip: int32ip2str(addr.ip),
port: addr.port port: addr.port
}) });
} }
} }
} }
@@ -86,7 +95,7 @@ export class PacketHighwaySession {
async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> { async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> {
await this.checkAvailable(); await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) { if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupImageReq(Number(peer.peerUid), img); await this.uploadGroupImageReq(+peer.peerUid, img);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) { } else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CImageReq(peer.peerUid, img); await this.uploadC2CImageReq(peer.peerUid, img);
} else { } else {
@@ -94,16 +103,53 @@ export class PacketHighwaySession {
} }
} }
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> { 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 preReq = await this.packer.packUploadGroupImgReq(groupUin, img);
const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true); const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex') Buffer.from(preRespRaw.hex_data, 'hex')
); );
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") { if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); this.logger.logDebug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
@@ -118,31 +164,32 @@ export class PacketHighwaySession {
hash: { hash: {
fileSha1: [sha1] fileSha1: [sha1]
} }
}) });
await this.packetHighwayClient.upload( await this.packetHighwayClient.upload(
1004, 1004,
fs.createReadStream(img.path, {highWaterMark: BlockSize}), fs.createReadStream(img.path, { highWaterMark: BlockSize }),
img.size, img.size,
md5, md5,
extend extend
); );
} else { } else {
this.logger.logDebug(`[Highway] get upload invalid ukey ${ukey}, don't need upload!`); this.logger.logDebug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`);
} }
img.msgInfo = preRespData.upload.msgInfo; img.msgInfo = preRespData.upload.msgInfo;
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg) // img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
} }
private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise<void> { 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 preReq = await this.packer.packUploadC2CImgReq(peerUid, img);
const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c5_100', preReq, true); const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex') Buffer.from(preRespRaw.hex_data, 'hex')
); );
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") { if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); this.logger.logDebug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
@@ -157,15 +204,367 @@ export class PacketHighwaySession {
hash: { hash: {
fileSha1: [sha1] fileSha1: [sha1]
} }
}) });
await this.packetHighwayClient.upload( await this.packetHighwayClient.upload(
1003, 1003,
fs.createReadStream(img.path, {highWaterMark: BlockSize}), fs.createReadStream(img.path, { highWaterMark: BlockSize }),
img.size, img.size,
md5, md5,
extend extend
); );
} else {
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
} }
img.msgInfo = preRespData.upload.msgInfo; 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;
}
} }

View File

@@ -2,13 +2,13 @@ import * as net from "node:net";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import * as http from "node:http"; import * as http from "node:http";
import * as stream from "node:stream"; import * as stream from "node:stream";
import {LogWrapper} from "@/common/log"; import { LogWrapper } from "@/common/log";
import * as tea from "@/core/packet/utils/crypto/tea"; import * as tea from "@/core/packet/utils/crypto/tea";
import {NapProtoMsg} from "@/core/packet/proto/NapProto"; import { NapProtoMsg } from "@napneko/nap-proto-core";
import {ReqDataHighwayHead, RespDataHighwayHead} from "@/core/packet/proto/highway/highway"; import { ReqDataHighwayHead, RespDataHighwayHead } from "@/core/packet/proto/highway/highway";
import {BlockSize} from "@/core/packet/highway/session"; import { BlockSize } from "@/core/packet/highway/session";
import {PacketHighwayTrans} from "@/core/packet/highway/client"; import { PacketHighwayTrans } from "@/core/packet/highway/client";
import {Frame} from "@/core/packet/highway/frame"; import { Frame } from "@/core/packet/highway/frame";
abstract class HighwayUploader { abstract class HighwayUploader {
readonly trans: PacketHighwayTrans; readonly trans: PacketHighwayTrans;
@@ -19,11 +19,20 @@ abstract class HighwayUploader {
this.logger = logger; this.logger = logger;
} }
encryptTransExt(key: Uint8Array) { private encryptTransExt(key: Uint8Array) {
if (!this.trans.encrypt) return; if (!this.trans.encrypt) return;
this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key)); 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 { buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array {
return new NapProtoMsg(ReqDataHighwayHead).encode({ return new NapProtoMsg(ReqDataHighwayHead).encode({
msgBaseHead: { msgBaseHead: {
@@ -53,7 +62,7 @@ abstract class HighwayUploader {
uint32LoginSigType: 8, uint32LoginSigType: 8,
appId: 1600001604, appId: 1600001604,
} }
}) });
} }
abstract upload(): Promise<void>; abstract upload(): Promise<void>;
@@ -86,15 +95,18 @@ class HighwayTcpUploaderTransform extends stream.Transform {
export class HighwayTcpUploader extends HighwayUploader { export class HighwayTcpUploader extends HighwayUploader {
async upload(): Promise<void> { async upload(): Promise<void> {
const highwayTransForm = new HighwayTcpUploaderTransform(this); const controller = new AbortController();
const upload = new Promise<void>((resolve, _) => { 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, () => { const socket = net.connect(this.trans.port, this.trans.server, () => {
this.trans.data.pipe(highwayTransForm).pipe(socket, {end: false}); this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false });
}) });
const handleRspHeader = (header: Buffer) => { const handleRspHeader = (header: Buffer) => {
const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header); const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header);
if (rsp.errorCode !== 0) { if (rsp.errorCode !== 0) {
this.logger.logWarn(`[Highway] tcpUpload failed (code: ${rsp.errorCode})`); 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); 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')}`); this.logger.logDebug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`);
@@ -105,62 +117,69 @@ export class HighwayTcpUploader extends HighwayUploader {
} }
}; };
socket.on('data', (chunk: Buffer) => { socket.on('data', (chunk: Buffer) => {
try { if (signal.aborted) {
const [head, _] = Frame.unpack(chunk); socket.end();
handleRspHeader(head); reject(new Error('Upload aborted due to timeout'));
} catch (e) {
this.logger.logError(`[Highway] tcpUpload parse response error: ${e}`);
} }
}) const [head, _] = Frame.unpack(chunk);
handleRspHeader(head);
});
socket.on('close', () => { socket.on('close', () => {
this.logger.logDebug('[Highway] tcpUpload socket closed.'); this.logger.logDebug('[Highway] tcpUpload socket closed.');
resolve(); resolve();
}) });
socket.on('error', (err) => { socket.on('error', (err) => {
this.logger.logError('[Highway] tcpUpload socket.on error:', err);
})
this.trans.data.on('error', (err) => {
this.logger.logError('[Highway] tcpUpload readable error:', err);
socket.end(); socket.end();
}) reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`));
}) });
const timeout = new Promise<void>((_, reject) => { this.trans.data.on('error', (err) => {
setTimeout(() => { socket.end();
reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`)) reject(new Error(`[Highway] tcpUpload readable error: ${err}`));
}, (this.trans.timeout ?? Infinity) * 1000 });
) });
}) const timeout = this.timeout().catch((err) => {
controller.abort();
throw new Error(err.message);
});
await Promise.race([upload, timeout]); await Promise.race([upload, timeout]);
} }
} }
// TODO: timeout impl
export class HighwayHttpUploader extends HighwayUploader { export class HighwayHttpUploader extends HighwayUploader {
async upload(): Promise<void> { async upload(): Promise<void> {
let offset = 0; const controller = new AbortController();
for await (const chunk of this.trans.data) { const { signal } = controller;
let block = chunk as Buffer; const upload = (async () => {
try { let offset = 0;
await this.uploadBlock(block, offset); for await (const chunk of this.trans.data) {
} catch (err) { if (signal.aborted) {
this.logger.logError(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`); throw new Error('Upload aborted due to timeout');
throw err; }
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;
} }
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> { private async uploadBlock(block: Buffer, offset: number): Promise<void> {
const chunkMD5 = crypto.createHash('md5').update(block).digest(); const chunkMD5 = crypto.createHash('md5').update(block).digest();
const payload = this.buildPicUpHead(offset, block.length, chunkMD5); const payload = this.buildPicUpHead(offset, block.length, chunkMD5);
const frame = Frame.pack(Buffer.from(payload), block) 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 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 [head, body] = Frame.unpack(resp);
const headData = new NapProtoMsg(RespDataHighwayHead).decode(head); 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')}`); this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`);
if (headData.errorCode !== 0) { if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
this.logger.logError(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
}
} }
private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> { private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> {
@@ -176,12 +195,12 @@ export class HighwayHttpUploader extends HighwayUploader {
}, },
}; };
const req = http.request(serverURL, options, (res) => { const req = http.request(serverURL, options, (res) => {
let data = Buffer.alloc(0); const data: Buffer[] = [];
res.on('data', (chunk) => { res.on('data', (chunk) => {
data = Buffer.concat([data, chunk]); data.push(chunk);
}); });
res.on('end', () => { res.on('end', () => {
resolve(data); resolve(Buffer.concat(data));
}); });
}); });
req.write(frame); req.write(frame);

View File

@@ -1,11 +1,11 @@
import {NapProtoEncodeStructType} from "@/core/packet/proto/NapProto"; import { NapProtoEncodeStructType } from "@napneko/nap-proto-core";
import {IPv4} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; import { IPv4 } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
import {NTHighwayIPv4} from "@/core/packet/proto/highway/highway"; import { NTHighwayIPv4 } from "@/core/packet/proto/highway/highway";
export const int32ip2str = (ip: number) => { export const int32ip2str = (ip: number) => {
ip = ip & 0xffffffff; ip = ip & 0xffffffff;
return [ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, ((ip & 0xff000000) >> 24) & 0xff].join('.'); return [ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, ((ip & 0xff000000) >> 24) & 0xff].join('.');
} };
export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType<typeof IPv4>[]): NapProtoEncodeStructType<typeof NTHighwayIPv4>[] =>{ export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType<typeof IPv4>[]): NapProtoEncodeStructType<typeof NTHighwayIPv4>[] =>{
return ipv4s.map((ip) => { return ipv4s.map((ip) => {
@@ -15,6 +15,6 @@ export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType<typeof IP
ip: int32ip2str(ip.outIP!), ip: int32ip2str(ip.outIP!),
}, },
port: ip.outPort! port: ip.outPort!
} as NapProtoEncodeStructType<typeof NTHighwayIPv4> } as NapProtoEncodeStructType<typeof NTHighwayIPv4>;
}) });
} };

View File

@@ -1,8 +1,10 @@
import * as crypto from "crypto"; import * as crypto from "crypto";
import {PushMsgBody} from "@/core/packet/proto/message/message"; import { PushMsgBody } from "@/core/packet/proto/message/message";
import {NapProtoEncodeStructType} from "@/core/packet/proto/NapProto"; import { NapProtoEncodeStructType } from "@napneko/nap-proto-core";
import {LogWrapper} from "@/common/log"; import { LogWrapper } from "@/common/log";
import {PacketMsg} from "@/core/packet/msg/message"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/element";
import { SendTextElement } from "@/core";
export class PacketMsgBuilder { export class PacketMsgBuilder {
private logger: LogWrapper; private logger: LogWrapper;
@@ -11,10 +13,23 @@ export class PacketMsgBuilder {
this.logger = logger; this.logger = logger;
} }
protected static failBackText = new PacketMsgTextElement(
{
textElement: { content: "[该消息类型暂不支持查看]" }!
} as SendTextElement
);
buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] { buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
return element.map((node): 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 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() ?? []); 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 { return {
responseHead: { responseHead: {
fromUid: "", fromUid: "",
@@ -35,7 +50,7 @@ export class PacketMsgBuilder {
divSeq: node.groupId ? undefined : 4, divSeq: node.groupId ? undefined : 4,
msgId: crypto.randomBytes(4).readUInt32LE(0), msgId: crypto.randomBytes(4).readUInt32LE(0),
sequence: crypto.randomBytes(4).readUInt32LE(0), sequence: crypto.randomBytes(4).readUInt32LE(0),
timeStamp: Math.floor(Date.now() / 1000), timeStamp: +node.time.toString().substring(0, 10),
field7: BigInt(1), field7: BigInt(1),
field8: 0, field8: 0,
field9: 0, field9: 0,
@@ -50,7 +65,8 @@ export class PacketMsgBuilder {
body: { body: {
richText: { richText: {
elems: msgElement elems: msgElement
} },
msgContent: msgContent,
} }
}; };
}); });

View 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);
}
};
}

View File

@@ -1,7 +1,5 @@
import assert from "node:assert";
import * as zlib from "node:zlib"; import * as zlib from "node:zlib";
import * as crypto from "node:crypto"; import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
import {NapProtoEncodeStructType, NapProtoMsg} from "@/core/packet/proto/NapProto";
import { import {
CustomFace, CustomFace,
Elem, Elem,
@@ -26,27 +24,32 @@ import {
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement
} from "@/core"; } from "@/core";
import {MsgInfo} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import {PacketMsg, PacketSendMsgElement} from "@/core/packet/msg/message"; 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 // raw <-> packet
// TODO: check ob11 -> raw impl!
// TODO: parse to raw element
// TODO: SendStructLongMsgElement // TODO: SendStructLongMsgElement
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> { export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
protected constructor(rawElement: T) { protected constructor(rawElement: T) {
} }
get valid(): boolean {
return true;
}
buildContent(): Uint8Array | undefined { buildContent(): Uint8Array | undefined {
return undefined; return undefined;
} }
buildElement(): NapProtoEncodeStructType<typeof Elem>[] | undefined { buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
return undefined; return [];
} }
toPreview(): string { toPreview(): string {
return '[nya~]'; return '[暂不支持该消息类型喵~]';
} }
} }
@@ -86,59 +89,15 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
text: { text: {
str: this.text, str: this.text,
pbReserve: new NapProtoMsg(MentionExtra).encode({ pbReserve: new NapProtoMsg(MentionExtra).encode({
type: this.atAll ? 1 : 2, type: this.atAll ? 1 : 2,
uin: 0, uin: 0,
field5: 0, field5: 0,
uid: this.targetUid, uid: this.targetUid,
} }
) )
} }
}]; }];
} }
toPreview(): string {
return `@${this.targetUid} ${this.text}`;
}
}
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 = Number(element.picElement.fileSize);
this.md5 = element.picElement.md5HexStr ?? '';
this.width = element.picElement.picWidth;
this.height = element.picElement.picHeight;
this.picType = element.picElement.picType;
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
assert(this.msgInfo !== null, 'msgInfo is null, expected not null');
return [{
commonElem: {
serviceType: 48,
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
businessType: 10,
}
}]
}
toPreview(): string {
return "[图片]";
}
} }
export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> { export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
@@ -153,11 +112,11 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
constructor(element: SendReplyElement) { constructor(element: SendReplyElement) {
super(element); super(element);
this.messageId = BigInt(element.replyElement.replayMsgId ?? 0); this.messageId = BigInt(element.replyElement.replayMsgId ?? 0);
this.messageSeq = Number(element.replyElement.replayMsgSeq ?? 0); this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0);
this.messageClientSeq = Number(element.replyElement.replyMsgClientSeq ?? 0); this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0);
this.targetUin = Number(element.replyElement.senderUin ?? 0); this.targetUin = +(element.replyElement.senderUin ?? 0);
this.targetUid = element.replyElement.senderUidStr ?? ''; this.targetUid = element.replyElement.senderUidStr ?? '';
this.time = Number(element.replyElement.replyMsgTime ?? 0); this.time = +(element.replyElement.replyMsgTime ?? 0);
this.elems = []; // TODO: in replyElement.sourceMsgTextElems this.elems = []; // TODO: in replyElement.sourceMsgTextElems
} }
@@ -187,11 +146,11 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
uid: String(this.targetUid), uid: String(this.targetUid),
}), }),
} : undefined, } : undefined,
}] }];
} }
toPreview(): string { toPreview(): string {
return "[回复]"; return "[回复消息]";
} }
} }
@@ -221,7 +180,7 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
}), }),
businessType: 1 businessType: 1
} }
}] }];
} else if (this.faceId < 260) { } else if (this.faceId < 260) {
return [{ return [{
face: { face: {
@@ -239,7 +198,7 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
}), }),
businessType: 1 businessType: 1
} }
}] }];
} }
} }
@@ -278,29 +237,224 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
field8: 1 field8: 1
} }
} }
}] }];
} }
toPreview(): string { toPreview(): string {
return this.emojiName; 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> { 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) { constructor(element: SendVideoElement) {
super(element); 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;
} }
}
export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> { get valid(): boolean {
constructor(element: SendFileElement) { return !!this.msgInfo;
super(element); }
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> { 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) { constructor(element: SendPttElement) {
super(element); 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}`;
} }
} }
@@ -320,11 +474,11 @@ export class PacketMsgLightAppElement extends IPacketMsgElement<SendArkElement>
zlib.deflateSync(Buffer.from(this.payload, 'utf-8')) zlib.deflateSync(Buffer.from(this.payload, 'utf-8'))
]) ])
} }
}] }];
} }
toPreview(): string { toPreview(): string {
return "[小程序]"; return "[卡片消息]";
} }
} }
@@ -345,11 +499,11 @@ export class PacketMsgMarkDownElement extends IPacketMsgElement<SendMarkdownElem
}), }),
businessType: 1 businessType: 1
} }
}] }];
} }
toPreview(): string { toPreview(): string {
return this.content; return `[Markdown消息 ${this.content}]`;
} }
} }
@@ -363,58 +517,15 @@ export class PacketMultiMsgElement extends IPacketMsgElement<SendStructLongMsgEl
this.message = message ?? []; this.message = message ?? [];
} }
get isGroupMsg(): boolean {
return this.message.some(msg => msg.groupId !== undefined);
}
get JSON() {
const id = crypto.randomUUID();
return {
app: "com.tencent.multimsg",
config: {
autosize: 1,
forward: 1,
round: 1,
type: "normal",
width: 300
},
desc: "[聊天记录]",
extra: {
filename: id,
tsum: this.message.length,
},
meta: {
detail: {
news: this.message.length === 0 ? [{
text: "[Nya~ This message is send from NapCat.Packet!]",
}] : this.message.map(packetMsg => ({
text: `${packetMsg.senderName}: ${packetMsg.msg.map(msg => msg.toPreview()).join('')}`,
})),
resid: this.resid,
source: this.isGroupMsg ? "群聊的聊天记录" :
this.message.length
? Array.from(new Set(this.message.map(msg => msg.senderName)))
.join('和') + '的聊天记录'
: '聊天记录',
summary: `查看${this.message.length}条转发消息`,
uniseq: id,
}
},
prompt: "[聊天记录]",
ver: "0.0.0.5",
view: "contact",
}
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] { buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
return [{ return [{
lightAppElem: { lightAppElem: {
data: Buffer.concat([ data: Buffer.concat([
Buffer.from([0x01]), Buffer.from([0x01]),
zlib.deflateSync(Buffer.from(JSON.stringify(this.JSON), 'utf-8')) zlib.deflateSync(Buffer.from(JSON.stringify(ForwardMsgBuilder.fromPacketMsg(this.resid, this.message)), 'utf-8'))
]) ])
} }
}] }];
} }
toPreview(): string { toPreview(): string {

View File

@@ -1,5 +1,5 @@
import {IPacketMsgElement} from "@/core/packet/msg/element"; import { IPacketMsgElement } from "@/core/packet/message/element";
import {SendMessageElement, SendStructLongMsgElement} from "@/core"; import { SendMessageElement, SendStructLongMsgElement } from "@/core";
export type PacketSendMsgElement = SendMessageElement | SendStructLongMsgElement export type PacketSendMsgElement = SendMessageElement | SendStructLongMsgElement

View File

@@ -1,131 +0,0 @@
import {
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/msg/element";
import {PacketMsg, PacketSendMsgElement} from "@/core/packet/msg/message";
import {LogWrapper} from "@/common/log";
type SendMessageElementMap = {
textElement: SendTextElement,
picElement: SendPicElement,
replyElement: SendReplyElement,
faceElement: SendFaceElement,
marketFaceElement: SendMarketFaceElement,
videoElement: SendVideoElement,
fileElement: SendFileElement,
pttElement: SendPttElement,
arkElement: SendArkElement,
markdownElement: SendMarkdownElement,
structLongMsgElement: SendStructLongMsgElement
};
type RawToPacketMsgConverters = {
[K in keyof SendMessageElementMap]: (
element: SendMessageElementMap[K],
msg?: RawMessage,
elementWrapper?: MessageElement,
) => IPacketMsgElement<SendMessageElementMap[K]> | null;
};
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;
}
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) => {
const key = (Object.keys(this.rawToPacketMsgConverters) as Array<keyof SendMessageElementMap>).find(
(k) => (element as any)[k] !== undefined // TODO:
);
if (key) {
const elementData = (element as any)[key]; // TODO:
if (elementData) return this.rawToPacketMsgConverters[key](element as any)
}
return null;
}).filter((e) => e !== null)
}
}
private rawToPacketMsgConverters: RawToPacketMsgConverters = {
textElement: (element: SendTextElement) => {
if (element.textElement.atType) {
return new PacketMsgAtElement(element)
}
return new PacketMsgTextElement(element)
},
picElement: (element: SendPicElement) => {
return new PacketMsgPicElement(element)
},
replyElement: (element: SendReplyElement) => {
return new PacketMsgReplyElement(element)
},
faceElement: (element: SendFaceElement) => {
return new PacketMsgFaceElement(element)
},
marketFaceElement: (element: SendMarketFaceElement) => {
return new PacketMsgMarkFaceElement(element)
},
videoElement: (element: SendVideoElement) => {
return new PacketMsgVideoElement(element)
},
fileElement: (element: SendFileElement) => {
return new PacketMsgFileElement(element)
},
pttElement: (element: SendPttElement) => {
return new PacketMsgPttElement(element)
},
arkElement: (element: SendArkElement) => {
return new PacketMsgLightAppElement(element)
},
markdownElement: (element: SendMarkdownElement) => {
return new PacketMsgMarkDownElement(element)
},
// TODO: check this logic, move it in arkElement?
structLongMsgElement: (element: SendStructLongMsgElement) => {
return new PacketMultiMsgElement(element)
}
}
}

View File

@@ -1,26 +1,43 @@
import * as zlib from "node:zlib"; import * as zlib from "node:zlib";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import {calculateSha1} from "@/core/packet/utils/crypto/hash" import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
import {NapProtoMsg} from "@/core/packet/proto/NapProto"; import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
import {OidbSvcTrpcTcpBase} from "@/core/packet/proto/oidb/OidbBase"; import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
import {OidbSvcTrpcTcp0X9067_202} from "@/core/packet/proto/oidb/Oidb.0x9067_202"; 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 { 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 { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.0XFE1_2";
import {OidbSvcTrpcTcp0XED3_1} from "@/core/packet/proto/oidb/Oidb.0xED3_1"; import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1";
import {NTV2RichMediaReq} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { IndexNode, NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import {HttpConn0x6ff_501} from "@/core/packet/proto/action/action"; import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
import {LongMsgResult, SendLongMsgReq} from "@/core/packet/proto/message/action"; import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
import {PacketMsgBuilder} from "@/core/packet/msg/builder"; import { PacketMsgBuilder } from "@/core/packet/message/builder";
import {PacketMsgPicElement} from "@/core/packet/msg/element"; import {
import {LogWrapper} from "@/common/log"; PacketMsgFileElement,
import {PacketMsg} from "@/core/packet/msg/message"; PacketMsgPicElement,
import {OidbSvcTrpcTcp0x6D6} from "@/core/packet/proto/oidb/Oidb.0x6D6"; PacketMsgPttElement,
import {OidbSvcTrpcTcp0XE37_1200} from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; PacketMsgVideoElement
import {PacketMsgConverter} from "@/core/packet/msg/converter"; } from "@/core/packet/message/element";
import {PacketClient} from "@/core/packet/client"; 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 { 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";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
import { OidbSvcTrpcTcp0X929B_0, OidbSvcTrpcTcp0X929D_0 } from "@/core/packet/proto/oidb/Oidb.0x929";
import { PacketClient } from "@/core/packet/client/client";
export type PacketHexStr = string & { readonly hexNya: unique symbol }; export type PacketHexStr = string & { readonly hexNya: unique symbol };
export interface OidbPacket {
cmd: string;
data: PacketHexStr
}
export class PacketPacker { export class PacketPacker {
readonly logger: LogWrapper; readonly logger: LogWrapper;
readonly client: PacketClient; readonly client: PacketClient;
@@ -34,30 +51,34 @@ export class PacketPacker {
this.packetConverter = new PacketMsgConverter(logger); this.packetConverter = new PacketMsgConverter(logger);
} }
private toHexStr(byteArray: Uint8Array): PacketHexStr { private packetPacket(byteArray: Uint8Array): PacketHexStr {
return Buffer.from(byteArray).toString('hex') as PacketHexStr; return Buffer.from(byteArray).toString('hex') as PacketHexStr;
} }
packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): Uint8Array { packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
return new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({
command: cmd, command: cmd,
subCommand: subCmd, subCommand: subCmd,
body: body, body: body,
isReserved: isUid ? 1 : 0 isReserved: isUid ? 1 : 0
}); });
return {
cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`,
data: this.packetPacket(data)
};
} }
packPokePacket(group: number, peer: number): PacketHexStr { packPokePacket(peer: number, group?: number): OidbPacket {
const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({ const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({
uin: peer, uin: peer,
groupUin: group, groupUin: group,
friendUin: group, friendUin: group ?? peer,
ext: 0 ext: 0
}); });
return this.toHexStr(this.packOidbPacket(0xed3, 1, oidb_0xed3)); return this.packOidbPacket(0xed3, 1, oidb_0xed3);
} }
packRkeyPacket(): PacketHexStr { packRkeyPacket(): OidbPacket {
const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({ const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({
reqHead: { reqHead: {
common: { common: {
@@ -77,10 +98,10 @@ export class PacketPacker {
key: [10, 20, 2] key: [10, 20, 2]
}, },
}); });
return this.toHexStr(this.packOidbPacket(0x9067, 202, oidb_0x9067_202)); return this.packOidbPacket(0x9067, 202, oidb_0x9067_202);
} }
packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): PacketHexStr { packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket {
const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({ const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({
targetUid: uid, targetUid: uid,
specialTitle: tittle, specialTitle: tittle,
@@ -91,15 +112,15 @@ export class PacketPacker {
groupUin: +groupCode, groupUin: +groupCode,
body: oidb_0x8FC_2_body body: oidb_0x8FC_2_body
}); });
return this.toHexStr(this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false)); return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false);
} }
packStatusPacket(uin: number): PacketHexStr { packStatusPacket(uin: number): OidbPacket {
const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({ const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({
uin: uin, uin: uin,
key: [{key: 27372}] key: [{ key: 27372 }]
}); });
return this.toHexStr(this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2)); return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2);
} }
async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> { async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> {
@@ -131,12 +152,12 @@ export class PacketPacker {
} }
); );
// this.logger.logDebug("packUploadForwardMsg REQ!!!", req); // this.logger.logDebug("packUploadForwardMsg REQ!!!", req);
return this.toHexStr(req); return this.packetPacket(req);
} }
// highway part // highway part
packHttp0x6ff_501(): PacketHexStr { packHttp0x6ff_501(): PacketHexStr {
return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({ return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({
httpConn: { httpConn: {
field1: 0, field1: 0,
field2: 0, field2: 0,
@@ -153,7 +174,7 @@ export class PacketPacker {
})); }));
} }
async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<PacketHexStr> { async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode( const req = new NapProtoMsg(NTV2RichMediaReq).encode(
{ {
reqHead: { reqHead: {
@@ -177,9 +198,9 @@ export class PacketPacker {
uploadInfo: [ uploadInfo: [
{ {
fileInfo: { fileInfo: {
fileSize: Number(img.size), fileSize: +img.size,
fileHash: img.md5, fileHash: img.md5,
fileSha1: this.toHexStr(await calculateSha1(img.path)), fileSha1: img.sha1!,
fileName: img.name, fileName: img.name,
type: { type: {
type: 1, type: 1,
@@ -217,93 +238,448 @@ export class PacketPacker {
noNeedCompatMsg: false, noNeedCompatMsg: false,
} }
} }
) );
return this.toHexStr(this.packOidbPacket(0x11c4, 100, req, true, false)); return this.packOidbPacket(0x11c4, 100, req, true, false);
} }
async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise<PacketHexStr> { async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode({ const req = new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: { reqHead: {
common: { common: {
requestId: 1, requestId: 1,
command: 100 command: 100
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: peerUin
}, },
scene: { },
requestType: 2, client: {
businessType: 1, agentType: 2,
sceneType: 1, }
c2C: { },
accountType: 2, upload: {
targetUid: peerUin 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:
}, },
client: { video: {
agentType: 2, bytesPbReserve: Buffer.alloc(0),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.alloc(0),
bytesGeneralFlags: Buffer.alloc(0),
} }
}, },
upload: { clientSeq: 0,
uploadInfo: [ noNeedCompatMsg: false,
{
fileInfo: {
fileSize: Number(img.size),
fileHash: img.md5,
fileSha1: this.toHexStr(await calculateSha1(img.path)),
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.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false)); );
return this.packOidbPacket(0x11c5, 100, req, true, false);
} }
packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<OidbPacket> {
return this.toHexStr( if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ const req = new NapProtoMsg(NTV2RichMediaReq).encode({
download: { reqHead: {
groupUin: groupUin, common: {
appId: 7, requestId: 3,
busId: 102, command: 100
fileId: fileUUID },
scene: {
requestType: 2,
businessType: 2,
sceneType: 2,
group: {
groupUin: groupUin
},
},
client: {
agentType: 2
} }
}), true, false) },
) 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 { packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr {
return this.toHexStr( return this.packetPacket(
new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({ new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({
subCommand: 1200, subCommand: 1200,
field2: 1, field2: 1,
@@ -319,6 +695,109 @@ export class PacketPacker {
field200: 1, field200: 1,
field99999: Buffer.from([0xc0, 0x85, 0x2c, 0x01]) field99999: Buffer.from([0xc0, 0x85, 0x2c, 0x01])
}) })
) );
}
packGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
return this.packOidbPacket(0x126E, 200, new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 4,
command: 200
},
scene: {
requestType: 1,
businessType: 3,
sceneType: 2,
group: {
groupUin: groupUin
}
},
client: {
agentType: 2
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0,
}
}
}
}), true, false);
}
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: ""
}
}
)
);
}
packFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType): OidbPacket {
return this.packOidbPacket(0x929D, 0,
new NapProtoMsg(OidbSvcTrpcTcp0X929D_0).encode({
groupUin: groupUin,
chatType: chatType
})
);
}
packAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType, sessionId: number): OidbPacket {
return this.packOidbPacket(0x929B, 0,
new NapProtoMsg(OidbSvcTrpcTcp0X929B_0).encode({
groupUin: groupUin,
voiceId: voiceId,
text: text,
chatType: chatType,
session: {
sessionId: sessionId
}
})
);
} }
} }

View File

@@ -1,139 +0,0 @@
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 extends unknown, 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 extends unknown, 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 extends unknown, 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 extends unknown, E extends boolean> = E extends true ? Partial<RequiredFieldsBaseType<T, E>> : RequiredFieldsBaseType<T, E>;
type OptionalFieldsType<T extends unknown, E extends boolean> = E extends true ? Partial<OptionalFieldsBaseType<T, E>> : OptionalFieldsBaseType<T, E>;
type NapProtoStructType<T extends unknown, E extends boolean> = RequiredFieldsType<T, E> & OptionalFieldsType<T, E>;
export type NapProtoEncodeStructType<T extends unknown> = NapProtoStructType<T, true>;
export type NapProtoDecodeStructType<T extends unknown> = 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>;
}
}

View File

@@ -1,6 +1,6 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
import {ContentHead, MessageBody, MessageControl, RoutingHead} from "@/core/packet/proto/message/message"; import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/proto/message/message";
export const FaceRoamRequest = { export const FaceRoamRequest = {
comm: ProtoField(1, () => PlatInfo, true), comm: ProtoField(1, () => PlatInfo, true),

View File

@@ -0,0 +1,49 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
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),
};

View File

@@ -1,6 +1,6 @@
import {ScalarType} from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import {ProtoField} from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
import {MsgInfo, MsgInfoBody} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { MsgInfo, MsgInfoBody } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
export const DataHighwayHead = { export const DataHighwayHead = {
version: ProtoField(1, ScalarType.UINT32), version: ProtoField(1, ScalarType.UINT32),
@@ -12,7 +12,7 @@ export const DataHighwayHead = {
dataFlag: ProtoField(7, ScalarType.UINT32), dataFlag: ProtoField(7, ScalarType.UINT32),
commandId: ProtoField(8, ScalarType.UINT32), commandId: ProtoField(8, ScalarType.UINT32),
buildVer: ProtoField(9, ScalarType.BYTES, true), buildVer: ProtoField(9, ScalarType.BYTES, true),
} };
export const FileUploadExt = { export const FileUploadExt = {
unknown1: ProtoField(1, ScalarType.INT32), unknown1: ProtoField(1, ScalarType.INT32),
@@ -20,7 +20,7 @@ export const FileUploadExt = {
unknown3: ProtoField(3, ScalarType.INT32), unknown3: ProtoField(3, ScalarType.INT32),
entry: ProtoField(100, () => FileUploadEntry), entry: ProtoField(100, () => FileUploadEntry),
unknown200: ProtoField(200, ScalarType.INT32), unknown200: ProtoField(200, ScalarType.INT32),
} };
export const FileUploadEntry = { export const FileUploadEntry = {
busiBuff: ProtoField(100, () => ExcitingBusiInfo), busiBuff: ProtoField(100, () => ExcitingBusiInfo),
@@ -28,14 +28,14 @@ export const FileUploadEntry = {
clientInfo: ProtoField(300, () => ExcitingClientInfo), clientInfo: ProtoField(300, () => ExcitingClientInfo),
fileNameInfo: ProtoField(400, () => ExcitingFileNameInfo), fileNameInfo: ProtoField(400, () => ExcitingFileNameInfo),
host: ProtoField(500, () => ExcitingHostConfig), host: ProtoField(500, () => ExcitingHostConfig),
} };
export const ExcitingBusiInfo = { export const ExcitingBusiInfo = {
busId: ProtoField(1, ScalarType.INT32), busId: ProtoField(1, ScalarType.INT32),
senderUin: ProtoField(100, ScalarType.UINT64), senderUin: ProtoField(100, ScalarType.UINT64),
receiverUin: ProtoField(200, ScalarType.UINT64), receiverUin: ProtoField(200, ScalarType.UINT64),
groupCode: ProtoField(400, ScalarType.UINT64), groupCode: ProtoField(400, ScalarType.UINT64),
} };
export const ExcitingFileEntry = { export const ExcitingFileEntry = {
fileSize: ProtoField(100, ScalarType.UINT64), fileSize: ProtoField(100, ScalarType.UINT64),
@@ -44,7 +44,7 @@ export const ExcitingFileEntry = {
md5S2: ProtoField(400, ScalarType.BYTES), md5S2: ProtoField(400, ScalarType.BYTES),
fileId: ProtoField(600, ScalarType.STRING), fileId: ProtoField(600, ScalarType.STRING),
uploadKey: ProtoField(700, ScalarType.BYTES), uploadKey: ProtoField(700, ScalarType.BYTES),
} };
export const ExcitingClientInfo = { export const ExcitingClientInfo = {
clientType: ProtoField(100, ScalarType.INT32), clientType: ProtoField(100, ScalarType.INT32),
@@ -52,31 +52,31 @@ export const ExcitingClientInfo = {
terminalType: ProtoField(300, ScalarType.INT32), terminalType: ProtoField(300, ScalarType.INT32),
clientVer: ProtoField(400, ScalarType.STRING), clientVer: ProtoField(400, ScalarType.STRING),
unknown: ProtoField(600, ScalarType.INT32), unknown: ProtoField(600, ScalarType.INT32),
} };
export const ExcitingFileNameInfo = { export const ExcitingFileNameInfo = {
fileName: ProtoField(100, ScalarType.STRING), fileName: ProtoField(100, ScalarType.STRING),
} };
export const ExcitingHostConfig = { export const ExcitingHostConfig = {
hosts: ProtoField(200, () => ExcitingHostInfo, false, true), hosts: ProtoField(200, () => ExcitingHostInfo, false, true),
} };
export const ExcitingHostInfo = { export const ExcitingHostInfo = {
url: ProtoField(1, () => ExcitingUrlInfo), url: ProtoField(1, () => ExcitingUrlInfo),
port: ProtoField(2, ScalarType.UINT32), port: ProtoField(2, ScalarType.UINT32),
} };
export const ExcitingUrlInfo = { export const ExcitingUrlInfo = {
unknown: ProtoField(1, ScalarType.INT32), unknown: ProtoField(1, ScalarType.INT32),
host: ProtoField(2, ScalarType.STRING), host: ProtoField(2, ScalarType.STRING),
} };
export const LoginSigHead = { export const LoginSigHead = {
uint32LoginSigType: ProtoField(1, ScalarType.UINT32), uint32LoginSigType: ProtoField(1, ScalarType.UINT32),
bytesLoginSig: ProtoField(2, ScalarType.BYTES), bytesLoginSig: ProtoField(2, ScalarType.BYTES),
appId: ProtoField(3, ScalarType.UINT32), appId: ProtoField(3, ScalarType.UINT32),
} };
export const NTV2RichMediaHighwayExt = { export const NTV2RichMediaHighwayExt = {
fileUuid: ProtoField(1, ScalarType.STRING), fileUuid: ProtoField(1, ScalarType.STRING),
@@ -85,25 +85,25 @@ export const NTV2RichMediaHighwayExt = {
msgInfoBody: ProtoField(6, () => MsgInfoBody, false, true), msgInfoBody: ProtoField(6, () => MsgInfoBody, false, true),
blockSize: ProtoField(10, ScalarType.UINT32), blockSize: ProtoField(10, ScalarType.UINT32),
hash: ProtoField(11, () => NTHighwayHash), hash: ProtoField(11, () => NTHighwayHash),
} };
export const NTHighwayHash = { export const NTHighwayHash = {
fileSha1: ProtoField(1, ScalarType.BYTES, false, true), fileSha1: ProtoField(1, ScalarType.BYTES, false, true),
} };
export const NTHighwayNetwork = { export const NTHighwayNetwork = {
ipv4s: ProtoField(1, () => NTHighwayIPv4, false, true), ipv4s: ProtoField(1, () => NTHighwayIPv4, false, true),
} };
export const NTHighwayIPv4 = { export const NTHighwayIPv4 = {
domain: ProtoField(1, () => NTHighwayDomain), domain: ProtoField(1, () => NTHighwayDomain),
port: ProtoField(2, ScalarType.UINT32), port: ProtoField(2, ScalarType.UINT32),
} };
export const NTHighwayDomain = { export const NTHighwayDomain = {
isEnable: ProtoField(1, ScalarType.BOOL), isEnable: ProtoField(1, ScalarType.BOOL),
ip: ProtoField(2, ScalarType.STRING), ip: ProtoField(2, ScalarType.STRING),
} };
export const ReqDataHighwayHead = { export const ReqDataHighwayHead = {
msgBaseHead: ProtoField(1, () => DataHighwayHead, true), msgBaseHead: ProtoField(1, () => DataHighwayHead, true),
@@ -111,7 +111,7 @@ export const ReqDataHighwayHead = {
bytesReqExtendInfo: ProtoField(3, ScalarType.BYTES, true), bytesReqExtendInfo: ProtoField(3, ScalarType.BYTES, true),
timestamp: ProtoField(4, ScalarType.UINT64), timestamp: ProtoField(4, ScalarType.UINT64),
msgLoginSigHead: ProtoField(5, () => LoginSigHead, true), msgLoginSigHead: ProtoField(5, () => LoginSigHead, true),
} };
export const RespDataHighwayHead = { export const RespDataHighwayHead = {
msgBaseHead: ProtoField(1, () => DataHighwayHead, true), msgBaseHead: ProtoField(1, () => DataHighwayHead, true),
@@ -124,7 +124,7 @@ export const RespDataHighwayHead = {
timestamp: ProtoField(8, ScalarType.UINT64), timestamp: ProtoField(8, ScalarType.UINT64),
range: ProtoField(9, ScalarType.UINT64), range: ProtoField(9, ScalarType.UINT64),
isReset: ProtoField(10, ScalarType.UINT32), isReset: ProtoField(10, ScalarType.UINT32),
} };
export const SegHead = { export const SegHead = {
serviceId: ProtoField(1, ScalarType.UINT32, true), serviceId: ProtoField(1, ScalarType.UINT32, true),
@@ -140,7 +140,7 @@ export const SegHead = {
queryTimes: ProtoField(11, ScalarType.UINT32), queryTimes: ProtoField(11, ScalarType.UINT32),
updateCacheIp: ProtoField(12, ScalarType.UINT32), updateCacheIp: ProtoField(12, ScalarType.UINT32),
cachePort: ProtoField(13, ScalarType.UINT32, true), cachePort: ProtoField(13, ScalarType.UINT32, true),
} };
export const GroupAvatarExtra = { export const GroupAvatarExtra = {
type: ProtoField(1, ScalarType.UINT32), type: ProtoField(1, ScalarType.UINT32),
@@ -148,8 +148,8 @@ export const GroupAvatarExtra = {
field3: ProtoField(3, () => GroupAvatarExtraField3), field3: ProtoField(3, () => GroupAvatarExtraField3),
field5: ProtoField(5, ScalarType.UINT32), field5: ProtoField(5, ScalarType.UINT32),
field6: ProtoField(6, ScalarType.UINT32), field6: ProtoField(6, ScalarType.UINT32),
} };
export const GroupAvatarExtraField3 = { export const GroupAvatarExtraField3 = {
field1: ProtoField(1, ScalarType.UINT32), field1: ProtoField(1, ScalarType.UINT32),
} };

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
import { PushMsgBody } from "@/core/packet/proto/message/message"; import { PushMsgBody } from "@/core/packet/proto/message/message";
export const LongMsgResult = { export const LongMsgResult = {

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const C2C = { export const C2C = {
uin: ProtoField(1, ScalarType.UINT32, true), uin: ProtoField(1, ScalarType.UINT32, true),

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
import { Elem } from "@/core/packet/proto/message/element"; import { Elem } from "@/core/packet/proto/message/element";
export const Attr = { export const Attr = {
@@ -113,6 +113,24 @@ export const Permission = {
export const FileExtra = { export const FileExtra = {
file: ProtoField(1, () => NotOnlineFile), 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 = { export const GroupFileExtra = {
@@ -132,8 +150,9 @@ export const GroupFileExtraInfo = {
fileSize: ProtoField(3, ScalarType.UINT64), fileSize: ProtoField(3, ScalarType.UINT64),
fileName: ProtoField(4, ScalarType.STRING), fileName: ProtoField(4, ScalarType.STRING),
field5: ProtoField(5, ScalarType.UINT32), field5: ProtoField(5, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.STRING), fileSha: ProtoField(6, ScalarType.BYTES),
fileMd5: ProtoField(8, ScalarType.STRING), extInfoString: ProtoField(7, ScalarType.STRING),
fileMd5: ProtoField(8, ScalarType.BYTES),
}; };
export const ImageExtraUrl = { export const ImageExtraUrl = {

View File

@@ -1,5 +1,5 @@
import {ScalarType} from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import {ProtoField} from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const Elem = { export const Elem = {
text: ProtoField(1, () => Text, true), text: ProtoField(1, () => Text, true),
@@ -118,7 +118,7 @@ export const MarketFace = {
export const MarketFacePbRes = { export const MarketFacePbRes = {
field8: ProtoField(8, ScalarType.INT32) field8: ProtoField(8, ScalarType.INT32)
} };
export const CustomFace = { export const CustomFace = {
guid: ProtoField(1, ScalarType.BYTES), guid: ProtoField(1, ScalarType.BYTES),
@@ -315,7 +315,7 @@ export const SrcMsgPbRes = {
senderUid: ProtoField(6, ScalarType.STRING, true), senderUid: ProtoField(6, ScalarType.STRING, true),
receiverUid: ProtoField(7, ScalarType.STRING, true), receiverUid: ProtoField(7, ScalarType.STRING, true),
friendSeq: ProtoField(8, ScalarType.UINT32, true), friendSeq: ProtoField(8, ScalarType.UINT32, true),
} };
export const LightAppElem = { export const LightAppElem = {
data: ProtoField(1, ScalarType.BYTES), data: ProtoField(1, ScalarType.BYTES),
@@ -358,4 +358,4 @@ export const QSmallFaceExtra = {
export const MarkdownData = { export const MarkdownData = {
content: ProtoField(1, ScalarType.STRING) content: ProtoField(1, ScalarType.STRING)
} };

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const GroupRecallMsg = { export const GroupRecallMsg = {
type: ProtoField(1, ScalarType.UINT32), type: ProtoField(1, ScalarType.UINT32),

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
import { ForwardHead, Grp, GrpTmp, ResponseForward, ResponseGrp, Trans0X211, WPATmp } from "@/core/packet/proto/message/routing"; import { ForwardHead, Grp, GrpTmp, ResponseForward, ResponseGrp, Trans0X211, WPATmp } from "@/core/packet/proto/message/routing";
import { RichText } from "@/core/packet/proto/message/component"; import { RichText } from "@/core/packet/proto/message/component";
import { C2C } from "@/core/packet/proto/message/c2c"; import { C2C } from "@/core/packet/proto/message/c2c";

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const FriendRecall = { export const FriendRecall = {
info: ProtoField(1, () => FriendRecallInfo), info: ProtoField(1, () => FriendRecallInfo),

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const ForwardHead = { export const ForwardHead = {
field1: ProtoField(1, ScalarType.UINT32, true), field1: ProtoField(1, ScalarType.UINT32, true),

View File

@@ -0,0 +1,62 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
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),
};

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0XFE1_2 = { export const OidbSvcTrpcTcp0XFE1_2 = {
uin: ProtoField(1, ScalarType.UINT32), uin: ProtoField(1, ScalarType.UINT32),

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0x6D6 = { export const OidbSvcTrpcTcp0x6D6 = {
file: ProtoField(1, () => OidbSvcTrpcTcp0x6D6Upload, true), file: ProtoField(1, () => OidbSvcTrpcTcp0x6D6Upload, true),

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
//设置群头衔 OidbSvcTrpcTcp.0x8fc_2 //设置群头衔 OidbSvcTrpcTcp.0x8fc_2
@@ -13,4 +13,4 @@ export const OidbSvcTrpcTcp0X8FC_2_Body = {
export const OidbSvcTrpcTcp0X8FC_2 = { export const OidbSvcTrpcTcp0X8FC_2 = {
groupUin: ProtoField(1, ScalarType.UINT32), groupUin: ProtoField(1, ScalarType.UINT32),
body: ProtoField(3, ScalarType.BYTES), body: ProtoField(3, ScalarType.BYTES),
}; };

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
import { MultiMediaReqHead } from "./common/Ntv2.RichMediaReq"; import { MultiMediaReqHead } from "./common/Ntv2.RichMediaReq";
//Req //Req
@@ -14,6 +14,7 @@ export const OidbSvcTrpcTcp0X9067_202Key = {
//Rsp //Rsp
export const OidbSvcTrpcTcp0X9067_202_RkeyList = { export const OidbSvcTrpcTcp0X9067_202_RkeyList = {
rkey: ProtoField(1, ScalarType.STRING), rkey: ProtoField(1, ScalarType.STRING),
ttl: ProtoField(2, ScalarType.UINT64),
time: ProtoField(4, ScalarType.UINT32), time: ProtoField(4, ScalarType.UINT32),
type: ProtoField(5, ScalarType.UINT32), type: ProtoField(5, ScalarType.UINT32),

View File

@@ -0,0 +1,42 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
export const OidbSvcTrpcTcp0X929D_0 = {
groupUin: ProtoField(1, ScalarType.UINT32),
chatType: ProtoField(2, ScalarType.UINT32),
};
export const OidbSvcTrpcTcp0X929D_0Resp = {
content: ProtoField(1, () => OidbSvcTrpcTcp0X929D_0RespContent, false, true),
};
export const OidbSvcTrpcTcp0X929D_0RespContent = {
category: ProtoField(1, ScalarType.STRING),
voices: ProtoField(2, () => OidbSvcTrpcTcp0X929D_0RespContentVoice, false, true),
};
export const OidbSvcTrpcTcp0X929D_0RespContentVoice = {
voiceId: ProtoField(1, ScalarType.STRING),
voiceDisplayName: ProtoField(2, ScalarType.STRING),
voiceExampleUrl: ProtoField(3, ScalarType.STRING),
};
export const OidbSvcTrpcTcp0X929B_0 = {
groupUin: ProtoField(1, ScalarType.UINT32),
voiceId: ProtoField(2, ScalarType.STRING),
text: ProtoField(3, ScalarType.STRING),
chatType: ProtoField(4, ScalarType.UINT32),
session: ProtoField(5, () => OidbSvcTrpcTcp0X929B_0_Session),
};
export const OidbSvcTrpcTcp0X929B_0_Session = {
sessionId: ProtoField(1, ScalarType.UINT32),
};
export const OidbSvcTrpcTcp0X929B_0Resp = {
statusCode: ProtoField(1, ScalarType.UINT32),
field2: ProtoField(2, ScalarType.UINT32, true),
field3: ProtoField(3, ScalarType.UINT32),
msgInfo: ProtoField(4, () => MsgInfo, true),
};

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0XE37_1200 = { export const OidbSvcTrpcTcp0XE37_1200 = {
subCommand: ProtoField(1, ScalarType.UINT32, true), subCommand: ProtoField(1, ScalarType.UINT32, true),
@@ -30,7 +30,7 @@ export const OidbSvcTrpcTcp0XE37_1200ResponseBody = {
field10: ProtoField(10, ScalarType.UINT32, true), field10: ProtoField(10, ScalarType.UINT32, true),
state: ProtoField(20, ScalarType.STRING, true), state: ProtoField(20, ScalarType.STRING, true),
result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true), result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true),
metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_1200Metadata, true), metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true),
}; };
export const OidbSvcTrpcTcp0XE37_1200Result = { export const OidbSvcTrpcTcp0XE37_1200Result = {
@@ -43,7 +43,7 @@ export const OidbSvcTrpcTcp0XE37_1200Result = {
extra: ProtoField(120, ScalarType.BYTES, true), extra: ProtoField(120, ScalarType.BYTES, true),
}; };
export const OidbSvcTrpcTcp0XE37_1200Metadata = { export const OidbSvcTrpcTcp0XE37_800_1200Metadata = {
uin: ProtoField(1, ScalarType.UINT32, true), uin: ProtoField(1, ScalarType.UINT32, true),
field2: ProtoField(2, ScalarType.UINT32, true), field2: ProtoField(2, ScalarType.UINT32, true),
field3: ProtoField(3, ScalarType.UINT32, true), field3: ProtoField(3, ScalarType.UINT32, true),

View File

@@ -0,0 +1,23 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0XE37_1700 = {
command: ProtoField(1, ScalarType.UINT32, true),
seq: ProtoField(2, ScalarType.INT32, true),
upload: ProtoField(19, () => ApplyUploadReqV3, true),
businessId: ProtoField(101, ScalarType.INT32, true),
clientType: ProtoField(102, ScalarType.INT32, true),
flagSupportMediaPlatform: ProtoField(200, ScalarType.INT32, true),
};
export const ApplyUploadReqV3 = {
senderUid: ProtoField(10, ScalarType.STRING, true),
receiverUid: ProtoField(20, ScalarType.STRING, true),
fileSize: ProtoField(30, ScalarType.UINT32, true),
fileName: ProtoField(40, ScalarType.STRING, true),
md510MCheckSum: ProtoField(50, ScalarType.BYTES, true),
sha1CheckSum: ProtoField(60, ScalarType.BYTES, true),
localPath: ProtoField(70, ScalarType.STRING, true),
md5CheckSum: ProtoField(110, ScalarType.BYTES, true),
sha3CheckSum: ProtoField(120, ScalarType.BYTES, true),
};

View File

@@ -0,0 +1,12 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0XEB7_Body = {
uin: ProtoField(1, ScalarType.STRING),
groupUin: ProtoField(2, ScalarType.STRING),
version: ProtoField(3, ScalarType.STRING),
};
export const OidbSvcTrpcTcp0XEB7 = {
body: ProtoField(2, () => OidbSvcTrpcTcp0XEB7_Body),
};

View File

@@ -1,5 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
// Send Poke // Send Poke
export const OidbSvcTrpcTcp0XED3_1 = { export const OidbSvcTrpcTcp0XED3_1 = {

View File

@@ -1,13 +1,14 @@
import { ScalarType } from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcpBase = { export const OidbSvcTrpcTcpBase = {
command: ProtoField(1, ScalarType.UINT32), command: ProtoField(1, ScalarType.UINT32),
subCommand: ProtoField(2, ScalarType.UINT32), subCommand: ProtoField(2, ScalarType.UINT32),
errorCode: ProtoField(3, ScalarType.UINT32),
body: ProtoField(4, ScalarType.BYTES), body: ProtoField(4, ScalarType.BYTES),
errorMsg: ProtoField(5, ScalarType.STRING, true), errorMsg: ProtoField(5, ScalarType.STRING, true),
isReserved: ProtoField(12, ScalarType.UINT32) isReserved: ProtoField(12, ScalarType.UINT32)
}; };
export const OidbSvcTrpcTcpBaseRsp = { export const OidbSvcTrpcTcpBaseRsp = {
body: ProtoField(4, ScalarType.BYTES) body: ProtoField(4, ScalarType.BYTES)
}; };

View File

@@ -1,5 +1,5 @@
import {ScalarType} from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import {ProtoField} from "../../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
export const NTV2RichMediaReq = { export const NTV2RichMediaReq = {
ReqHead: ProtoField(1, () => MultiMediaReqHead), ReqHead: ProtoField(1, () => MultiMediaReqHead),
@@ -160,7 +160,7 @@ export const PicUrlExtInfo = {
export const VideoExtInfo = { export const VideoExtInfo = {
VideoCodecFormat: ProtoField(1, ScalarType.UINT32), VideoCodecFormat: ProtoField(1, ScalarType.UINT32),
} };
export const ExtBizInfo = { export const ExtBizInfo = {
Pic: ProtoField(1, () => PicExtBizInfo), Pic: ProtoField(1, () => PicExtBizInfo),

View File

@@ -1,6 +1,6 @@
import {ScalarType} from "@protobuf-ts/runtime"; import { ScalarType } from "@protobuf-ts/runtime";
import {ProtoField} from "../../NapProto"; import { ProtoField } from "@napneko/nap-proto-core";
import {CommonHead, MsgInfo, PicUrlExtInfo, VideoExtInfo} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { CommonHead, MsgInfo, PicUrlExtInfo, VideoExtInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
export const NTV2RichMediaResp = { export const NTV2RichMediaResp = {
respHead: ProtoField(1, () => MultiMediaRespHead), respHead: ProtoField(1, () => MultiMediaRespHead),
@@ -13,20 +13,20 @@ export const NTV2RichMediaResp = {
uploadKeyRenewal: ProtoField(8, () => UploadKeyRenewalResp), uploadKeyRenewal: ProtoField(8, () => UploadKeyRenewalResp),
downloadSafe: ProtoField(9, () => DownloadSafeResp), downloadSafe: ProtoField(9, () => DownloadSafeResp),
extension: ProtoField(99, ScalarType.BYTES, true), extension: ProtoField(99, ScalarType.BYTES, true),
} };
export const MultiMediaRespHead = { export const MultiMediaRespHead = {
common: ProtoField(1, () => CommonHead), common: ProtoField(1, () => CommonHead),
retCode: ProtoField(2, ScalarType.UINT32), retCode: ProtoField(2, ScalarType.UINT32),
message: ProtoField(3, ScalarType.STRING), message: ProtoField(3, ScalarType.STRING),
} };
export const DownloadResp = { export const DownloadResp = {
rKeyParam: ProtoField(1, ScalarType.STRING), rKeyParam: ProtoField(1, ScalarType.STRING),
rKeyTtlSecond: ProtoField(2, ScalarType.UINT32), rKeyTtlSecond: ProtoField(2, ScalarType.UINT32),
info: ProtoField(3, () => DownloadInfo), info: ProtoField(3, () => DownloadInfo),
rKeyCreateTime: ProtoField(4, ScalarType.UINT32), rKeyCreateTime: ProtoField(4, ScalarType.UINT32),
} };
export const DownloadInfo = { export const DownloadInfo = {
domain: ProtoField(1, ScalarType.STRING), domain: ProtoField(1, ScalarType.STRING),
@@ -36,7 +36,7 @@ export const DownloadInfo = {
ipv6s: ProtoField(5, () => IPv6, false, true), ipv6s: ProtoField(5, () => IPv6, false, true),
picUrlExtInfo: ProtoField(6, () => PicUrlExtInfo), picUrlExtInfo: ProtoField(6, () => PicUrlExtInfo),
videoExtInfo: ProtoField(7, () => VideoExtInfo), videoExtInfo: ProtoField(7, () => VideoExtInfo),
} };
export const IPv4 = { export const IPv4 = {
outIP: ProtoField(1, ScalarType.UINT32), outIP: ProtoField(1, ScalarType.UINT32),
@@ -44,7 +44,7 @@ export const IPv4 = {
inIP: ProtoField(3, ScalarType.UINT32), inIP: ProtoField(3, ScalarType.UINT32),
inPort: ProtoField(4, ScalarType.UINT32), inPort: ProtoField(4, ScalarType.UINT32),
ipType: ProtoField(5, ScalarType.UINT32), ipType: ProtoField(5, ScalarType.UINT32),
} };
export const IPv6 = { export const IPv6 = {
outIP: ProtoField(1, ScalarType.BYTES), outIP: ProtoField(1, ScalarType.BYTES),
@@ -52,7 +52,7 @@ export const IPv6 = {
inIP: ProtoField(3, ScalarType.BYTES), inIP: ProtoField(3, ScalarType.BYTES),
inPort: ProtoField(4, ScalarType.UINT32), inPort: ProtoField(4, ScalarType.UINT32),
ipType: ProtoField(5, ScalarType.UINT32), ipType: ProtoField(5, ScalarType.UINT32),
} };
export const UploadResp = { export const UploadResp = {
uKey: ProtoField(1, ScalarType.STRING, true), uKey: ProtoField(1, ScalarType.STRING, true),
@@ -64,13 +64,13 @@ export const UploadResp = {
ext: ProtoField(7, () => RichMediaStorageTransInfo, false, true), ext: ProtoField(7, () => RichMediaStorageTransInfo, false, true),
compatQMsg: ProtoField(8, ScalarType.BYTES), compatQMsg: ProtoField(8, ScalarType.BYTES),
subFileInfos: ProtoField(10, () => SubFileInfo, false, true), subFileInfos: ProtoField(10, () => SubFileInfo, false, true),
} };
export const RichMediaStorageTransInfo = { export const RichMediaStorageTransInfo = {
subType: ProtoField(1, ScalarType.UINT32), subType: ProtoField(1, ScalarType.UINT32),
extType: ProtoField(2, ScalarType.UINT32), extType: ProtoField(2, ScalarType.UINT32),
extValue: ProtoField(3, ScalarType.BYTES), extValue: ProtoField(3, ScalarType.BYTES),
} };
export const SubFileInfo = { export const SubFileInfo = {
subType: ProtoField(1, ScalarType.UINT32), subType: ProtoField(1, ScalarType.UINT32),
@@ -78,32 +78,32 @@ export const SubFileInfo = {
uKeyTtlSecond: ProtoField(3, ScalarType.UINT32), uKeyTtlSecond: ProtoField(3, ScalarType.UINT32),
ipv4s: ProtoField(4, () => IPv4, false, true), ipv4s: ProtoField(4, () => IPv4, false, true),
ipv6s: ProtoField(5, () => IPv6, false, true), ipv6s: ProtoField(5, () => IPv6, false, true),
} };
export const DownloadSafeResp = { export const DownloadSafeResp = {
} };
export const UploadKeyRenewalResp = { export const UploadKeyRenewalResp = {
ukey: ProtoField(1, ScalarType.STRING), ukey: ProtoField(1, ScalarType.STRING),
ukeyTtlSec: ProtoField(2, ScalarType.UINT64), ukeyTtlSec: ProtoField(2, ScalarType.UINT64),
} };
export const MsgInfoAuthResp = { export const MsgInfoAuthResp = {
authCode: ProtoField(1, ScalarType.UINT32), authCode: ProtoField(1, ScalarType.UINT32),
msg: ProtoField(2, ScalarType.BYTES), msg: ProtoField(2, ScalarType.BYTES),
resultTime: ProtoField(3, ScalarType.UINT64), resultTime: ProtoField(3, ScalarType.UINT64),
} };
export const UploadCompletedResp = { export const UploadCompletedResp = {
msgSeq: ProtoField(1, ScalarType.UINT64), msgSeq: ProtoField(1, ScalarType.UINT64),
} };
export const DeleteResp = { export const DeleteResp = {
} };
export const DownloadRKeyResp = { export const DownloadRKeyResp = {
rKeys: ProtoField(1, () => RKeyInfo, false, true), rKeys: ProtoField(1, () => RKeyInfo, false, true),
} };
export const RKeyInfo = { export const RKeyInfo = {
rkey: ProtoField(1, ScalarType.STRING), rkey: ProtoField(1, ScalarType.STRING),
@@ -111,4 +111,4 @@ export const RKeyInfo = {
storeId: ProtoField(3, ScalarType.UINT32), storeId: ProtoField(3, ScalarType.UINT32),
rkeyCreateTime: ProtoField(4, ScalarType.UINT32, true), rkeyCreateTime: ProtoField(4, ScalarType.UINT32, true),
type: ProtoField(5, ScalarType.UINT32, true), type: ProtoField(5, ScalarType.UINT32, true),
} };

View File

@@ -1,7 +1,19 @@
import { PacketClient } from "@/core/packet/client";
import { PacketHighwaySession } from "@/core/packet/highway/session"; import { PacketHighwaySession } from "@/core/packet/highway/session";
import { LogWrapper } from "@/common/log"; import { LogWrapper } from "@/common/log";
import {PacketPacker} from "@/core/packet/packer"; import { PacketPacker } from "@/core/packet/packer";
import { PacketClient } from "@/core/packet/client/client";
import { NativePacketClient } from "@/core/packet/client/nativeClient";
import { wsPacketClient } from "@/core/packet/client/wsClient";
import { NapCatCore } from "@/core";
type clientPriority = {
[key: number]: (core: NapCatCore) => PacketClient;
}
const clientPriority: clientPriority = {
10: (core: NapCatCore) => new NativePacketClient(core),
1: (core: NapCatCore) => new wsPacketClient(core),
};
export class PacketSession { export class PacketSession {
readonly logger: LogWrapper; readonly logger: LogWrapper;
@@ -9,10 +21,42 @@ export class PacketSession {
readonly packer: PacketPacker; readonly packer: PacketPacker;
readonly highwaySession: PacketHighwaySession; readonly highwaySession: PacketHighwaySession;
constructor(logger: LogWrapper, client: PacketClient) { constructor(core: NapCatCore) {
this.logger = logger; this.logger = core.context.logger;
this.client = client; this.client = this.newClient(core);
this.packer = new PacketPacker(this.logger, this.client); this.packer = new PacketPacker(this.logger, this.client);
this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer); this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer);
} }
private newClient(core: NapCatCore): PacketClient {
const prefer = core.configLoader.configData.packetBackend;
switch (prefer) {
case "native":
return new NativePacketClient(core);
case "frida":
return new wsPacketClient(core);
case "auto":
case undefined:
return this.judgeClient(core);
default:
throw new Error(`[Core] [Packet] 未知的Packet后端类型 ${prefer},请检查配置文件!`);
}
}
private judgeClient(core: NapCatCore): PacketClient {
const sortedClients = Object.entries(clientPriority)
.map(([priority, clientFactory]) => {
const client = clientFactory(core);
const score = +priority * +client.check(core);
return { client, score };
})
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score);
const selectedClient = sortedClients[0]?.client;
if (!selectedClient) {
throw new Error("[Core] [Packet] 无可用的后端NapCat.Packet将不会加载");
}
this.logger.log(`[Core] [Packet] 自动选择 ${selectedClient.constructor.name} 作为后端`);
return selectedClient;
}
} }

View File

@@ -2,6 +2,7 @@
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as stream from 'stream'; import * as stream from 'stream';
import * as fs from 'fs'; import * as fs from 'fs';
import { CalculateStreamBytesTransform } from "@/core/packet/utils/crypto/sha1StreamBytesTransform";
function sha1Stream(readable: stream.Readable) { function sha1Stream(readable: stream.Readable) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -10,7 +11,37 @@ function sha1Stream(readable: stream.Readable) {
}) as Promise<Buffer>; }) as Promise<Buffer>;
} }
function md5Stream(readable: stream.Readable) {
return new Promise((resolve, reject) => {
readable.on('error', reject);
readable.pipe(crypto.createHash('md5').on('error', reject).on('data', resolve));
}) as Promise<Buffer>;
}
export function calculateSha1(filePath: string): Promise<Buffer> { export function calculateSha1(filePath: string): Promise<Buffer> {
const readable = fs.createReadStream(filePath); const readable = fs.createReadStream(filePath);
return sha1Stream(readable); return sha1Stream(readable);
} }
export function computeMd5AndLengthWithLimit(filePath: string, limit?: number): Promise<Buffer> {
const readStream = fs.createReadStream(filePath, limit ? { start: 0, end: limit - 1 } : {});
return md5Stream(readStream);
}
export function calculateSha1StreamBytes(filePath: string): Promise<Buffer[]> {
return new Promise((resolve, reject) => {
const readable = fs.createReadStream(filePath);
const calculateStreamBytes = new CalculateStreamBytesTransform();
const byteArrayList: Buffer[] = [];
calculateStreamBytes.on('data', (chunk: Buffer) => {
byteArrayList.push(chunk);
});
calculateStreamBytes.on('end', () => {
resolve(byteArrayList);
});
calculateStreamBytes.on('error', (err) => {
reject(err);
});
readable.pipe(calculateStreamBytes);
});
}

View File

@@ -0,0 +1,19 @@
import crypto from 'crypto';
import assert from 'assert';
import { Sha1Stream } from './sha1Stream';
function testSha1Stream() {
for (let i = 0; i < 100000; i++) {
const randomLength = Math.floor(Math.random() * 1024);
const randomData = crypto.randomBytes(randomLength);
const sha1Stream = new Sha1Stream();
sha1Stream.update(randomData);
const hash = sha1Stream.final();
const expectedDigest = crypto.createHash('sha1').update(randomData).digest();
assert.strictEqual(hash.toString('hex'), expectedDigest.toString('hex'));
console.log(`Test ${i + 1}: Passed`);
}
console.log('All tests passed successfully.');
}
testSha1Stream();

View File

@@ -0,0 +1,118 @@
export class Sha1Stream {
readonly Sha1BlockSize = 64;
readonly Sha1DigestSize = 20;
private readonly _padding = Buffer.concat([Buffer.from([0x80]), Buffer.alloc(63)]);
private readonly _state = new Uint32Array(5);
private readonly _count = new Uint32Array(2);
private readonly _buffer = Buffer.allocUnsafe(this.Sha1BlockSize);
private readonly _w = new Uint32Array(80);
constructor() {
this.reset();
}
private reset(): void {
this._state[0] = 0x67452301;
this._state[1] = 0xEFCDAB89;
this._state[2] = 0x98BADCFE;
this._state[3] = 0x10325476;
this._state[4] = 0xC3D2E1F0;
this._count[0] = 0;
this._count[1] = 0;
this._buffer.fill(0);
}
private rotateLeft(v: number, o: number): number {
return ((v << o) | (v >>> (32 - o))) >>> 0;
}
private transform(chunk: Buffer, offset: number): void {
const w = this._w;
const view = new DataView(chunk.buffer, chunk.byteOffset + offset, 64);
for (let i = 0; i < 16; i++) {
w[i] = view.getUint32(i * 4, false);
}
for (let i = 16; i < 80; i++) {
w[i] = this.rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1) >>> 0;
}
let a = this._state[0];
let b = this._state[1];
let c = this._state[2];
let d = this._state[3];
let e = this._state[4];
for (let i = 0; i < 80; i++) {
const [f, k] = (i < 20) ? [(b & c) | ((~b) & d), 0x5A827999] :
(i < 40) ? [b ^ c ^ d, 0x6ED9EBA1] :
(i < 60) ? [(b & c) | (b & d) | (c & d), 0x8F1BBCDC] :
[b ^ c ^ d, 0xCA62C1D6];
const temp = (this.rotateLeft(a, 5) + f + k + e + w[i]) >>> 0;
e = d;
d = c;
c = this.rotateLeft(b, 30) >>> 0;
b = a;
a = temp;
}
this._state[0] = (this._state[0] + a) >>> 0;
this._state[1] = (this._state[1] + b) >>> 0;
this._state[2] = (this._state[2] + c) >>> 0;
this._state[3] = (this._state[3] + d) >>> 0;
this._state[4] = (this._state[4] + e) >>> 0;
}
public update(data: Buffer, len?: number): void {
let index = ((this._count[0] >>> 3) & 0x3F) >>> 0;
const dataLen = len ?? data.length;
this._count[0] = (this._count[0] + (dataLen << 3)) >>> 0;
if (this._count[0] < (dataLen << 3)) this._count[1] = (this._count[1] + 1) >>> 0;
this._count[1] = (this._count[1] + (dataLen >>> 29)) >>> 0;
const partLen = (this.Sha1BlockSize - index) >>> 0;
let i = 0;
if (dataLen >= partLen) {
data.copy(this._buffer, index, 0, partLen);
this.transform(this._buffer, 0);
for (i = partLen; (i + this.Sha1BlockSize) <= dataLen; i = (i + this.Sha1BlockSize) >>> 0) {
this.transform(data, i);
}
index = 0;
}
data.copy(this._buffer, index, i, dataLen);
}
public hash(bigEndian: boolean = true): Buffer {
const digest = Buffer.allocUnsafe(this.Sha1DigestSize);
if (bigEndian) {
for (let i = 0; i < 5; i++) digest.writeUInt32BE(this._state[i], i * 4);
} else {
for (let i = 0; i < 5; i++) digest.writeUInt32LE(this._state[i], i * 4);
}
return digest;
}
public final(): Buffer {
const digest = Buffer.allocUnsafe(this.Sha1DigestSize);
const bits = Buffer.allocUnsafe(8);
bits.writeUInt32BE(this._count[1], 0);
bits.writeUInt32BE(this._count[0], 4);
const index = ((this._count[0] >>> 3) & 0x3F) >>> 0;
const padLen = ((index < 56) ? (56 - index) : (120 - index)) >>> 0;
this.update(this._padding, padLen);
this.update(bits);
for (let i = 0; i < 5; i++) {
digest.writeUInt32BE(this._state[i], i * 4);
}
return digest;
}
}

View File

@@ -0,0 +1,53 @@
import * as stream from "node:stream";
import { Sha1Stream } from "@/core/packet/utils/crypto/sha1Stream";
export class CalculateStreamBytesTransform extends stream.Transform {
private readonly blockSize = 1024 * 1024;
private sha1: Sha1Stream;
private buffer: Buffer;
private bytesRead: number;
private readonly byteArrayList: Buffer[];
constructor() {
super();
this.sha1 = new Sha1Stream();
this.buffer = Buffer.alloc(0);
this.bytesRead = 0;
this.byteArrayList = [];
}
_transform(chunk: Buffer, _: BufferEncoding, callback: stream.TransformCallback): void {
try {
this.buffer = Buffer.concat([this.buffer, chunk]);
let offset = 0;
while (this.buffer.length - offset >= this.sha1.Sha1BlockSize) {
const block = this.buffer.subarray(offset, offset + this.sha1.Sha1BlockSize);
this.sha1.update(block);
offset += this.sha1.Sha1BlockSize;
this.bytesRead += this.sha1.Sha1BlockSize;
if (this.bytesRead % this.blockSize === 0) {
const digest = this.sha1.hash(false);
this.byteArrayList.push(Buffer.from(digest));
}
}
this.buffer = this.buffer.subarray(offset);
callback(null);
} catch (err) {
callback(err as Error);
}
}
_flush(callback: stream.TransformCallback): void {
try {
if (this.buffer.length > 0) this.sha1.update(this.buffer);
const finalDigest = this.sha1.final();
this.byteArrayList.push(Buffer.from(finalDigest));
for (const digest of this.byteArrayList) {
this.push(digest);
}
callback(null);
} catch (err) {
callback(err as Error);
}
}
}

View File

@@ -76,7 +76,7 @@ export function decrypt(encrypted: Buffer, key: Buffer) {
encrypted.writeInt32BE(r1, i); encrypted.writeInt32BE(r1, i);
encrypted.writeInt32BE(r2, i + 4); encrypted.writeInt32BE(r2, i + 4);
} }
if (Buffer.compare(encrypted.subarray(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL if (Buffer.compare(encrypted.subarray(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL;
// if (Buffer.compare(encrypted.slice(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL; // if (Buffer.compare(encrypted.slice(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL;
return encrypted.subarray((encrypted[0] & 0x07) + 3, encrypted.length - 7); return encrypted.subarray((encrypted[0] & 0x07) + 3, encrypted.length - 7);
// return encrypted.slice((encrypted[0] & 0x07) + 3, encrypted.length - 7); // return encrypted.slice((encrypted[0] & 0x07) + 3, encrypted.length - 7);

View File

@@ -66,7 +66,11 @@ export interface NodeIKernelBuddyService {
accept: boolean; accept: boolean;
}): Promise<void>; }): Promise<void>;
delBuddy(uid: number): void; delBuddy(param: {
friendUid: string;
tempBlock: boolean;
tempBothDel: boolean;
}): Promise<unknown>;
delBatchBuddy(uids: number[]): void; delBatchBuddy(uids: number[]): void;

View File

@@ -12,6 +12,13 @@ import {
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService { export interface NodeIKernelGroupService {
// --->
// 待启用 For Next Version 3.2.0
// isTroopMember ? 0 : 111
getGroupMemberMaxNum(groupCode: string, serviceType: number): Promise<unknown>;
getAllGroupPrivilegeFlag(troopUinList: string[], serviceType: number): Promise<unknown>;
// <---
getGroupExt0xEF0Info(enableGroupCodes: string[], bannedGroupCodes: string[], filter: GroupExt0xEF0InfoFilter, forceFetch: boolean): getGroupExt0xEF0Info(enableGroupCodes: string[], bannedGroupCodes: string[], filter: GroupExt0xEF0InfoFilter, forceFetch: boolean):
Promise<GeneralCallResult & { result: { groupExtInfos: Map<string, any> } }>; Promise<GeneralCallResult & { result: { groupExtInfos: Map<string, any> } }>;
@@ -98,13 +105,13 @@ export interface NodeIKernelGroupService {
uid: string, uid: string,
index: number//0 index: number//0
}>, }>,
infos: unknown, infos: Map<string, GroupMember>,
finish: true, finish: true,
hasRobot: false hasRobot: false
} }
}>; }>;
setHeader(uid: string, path: string): unknown; setHeader(uid: string, path: string): Promise<GeneralCallResult>;
addKernelGroupListener(listener: NodeIKernelGroupListener): number; addKernelGroupListener(listener: NodeIKernelGroupListener): number;
@@ -114,8 +121,8 @@ export interface NodeIKernelGroupService {
destroyMemberListScene(SceneId: string): void; destroyMemberListScene(SceneId: string): void;
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{ getNextMemberList(sceneId: string, groupMemberInfoListId: { index: number, uid: string } | undefined, num: number): Promise<{
errCode: number, errCode: number,
errMsg: string, errMsg: string,
result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean } result: { ids: string[], infos: Map<string, GroupMember>, finish: boolean, hasRobot: boolean }
}>; }>;
@@ -225,7 +232,16 @@ export interface NodeIKernelGroupService {
getGroupStatisticInfo(groupCode: string): unknown; getGroupStatisticInfo(groupCode: string): unknown;
getGroupRemainAtTimes(groupCode: string): number; getGroupRemainAtTimes(groupCode: string): Promise<Omit<GeneralCallResult, 'result'> & {
errCode: number,
atInfo: {
canAtAll: boolean
RemainAtAllCountForUin: number
RemainAtAllCountForGroup: number
atTimesMsg: string
canNotAtAllMsg: ''
}
}>;
getJoinGroupNoVerifyFlag(groupCode: string): unknown; getJoinGroupNoVerifyFlag(groupCode: string): unknown;
@@ -239,7 +255,7 @@ export interface NodeIKernelGroupService {
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>; setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>;
getGroupRecommendContactArkJson(groupCode: string): unknown; getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>;
getJoinGroupLink(param: { getJoinGroupLink(param: {
groupCode: string, groupCode: string,

View File

@@ -172,7 +172,7 @@ export interface NodeIKernelMsgService {
msgList: RawMessage[] msgList: RawMessage[]
}>; }>;
//@deprecated //@deprecated
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[] msgList: RawMessage[]
}>; }>;
@@ -186,27 +186,29 @@ export interface NodeIKernelMsgService {
getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>; getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getSourceOfReplyMsg(peer: Peer, MsgId: string, SourceSeq: string): unknown; // 下面的msgid全部不真实
getSourceOfReplyMsg(peer: Peer, msgId: string, sourceSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getSourceOfReplyMsgV2(peer: Peer, RootMsgId: string, ReplyMsgId: string): unknown; //用法和聊天记录一样
getSourceOfReplyMsgV2(peer: Peer, rootMsgId: string, replyMsgId: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown; getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown; getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string, replyMsgId: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: {
type: number, type: number,
subtype: Array<number> subtype: Array<number>
}): unknown; }): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{
type: number, type: number,
subtype: Array<number> subtype: Array<number>
}>): unknown; }>): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown; getMsgWithAbstractByFilterParam(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
queryMsgsWithFilter(...args: unknown[]): unknown; queryMsgsWithFilter(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
//queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>; //queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>;
@@ -327,8 +329,7 @@ export interface NodeIKernelMsgService {
setPttPlayedState(...args: unknown[]): unknown; setPttPlayedState(...args: unknown[]): unknown;
//uk1 uk2 true fetchFavEmojiList(str: string, num: number, backward: boolean, forceRefresh: boolean): Promise<GeneralCallResult & {
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: Array<{ emojiInfoList: Array<{
uin: string, uin: string,
emoId: number, emoId: number,

View File

@@ -86,7 +86,7 @@ export interface NodeQQNTWrapperUtil {
calcThumbSize(arg0: number, arg1: number, arg2: unknown): unknown; calcThumbSize(arg0: number, arg1: number, arg2: unknown): unknown;
fullWordToHalfWord(arg0: string): unknown; fullWordToHalfWord(word: string): unknown;
getNTUserDataInfoConfig(): unknown; getNTUserDataInfoConfig(): unknown;

View File

@@ -41,7 +41,7 @@ export async function NCoreInitFramework(
online: true, online: true,
}); });
}; };
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger) as any); loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
}); });
// 过早进入会导致addKernelMsgListener等Listener添加失败 // 过早进入会导致addKernelMsgListener等Listener添加失败
// await sleep(2500); // await sleep(2500);

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,41 @@
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
chat_type: { type: ['number', 'string'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
interface GetAiCharactersResponse {
type: string;
characters: {
character_id: string;
character_name: string;
preview_url: string;
}[];
}
export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharactersResponse[]> {
actionName = ActionName.GetAiCharacters;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const rawList = await this.core.apis.PacketApi.sendFetchAiVoiceListReq(+payload.group_id, +(payload.chat_type ?? 1) as AIVoiceChatType);
return rawList?.map((item) => ({
type: item.category,
characters: item.voices.map((voice) => ({
character_id: voice.voiceId,
character_name: voice.voiceDisplayName,
preview_url: voice.voiceExampleUrl,
})),
})) ?? [];
}
}

View File

@@ -1,3 +1,4 @@
import { GroupNotifyMsgStatus } from '@/core';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
@@ -11,18 +12,22 @@ export default class GetGroupAddRequest extends BaseAction<null, OB11GroupReques
actionName = ActionName.GetGroupIgnoreAddRequest; actionName = ActionName.GetGroupIgnoreAddRequest;
async _handle(payload: null): Promise<OB11GroupRequestNotify[] | null> { async _handle(payload: null): Promise<OB11GroupRequestNotify[] | null> {
// const data = await this.core.apis.GroupApi.getGroupIgnoreNotifies(); const ignoredNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 10);
// log(data); const retData: any = {
// const notifies: GroupNotify[] = data.notifies.filter(notify => notify.status === GroupNotifyStatus.WAIT_HANDLE); join_requests: await Promise.all(
// const returnData: OB11GroupRequestNotify[] = []; ignoredNotifies
// for (const notify of notifies) { .filter(notify => notify.type === 7)
// const uin = || (await NTQQUserApi.getUserDetailInfo(notify.user1.uid))?.uin; .map(async SSNotify => ({
// returnData.push({ request_id: SSNotify.seq,
// group_id: parseInt(notify.group.groupCode), requester_uin: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1?.uid),
// user_id: parseInt(uin), requester_nick: SSNotify.user1?.nickName,
// flag: notify.seq group_id: SSNotify.group?.groupCode,
// }); group_name: SSNotify.group?.groupName,
// } checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
return null; actor: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2?.uid) || 0,
}))),
};
return retData;
} }
} }

View File

@@ -0,0 +1,85 @@
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from "@/core/packet/entities/miniApp";
import { MiniAppInfo, MiniAppInfoHelper } from "@/core/packet/helper/miniAppHelper";
const SchemaData = {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['bili', 'weibo']
},
title: { type: 'string' },
desc: { type: 'string' },
picUrl: { type: 'string' },
jumpUrl: { type: 'string' },
iconUrl: { type: 'string' },
sdkId: { type: 'string' },
appId: { type: 'string' },
scene: { type: ['number', 'string'] },
templateType: { type: ['number', 'string'] },
businessType: { type: ['number', 'string'] },
verType: { type: ['number', 'string'] },
shareType: { type: ['number', 'string'] },
versionId: { type: 'string' },
withShareTicket: { type: ['number', 'string'] },
rawArkData: { type: ['boolean', 'string'] }
},
oneOf: [
{
required: ['type', 'title', 'desc', 'picUrl', 'jumpUrl']
},
{
required: [
'title', 'desc', 'picUrl', 'jumpUrl',
'iconUrl', 'appId', 'scene', 'templateType', 'businessType',
'verType', 'shareType', 'versionId', 'withShareTicket'
]
}
]
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
data: MiniAppData | MiniAppRawData
}> {
actionName = ActionName.GetMiniAppArk;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
let reqParam: MiniAppReqParams;
const customParams = {
title: payload.title,
desc: payload.desc,
picUrl: payload.picUrl,
jumpUrl: payload.jumpUrl
} as MiniAppReqCustomParams;
if (payload.type) {
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
} else {
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload as Required<Payload>;
reqParam = MiniAppInfoHelper.generateReq(
customParams,
{
sdkId: payload.sdkId ?? MiniAppInfo.sdkId,
appId: appId,
scene: +scene,
iconUrl: iconUrl,
templateType: +templateType,
businessType: +businessType,
verType: +verType,
shareType: +shareType,
versionId: versionId,
withShareTicket: +withShareTicket
}
);
}
const arkData = await this.core.apis.PacketApi.sendMiniAppShareInfoReq(reqParam);
return {
data: payload.rawArkData ? arkData : MiniAppInfoHelper.RawToSend(arkData)
};
}
}

View File

@@ -1,15 +1,22 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName, BaseCheckResult } from '../types';
export class GetProfileLike extends BaseAction<void, any> { interface Payload {
start: number,
count: number
}
export class GetProfileLike extends BaseAction<Payload, any> {
actionName = ActionName.GetProfileLike; actionName = ActionName.GetProfileLike;
async _handle(payload: void) { async _handle(payload: Payload) {
const ret = await this.core.apis.UserApi.getProfileLike(this.core.selfInfo.uid); const start = payload.start ? Number(payload.start) : 0;
const listdata: any[] = ret.info.userLikeInfos[0].favoriteInfo.userInfos; const count = payload.count ? Number(payload.count) : 10;
const ret = await this.core.apis.UserApi.getProfileLike(this.core.selfInfo.uid, start, count);
const listdata: any[] = ret.info.userLikeInfos[0].voteInfo.userInfos;
for (let i = 0; i < listdata.length; i++) { for (let i = 0; i < listdata.length; i++) {
listdata[i].uin = parseInt((await this.core.apis.UserApi.getUinByUidV2(listdata[i].uid)) || ''); listdata[i].uin = parseInt((await this.core.apis.UserApi.getUinByUidV2(listdata[i].uid)) || '');
} }
return listdata; return ret.info.userLikeInfos[0].voteInfo;
} }
} }

View File

@@ -1,6 +1,6 @@
import { ActionName } from '../types'; import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import {GetPacketStatusDepends} from "@/onebot/action/packet/GetPacketStatus"; import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
// no_cache get时传字符串 // no_cache get时传字符串
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',

View File

@@ -0,0 +1,22 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: 'string' },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class SetGroupSign extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupSign;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.PacketApi.sendGroupSignPacket(payload.group_id);
}
}

View File

@@ -1,16 +1,15 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName, BaseCheckResult } from '../types';
import { ChatType, Peer } from '@/core'; import { ChatType, Peer } from '@/core';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
properties: { properties: {
eventType: { type: 'string' }, event_type: { type: 'number' },
group_id: { type: 'string' }, user_id: { type: ['number', 'string'] },
user_id: { type: 'string' },
}, },
required: ['eventType'], required: ['event_type','user_id'],
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = FromSchema<typeof SchemaData>;
@@ -19,23 +18,12 @@ export class SetInputStatus extends BaseAction<Payload, any> {
actionName = ActionName.SetInputStatus; actionName = ActionName.SetInputStatus;
async _handle(payload: Payload) { async _handle(payload: Payload) {
let peer: Peer; const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (payload.group_id) { if (!uid) throw new Error('uid is empty');
peer = { const peer = {
chatType: ChatType.KCHATTYPEGROUP, chatType: ChatType.KCHATTYPEC2C,
peerUid: payload.group_id, peerUid: uid,
}; };
} else if (payload.user_id) { return await this.core.apis.MsgApi.sendShowInputStatusReq(peer, payload.event_type);
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id);
if (!uid) throw new Error('uid is empty');
peer = {
chatType: ChatType.KCHATTYPEC2C,
peerUid: uid,
};
} else {
throw new Error('请指定 group_id 或 user_id');
}
return await this.core.apis.MsgApi.sendShowInputStatusReq(peer, parseInt(payload.eventType));
} }
} }

View File

@@ -38,11 +38,10 @@ export default class SetAvatar extends BaseAction<Payload, null> {
throw `头像${payload.file}设置失败,api无返回`; throw `头像${payload.file}设置失败,api无返回`;
} }
// log(`头像设置返回:${JSON.stringify(ret)}`) // log(`头像设置返回:${JSON.stringify(ret)}`)
// @ts-ignore if (ret.result as number == 1004022) {
if (ret['result'] == 1004022) {
throw `头像${payload.file}设置失败,文件可能不是图片格式`; throw `头像${payload.file}设置失败,文件可能不是图片格式`;
} else if (ret['result'] != 0) { } else if (ret.result != 0) {
throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`; throw `头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`;
} }
} else { } else {
fs.unlink(path, () => { }); fs.unlink(path, () => { });

View File

@@ -5,9 +5,9 @@ import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types'; import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types';
export interface GetFilePayload { // interface GetFilePayload {
file: string; // 文件名或者fileUuid // file: string; // 文件名或者fileUuid
} // }
export interface GetFileResponse { export interface GetFileResponse {
file?: string; // path file?: string; // path
@@ -16,19 +16,25 @@ export interface GetFileResponse {
file_name?: string; file_name?: string;
base64?: string; base64?: string;
} }
const GetFileBase_PayloadSchema = { const GetFileBase_PayloadSchema = {
type: 'object', type: 'object',
properties: { properties: {
file: { type: 'string' }, file: { type: 'string' },
file_id: { type: 'string' }
}, },
required: ['file'], oneOf: [
{ required: ['file'] },
{ required: ['file_id'] }
]
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
export type GetFilePayload = FromSchema<typeof GetFileBase_PayloadSchema>;
export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> { export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
payloadSchema: any = GetFileBase_PayloadSchema; payloadSchema = GetFileBase_PayloadSchema;
async _handle(payload: GetFilePayload): Promise<GetFileResponse> { async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
payload.file ||= payload.file_id || '';
//接收消息标记模式 //接收消息标记模式
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file); const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file);
if (contextMsgFile) { if (contextMsgFile) {
@@ -115,27 +121,6 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
} }
} }
const GetFile_PayloadSchema = {
type: 'object',
properties: {
file_id: { type: 'string' },
file: { type: 'string' },
},
required: ['file_id'],
} as const satisfies JSONSchema;
type GetFile_Payload_Internal = FromSchema<typeof GetFile_PayloadSchema>;
interface GetFile_Payload extends GetFile_Payload_Internal {
file: string;
}
export default class GetFile extends GetFileBase { export default class GetFile extends GetFileBase {
actionName = ActionName.GetFile; actionName = ActionName.GetFile;
payloadSchema = GetFile_PayloadSchema;
async _handle(payload: GetFile_Payload): Promise<GetFileResponse> {
payload.file = payload.file_id;
return super._handle(payload);
}
} }

View File

@@ -27,7 +27,7 @@ export class GetGroupFileUrl extends GetPacketStatusDepends<Payload, GetGroupFil
if (contextMsgFile?.fileUUID) { if (contextMsgFile?.fileUUID) {
return { return {
url: await this.core.apis.PacketApi.sendGroupFileDownloadReq(+payload.group_id, contextMsgFile.fileUUID) url: await this.core.apis.PacketApi.sendGroupFileDownloadReq(+payload.group_id, contextMsgFile.fileUUID)
} };
} }
throw new Error('real fileUUID not found!'); throw new Error('real fileUUID not found!');
} }

View File

@@ -5,9 +5,11 @@ import { promises as fs } from 'fs';
import { decode } from 'silk-wasm'; import { decode } from 'silk-wasm';
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg'; const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
interface Payload extends GetFilePayload { const out_format = ['mp3' , 'amr' , 'wma' , 'm4a' , 'spx' , 'ogg' , 'wav' , 'flac'];
out_format: 'mp3' | 'amr' | 'wma' | 'm4a' | 'spx' | 'ogg' | 'wav' | 'flac';
} type Payload = {
out_format : string
} & GetFilePayload
export default class GetRecord extends GetFileBase { export default class GetRecord extends GetFileBase {
actionName = ActionName.GetRecord; actionName = ActionName.GetRecord;
@@ -17,12 +19,19 @@ export default class GetRecord extends GetFileBase {
if (payload.out_format && typeof payload.out_format === 'string') { if (payload.out_format && typeof payload.out_format === 'string') {
const inputFile = res.file; const inputFile = res.file;
if (!inputFile) throw new Error('file not found'); if (!inputFile) throw new Error('file not found');
if (!out_format.includes(payload.out_format)) {
throw new Error('转换失败 out_format 字段可能格式不正确');
}
const pcmFile = `${inputFile}.pcm`; const pcmFile = `${inputFile}.pcm`;
const outputFile = `${inputFile}.${payload.out_format}`; const outputFile = `${inputFile}.${payload.out_format}`;
try { try {
await fs.access(inputFile); await fs.access(inputFile);
await this.decodeFile(inputFile, pcmFile); try {
await this.convertFile(pcmFile, outputFile, payload.out_format); await fs.access(outputFile);
} catch (error) {
await this.decodeFile(inputFile, pcmFile);
await this.convertFile(pcmFile, outputFile, payload.out_format);
}
const base64Data = await fs.readFile(outputFile, { encoding: 'base64' }); const base64Data = await fs.readFile(outputFile, { encoding: 'base64' });
res.file = outputFile; res.file = outputFile;
res.url = outputFile; res.url = outputFile;
@@ -48,7 +57,8 @@ export default class GetRecord extends GetFileBase {
private convertFile(inputFile: string, outputFile: string, format: string): Promise<void> { private convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ffmpeg = spawn(FFMPEG_PATH, ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFile, outputFile]); const params = format === 'amr' ? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFile, '-ar', '8000', '-b:a', '12.2k', outputFile] : ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFile, outputFile];
const ffmpeg = spawn(FFMPEG_PATH, params);
ffmpeg.on('close', (code) => { ffmpeg.on('close', (code) => {
if (code === 0) { if (code === 0) {
@@ -63,4 +73,4 @@ export default class GetRecord extends GetFileBase {
}); });
}); });
} }
} }

View File

@@ -1,5 +1,5 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNodePlain as OB11MessageNode} from '@/onebot'; import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNodePlain as OB11MessageNode } from '@/onebot';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
@@ -73,16 +73,14 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
const singleMsg = data.msgList[0]; const singleMsg = data.msgList[0];
const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理 const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理
if (!resMsg) { if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) {
throw new Error('找不到相关的聊天记录'); throw new Error('找不到相关的聊天记录');
} }
//if (this.obContext.configLoader.configData.messagePostFormat === 'array') { return {
//提取 messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content
const realmsg = ((await this.parseForward([resMsg]))[0].data.message as OB11MessageNode[])[0].data.message; };
//里面都是offline消息 id都是0 没得说话
return { message: realmsg };
//} //}
// return { message: resMsg }; // return { message: resMsg };
} }
} }

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