Compare commits

...

842 Commits

Author SHA1 Message Date
手瓜一十雪
a06158bf01 fix: 标准化接口 2024-11-07 12:15:49 +08:00
pk5ls20
314e7485b8 chore: format 2024-11-07 10:33:01 +08:00
pk5ls20
aed5d2d9f0 chore: log 2024-11-07 10:31:50 +08:00
pk5ls20
f44e48a28b fix: remove useless import 2024-11-06 16:58:12 +08:00
手瓜一十雪
38be90450c feat: 兼容gocq标准 2024-11-06 16:48:18 +08:00
手瓜一十雪
2dd57d7676 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-06 16:45:04 +08:00
手瓜一十雪
6b3b163fa8 fix: 错误代码 2024-11-06 16:45:01 +08:00
Mlikiowa
9792ebafdc release: v3.6.2 2024-11-06 08:00:05 +00:00
手瓜一十雪
d10e7c37cb fix: link 2024-11-06 15:59:39 +08:00
Mlikiowa
d38f1853a4 release: v3.6.1 2024-11-06 06:42:33 +00:00
手瓜一十雪
bdec16266e fix: #498 2024-11-06 14:42:05 +08:00
Mlikiowa
49ca698ab9 release: v3.6.0 2024-11-06 03:30:17 +00:00
pk5ls20
3efd8163c9 fix: MoeHoo-Linux Amd64 2024-11-06 11:28:57 +08:00
pk5ls20
cc2d11449c release: v3.5.2 2024-11-06 09:28:14 +08:00
pk5ls20
7e9c19ca5b fix: MoeHoo-Linux Arm64 2024-11-06 09:26:15 +08:00
Mlikiowa
3b01b6827f release: v3.5.1 2024-11-05 14:45:36 +00:00
手瓜一十雪
8d9ef851ba fix: linux arm64 2024-11-05 22:45:00 +08:00
手瓜一十雪
b070bc59bc fix: MoeHoo-Linux Amd64 2024-11-05 22:36:47 +08:00
Mlikiowa
8d663946e1 release: v3.5.0 2024-11-05 14:13:33 +00:00
pk5ls20
2a2328b029 feat: better edge case handling 2024-11-05 22:11:01 +08:00
pk5ls20
efc9064abb fix: better log 2024-11-05 21:54:52 +08:00
Mlikiowa
dd70adf071 release: v3.4.11 2024-11-05 13:52:22 +00:00
pk5ls20
0f427375cb fix: sendCommand 2024-11-05 21:48:39 +08:00
Mlikiowa
4001270b93 release: v3.4.10 2024-11-05 13:34:25 +00:00
手瓜一十雪
e7f5ed3bcc Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-05 21:33:51 +08:00
手瓜一十雪
05cdc37d0a fix: qqnt 29271 最新版兼容问题 2024-11-05 21:33:36 +08:00
Mlikiowa
27920e0bee release: v3.4.9 2024-11-05 13:22:43 +00:00
手瓜一十雪
ae409b7249 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-05 21:22:10 +08:00
手瓜一十雪
8276258348 fix: Once 2024-11-05 21:21:59 +08:00
Mlikiowa
1bf96a97a5 release: v3.4.8 2024-11-05 13:18:35 +00:00
手瓜一十雪
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
手瓜一十雪
b0eae307c2 release: 3.0.5 2024-10-20 22:18:57 +08:00
手瓜一十雪
f5d2b54cca fix: 兼容晚启动 2024-10-20 22:18:34 +08:00
手瓜一十雪
3eefec3899 release: v3.0.4 2024-10-20 19:52:23 +08:00
手瓜一十雪
b6a8094554 release: v3.0.3 2024-10-20 18:56:52 +08:00
Version
4083b35436 chore:version change 2024-10-20 10:55:12 +00:00
手瓜一十雪
bb72d70baf fix: #444 尝试修复 2024-10-20 18:52:18 +08:00
手瓜一十雪
95d1a77f52 fix: remark 2024-10-20 18:30:14 +08:00
手瓜一十雪
051729886e fix 2024-10-20 17:16:05 +08:00
手瓜一十雪
0f00123dc7 fix 2024-10-20 17:01:09 +08:00
手瓜一十雪
0b0a089d86 release: 3.0.1 2024-10-20 10:07:19 +08:00
手瓜一十雪
c711a7d99a fix: error 2024-10-20 10:05:58 +08:00
手瓜一十雪
43f1d8c88c Merge pull request #443 from pk5ls20/feat/i18n-packet-server-error-msg
feat: More user-friendly packetServer error message
2024-10-20 08:14:33 +08:00
手瓜一十雪
e818e79d20 Merge pull request #442 from pk5ls20/feat/better-forward-msg
feat: better fake forwardMsg display
2024-10-20 08:14:18 +08:00
pk5ls20
cbad3ff1de feat: More user-friendly packetServer error message x2 2024-10-20 07:46:57 +08:00
pk5ls20
16a2e5e996 feat: More user-friendly packetServer error message 2024-10-20 07:28:55 +08:00
pk5ls20
331c6a50d0 feat: better fake forwardMsg display 2024-10-20 07:06:13 +08:00
手瓜一十雪
31c4540ec6 fix: error 2024-10-19 23:00:39 +08:00
手瓜一十雪
1e6116554f fix: error Version 2024-10-19 22:53:32 +08:00
手瓜一十雪
a12ea0e761 Merge pull request #436 from pk5ls20/refactor/proto
Progressive NapCat.Packet
2024-10-19 22:20:40 +08:00
pk5ls20
c9e3bbcd9f feat: Implement complete transform & Build & Upload FakeForwardMsg 2024-10-19 22:13:31 +08:00
手瓜一十雪
9c17dc1b8f fix: error 2024-10-19 21:21:54 +08:00
手瓜一十雪
69d1cae686 fix: 进一步简化 2024-10-19 21:21:37 +08:00
手瓜一十雪
1c2404b6af fix: 再次简化日志 2024-10-19 21:19:14 +08:00
手瓜一十雪
b33b33739d fix: 日志梳理 2024-10-19 21:17:49 +08:00
手瓜一十雪
2b7886c682 fix: 日志有点乱 2024-10-19 19:33:01 +08:00
手瓜一十雪
106d1f6374 fix 2024-10-19 18:08:25 +08:00
手瓜一十雪
e601786bd7 fix: CloudFlare Url 2024-10-19 18:00:34 +08:00
手瓜一十雪
fda2a98b40 version: 3.0.0 2024-10-19 17:54:26 +08:00
手瓜一十雪
c01d70b8fc fix: Mirrror Docs 2024-10-19 17:47:05 +08:00
手瓜一十雪
eccbcc3e28 fix:docs 2024-10-19 09:25:40 +08:00
pk5ls20
7a4a255a89 refactor: simplify the PacketClient availability check process & add action nc_get_packet_status 2024-10-19 04:41:32 +08:00
pk5ls20
83bced82b1 feat: add action get_group_file_url 2024-10-19 04:14:01 +08:00
pk5ls20
f3033ce732 feat: remove 10ms delay in sendSsoCmdReqByContend 2024-10-19 02:12:22 +08:00
pk5ls20
5c21a1727c feat: add packGroupFileDownloadReq & packC2CFileDownloadReq 2024-10-19 02:05:46 +08:00
pk5ls20
93aab437b7 chore: standardize proto file naming 2024-10-19 01:59:11 +08:00
pk5ls20
34e797270f feat: adjust FileNapCatOneBotUUID to support encode fileUUID 2024-10-19 01:39:14 +08:00
手瓜一十雪
0f337a8d8c feat: 引导使用28788 2024-10-18 22:34:58 +08:00
手瓜一十雪
cc9b83089e Merge branch 'main' into pr/436 2024-10-18 21:44:34 +08:00
手瓜一十雪
a565929686 fix 2024-10-18 21:43:54 +08:00
手瓜一十雪
6adacea774 support linux x64 2024-10-18 21:07:32 +08:00
手瓜一十雪
47ab5421ed fix: appid-msf 2024-10-18 20:04:58 +08:00
pk5ls20
10c404d455 feat: adjust packetElement & packetMsg 2024-10-18 16:57:55 +08:00
pk5ls20
dfdca11155 feat & fix: revert assert import & support MFace element 2024-10-18 16:34:45 +08:00
pk5ls20
698e095364 feat & refactor: add more packetElement & refactor packetMsg 2024-10-18 16:01:54 +08:00
手瓜一十雪
524fd258d8 fix: 定义 2024-10-18 14:35:26 +08:00
pk5ls20
17e70a4360 feat: add more msgElement 2024-10-18 04:49:38 +08:00
pk5ls20
e4a533e7b7 feat: add more msgElement 2024-10-18 04:35:23 +08:00
pk5ls20
0cb68d3737 chore: remove useless comment 2024-10-17 23:38:44 +08:00
pk5ls20
9faeadbebe feat: parse trpc.group.long_msg_interface.MsgService.SsoSendLongMsg resp 2024-10-17 23:34:29 +08:00
pk5ls20
35d201cfb8 feat: support upload c2c pic 2024-10-17 23:29:53 +08:00
pk5ls20
205174255f feat: Introduce a 10ms delay to sendSsoCmdReqByContend and cache prepareUpload requests 2024-10-17 22:41:39 +08:00
pk5ls20
8873a030ab feat: packet highway (in right impl) 2024-10-17 19:45:21 +08:00
pk5ls20
0ab61bac12 refactor: optimised code 2024-10-17 03:29:36 +08:00
pk5ls20
b1157f60f5 feat & refactor: packet highway (in almost right impl) 2024-10-17 03:03:36 +08:00
手瓜一十雪
bb93df06b2 fix 2024-10-16 21:02:28 +08:00
手瓜一十雪
82e807fd80 fix 2024-10-16 21:02:09 +08:00
手瓜一十雪
29da539467 fix: cache 2024-10-16 20:50:44 +08:00
手瓜一十雪
659aa005b0 fix: 部分离谱情况 2024-10-16 20:49:12 +08:00
手瓜一十雪
3f20733e7e refactor: groupMember 2024-10-16 20:09:01 +08:00
手瓜一十雪
b15e1174d6 update: LL 2024-10-16 19:23:38 +08:00
pk5ls20
05b05fd74e Merge remote-tracking branch 'fork/refactor/proto' into refactor/proto 2024-10-16 11:59:57 +08:00
pk5ls20
d30d467a21 feat: broken highway 2024-10-16 11:58:47 +08:00
手瓜一十雪
cd62e8ca37 fix: memberLevel 2024-10-16 11:35:12 +08:00
手瓜一十雪
f9e44820c1 style: 标准化 2024-10-15 09:20:54 +08:00
手瓜一十雪
169ae6a4d0 style: 规范写法 2024-10-15 09:11:00 +08:00
手瓜一十雪
030ba15952 style: lint 2024-10-15 09:06:47 +08:00
手瓜一十雪
964874bdad fix: 临时会话上报问题 2024-10-15 08:58:28 +08:00
手瓜一十雪
7affa081ac fix #398 2024-10-14 22:13:41 +08:00
手瓜一十雪
10e281ed35 fix #428 2024-10-14 22:08:00 +08:00
手瓜一十雪
27081ae599 fix #391 2024-10-14 22:04:02 +08:00
手瓜一十雪
61cbcdffe8 fix #431 2024-10-14 21:56:51 +08:00
手瓜一十雪
eeb15ea564 fix: #429 2024-10-14 21:49:42 +08:00
手瓜一十雪
565c820925 fix: #408 2024-10-14 21:45:36 +08:00
pk5ls20
325dff5735 feat: minor feat client & add TODO 2024-10-14 18:03:30 +08:00
pk5ls20
397c2cf5f0 refactor: further decoupling of Packet and Core parts 2024-10-14 17:51:21 +08:00
pk5ls20
1fbc339a42 feat: update type define in packet 2024-10-14 17:05:04 +08:00
pk5ls20
f2c719c60d chore: format & minor refactor 2024-10-14 15:37:02 +08:00
pk5ls20
08505fcc9a feat: simplify code 2024-10-14 15:29:20 +08:00
pk5ls20
a79c933693 refactor: add available accessor property in PacketClient 2024-10-14 15:13:44 +08:00
pk5ls20
b4cb3ddf1c refactor: packet 2024-10-14 13:59:34 +08:00
手瓜一十雪
aa188a6e89 fix 2024-10-14 09:13:03 +08:00
手瓜一十雪
a04b6b8a70 fix 2024-10-14 09:09:29 +08:00
手瓜一十雪
11149d2743 fix 2024-10-14 09:07:03 +08:00
pk5ls20
86bfd990db feat: partly impl UploadForwardMsg 2024-10-14 02:25:56 +08:00
pk5ls20
9304430889 fix: deprecate the cache in constructor in NapProtoMsg 2024-10-14 01:07:14 +08:00
手瓜一十雪
095f1c270b Merge branch 'refactor/proto' of https://github.com/pk5ls20/NapCatQQ into pr/436 2024-10-13 19:39:25 +08:00
手瓜一十雪
d3f91a832b fix 2024-10-13 19:39:03 +08:00
pk5ls20
4790a1170f Merge remote-tracking branch 'fork/refactor/proto' into refactor/proto 2024-10-13 19:31:47 +08:00
pk5ls20
501c392028 feat: flexible NapProtoStructType 2024-10-13 19:31:32 +08:00
手瓜一十雪
9200520f70 fix: add test 2024-10-13 19:23:28 +08:00
手瓜一十雪
8122561337 fix 2024-10-13 19:06:12 +08:00
手瓜一十雪
c6dc86ef8d Merge branch 'refactor/proto' of https://github.com/pk5ls20/NapCatQQ into pr/436 2024-10-13 19:03:47 +08:00
手瓜一十雪
bea3b8485f fix 2024-10-13 17:32:49 +08:00
pk5ls20
b807b89cdc Merge remote-tracking branch 'fork/refactor/proto' into refactor/proto 2024-10-13 17:23:32 +08:00
pk5ls20
daac2f7fd9 refactor: packet 2024-10-13 17:23:07 +08:00
手瓜一十雪
f0a5523174 fix 2024-10-13 17:21:03 +08:00
手瓜一十雪
eda8fbb178 fix 2024-10-13 17:15:59 +08:00
手瓜一十雪
67ca6184e9 Revert "fix"
This reverts commit d79e91fc1e.
2024-10-13 17:09:23 +08:00
手瓜一十雪
d79e91fc1e fix 2024-10-13 17:05:19 +08:00
手瓜一十雪
1cdb93baa2 feat: get_rkey 2024-10-13 16:47:22 +08:00
手瓜一十雪
f91991e25c feat: Packet Rkey 2024-10-13 16:46:12 +08:00
pk5ls20
d21da47a7d feat & fix: feat proto & revert NapProto changes 2024-10-13 16:13:34 +08:00
手瓜一十雪
b4e22a345d feat:GetUserStatus 2024-10-13 14:18:35 +08:00
手瓜一十雪
30e594ae5f fix: arch 2024-10-13 14:07:04 +08:00
手瓜一十雪
ffba3573ba feat: 区分arch 2024-10-13 14:05:40 +08:00
手瓜一十雪
9df5bee8d3 fix 2024-10-13 13:55:20 +08:00
手瓜一十雪
71c0728622 feat: buildSetSpecialTittlePacket 2024-10-13 13:54:52 +08:00
手瓜一十雪
476d8ba14d fix 2024-10-13 13:35:17 +08:00
手瓜一十雪
274c956f16 fix 2024-10-13 11:58:08 +08:00
手瓜一十雪
3068f9ee3d fix: catch 2024-10-13 11:22:33 +08:00
手瓜一十雪
a0c49d5f7f feat: errorMsg opt 2024-10-13 10:57:03 +08:00
手瓜一十雪
a8534974fe fix 2024-10-13 10:18:45 +08:00
手瓜一十雪
c517790391 fix: oidb 2024-10-13 10:10:12 +08:00
pk5ls20
b7e875c77f feat: add more proto 2024-10-13 04:15:10 +08:00
pk5ls20
befd9c0624 refactor: adjust NapProto & proto structure 2024-10-13 02:57:12 +08:00
手瓜一十雪
7a46f11089 feat: oidb_0x9067_202 2024-10-12 23:09:33 +08:00
手瓜一十雪
dc168bf8b9 feat: proto 整理 2024-10-12 22:25:10 +08:00
手瓜一十雪
eef5293ca0 refactor: Poke 2024-10-12 22:17:40 +08:00
手瓜一十雪
a2c4498694 style: NapProto 2024-10-12 21:35:49 +08:00
pk5ls20
938a84a460 feat: introduce NapProtoMsg 2024-10-12 21:27:25 +08:00
手瓜一十雪
978d2c24ee fix: 注释掉无用代码 2024-10-12 19:57:51 +08:00
手瓜一十雪
cdd00d665d fix: packet send/recv 2024-10-12 19:51:29 +08:00
手瓜一十雪
bb8b06c044 feat: linux 2024-10-12 18:05:03 +08:00
手瓜一十雪
604c5dcdc1 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-12 16:10:40 +08:00
手瓜一十雪
6bc2ecdbf0 Update onebot11.json 2024-10-12 16:10:28 +08:00
手瓜一十雪
e91c81def7 Merge pull request #435 from NapNeko/dev-packet
build: poke test
2024-10-12 16:09:52 +08:00
手瓜一十雪
bedd2fa15a build: poke test 2024-10-12 15:39:49 +08:00
手瓜一十雪
50465eef54 Merge pull request #434 from NapNeko/dev-packet
Dev packet
2024-10-12 15:38:16 +08:00
手瓜一十雪
07689adfcd fix 2024-10-12 15:36:30 +08:00
手瓜一十雪
8f4f898675 fix 2024-10-12 15:34:10 +08:00
手瓜一十雪
968bd7a437 fix 2024-10-12 15:27:58 +08:00
手瓜一十雪
eba5900ba8 fix: send packet 2024-10-12 15:18:20 +08:00
手瓜一十雪
69c477b104 fix: new server 2024-10-12 14:49:54 +08:00
手瓜一十雪
c8df8f4f54 release: 2.6.27 2024-10-11 23:07:26 +08:00
手瓜一十雪
d35a19b4fd release: olpush remove 2024-10-11 23:06:18 +08:00
手瓜一十雪
a97437a6e5 fix 2024-10-11 23:03:09 +08:00
手瓜一十雪
39c4473367 feat: poke oidb.0xed3_1 2024-10-10 19:04:29 +08:00
手瓜一十雪
b882bc721d release: v2.6.24 2024-10-09 20:50:00 +08:00
手瓜一十雪
405cace489 feat: 9.9.15-28498 2024-10-09 20:14:42 +08:00
手瓜一十雪
402a7b7fc9 docs: 移除误导语句 2024-10-09 14:12:13 +08:00
手瓜一十雪
8ad805e654 docs: 修正 2024-10-08 14:26:03 +08:00
手瓜一十雪
b23c357f73 release: 2.6.24 2024-10-06 10:07:39 +08:00
Wesley F. Young
f561c2b0fa refactor: rewrite switch with object mapping or if-else 2024-10-05 17:03:02 +08:00
手瓜一十雪
5a8eea668f release: 2.6.23 2024-10-02 13:23:07 +08:00
手瓜一十雪
777143e502 feat: new log 2024-10-02 13:13:55 +08:00
手瓜一十雪
0d8c9a82fe fix: 空格目录 2024-10-02 11:45:19 +08:00
手瓜一十雪
d10ab1cce3 feat: 依赖调整 2024-10-02 11:29:05 +08:00
手瓜一十雪
ec25e09d73 release: 2.6.22 2024-10-02 10:10:01 +08:00
手瓜一十雪
cba9c78ab1 release: v2.6.21 2024-10-02 10:05:01 +08:00
手瓜一十雪
c32db4a881 fix: rkey v2 2024-10-02 10:03:48 +08:00
手瓜一十雪
871add3071 Merge pull request #419 from hguandl/macos
Fix Protobuf Dependencies & macOS Support
2024-10-01 22:59:07 +08:00
Hao Guan
e661c617a3 update: macOS support 2024-10-01 22:48:51 +08:00
Hao Guan
d4bf721540 fix: protobuf dependencies 2024-10-01 22:48:31 +08:00
Wesley F. Young
d91b55faed update: log group name & sender nickname onto console 2024-10-01 11:01:32 +08:00
Alen
9687832d4d chore: 拉高linuxQQ版本 2024-10-01 02:37:44 +08:00
手瓜一十雪
fc3e436744 fix: log 2024-09-30 17:07:30 +08:00
手瓜一十雪
da90245f7b release: v2.6.20 2024-09-30 17:05:02 +08:00
手瓜一十雪
410d6a85d7 fix: protobuf #417 2024-09-30 08:30:39 +08:00
手瓜一十雪
b693342e4f fix 2024-09-30 08:18:02 +08:00
手瓜一十雪
acca361f2e release: v2.6.18 2024-09-29 20:11:30 +08:00
手瓜一十雪
b663f47713 style: ScalarType 2024-09-29 20:10:14 +08:00
手瓜一十雪
d332b199b5 refactor: protobufjs给我似! 2024-09-29 20:06:11 +08:00
Alen
78bac1dbd1 Merge pull request #416 from cnxysoft/upmain
fix: 28418下载HASH
2024-09-29 16:21:52 +08:00
Alen
724ff215f9 fix: 28418下载HASH 2024-09-29 16:20:01 +08:00
手瓜一十雪
68ea146469 release: v2.6.17 2024-09-29 13:07:52 +08:00
手瓜一十雪
82583e616f fix: #415 2024-09-29 12:57:50 +08:00
手瓜一十雪
bfc339c58d refactor: #415 2024-09-29 12:53:18 +08:00
手瓜一十雪
fe4427c076 feat: message字段返回 #415 2024-09-29 12:30:29 +08:00
手瓜一十雪
5745f388a9 feat: 简化代码 #415 2024-09-29 12:19:04 +08:00
手瓜一十雪
377e3c253f feat: parseForward for array 2024-09-29 11:26:45 +08:00
手瓜一十雪
3007a0c00e feat: nativeNode 2024-09-28 23:00:47 +08:00
手瓜一十雪
f51ffc091d Merge pull request #410 from NapNeko/hook
[Hook] NapcatNative
2024-09-28 21:38:04 +08:00
手瓜一十雪
c37c364a08 release: 2.6.16 2024-09-28 21:37:38 +08:00
手瓜一十雪
331a106e9a Merge branch 'hook' of https://github.com/NapNeko/NapCatQQ into hook 2024-09-28 21:37:17 +08:00
手瓜一十雪
cd74687b7b fix: getMsg 2024-09-28 21:36:54 +08:00
Alen
b3e145c1e6 fix: 解析增加字段 2024-09-28 21:29:18 +08:00
Alen
d8e1547736 Merge pull request #414 from NapNeko/hook_test
fix: 撤回SEQ
2024-09-28 21:00:32 +08:00
Alen
8617f01924 fix: 撤回SEQ 2024-09-28 20:58:09 +08:00
手瓜一十雪
55f9e75e6a Merge branch 'main' into hook 2024-09-28 20:17:02 +08:00
手瓜一十雪
b93e7b7ed1 feat: get pskey 2024-09-28 20:16:47 +08:00
手瓜一十雪
89cc79ad60 fix 2024-09-28 19:08:18 +08:00
手瓜一十雪
8dd0e60eea fix 2024-09-28 19:04:11 +08:00
手瓜一十雪
df6113fdf6 fix:#如好 2024-09-28 17:38:26 +08:00
手瓜一十雪
3a3095d15a feat:28418 2024-09-28 17:13:06 +08:00
手瓜一十雪
fb4d07391e Hook: GroupRecall 2024-09-28 15:41:11 +08:00
手瓜一十雪
9bef9c85cf fix 2024-09-28 15:36:25 +08:00
Alen
b77b3f227f Merge pull request #411 from cnxysoft/upmain
chore: qqnt.json增加linux版本
2024-09-28 14:25:42 +08:00
Alen
6a065f0a34 Merge branch 'main' into upmain 2024-09-28 14:22:10 +08:00
手瓜一十雪
4e1e190797 Merge pull request #409 from Zengfanqiang06/patch-1
Update README.md
2024-09-28 13:28:58 +08:00
手瓜一十雪
1ce8cd2100 napcat native 2024-09-28 13:27:13 +08:00
Alen
c03af6b9ad Merge branch 'main' into upmain 2024-09-28 13:24:47 +08:00
Alen
adca850075 chore: 增加linux目标QQ版本配置 2024-09-28 13:20:19 +08:00
Qiao
e3616b484e Update README.md
删除句号让其更统一(
2024-09-28 13:05:03 +08:00
手瓜一十雪
cfd7808169 Merge pull request #404 from NapNeko/dependabot/npm_and_yarn/types/express-5.0.0
chore(deps-dev): bump @types/express from 4.17.21 to 5.0.0
2024-09-27 17:08:57 +08:00
手瓜一十雪
addcedc588 Merge pull request #402 from NapNeko/v3
[Refactor] 推进版本重构
2024-09-27 16:47:48 +08:00
Alen
bfea786088 Merge pull request #405 from cnxysoft/upmain
chore: Once去除LL默认config
2024-09-26 17:21:31 +08:00
手瓜一十雪
50e84c3c9e Revert "feat: FrameWork调整"
This reverts commit 652fe8d21e.
2024-09-26 17:01:18 +08:00
手瓜一十雪
dc92ace85e Revert "feat: 调整"
This reverts commit 1a543928b1.
2024-09-26 17:01:13 +08:00
手瓜一十雪
1a543928b1 feat: 调整 2024-09-26 16:33:04 +08:00
手瓜一十雪
652fe8d21e feat: FrameWork调整 2024-09-26 16:22:21 +08:00
dependabot[bot]
199690f45f chore(deps-dev): bump @types/express from 4.17.21 to 5.0.0
Bumps [@types/express](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/express) from 4.17.21 to 5.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/express)

---
updated-dependencies:
- dependency-name: "@types/express"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-26 08:20:36 +00:00
Alen
37a4dd4b00 chore: Once去除LL默认config 2024-09-26 16:16:46 +08:00
手瓜一十雪
34d4358bfc feat: 依赖简化 2024-09-26 16:10:52 +08:00
手瓜一十雪
90906b9019 style: lint 2024-09-26 16:08:50 +08:00
手瓜一十雪
1c212ff2b4 feat: new 2024-09-26 15:54:24 +08:00
手瓜一十雪
7d709f44a8 fix: 调整逻辑 2024-09-26 15:37:07 +08:00
手瓜一十雪
ea9e88a18a fix: 不可抗力 2024-09-26 15:31:50 +08:00
Alen
0be8a9c805 chore: 拉高目标QQ版本 2024-09-26 13:00:49 +08:00
手瓜一十雪
fcf8139afe release: 2.6.15 2024-09-25 15:51:25 +08:00
手瓜一十雪
62f969b50b feat: ver28327 2024-09-25 15:33:19 +08:00
Alen
6726062500 Merge pull request #397 from cnxysoft/upmain
chore: 增加下载链接HASH
2024-09-24 13:45:59 +08:00
Alen
cf1f4bdcaf chore: 增加下载链接HASH 2024-09-24 13:44:24 +08:00
手瓜一十雪
b09a14ad4e fix 2024-09-23 16:52:35 +08:00
手瓜一十雪
1dc62c9ca3 release:2.6.14 2024-09-23 16:46:39 +08:00
手瓜一十雪
beaa89a2dc release: v2.6.14 2024-09-23 16:42:34 +08:00
手瓜一十雪
f39a000b49 fix 2024-09-23 16:39:26 +08:00
手瓜一十雪
013a74fb14 fix2 2024-09-23 16:35:54 +08:00
手瓜一十雪
7c4964753b release: 2.6.14 2024-09-23 16:33:02 +08:00
手瓜一十雪
8353533d60 v2.6.13 2024-09-23 15:52:40 +08:00
手瓜一十雪
c06df27424 feat: 修复空格与中文问题 2024-09-23 15:52:16 +08:00
手瓜一十雪
ad82919ddf fix: 2.6.12 2024-09-23 09:35:08 +08:00
手瓜一十雪
44dbba17e1 rollup 2024-09-23 09:34:44 +08:00
手瓜一十雪
5ba110e1da feat: bind 2024-09-22 16:59:45 +08:00
手瓜一十雪
b6e392fdb2 release: v2.6.11 2024-09-22 11:42:13 +08:00
手瓜一十雪
2280e83aa2 fix: type 2024-09-21 17:40:42 +08:00
手瓜一十雪
f49b94edb9 Merge pull request #392 from Fripine/feat/more-music-types
feat: support more types of music cards
2024-09-21 15:00:59 +08:00
Fripine
2428a12221 chore 2024-09-21 12:30:05 +08:00
Fripine
9c353f3760 feat: support more types of music cards 2024-09-21 12:17:05 +08:00
手瓜一十雪
5b86d25d7f Merge pull request #389 from Fripine/fix/FriendAdd
fix: FriendAddNoticeEvent
2024-09-20 21:18:14 +08:00
Fripine
2b168e8bbc fix: FriendAdd 2024-09-20 15:50:31 +08:00
手瓜一十雪
537db32847 Merge pull request #388 from NapNeko/revert-387-fix/friendAddEvent
Revert "fix: 好友添加成功事件"
2024-09-20 15:36:32 +08:00
手瓜一十雪
498b7f9f2b Revert "fix: 好友添加成功事件" 2024-09-20 14:33:52 +08:00
手瓜一十雪
9935568597 Merge pull request #387 from Fripine/fix/friendAddEvent
fix: 好友添加成功事件
2024-09-20 13:00:13 +08:00
Fripine
467003af8c chore 2024-09-20 10:24:19 +08:00
Fripine
4c9edcc47b chore 2024-09-20 10:21:26 +08:00
Fripine
24bf9cf121 chore: 换一种方法 2024-09-20 10:17:30 +08:00
手瓜一十雪
e06f6f39a9 Merge pull request #386 from 123233513/main
增加处理消息段时的检查,过滤无效消息段。
2024-09-20 07:54:52 +08:00
123233513
98ee0c307b Merge branch 'main' of https://github.com/123233513/NapCatQQ 2024-09-20 04:57:41 +08:00
Fripine
5e53ea0bc3 fix: cant emit FriendAddNoticeEvent 2024-09-20 04:38:09 +08:00
123233513
847d88ea77 Update msg.ts
处理消息段时的检查,过滤无效消息段。
2024-09-19 22:14:18 +08:00
123233513
d5046cc2b3 Merge branch 'main' of https://githubfast.com/123233513/NapCatQQ 2024-09-19 21:36:52 +08:00
123233513
3ad64b7cbb 增加处理消息段时的检查,过滤无效消息段。 2024-09-19 21:31:33 +08:00
手瓜一十雪
0dbfe8ca55 feat: 拦截不合法消息 2024-09-19 20:49:33 +08:00
手瓜一十雪
91b794d66d release: 2.6.10 2024-09-19 20:45:09 +08:00
手瓜一十雪
0d65e1e314 release: 2.6.9 2024-09-18 21:52:16 +08:00
手瓜一十雪
2d8f58c6d8 feat: close xlog 2024-09-18 20:22:21 +08:00
手瓜一十雪
65888fa816 feat: close log 2024-09-18 20:18:35 +08:00
手瓜一十雪
857e882c6e release: fk tx 2024-09-18 11:20:58 +08:00
手瓜一十雪
add2931834 remove: debug 2024-09-18 11:19:59 +08:00
手瓜一十雪
cdda5f45ee refactor: guid fk tx 2024-09-18 11:19:29 +08:00
手瓜一十雪
5f73d6a913 feat: reportAmgomWeather a1 rnm tx 2024-09-18 11:01:12 +08:00
手瓜一十雪
0637882fbc release: rnm tx 2024-09-18 10:53:37 +08:00
手瓜一十雪
3f785bab20 feat: NodeIO3MiscService 2024-09-18 10:44:35 +08:00
手瓜一十雪
a4ca89bdd6 fi: 2.6.4 2024-09-17 23:40:44 +08:00
手瓜一十雪
1a64e796bd release: 2.6.4 2024-09-17 23:17:19 +08:00
手瓜一十雪
a8b85a34f7 feat: 追平NT逻辑 2024-09-17 23:17:02 +08:00
手瓜一十雪
e7bec7d6b0 feat: systemPlatform标准化 2024-09-17 22:24:09 +08:00
手瓜一十雪
a582026037 release:2.6.3 2024-09-17 13:57:54 +08:00
手瓜一十雪
1a67a001c5 style: lint 2024-09-17 13:15:12 +08:00
手瓜一十雪
406deac592 fix:api外的推送事件 2024-09-17 13:14:18 +08:00
手瓜一十雪
e719ae0676 release: 2.6.2 2024-09-17 13:01:02 +08:00
手瓜一十雪
d8b7726440 release:2.6.1 2024-09-17 11:46:20 +08:00
手瓜一十雪
49f642e712 Merge pull request #379 from NapNeko/2.6.0
推进2.6.0大幅度重写
2024-09-17 11:43:13 +08:00
手瓜一十雪
70117016ce shell: 移除旧代码实现 2024-09-17 11:23:41 +08:00
手瓜一十雪
a4738f6281 feat: 开发依赖清理 2024-09-17 11:14:23 +08:00
手瓜一十雪
b1fc72d696 chore: 移除旧版本逻辑 2024-09-17 11:08:47 +08:00
手瓜一十雪
457c2c2b50 推进2.6.0大幅度重写 2024-09-17 11:06:07 +08:00
手瓜一十雪
48848d7d1a feat: 暂时砍掉V2Event 2024-09-17 10:59:28 +08:00
手瓜一十雪
55b07ca3ab feat: 彻底移除event旧实现 2024-09-17 09:23:19 +08:00
手瓜一十雪
a1d4882e18 feat: 规范化Promise 2024-09-17 09:19:45 +08:00
手瓜一十雪
3843795d8f Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-09-17 08:56:53 +08:00
手瓜一十雪
f2bf8d42da release: 2.5.5 2024-09-17 08:56:43 +08:00
Alen
a3b244e114 Merge pull request #378 from cnxysoft/upmain
fix: 下载文件失败
2024-09-17 03:50:22 +08:00
Alen
3093bdbc68 fix: 下载失败
优化下载逻辑
2024-09-17 03:39:43 +08:00
Alen
9ab0799283 Merge branch 'main' into upmain 2024-09-16 20:59:41 +08:00
手瓜一十雪
236bec11ed release: 2.5.4 2024-09-16 20:57:56 +08:00
手瓜一十雪
de48b0f940 Merge pull request #370 from NapNeko/28060
for: 28060
2024-09-16 20:54:54 +08:00
手瓜一十雪
4885d4db86 support: linux28060 2024-09-16 20:33:27 +08:00
手瓜一十雪
0c7bbda936 feat: Linux28060Appid 2024-09-16 20:29:04 +08:00
手瓜一十雪
fa07c2c1fb update: appid 2024-09-16 19:31:24 +08:00
手瓜一十雪
5d17a191f6 Merge branch 'main' into 28060 2024-09-16 19:12:10 +08:00
手瓜一十雪
67fb74d3c2 fix 2024-09-16 19:07:05 +08:00
手瓜一十雪
dc04cfc1b3 Revert "chore: workflow"
This reverts commit 58cd38c4a8.
2024-09-16 19:03:14 +08:00
手瓜一十雪
d61d481965 Merge branch 'main' into 28060 2024-09-16 19:02:01 +08:00
手瓜一十雪
6b346ee1de fix 2024-09-16 19:01:01 +08:00
手瓜一十雪
d0f248aaf9 fix 2024-09-16 18:53:26 +08:00
手瓜一十雪
85c9227515 Merge branch 'main' into 28060 2024-09-16 18:52:32 +08:00
手瓜一十雪
73b6d3be84 release: 2.5.3 2024-09-16 18:51:26 +08:00
手瓜一十雪
1ff6ce2343 feat: FetchOtherProfileLike 2024-09-16 18:51:05 +08:00
手瓜一十雪
c145935d46 feat: contact 2024-09-16 18:47:51 +08:00
手瓜一十雪
e9ede6924e release: 2.5.2 2024-09-16 18:12:00 +08:00
手瓜一十雪
515a21761d back: linux to 27254 2024-09-16 18:11:33 +08:00
Alen
8d6397028b Revert "style"
This reverts commit 7e74578312.
2024-09-15 17:31:01 +08:00
Alen
eb4828d81f Merge branch 'main' into upmain 2024-09-15 17:24:43 +08:00
Alen
7e74578312 style 2024-09-15 17:24:02 +08:00
Alen
640e3516d4 style 2024-09-15 17:23:10 +08:00
手瓜一十雪
bd295a4632 Merge branch 'main' into 28060 2024-09-15 17:15:03 +08:00
Alen
166c30fe2c Merge pull request #375 from cnxysoft/test
fix: friend_add
2024-09-15 16:49:10 +08:00
Alen
66c1bab629 fix: friend_add
修复该事件中user_id为0的问题
2024-09-15 16:47:46 +08:00
手瓜一十雪
66656304f9 fix 2024-09-15 16:24:08 +08:00
手瓜一十雪
07f66e379d Merge branch 'main' into 28060 2024-09-15 16:20:37 +08:00
手瓜一十雪
7ae8fd60c4 release: 2.5.1 2024-09-15 16:20:26 +08:00
手瓜一十雪
7275066994 feat: skip Qrcode When Login 2024-09-15 16:18:43 +08:00
手瓜一十雪
385adec186 Merge branch 'main' into 28060 2024-09-15 16:04:19 +08:00
手瓜一十雪
96b5bec5ab feat: revert 2024-09-15 16:04:10 +08:00
手瓜一十雪
6a9ec4e5f0 Merge branch 'main' into 28060 2024-09-15 15:52:52 +08:00
手瓜一十雪
d9851493df fix: #361 2024-09-15 15:51:23 +08:00
手瓜一十雪
efdb520414 Merge branch 'main' into 28060 2024-09-15 15:38:32 +08:00
Alen
5548644aeb Merge pull request #373 from cnxysoft/test
fix: bugs
2024-09-15 15:37:09 +08:00
Alen
e3fcd91b2d Merge branch 'main' into test 2024-09-15 15:29:50 +08:00
手瓜一十雪
2cae30ba88 Merge branch 'main' into 28060 2024-09-15 15:20:52 +08:00
手瓜一十雪
58cd38c4a8 chore: workflow 2024-09-15 15:20:44 +08:00
手瓜一十雪
3300304feb Merge branch 'main' into 28060 2024-09-15 15:00:42 +08:00
手瓜一十雪
f0e376d06b fix: 移除错误action 2024-09-15 14:55:18 +08:00
手瓜一十雪
16f7bb48f2 fix: launcher 28060 2024-09-15 14:34:47 +08:00
手瓜一十雪
7f383dd29b Merge branch 'main' into 28060 2024-09-15 09:49:49 +08:00
手瓜一十雪
3dc529edf4 fix: #369 2024-09-15 09:39:17 +08:00
手瓜一十雪
45dedb4872 fix: 28060 2024-09-15 09:35:10 +08:00
手瓜一十雪
afcdd01c0d fix: typo 9.9.15-28060 2024-09-14 19:22:36 +08:00
手瓜一十雪
1164877e9a Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-09-14 19:10:34 +08:00
手瓜一十雪
fe92a449ba feat: 准备适配9.9.15-28060版本 2024-09-14 19:10:17 +08:00
Alen
401b0e2bd0 fix: 部分语音播放速率异常 2024-09-14 17:50:09 +08:00
手瓜一十雪
cf9c71fcc1 feat: 准备适配9.9.15-28606 2024-09-14 17:24:52 +08:00
Alen
15a2400069 fix: 修复文件删除失败
此处为重复插入待删列表
2024-09-14 11:12:43 +08:00
Alen
d68a39b49e fix: 定义错误 2024-09-14 10:49:43 +08:00
Alen
066ca22e24 Merge pull request #362 from cnxysoft/upmain
fix: 点赞通知解析
2024-09-14 01:24:20 +08:00
Alen
0418b926fe fix: 点赞通知解析失败 2024-09-14 00:42:10 +08:00
手瓜一十雪
be40bbdf40 release: 2.5.0 2024-09-13 17:42:11 +08:00
手瓜一十雪
df4f42e79e fix: video name 2024-09-13 17:38:11 +08:00
Alen
5f80058f70 Merge pull request #360 from cnxysoft/upmain
fix: bugs
2024-09-13 17:30:23 +08:00
Alen
0cbe59052d Merge branch 'main' into upmain 2024-09-13 17:25:59 +08:00
Alen
af28a26e37 fix: 无法发送url视频 2024-09-13 17:22:32 +08:00
Alen
70c596df93 fix: headers分割 2024-09-13 16:11:15 +08:00
手瓜一十雪
748b51428c feat: createUidFromTinyId 2024-09-13 16:05:25 +08:00
手瓜一十雪
8ad746397c feat: JoinDragonGroupEmoji 2024-09-13 15:35:05 +08:00
手瓜一十雪
45baed2f9a tag: deprecated 2024-09-13 15:30:04 +08:00
手瓜一十雪
74185f2d33 fix 2024-09-13 14:05:04 +08:00
手瓜一十雪
90a91e4105 fix 2024-09-13 13:54:12 +08:00
手瓜一十雪
11aa3a0315 feat: fetchOtherProfileLike 2024-09-12 20:05:46 +08:00
手瓜一十雪
0c2e39214f style: lint 2024-09-12 19:55:56 +08:00
手瓜一十雪
d89620d7a6 style: folder 2024-09-12 19:55:26 +08:00
手瓜一十雪
edf80775b7 release: v2.4.9 2024-09-12 19:47:57 +08:00
手瓜一十雪
46e56ac726 remove: polyFill 2024-09-12 19:35:54 +08:00
手瓜一十雪
40b2f6bfd6 release: 2.4.7 2024-09-12 18:31:11 +08:00
手瓜一十雪
911e4921e2 fix: 删除旧文件 2024-09-12 18:15:38 +08:00
手瓜一十雪
1db9bb419d fix: 一处异常字段 2024-09-12 10:55:18 +08:00
手瓜一十雪
c6241a94e3 style: lint 2024-09-12 09:28:41 +08:00
手瓜一十雪
1cbf75ca36 style: lint 2024-09-12 09:28:26 +08:00
手瓜一十雪
8f85c897c8 refactor: SysMsg 2024-09-12 09:20:10 +08:00
手瓜一十雪
29c31b7aba release: 2.4.6 2024-09-12 09:01:13 +08:00
手瓜一十雪
402919d6f2 feat: qucikLogin 2024-09-12 09:00:53 +08:00
手瓜一十雪
82608dd5ff fix: build 2024-09-12 00:17:35 +08:00
手瓜一十雪
f312368df2 build: fix2 2024-09-11 23:40:05 +08:00
手瓜一十雪
374fc64427 feat: delFile 2024-09-11 23:29:26 +08:00
手瓜一十雪
95bd74bb0d BUILD: TEST 2024-09-11 23:18:38 +08:00
手瓜一十雪
a9f5069649 Revert "build: debug"
This reverts commit 957f7ffd8d.
2024-09-11 22:41:59 +08:00
手瓜一十雪
957f7ffd8d build: debug 2024-09-11 22:28:28 +08:00
手瓜一十雪
336dd3ce10 chore: 扩展 2024-09-11 22:13:45 +08:00
手瓜一十雪
47a7295477 fix: typo
copilot
2024-09-11 20:24:50 +08:00
手瓜一十雪
341a0e1c2a chore: code 2024-09-11 20:10:52 +08:00
手瓜一十雪
c4f73d0eb8 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-09-11 20:01:36 +08:00
手瓜一十雪
bd9258bae4 release: 2.4.5 2024-09-11 20:01:27 +08:00
手瓜一十雪
e3b3260aa0 Merge pull request #357 from cnxysoft/test
feat: 被点赞事件
2024-09-11 20:00:11 +08:00
手瓜一十雪
676766c99e refactor: protobuf 2024-09-11 19:56:51 +08:00
Alen
1025a07593 revert 2024-09-11 17:17:49 +08:00
Alen
00c3fcd033 Merge branch 'main' into test 2024-09-11 16:32:45 +08:00
Alen
b8457d4aff feat: 被点赞事件 2024-09-11 16:32:28 +08:00
手瓜一十雪
a2ecf10d19 feat: 全面迁移V2 2024-09-11 15:44:41 +08:00
Alen
1e63a2a7e7 test: 被赞事件(未完成) 2024-09-11 02:28:47 +08:00
手瓜一十雪
964014fc5c fix: #355 2024-09-10 22:27:06 +08:00
手瓜一十雪
fc2bb6d8c3 docs: 移除注释 2024-09-10 18:58:27 +08:00
手瓜一十雪
1b10252d76 remove: NTQQCacheApi 2024-09-10 18:42:49 +08:00
手瓜一十雪
ad8af12a10 refactor: fsPromise catch 2024-09-10 18:41:01 +08:00
手瓜一十雪
b040c9b118 refactor: audio 2024-09-10 18:39:14 +08:00
Alen
f6da7da90b Merge pull request #352 from cnxysoft/upmain
fix: 踢官方机器人报错
2024-09-10 00:40:58 +08:00
Alen
a745185408 fix: 踢官方机器人报错 2024-09-10 00:38:10 +08:00
手瓜一十雪
d3336f9027 release: 2.4.3 2024-09-09 21:37:22 +08:00
手瓜一十雪
daf42c8203 release: 2.4.2 2024-09-09 15:04:19 +08:00
手瓜一十雪
0a18bae3b5 fix: #351 2024-09-09 15:04:00 +08:00
手瓜一十雪
919705966c build: 2.4.1 2024-09-09 09:19:23 +08:00
手瓜一十雪
2c54aee63e build: test 2024-09-08 21:39:53 +08:00
手瓜一十雪
3f80bdf2a3 fix 2024-09-08 18:29:21 +08:00
手瓜一十雪
1c429b8dd3 fix: type 2024-09-08 18:26:07 +08:00
手瓜一十雪
5669e2b0b7 fix: 跟进实际逻辑 2024-09-08 18:21:55 +08:00
手瓜一十雪
1a6a43babf release: 2.4.0 2024-09-08 10:50:30 +08:00
手瓜一十雪
2650db5ddc fix: 字段V2 2024-09-08 10:48:33 +08:00
手瓜一十雪
255491a107 fix: hex计算问题 2024-09-08 10:41:30 +08:00
手瓜一十雪
5c64147dfa fix: encodeFile 2024-09-08 10:34:49 +08:00
手瓜一十雪
39f4118577 fix: #347 2024-09-08 10:30:30 +08:00
手瓜一十雪
f7f6e4736a fix #349 2024-09-08 10:24:36 +08:00
手瓜一十雪
c635da7ebb style: lint 2024-09-08 10:10:47 +08:00
手瓜一十雪
58124b006a Merge pull request #346 from NapNeko/Refactor-2.4.x
refactor: v2.4.0
2024-09-08 10:08:41 +08:00
手瓜一十雪
563aeccd0f refactor: fileNameEncode 2024-09-08 10:07:49 +08:00
手瓜一十雪
bd1a95a7f5 style: apis 2024-09-07 23:48:41 +08:00
手瓜一十雪
cdb25828f2 chore: code 2024-09-07 23:34:52 +08:00
手瓜一十雪
45803b3b23 Merge branch 'main' into Refactor-2.4.x 2024-09-07 23:34:23 +08:00
手瓜一十雪
0e5e3d3383 refactor: Service 2024-09-07 23:33:40 +08:00
Wesley F. Young
4672930037 update: move to recallMsg(V2) 2024-09-07 23:28:35 +08:00
手瓜一十雪
09be7131c3 refactor: 整理Service 2024-09-07 23:07:24 +08:00
手瓜一十雪
a804f90b9c refactor: adapter 2024-09-07 22:58:29 +08:00
手瓜一十雪
264cb6bbd2 release: 2.3.7 2024-09-07 21:29:22 +08:00
手瓜一十雪
b7772e867b Merge pull request #344 from qhy040404/patch-1
feat: 允许保存的token自动填写
2024-09-07 20:05:25 +08:00
Alen
cc0e77abfb Merge pull request #345 from cnxysoft/upmain
fix: 群成员列表获取
2024-09-07 19:55:41 +08:00
Alen
537d1c6f4f fix: 群成员列表获取
修复部分群成员列表返回为空的问题
2024-09-07 19:54:46 +08:00
Seijo Cecilia
80facadd67 chore: fix prefer-const 2024-09-07 19:49:54 +08:00
Seijo Cecilia
ba097dad23 update: recallMsgV2 2024-09-07 19:49:18 +08:00
qhy040404
c13c15d046 feat: 允许保存的token自动填写 2024-09-07 19:37:39 +08:00
手瓜一十雪
4f52128a06 fix 2024-09-07 17:17:07 +08:00
手瓜一十雪
500b2d0e6d release: 2.3.6 2024-09-07 13:31:32 +08:00
手瓜一十雪
e59d094feb feat: 提升使用体验 2024-09-07 10:45:45 +08:00
手瓜一十雪
a8372f14f8 fix: member info 2024-09-07 09:18:29 +08:00
Wesley F. Young
5174ff422d update: optimize message logging to console 2024-09-07 01:29:50 +08:00
Wesley F. Young
5c06751c3b fix: reference error 2024-09-07 00:04:35 +08:00
手瓜一十雪
ac2b0118a6 release: 2.3.5 2024-09-06 12:13:22 +08:00
手瓜一十雪
3eb8fd4abe Merge pull request #343 from drsanwujiang/patch-1
Fix bug: active HTTP adapter
2024-09-06 11:42:05 +08:00
Drsanwujiang
48b389ebe3 Fix bug: active HTTP adapter 2024-09-06 11:02:41 +08:00
手瓜一十雪
065adeb2cd fix 2024-09-06 07:40:33 +08:00
Wesley F. Young
269d0a06fe docs: deprecated features 2024-09-06 01:03:04 +08:00
手瓜一十雪
8eca26b1a5 fix 2024-09-05 18:03:32 +08:00
手瓜一十雪
3019ef7de4 release: 2.3.4 2024-09-05 16:15:21 +08:00
手瓜一十雪
522311b547 release: 2.3.4 2024-09-05 16:00:13 +08:00
手瓜一十雪
21061561ec fix: 字段兼容 2024-09-05 15:06:11 +08:00
手瓜一十雪
b83c41ad56 fix: #339 2024-09-05 14:55:11 +08:00
Version
e80a1cc64a chore:version change 2024-09-05 06:03:22 +00:00
手瓜一十雪
a01e4ca89f release: 2.3.1 2024-09-05 14:03:03 +08:00
手瓜一十雪
c20362e9b6 release: 2.3.0 2024-09-05 14:00:57 +08:00
手瓜一十雪
c90cfb99bd Merge pull request #340 from 123233513/main
%RetString% 增加引号,解决QQ目录包含空格的问题
2024-09-05 13:56:58 +08:00
123233513
7bcea14799 Update launcher.bat
%RetString% 增加引号,解决QQ目录包含空格的问题,比如安装在:C:\Program Files\Tencent\QQNT时,获取不到正确的路径
2024-09-05 10:41:04 +08:00
Wesley F. Young
b415c1a6d1 fix: file encoding 2024-09-04 23:26:55 +08:00
手瓜一十雪
452c72d280 release: 2.2.47 2024-09-04 23:12:38 +08:00
手瓜一十雪
48350be625 fix: 进一步筛选 2024-09-04 18:05:44 +08:00
手瓜一十雪
ab824fb219 Revert "update: normalize log"
This reverts commit d36a28fa81.
2024-09-04 18:05:01 +08:00
手瓜一十雪
043d8a1861 Revert "rollback unlink -> unlinkSync"
This reverts commit 074ac15d0f.
2024-09-04 18:04:58 +08:00
Seijo Cecilia
074ac15d0f rollback unlink -> unlinkSync 2024-09-04 15:50:23 +08:00
Seijo Cecilia
d36a28fa81 update: normalize log 2024-09-04 15:47:14 +08:00
手瓜一十雪
ba12bc6c91 docs: fix 2024-09-04 14:03:55 +08:00
手瓜一十雪
87332778e5 docs: fix 2024-09-04 13:26:38 +08:00
手瓜一十雪
453feb8473 release: 2.2.46 2024-09-04 13:21:40 +08:00
手瓜一十雪
8ff469974c build: test2 2024-09-04 13:19:02 +08:00
手瓜一十雪
994ec5ac0f fix: 极端情况下uin暴毙的情况 2024-09-04 12:40:59 +08:00
手瓜一十雪
43f7f9a363 build: test 2024-09-04 12:37:23 +08:00
Wesley F. Young
4a11ebc9b9 Revert "chore: optimize vite.config.ts"
This reverts commit 6a0d592491.
2024-09-04 10:38:10 +08:00
Wesley F. Young
d76a1305e7 chore: optimize import 2024-09-04 10:06:09 +08:00
Wesley F. Young
6a0d592491 chore: optimize vite.config.ts 2024-09-04 10:05:24 +08:00
Wesley F. Young
9898c2196d chore: optimize tsconfig.json 2024-09-04 00:29:55 +08:00
Wesley F. Young
41a8dc840f chore: completely comment onRecvSysMsg 2024-09-03 23:54:46 +08:00
Wesley F. Young
c3eaae9d88 chore: optimize imports 2024-09-03 23:46:10 +08:00
手瓜一十雪
3ca959b7a6 docs: update 2024-09-03 21:51:16 +08:00
手瓜一十雪
1d2e2b6e5c release: 2.2.44 2024-09-03 21:33:48 +08:00
手瓜一十雪
31d963c4d1 Merge pull request #336 from hguandl/feat/macos
feat: 支持 macOS
2024-09-03 21:16:03 +08:00
Hao Guan
7e96118cdc feat: support macOS 2024-09-03 20:17:10 +08:00
Hao Guan
709a0744bd chore: refactor path config 2024-09-03 20:14:35 +08:00
手瓜一十雪
f59248cc5a release: 2.2.43 2024-09-03 18:49:45 +08:00
手瓜一十雪
8647c5c607 fix: echo丢失问题 2024-09-03 18:37:28 +08:00
手瓜一十雪
6699ff38a1 Revert "fix: Error Handle"
This reverts commit d79b98bd55.
2024-09-03 18:36:20 +08:00
手瓜一十雪
d79b98bd55 fix: Error Handle 2024-09-03 18:34:33 +08:00
手瓜一十雪
5065a052fb release: 2.2.42 2024-09-03 18:12:59 +08:00
Wesley F. Young
45603bb78c fix(docs): unexpected spaces 2024-09-03 15:48:08 +08:00
手瓜一十雪
40948995b4 build: test 2024-09-03 14:09:17 +08:00
手瓜一十雪
4ccdd8d1d3 docs: update 2024-09-03 13:37:19 +08:00
手瓜一十雪
30d0174f47 fix: #334 2024-09-03 13:20:10 +08:00
手瓜一十雪
5a986ba25c release: 2.2.40 2024-09-03 13:01:37 +08:00
手瓜一十雪
fe63c24ac3 release: 2.2.39 2024-09-03 12:40:13 +08:00
手瓜一十雪
c384bd6875 release: 2.2.38 2024-09-02 17:31:27 +08:00
手瓜一十雪
dcbff3f569 release: 2.2.37 2024-09-02 16:31:42 +08:00
手瓜一十雪
7d91e05a69 fix: #332 2024-09-02 16:31:18 +08:00
手瓜一十雪
a5ce424a40 release: 2.2.36 2024-09-01 18:44:23 +08:00
手瓜一十雪
47c36ca062 release: 2.2.35 2024-09-01 18:24:49 +08:00
手瓜一十雪
c4c5b3bf8b fix: remark 2024-09-01 18:24:29 +08:00
手瓜一十雪
b1a81b0d12 release: 2.2.32 2024-09-01 16:32:13 +08:00
手瓜一十雪
ad9fe64850 release: 2.2.32 2024-09-01 16:13:41 +08:00
手瓜一十雪
f236349dc6 Revert "release:2.2.31"
This reverts commit 309d8a9f18.
2024-09-01 16:13:14 +08:00
手瓜一十雪
5f56c8a7d4 fix 2024-09-01 16:10:16 +08:00
手瓜一十雪
309d8a9f18 release:2.2.31 2024-09-01 15:59:02 +08:00
手瓜一十雪
2981799803 fix: file api 2024-09-01 14:11:28 +08:00
手瓜一十雪
00f8e1c0da Revert "fix: fileId"
This reverts commit ae009f98c1.
2024-09-01 13:41:19 +08:00
手瓜一十雪
e9482e2ec4 Revert "fix: encode fileId"
This reverts commit 9bff327377.
2024-09-01 13:41:14 +08:00
手瓜一十雪
9bff327377 fix: encode fileId 2024-09-01 12:17:42 +08:00
手瓜一十雪
ae009f98c1 fix: fileId 2024-09-01 12:17:17 +08:00
手瓜一十雪
77505a6f5b release: 2.2.31 2024-09-01 09:31:59 +08:00
手瓜一十雪
19c729aa23 chore: appid 2024-09-01 09:30:38 +08:00
Wesley F. Young
595888128a release: 2.2.30 2024-08-31 23:43:01 +08:00
手瓜一十雪
51589d0eae fix: 群精华上限修改 2024-08-31 22:11:18 +08:00
Wesley F. Young
f1643ac549 fix: get file way 01 get by msg & seq id 2024-08-31 20:30:42 +08:00
手瓜一十雪
3f24461612 feat: support 27597 2024-08-31 19:01:25 +08:00
Wesley F. Young
b5deb198de refactor: inline all NTQQXxxApi uses 2024-08-31 16:00:03 +08:00
Wesley F. Young
78452cf6a9 chore: clean code for webapi.ts 2024-08-31 14:18:11 +08:00
Wesley F. Young
4b4a784f56 chore: clean code for user.ts 2024-08-31 14:11:22 +08:00
Wesley F. Young
3e53cbcf8f chore: clean code for system.ts 2024-08-31 14:09:25 +08:00
Wesley F. Young
f34740f1f0 chore: clean code for group.ts 2024-08-31 14:07:48 +08:00
Wesley F. Young
b406bdfc37 chore: clean code for group.ts 2024-08-31 14:02:36 +08:00
Wesley F. Young
03c056702c chore: clean code for friend.ts 2024-08-31 13:53:30 +08:00
Wesley F. Young
9c5f3f1946 chore: clean code for collection.ts 2024-08-31 13:37:22 +08:00
Wesley F. Young
b50d7c24e7 refactor: move CacheApi to cache.ts 2024-08-31 13:36:21 +08:00
Wesley F. Young
f05cf68945 chore: clean code for file.ts 2024-08-31 13:35:29 +08:00
Alen
efc1875e35 release: 2.2.29 2024-08-31 12:53:50 +08:00
Alen
df063e6762 Merge pull request #326 from cnxysoft/upmain
fix: 群成员信息
2024-08-31 11:55:56 +08:00
Alen
e5c55b4339 fix: 群成员信息 2024-08-31 11:53:07 +08:00
Wesley F. Young
bee9095d6f release: 2.2.28 2024-08-31 10:35:33 +08:00
Wesley F. Young
92f8eaaac9 fix: get file by msgId and elemId 2024-08-31 10:34:19 +08:00
Wesley F. Young
f5e7288fe5 fix: report encoded msgId+elemId in upload event 2024-08-30 20:47:48 +08:00
Seijo Cecilia
214aa7b6e4 update(workflow): 'build' can only be triggered manually 2024-08-30 16:00:33 +08:00
Seijo Cecilia
5b5d5b41f5 build: snapshot-fix-get-file 2024-08-30 15:47:18 +08:00
Seijo Cecilia
23d613321e Revert "fix: arg3 no longer needed for downloadFileForModelId"
This reverts commit e1e4d038d9.
2024-08-30 15:41:50 +08:00
Wesley F. Young
0b6be0923f release: 2.2.27 2024-08-30 12:02:50 +08:00
Wesley F. Young
aba748ea13 Merge pull request #323 from LingLambda/main
fix: 规范 setSelfOnlineStatus 接口的参数命名
2024-08-30 11:55:22 +08:00
Wesley F. Young
f1f1ac582d chore: optimize imports 2024-08-30 11:45:58 +08:00
Wesley F. Young
54a7cbc3f4 feat: go-cqhttp style group file apis 2024-08-30 11:44:15 +08:00
ling
2f4dbaec4c fix: 规范setSelfOnlineStatus接口参数命名 2024-08-30 11:38:15 +08:00
Wesley F. Young
578f518aaf fix: others invited by others 2024-08-30 10:04:48 +08:00
Wesley F. Young
077ba74b22 fix: missing parameter for file searching 2024-08-30 09:31:18 +08:00
手瓜一十雪
e0efe635c7 release: 2.2.26 2024-08-29 22:47:24 +08:00
手瓜一十雪
1a06841de0 feat: notify.type == GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS 2024-08-29 22:46:55 +08:00
手瓜一十雪
3987e0ee0b feat: 2.2.25 2024-08-29 22:38:48 +08:00
Wesley F. Young
9f53bea02f fix: duplicate type definition 2024-08-29 22:30:19 +08:00
Wesley F. Young
737709f9e7 Merge remote-tracking branch 'origin/main' 2024-08-29 22:27:57 +08:00
Wesley F. Young
39477aa6a0 fix: try to fix '搜索名字模式' of GetFile 2024-08-29 22:27:51 +08:00
手瓜一十雪
f097050b56 chore: 移除测试 2024-08-29 21:57:05 +08:00
手瓜一十雪
f14726ed1a fix: getfile 2024-08-29 21:55:44 +08:00
Wesley F. Young
e1e4d038d9 fix: arg3 no longer needed for downloadFileForModelId 2024-08-29 21:21:45 +08:00
Wesley F. Young
d2db4cf887 fix: 有笨蛋塞了 console.log 忘记删掉 2024-08-29 20:58:39 +08:00
手瓜一十雪
2f3ece9ca3 build: 2.2.25-test 2024-08-29 20:50:09 +08:00
手瓜一十雪
9f82007116 revert: eslint 2024-08-29 20:40:33 +08:00
手瓜一十雪
f79198a472 release: 2.2.24 2024-08-29 20:35:30 +08:00
手瓜一十雪
ce3d35d7ec fix: modelId 2024-08-29 20:34:24 +08:00
手瓜一十雪
f4d40f0466 release: 2.2.23 2024-08-29 20:14:40 +08:00
手瓜一十雪
a2fa085d5f fix: getfile 2024-08-29 20:14:20 +08:00
手瓜一十雪
a598266a6e fix: #320 2024-08-29 19:32:37 +08:00
手瓜一十雪
f5fe33cee7 fix: GroupEssenceMsg 2024-08-29 19:25:16 +08:00
手瓜一十雪
200c7226ef fix: getGroupEssenceMsgAll 2024-08-29 19:21:03 +08:00
Wesley F. Young
53475a6a0e fix: filter emoji un-like by operation 2024-08-29 18:15:52 +08:00
Wesley F. Young
b4ec1ad6c0 Merge remote-tracking branch 'origin/main' 2024-08-29 17:10:27 +08:00
Wesley F. Young
ef511a729d fix: solve export conflict 2024-08-29 17:10:14 +08:00
Wesley F. Young
275c4ce226 feat: GetGroupIgnoredNotifies 2024-08-29 17:08:36 +08:00
手瓜一十雪
45f9c029c8 Merge pull request #319 from NapNeko/dependabot/npm_and_yarn/eslint-9.9.1
build(deps-dev): bump eslint from 8.57.0 to 9.9.1
2024-08-29 17:01:34 +08:00
dependabot[bot]
db5e4ad5d9 build(deps-dev): bump eslint from 8.57.0 to 9.9.1
Bumps [eslint](https://github.com/eslint/eslint) from 8.57.0 to 9.9.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.57.0...v9.9.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-29 08:54:04 +00:00
Wesley F. Young
f05d0a9727 chore: run eslint 2024-08-29 08:37:10 +08:00
Wesley F. Young
04593e9d9a feat: code style 2024-08-29 00:28:53 +08:00
手瓜一十雪
b1ecf13f8e release: 2.2.22 2024-08-29 00:10:52 +08:00
手瓜一十雪
e91e054f20 refactor: GetGroupEssence 2024-08-29 00:10:29 +08:00
手瓜一十雪
130ff7517e refactor: parseEssence 2024-08-29 00:02:24 +08:00
手瓜一十雪
c7042d9684 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-08-29 00:01:48 +08:00
手瓜一十雪
5752e45dd1 refactor: GetFile 2024-08-28 23:45:33 +08:00
Alen
1a034ecb53 Merge pull request #317 from cnxysoft/upmain
fix: 转发消息报错
2024-08-28 23:30:04 +08:00
手瓜一十雪
025da8fb76 refactor: getFile 2024-08-28 23:27:29 +08:00
Alen
2027da1db5 chore: 优化代码 2024-08-28 23:24:44 +08:00
Alen
7732f28ca8 fix: reply消息转换 2024-08-28 22:51:54 +08:00
手瓜一十雪
7f9da8cc2d feat: support folder_id 2024-08-28 21:19:17 +08:00
手瓜一十雪
c6342b80a7 release: 2.2.21 2024-08-28 20:51:59 +08:00
Wesley F. Young
f99c82de4b fix: unexpected return in buddy req parsing 2024-08-28 19:32:07 +08:00
Wesley F. Young
56fa57ea02 refactor: rename createMsg -> createUniqueMsgId to prevent ambiguity 2024-08-28 19:03:19 +08:00
手瓜一十雪
cc85985d08 feat: 设置noify已读 2024-08-28 18:18:40 +08:00
手瓜一十雪
bd1751903e chore: clearGroupNotifiesUnreadCount 2024-08-28 18:01:32 +08:00
手瓜一十雪
03a298a70f release: 2.2.20 2024-08-28 17:48:51 +08:00
手瓜一十雪
2722ca2b0e feat: getGroupInfoEx 2024-08-28 17:05:00 +08:00
手瓜一十雪
179c4b800e Merge pull request #314 from NapNeko/dependabot/npm_and_yarn/typescript-eslint/parser-8.3.0
build(deps-dev): bump @typescript-eslint/parser from 7.18.0 to 8.3.0
2024-08-28 17:04:37 +08:00
dependabot[bot]
6bdf14223d build(deps-dev): bump @typescript-eslint/parser from 7.18.0 to 8.3.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.18.0 to 8.3.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.3.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 08:58:48 +00:00
dependabot[bot]
1b8252aa4f Merge pull request #312 from NapNeko/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.3.0 2024-08-28 08:27:34 +00:00
dependabot[bot]
8219889154 build(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 7.18.0 to 8.3.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.3.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 08:26:00 +00:00
手瓜一十雪
df4ac5dcce release: 2.2.19 2024-08-28 16:19:30 +08:00
Seijo Cecilia
738eaf9de9 refactor: directory structure of core 2024-08-28 15:29:13 +08:00
Seijo Cecilia
c483ccbbbc fix: ob11 config file location 2024-08-28 15:27:38 +08:00
Seijo Cecilia
0d65f846ae refactor: remove helper directory in onebot 2024-08-28 15:09:17 +08:00
Seijo Cecilia
f47e75c423 refactor: make methods in event.ts instance methods 2024-08-28 15:02:44 +08:00
Wesley F. Young
c008e58fb8 Merge remote-tracking branch 'origin/main' 2024-08-28 13:05:15 +08:00
Wesley F. Young
26e0f17bc5 feat: emoji like event from other to other 2024-08-28 13:05:02 +08:00
Wesley F. Young
6543f28bdb feat: proto files of messages 2024-08-28 13:04:27 +08:00
手瓜一十雪
a86851b338 feat: 提高效率 2024-08-28 13:04:05 +08:00
Wesley F. Young
3a03e455c6 refactor: extract emoji like parsing logic 2024-08-28 10:50:39 +08:00
手瓜一十雪
3d39fd1580 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-08-27 22:51:14 +08:00
手瓜一十雪
601b0add26 release: 2.2.18 2024-08-27 22:50:53 +08:00
Version
4f974cc913 chore:version change 2024-08-27 14:50:42 +00:00
手瓜一十雪
f691320453 fix: handleQuickOperation 2024-08-27 22:47:30 +08:00
手瓜一十雪
be39fc3a21 chore: 移除 2024-08-27 22:17:54 +08:00
手瓜一十雪
d2fafaf33a chore: rename nekodoge 2024-08-27 20:53:07 +08:00
手瓜一十雪
27ae331352 chore: new adapter 2024-08-27 20:37:23 +08:00
Seijo Cecilia
3f2dcfbacc (partially) fix: 'throw' of exception caught locally 2024-08-27 15:05:51 +08:00
Alen
8565aee8b6 Merge pull request #310 from cnxysoft/upmain
style: 规范代码
2024-08-27 14:11:33 +08:00
Alen
f983add599 style: 规范代码 2024-08-27 14:09:28 +08:00
Seijo Cecilia
030192afeb Merge remote-tracking branch 'origin/main' 2024-08-27 14:00:05 +08:00
Seijo Cecilia
c8b6a158f1 chore: optimize imports 2024-08-27 13:59:47 +08:00
Alen
e71f7849a7 Merge pull request #309 from cnxysoft/upmain
fix: 群员获取异常
2024-08-27 13:54:33 +08:00
Alen
b64d1ff4ff revert: 入群事件
因getGroupMembers切换到V2,恢复原本逻辑
2024-08-27 13:52:23 +08:00
Alen
5a0028be26 chore: 去除无用代码 2024-08-27 13:41:30 +08:00
Alen
926d7deb43 Merge branch 'main' into upmain 2024-08-27 13:39:38 +08:00
Seijo Cecilia
6384b50bae chore: run eslint 2024-08-27 13:35:25 +08:00
手瓜一十雪
9feb0f4b53 release: v2.2.16 2024-08-27 12:18:20 +08:00
手瓜一十雪
43ec1b7cfd feat: 更高效率的识别 2024-08-27 12:16:57 +08:00
Wesley F. Young
05b7a59f8d refactor: move version info into version.ts 2024-08-27 10:30:38 +08:00
Wesley F. Young
17e680f7af refactor: move all utility files to /common 2024-08-27 10:18:31 +08:00
Wesley F. Young
035d256d4e refactor: rename event-legacy -> event (currently in use) 2024-08-27 10:15:46 +08:00
Wesley F. Young
8939adf886 refactor: rename event -> event-v2 (not ready for use) 2024-08-27 10:14:55 +08:00
Wesley F. Young
027ffbffa6 chore: remove redundant eslint-disable 2024-08-27 10:05:23 +08:00
Alen
3cca06712b fix: 群成员拉取失败(实验性) 2024-08-27 02:43:27 +08:00
手瓜一十雪
2b9359dbf4 fix: Type
NodeIKernelSessionListener/onNTSessionCreate->NodeIKernelGroupListener/onGroupListInited
2024-08-26 21:33:33 +08:00
手瓜一十雪
c0f5d3bd2e release:2.2.15 2024-08-26 19:56:16 +08:00
手瓜一十雪
2a2d5382e1 Merge pull request #308 from LingLambda/main
规范了_send_group_notice接口参数命名,适当的规范了部分变量命名
2024-08-26 19:30:55 +08:00
ling
2e4986024c 规范了_send_group_notice接口参数命名,适当的规范了部分变量命名 2024-08-26 18:53:33 +08:00
手瓜一十雪
8a9c605dae release: 2.2.15 2024-08-26 18:02:21 +08:00
301 changed files with 11457 additions and 4507 deletions

View File

@@ -17,5 +17,8 @@ charset = utf-8
indent_style = space
indent_size = 4
[*.bat]
charset = latin1
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.

View File

@@ -4,7 +4,7 @@ module.exports = {
'es2021': true,
'node': true
},
'ignorePatterns': ['src/proto/'],
'ignorePatterns': ['src/core/proto/'],
'extends': [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'

View File

@@ -10,6 +10,7 @@ body:
在提交新的 Bug 反馈前,请确保您:
* 已经搜索了现有的 issues并且没有找到可以解决您问题的方法
* 不与现有的某一 issue 重复
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
- type: input
id: system-version
attributes:
@@ -78,4 +79,4 @@ body:
attributes:
label: OneBot 客户端运行日志
description: 粘贴 OneBot 客户端的相关日志内容到此处
render: shell
render: shell

View File

@@ -1,15 +1,11 @@
name: "Build Action"
on:
workflow_dispatch:
push:
branches:
- main
permissions: write-all
jobs:
Build-LiteLoader:
if: ${{ startsWith(github.event.head_commit.message, 'build:') }}
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
@@ -37,7 +33,6 @@ jobs:
name: NapCat.Framework
path: dist
Build-Shell:
if: ${{ startsWith(github.event.head_commit.message, 'build:') }}
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
@@ -63,4 +58,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell
path: dist
path: dist

View File

@@ -93,15 +93,18 @@ jobs:
needs: [Build-LiteLoader,Build-Shell]
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
uses: actions/checkout@v4
with:
repository: 'NapNeko/NapCatQQ'
submodules: true
ref: main
token: ${{ secrets.NAPCAT_BUILD }}
- name: Download All Artifact
uses: actions/download-artifact@v4
# - name: Compress subdirectories
# run: |
# cd ./NapCat.Shell/
# zip -q -r NapCat.Shell.zip *
# cd ..
# rm ./NapCat.Shell.zip -rf
# mv ./NapCat.Shell/NapCat.Shell.zip ./
- name: Compress subdirectories
run: |
cd ./NapCat.Shell/
@@ -114,6 +117,20 @@ jobs:
rm ./NapCat.Framework.zip -rf
mv ./NapCat.Shell/NapCat.Shell.zip ./
mv ./NapCat.Framework/NapCat.Framework.zip ./
mkdir ./NapCat.Framework.Windows.Once
unzip -q ./external/LiteLoaderWrapper.zip -d ./NapCat.Framework.Windows.Once
cd ./NapCat.Framework.Windows.Once
ls
mkdir -p ./LL/plugins/NapCatQQ
unzip -q ../NapCat.Framework.zip -d ./LL/plugins/NapCatQQ
zip -q -r NapCat.Framework.Windows.Once.zip *
cd ..
mv ./NapCat.Framework.Windows.Once/NapCat.Framework.Windows.Once.zip ./
mv ./external/packet/napcat.packet.arm64 ./
mv ./external/packet/napcat.packet.exe ./
mv ./external/packet/napcat.packet.linux ./
mv ./external/packet/napcat.packet.production.py ./
- name: Extract version from tag
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
@@ -129,4 +146,9 @@ jobs:
files: |
NapCat.Framework.zip
NapCat.Shell.zip
NapCat.Framework.Windows.Once.zip
napcat.packet.arm64
napcat.packet.exe
napcat.packet.linux
napcat.packet.production.py
draft: true

View File

@@ -1,33 +1,50 @@
<div align="center">
<img src="https://socialify.git.ci/NapNeko/NapCatQQ/image?description=1&language=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2FNapNeko%2FNapCatQQ%2Fmain%2Flogo.png&name=1&stargazers=1&theme=Auto" alt="NapCatQQ" width="640" height="320" />
![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>
---
## 欢迎回来
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
## 猫猫技能
- [x] **多种启动方式**支持以无头、LiteLoader 插件、仅 QQ GUI 三种方式启动
- [x] **低占用**:无头模式占用资源极低,适合在服务器上运行
- [x] **超多接口**在实现大部分Onebot接口上扩展了一套私有API
- [x] **WebUI**:自带 WebUI 支持,远程管理更加便捷
- [x] **超高性能**:轻松数千群聊 独创消息队列
- [x] **启动方式**支持以无头、LiteLoader 插件、仅 QQ GUI 三种方式启动
- [x] **覆盖平台**: 覆盖 Windows / Linux (可选 Docker) / Android Termux / MacOS
- [x] **安装简单**: 支持一键脚本/程序自动部署/镜像部署等多种覆盖范围
- [x] **超低占用**:无头模式占用资源极低,适合在服务器上运行
- [x] **超多接口**:实现大部分 OneBot 和 go-cqhttp 接口,超多扩展 API
- [x] **远程管理**:自带 WebUI 支持,远程管理更加便捷
- [x] **扩展支持**:基于 MoeHoo 的Native 可实现发包与收包
## 使用猫猫
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
**首次使用**请务必前往[官方文档](https://napneko.github.io/)查看使用教程
**首次使用**请务必查看如下文档看使用教程
### 文档地址
[Cloudflare.Worker](https://doc.napneko.icu/)
[Cloudflare.HKServer](https://napcat.napneko.icu/)
[Cloudflare.Pages](https://napneko.pages.dev/)
[Github.IO](https://napneko.github.io/)
## 回家旅途
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS)
[Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl)
## 猫猫朋友
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot) 提供初始版本基础
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持
不过最最重要的 还是需要感谢屏幕前的你哦~
---
## 约法三章

BIN
external/LiteLoaderWrapper.zip vendored Normal file

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

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,40 @@
@echo off
chcp 65001
net session >nul 2>&1
if %errorLevel% == 0 (
echo Administrator mode detected.
) else (
echo Please run this script in administrator mode.
powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
exit
)
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456

39
launcher/launcher.bat Normal file
View File

@@ -0,0 +1,39 @@
@echo off
chcp 65001
net session >nul 2>&1
if %errorLevel% == 0 (
echo Administrator mode detected.
) else (
echo Please run this script in administrator mode.
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
exit
)
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1

5
launcher/loadNapCat.js Normal file
View File

@@ -0,0 +1,5 @@
const path = require('path');
const CurrentPath = path.dirname(__filename);
(async () => {
await import("file://" + path.join(CurrentPath, './napcat/napcat.mjs'));
})();

26
launcher/qqnt.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "qq-chat",
"version": "9.9.16-28788",
"verHash": "73b0c8f6",
"linuxVersion": "3.2.13-28788",
"linuxVerHash": "55fb6434",
"type": "module",
"private": true,
"description": "QQ",
"productName": "QQ",
"author": {
"name": "Tencent",
"email": "QQ-Team@tencent.com"
},
"homepage": "https://im.qq.com",
"sideEffects": true,
"bin": {
"qd": "externals/devtools/cli/index.js"
},
"main": "./loadNapCat.js",
"buildVersion": "28788",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",
"eleArch": "x64"
}

View File

@@ -0,0 +1,4 @@
@echo off
REM ./launcher.bat 123456
REM ./launcher-win10.bat 123456
REM 带有REM的为注释 删掉你需要的系统的那行REM这三个单词 修改QQ本脚本启动即可

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",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
"version": "2.2.13",
"version": "3.6.2",
"icon": "./logo.png",
"authors": [
{

View File

@@ -1,65 +1,53 @@
{
"name": "napcat",
"private": true,
"type": "module",
"version": "2.2.13",
"scripts": {
"build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell",
"build:webui": "cd ./src/webui && vite build",
"lint": "eslint --fix src/**/*.{js,ts}",
"depend": "cd dist && npm install --omit=dev"
},
"devDependencies": {
"@babel/core": "^7.24.7",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@log4js-node/log4js-api": "^1.0.2",
"@protobuf-ts/plugin": "^2.9.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/figlet": "^1.5.8",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/jest": "^29.5.12",
"@types/node": "^22.0.1",
"@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"i": "^0.3.7",
"javascript-obfuscator": "^4.1.0",
"rollup": "^4.13.2",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-obfuscator": "^1.1.0",
"typescript": "^5.3.3",
"vite": "^5.2.6",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-cp": "^4.0.8",
"vite-plugin-dts": "^3.8.2",
"vite-tsconfig-paths": "^4.3.2"
},
"dependencies": {
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"cors": "^2.8.5",
"express": "^5.0.0-beta.2",
"fast-xml-parser": "^4.3.6",
"file-type": "^19.0.0",
"fluent-ffmpeg": "^2.1.2",
"image-size": "^1.1.1",
"json-schema-to-ts": "^3.1.0",
"log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"strtok3": "8.0.1",
"ws": "^8.18.0"
}
}
{
"name": "napcat",
"private": true,
"type": "module",
"version": "3.6.2",
"scripts": {
"build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell",
"build:webui": "cd ./src/webui && vite build",
"lint": "eslint --fix src/**/*.{js,ts}",
"depend": "cd dist && npm install --omit=dev"
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.7",
"@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.2",
"@protobuf-ts/runtime": "^2.9.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^22.0.1",
"@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"cors": "^2.8.5",
"eslint": "^8.57.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"fast-xml-parser": "^4.3.6",
"file-type": "^19.0.0",
"image-size": "^1.1.1",
"json-schema-to-ts": "^3.1.1",
"typescript": "^5.3.3",
"vite": "^5.2.6",
"vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0"
},
"dependencies": {
"express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2",
"log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}
}

View File

@@ -1,45 +0,0 @@
# Dont Use This Script
# 2024.7.3
function Get-QQpath {
try {
$key = Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ"
$uninstallString = $key.UninstallString
return [System.IO.Path]::GetDirectoryName($uninstallString) + "\QQ.exe"
}
catch {
throw "get QQ path error: $_"
}
}
function Select-QQPath {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$dialogTitle = "Select QQ.exe"
$filePicker = New-Object System.Windows.Forms.OpenFileDialog
$filePicker.Title = $dialogTitle
$filePicker.Filter = "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*"
$filePicker.FilterIndex = 1
$null = $filePicker.ShowDialog()
if (-not ($filePicker.FileName)) {
throw "User did not select an .exe file."
}
return $filePicker.FileName
}
$params = $args -join " "
Try {
$QQpath = Get-QQpath
}
Catch {
$QQpath = Select-QQPath
}
if (!(Test-Path $QQpath)) {
throw "provided QQ path is invalid: $QQpath"
}
$Bootfile = Join-Path $PSScriptRoot "napcat.mjs"
$env:ELECTRON_RUN_AS_NODE = 1
$commandInfo = Get-Command $QQpath -ErrorAction Stop
Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& chcp 65001;& '$($commandInfo.Path)' --enable-logging }"

View File

@@ -1,90 +0,0 @@
@echo off
REM 检查当前会话是否具有管理员权限
openfiles >nul 2>&1
if %errorlevel% neq 0 (
REM 如果不是管理员,则重新启动脚本以管理员模式运行
echo 请求管理员权限...
powershell -Command "Start-Process cmd -ArgumentList '/c %~f0 %*' -Verb RunAs"
exit /b
)
REM 设置当前工作目录
cd /d %~dp0
REM 获取当前目录路径
set currentPath=%cd%
set currentPath=%currentPath:\=/%
REM 生成JavaScript代码
set "jsCode=(async () =^>await import('file:///%currentPath%/napcat.mjs'))();"
REM 将JavaScript代码保存到文件中
echo %jsCode% > loadScript.js
echo JavaScript code has been generated and saved to loadScript.js
REM 设置NAPCAT_PATH环境变量为 当前目录的loadScript.js地址
set NAPCAT_PATH=%cd%\loadScript.js
REM 获取QQ路径
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in (%RetString%) do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
REM 拿不到QQ路径则退出
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
REM 收集dbghelp.dll路径和HASH信息
set QQdir=%~dp0
set oldDllPath=%QQdir%dbghelp.dll
set newDllPath=%currentPath%\dbghelp.dll
for /f "tokens=*" %%A in ('certutil -hashfile "%oldDllPath%" MD5') do (
if not defined oldDllHash set oldDllHash=%%A
)
for /f "tokens=*" %%A in ('certutil -hashfile "%newDllPath%" MD5') do (
if not defined newDllHash set newDllHash=%%A
)
REM 如果文件一致则跳过
if "%oldDllHash%" neq "%newDllHash%" (
tasklist /fi "imagename eq QQ.exe" 2>nul | find /i "QQ.exe" >nul
if %errorlevel% equ 0 (
REM 文件占用则退出
echo dbghelp.dll is in use, cannot continue.
) else (
REM 文件未占用则尝试覆盖
copy /y "%newDllPath%" "%oldDllPath%"
if %errorlevel% neq 0 (
echo Failed to copy dbghelp.dll
pause
exit /b
) else (
echo dbghelp.dll has been copied to %QQdir%
)
)
)
REM 带参数启动QQ
REM 判断wt是否存在存在则通过wt启动不存在则通过cmd启动
REM %QQPath% --enable-logging %*
where wt >nul 2>nul
if %errorlevel% equ 0 (
wt "cmd" /c "%QQPath%" --enable-logging %*
) else (
"%QQPath%" --enable-logging %*
)

View File

@@ -1,123 +0,0 @@
# 检查当前会话是否具有管理员权限
function Test-Administrator {
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
(New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
if (-not (Test-Administrator)) {
# 如果不是管理员,则重新启动脚本以管理员模式运行
$scriptPath = $myInvocation.MyCommand.Path
if (-not $scriptPath) {
$scriptPath = $PSCommandPath
}
$newProcess = New-Object System.Diagnostics.ProcessStartInfo "powershell";
$newProcess.Arguments = "-File `"$scriptPath`" $args"
$newProcess.Verb = "runas";
[System.Diagnostics.Process]::Start($newProcess);
exit
}
function Get-QQpath {
try {
$key = Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ"
$uninstallString = $key.UninstallString
return [System.IO.Path]::GetDirectoryName($uninstallString) + "\QQ.exe"
}
catch {
throw "get QQ path error: $_"
}
}
function Select-QQPath {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$dialogTitle = "Select QQ.exe"
$filePicker = New-Object System.Windows.Forms.OpenFileDialog
$filePicker.Title = $dialogTitle
$filePicker.Filter = "Executable Files (*.exe)|*.exe|All Files (*.*)|*.*"
$filePicker.FilterIndex = 1
$null = $filePicker.ShowDialog()
if (-not ($filePicker.FileName)) {
throw "User did not select an .exe file."
}
return $filePicker.FileName
}
# 设置当前工作目录
$scriptDirectory = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
Set-Location $scriptDirectory
# 获取当前目录路径
$currentPath = Get-Location
# 替换\为/
$currentPath = $currentPath -replace '\\', '/'
# 生成JavaScript代码
$jsCode = @"
(async () => {
await import('file:///$currentPath/napcat.mjs');
})();
"@
# 将JavaScript代码保存到文件中
$jsFilePath = Join-Path $currentPath "loadScript.js"
$jsCode | Out-File -FilePath $jsFilePath -Encoding UTF8
Write-Output "JavaScript code has been generated and saved to $jsFilePath"
# 设置NAPCAT_PATH环境变量为 当前目录的loadScript.js地址
$env:NAPCAT_PATH = $jsFilePath
$params = $args -join " "
Try {
$QQpath = Get-QQpath
}
Catch {
$QQpath = Select-QQPath
}
# 拿不到QQ路径则退出
if (!(Test-Path $QQpath)) {
Write-Output "provided QQ path is invalid: $QQpath"
Read-Host "Press any key to continue..."
exit
}
$commandInfo = Get-Command $QQpath -ErrorAction Stop
# 收集dbghelp.dll路径和HASH信息
$QQpath = Split-Path $QQpath
$oldDllPath = Join-Path $QQpath "dbghelp.dll"
$oldDllHash = Get-FileHash $oldDllPath -Algorithm MD5
$newDllPath = Join-Path $currentPath "dbghelp.dll"
$newDllHash = Get-FileHash $newDllPath -Algorithm MD5
# 如果文件一致则跳过
if ($oldDllHash.Hash -ne $newDllHash.Hash) {
$processes = Get-Process -Name QQ -ErrorAction SilentlyContinue
if ($processes) {
# 文件占用则退出
Write-Output "dbghelp.dll is in use by the following processes:"
$processes | ForEach-Object { Write-Output "$($_.Id) $($_.Name) $($_.Path)" }
Write-Output "dbghelp.dll is in use, cannot continue."
Read-Host "Press any key to continue..."
exit
} else {
# 文件未占用则尝试覆盖
try {
Copy-Item -Path "$newDllPath" -Destination "$oldDllPath" -Force
Write-Output "dbghelp.dll has been copied to $QQpath"
} catch {
Write-Output "Failed to copy dbghelp.dll: $_"
Read-Host "Press any key to continue..."
exit
}
}
}
# 带参数启动QQ
try {
Start-Process powershell -ArgumentList '-noexit', '-noprofile', "-command &{& chcp 65001;& '$($commandInfo.Path)' --enable-logging $params}" -NoNewWindow -ErrorAction Stop
} catch {
Write-Output "Failed to start process as administrator: $_"
Read-Host "Press any key to continue..."
}

View File

@@ -1,93 +0,0 @@
@echo off
REM 检查当前会话是否具有管理员权限
openfiles >nul 2>&1
if %errorlevel% neq 0 (
REM 如果不是管理员,则重新启动脚本以管理员模式运行
echo 请求管理员权限...
where wt >nul 2>nul
if %errorlevel% equ 0 (
powershell -Command "Start-Process cmd -ArgumentList ' /c %~f0 %*' -Verb RunAs"
) else (
powershell -Command "Start-Process wt -ArgumentList 'cmd /c %~f0 %*' -Verb RunAs"
)
REM wt "cmd" /c "%~f0 %*"
exit /b
)
REM 设置当前工作目录
cd /d %~dp0
REM 获取当前目录路径
set currentPath=%cd%
set currentPath=%currentPath:\=/%
REM 生成JavaScript代码
set "jsCode=(async () =^>await import('file:///%currentPath%/napcat.mjs'))();"
REM 将JavaScript代码保存到文件中
echo %jsCode% > loadScript.js
echo JavaScript code has been generated and saved to loadScript.js
REM 设置NAPCAT_PATH环境变量为 当前目录的loadScript.js地址
set NAPCAT_PATH=%cd%\loadScript.js
REM 获取QQ路径
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in (%RetString%) do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
REM 拿不到QQ路径则退出
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
REM 收集dbghelp.dll路径和HASH信息
set QQdir=%~dp0
set oldDllPath=%QQdir%dbghelp.dll
set newDllPath=%currentPath%\dbghelp.dll
for /f "tokens=*" %%A in ('certutil -hashfile "%oldDllPath%" MD5') do (
if not defined oldDllHash set oldDllHash=%%A
)
for /f "tokens=*" %%A in ('certutil -hashfile "%newDllPath%" MD5') do (
if not defined newDllHash set newDllHash=%%A
)
REM 如果文件一致则跳过
if "%oldDllHash%" neq "%newDllHash%" (
tasklist /fi "imagename eq QQ.exe" 2>nul | find /i "QQ.exe" >nul
if %errorlevel% equ 0 (
REM 文件占用则退出
echo dbghelp.dll is in use, cannot continue.
) else (
REM 文件未占用则尝试覆盖
copy /y "%newDllPath%" "%oldDllPath%"
if %errorlevel% neq 0 (
echo Failed to copy dbghelp.dll
pause
exit /b
) else (
echo dbghelp.dll has been copied to %QQdir%
)
)
)
REM 带参数启动QQ
REM 判断wt是否存在存在则通过wt启动不存在则通过cmd启动
REM %QQPath% --enable-logging %*
chcp 65001
"%QQPath%" --enable-logging %*

View File

@@ -1,77 +0,0 @@
@echo off
REM Check if the script is running as administrator
openfiles >nul 2>&1
if %errorlevel% neq 0 (
REM If not, restart the script in administrator mode
echo Requesting administrator privileges...
powershell -Command "Start-Process cmd -ArgumentList '/c %~f0 %*' -Verb RunAs"
exit /b
)
cd /d %~dp0
set currentPath=%cd%
set currentPath=%currentPath:\=/%
REM Generate JavaScript code
set "jsCode=(async () =^>await import('file:///%currentPath%/napcat.mjs'))();"
REM Save JavaScript code to a file
echo %jsCode% > loadScript.js
echo JavaScript code has been generated and saved to loadScript.js
REM Set NAPCAT_PATH environment variable to the address of loadScript.js in the current directory
set NAPCAT_PATH=%cd%\loadScript.js
REM Get QQ path and cache it
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set "RetString=%%b"
)
set "pathWithoutUninstall=%RetString:Uninstall.exe=%"
SET QQPath=%pathWithoutUninstall%QQ.exe
echo %QQPath%>qq_path_cache.txt
echo QQ path %QQPath% has been cached to qq_path_cache.txt
REM Exit if QQ path is invalid
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
REM Collect dbghelp.dll path and HASH information
set QQdir=%~dp0
set oldDllPath=%QQdir%dbghelp.dll
set newDllPath=%currentPath%\dbghelp.dll
for /f "tokens=*" %%A in ('certutil -hashfile "%oldDllPath%" MD5') do (
if not defined oldDllHash set oldDllHash=%%A
)
for /f "tokens=*" %%A in ('certutil -hashfile "%newDllPath%" MD5') do (
if not defined newDllHash set newDllHash=%%A
)
REM Compare the HASH of the old and new dbghelp.dll, and replace the old one if they are different
if "%oldDllHash%" neq "%newDllHash%" (
tasklist /fi "imagename eq QQ.exe" 2>nul | find /i "QQ.exe" >nul
if %errorlevel% equ 0 (
REM If the file is in use, prompt the user to close QQ
echo dbghelp.dll is in use, please close QQ first.
) else (
copy /y "%newDllPath%" "%oldDllPath%"
if %errorlevel% neq 0 (
echo Copy dbghelp.dll failed, please check and try again.
pause
exit /b
) else (
echo dbghelp.dll has been updated.
echo Please run BootWay05_run.bat to start QQ.
echo If you update QQ in the future, please run BootWay05_init.bat again.
pause
exit /b
)
)
)

View File

@@ -1,10 +0,0 @@
@echo off
set /p QQPath=<qq_path_cache.txt
echo QQ path %QQPath% has been read from qq_path_cache.txt
echo If failed to start QQ, please try running this script in administrator mode.
set NAPCAT_PATH=%cd%\loadScript.js
REM Launch QQ.exe with params provided
"%QQPath%" --enable-logging %*

View File

@@ -1,13 +0,0 @@
@echo off
chcp 65001
set /p QQPath=<qq_path_cache.txt
echo QQ path %QQPath% has been read from qq_path_cache.txt
echo If failed to start QQ, please try running this script in administrator mode.
set NAPCAT_PATH=%cd%\loadScript.js
REM Launch QQ.exe with params provided
"%QQPath%" --enable-logging %*

2
script/KillQQ.bat Normal file
View File

@@ -0,0 +1,2 @@
@echo off
taskkill /f /im QQ.exe

View File

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

Binary file not shown.

89
src/common/audio.ts Normal file
View File

@@ -0,0 +1,89 @@
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { LogWrapper } from './log';
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const EXIT_CODES = [0, 255];
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
async function convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const cp = spawn(FFMPEG_PATH, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
cp.on('error', (err: Error) => {
logger.log('FFmpeg处理转换出错: ', err.message);
reject(err);
});
cp.on('exit', async (code, signal) => {
if (code == null || EXIT_CODES.includes(code)) {
try {
const data = await fsPromise.readFile(pcmPath);
await fsPromise.unlink(pcmPath);
resolve(data);
} catch (err) {
reject(err);
}
} else {
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
reject(new Error('FFmpeg处理转换失败'));
}
});
});
}
async function handleWavFile(
file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper
): Promise<{input: Buffer, sampleRate: number}> {
const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
}
return { input: file, sampleRate: fmt.sampleRate };
}
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
logger.log(`语音文件${filePath}需要转换成silk`);
const pcmPath = `${pttPath}.pcm`;
const { input, sampleRate } = isWav(file)
? (await handleWavFile(file, filePath, pcmPath, logger))
: { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
const silk = await encode(input, sampleRate);
await fsPromise.writeFile(pttPath, silk.data);
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
};
} else {
let duration = 0;
try {
duration = getDuration(file) / 1000;
} catch (e: any) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack);
duration = await guessDuration(filePath, logger);
}
return {
converted: false,
path: filePath,
duration,
};
}
} catch (error: any) {
logger.logError.bind(logger)('convert silk failed', error.stack);
return {};
}
}

View File

@@ -22,9 +22,14 @@ export abstract class ConfigBase<T> {
}
getConfigPath(pathName: string | undefined): string {
const suffix = pathName ? `_${pathName}` : '';
const filename = `${this.name}${suffix}.json`;
return path.join(this.configPath, filename);
if (!pathName) {
const filename = `${this.name}.json`;
const mainPath = this.core.context.pathWrapper.binaryPath;
return path.join(mainPath, 'config', filename);
} else {
const filename = `${this.name}_${pathName}.json`;
return path.join(this.configPath, filename);
}
}
read(): T {
@@ -35,7 +40,7 @@ export abstract class ConfigBase<T> {
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
} catch (e: any) {
logger.logError(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
}
}
try {
@@ -44,9 +49,9 @@ export abstract class ConfigBase<T> {
return this.configData;
} catch (e: any) {
if (e instanceof SyntaxError) {
logger.logError(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
logger.logError.bind(logger)(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
} else {
logger.logError(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
logger.logError.bind(logger)(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
}
return {} as T;
}
@@ -61,7 +66,7 @@ export abstract class ConfigBase<T> {
try {
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
} catch (e: any) {
logger.logError(`保存配置文件 ${configPath} 时发生错误:`, e.message);
logger.logError.bind(logger)(`保存配置文件 ${configPath} 时发生错误:`, e.message);
}
}
}

View File

@@ -1,4 +1,4 @@
import { NodeIQQNTWrapperSession } from '@/core/wrapper/wrapper';
import { NodeIQQNTWrapperSession } from '@/core/wrapper';
import { randomUUID } from 'crypto';
import { ListenerNamingMapping, ServiceNamingMapping } from '@/core';
@@ -9,9 +9,18 @@ interface InternalMapKey {
checker: ((...args: any[]) => boolean) | undefined;
}
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
type FuncKeys<T> = Extract<
{
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
}[keyof T],
string
>;
export type ListenerClassBase = Record<string, string>;
export class LegacyNTEventWrapper {
export class NTEventWrapper {
private WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
private listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
@@ -43,10 +52,8 @@ export class LegacyNTEventWrapper {
createEventFunction<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
// eslint-disable-next-line
// @ts-ignore
T extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
const eventNameArr = eventName.split('/');
type eventType = {
@@ -98,10 +105,8 @@ export class LegacyNTEventWrapper {
async callNoListenerEvent<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
// eslint-disable-next-line
// @ts-ignore
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
...args: Parameters<EventType>
@@ -111,10 +116,8 @@ export class LegacyNTEventWrapper {
async registerListen<
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
// eslint-disable-next-line
// @ts-ignore
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod],
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
>(
listenerAndMethod: `${Listener}/${ListenerMethod}`,
waitTimes = 1,
@@ -164,15 +167,11 @@ export class LegacyNTEventWrapper {
async callNormalEventV2<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
// eslint-disable-next-line
// @ts-ignore
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
// eslint-disable-next-line
// @ts-ignore
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod]
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
listenerAndMethod: `${Listener}/${ListenerMethod}`,
@@ -236,7 +235,7 @@ export class LegacyNTEventWrapper {
this.createListenerFunction(ListenerMainName);
const eventFunction = this.createEventFunction(serviceAndMethod);
retEvent = await eventFunction!(...(args));
if (!checkerEvent(retEvent)) {
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
clearTimeout(timeoutRef);
reject(
new Error(
@@ -250,86 +249,8 @@ export class LegacyNTEventWrapper {
),
);
}
},
);
}
/*
async callNormalEvent<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
// eslint-disable-next-line
// @ts-ignore
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
// eslint-disable-next-line
// @ts-ignore
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod]
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
listenerAndMethod: `${Listener}/${ListenerMethod}`,
waitTimes = 1,
timeout: number = 3000,
checker: (...args: Parameters<ListenerType>) => boolean,
...args: Parameters<EventType>
) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
async (resolve, reject) => {
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined;
let retEvent: any = {};
const databack = () => {
if (complete == 0) {
reject(
new Error(
'Timeout: NTEvent EventName:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n',
),
);
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
}
};
const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0];
const ListenerSubName = ListenerNameList[1];
const Timeouter = setTimeout(databack, timeout);
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: any[]) => {
complete++;
//console.log('func', ...args);
retData = args as Parameters<ListenerType>;
if (complete >= waitTimes) {
clearTimeout(Timeouter);
databack();
}
},
};
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map());
}
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak);
this.createListenerFunction(ListenerMainName);
const EventFunc = this.createEventFunction<EventType>(serviceAndMethod);
retEvent = await EventFunc!(...(args as any[]));
},
);
}
*/
}

View File

@@ -123,7 +123,7 @@ export interface HttpDownloadOptions {
headers?: Record<string, string> | string;
}
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
async function tryDownload(options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
// const chunks: Buffer[] = [];
let url: string;
let headers: Record<string, string> = {
@@ -142,15 +142,26 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
}
}
}
if (useReferer && !headers['Referer']) {
headers['Referer'] = url;
}
const fetchRes = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause;
}
throw err;
});
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`);
return fetchRes;
}
const blob = await fetchRes.blob();
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
const useReferer = typeof options === 'string';
let resp = await tryDownload(options);
if (resp.status === 403 && useReferer) {
resp = await tryDownload(options, true);
}
if (!resp.ok) throw new Error(`下载文件失败: ${resp.statusText}`);
const blob = await resp.blob();
const buffer = await blob.arrayBuffer();
return Buffer.from(buffer);
}
@@ -160,8 +171,7 @@ type Uri2LocalRes = {
errMsg: string,
fileName: string,
ext: string,
path: string,
isLocal: boolean
path: string
}
export async function checkFileV2(filePath: string) {
@@ -194,7 +204,6 @@ export async function checkUriType(Uri: string) {
return undefined;
}, Uri);
if (LocalFileRet) return LocalFileRet;
const OtherFileRet = await solveProblem((uri: string) => {
//再判断是否是Http
if (uri.startsWith('http://') || uri.startsWith('https://')) {
@@ -206,15 +215,19 @@ export async function checkUriType(Uri: string) {
}
if (uri.startsWith('file://')) {
let filePath: string;
// await fs.copyFile(url.pathname, filePath);
const pathname = decodeURIComponent(new URL(uri).pathname);
if (process.platform === 'win32') {
filePath = pathname.slice(1);
} else {
filePath = pathname;
}
return { Uri: filePath, Type: FileUriType.Local };
}
if (uri.startsWith('data:')) {
const data = uri.split(',')[1];
if (data) return { Uri: data, Type: FileUriType.Base64 };
}
}, Uri);
if (OtherFileRet) return OtherFileRet;
@@ -224,34 +237,42 @@ export async function checkUriType(Uri: string) {
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
//解析失败
const tempName = randomUUID();
if (!filename) filename = randomUUID();
//解析Http和Https协议
if (UriType == FileUriType.Unknown) {
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '', isLocal: false };
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
}
//解析File协议和本地文件
if (UriType == FileUriType.Local) {
const fileExt = path.extname(HandledUri);
const filename = path.basename(HandledUri, fileExt);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: HandledUri, isLocal: true };
let filename = path.basename(HandledUri, fileExt);
filename += fileExt;
//复制文件到临时文件并保持后缀
const filenameTemp = tempName + fileExt;
const filePath = path.join(dir, filenameTemp);
fs.copyFileSync(HandledUri, filePath);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
//接下来都要有文件名
if (!filename) filename = randomUUID();
//解析Http和Https协议
if (UriType == FileUriType.Remote) {
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
if (pathInfo.name) {
filename = pathInfo.name;
const pathlen = 200 - dir.length - pathInfo.name.length;
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
if (pathInfo.ext) {
filename += pathInfo.ext;
}
}
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
const fileExt = path.extname(HandledUri);
const filePath = path.join(dir, filename);
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
const filePath = path.join(dir, tempName + fileExt);
const buffer = await httpDownload(HandledUri);
fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath, isLocal: true };
//没有文件就创建
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
//解析Base64
if (UriType == FileUriType.Base64) {
@@ -266,7 +287,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und
fileExt = ext;
filename = filename + '.' + ext;
}
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath, isLocal: true };
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '', isLocal: false };
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
}

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

@@ -1,133 +0,0 @@
import type { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper/wrapper';
import EventEmitter from 'node:events';
export type ListenerClassBase = Record<string, string>;
export interface ListenerIBase {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: any): ListenerClassBase;
[key: string]: any;
}
export class NTEventChannel extends EventEmitter {
private wrapperApi: WrapperNodeApi;
private wrapperSession: NodeIQQNTWrapperSession;
private listenerRefStorage = new Map<string, ListenerIBase>();
constructor(WrapperApi: WrapperNodeApi, WrapperSession: NodeIQQNTWrapperSession) {
super();
this.on('error', () => {
});
this.wrapperApi = WrapperApi;
this.wrapperSession = WrapperSession;
}
dispatcherListener(ListenerEvent: string, ...args: any[]) {
this.emit(ListenerEvent, ...args);
}
createProxyDispatch(ListenerMainName: string) {
const dispatcherListener = this.dispatcherListener.bind(this);
return new Proxy({}, {
get(_target: any, prop: any, _receiver: any) {
return (...args: any[]) => {
dispatcherListener(ListenerMainName + '/' + prop, ...args);
};
},
});
}
async getOrInitListener<T>(listenerMainName: string): Promise<T> {
const ListenerType = this.wrapperApi[listenerMainName];
//获取NTQQ 外部 Listener包装
if (!ListenerType) throw new Error('Init Listener not found');
let Listener = this.listenerRefStorage.get(listenerMainName);
//判断是否已创建 创建则跳过
if (!Listener && ListenerType) {
Listener = new ListenerType(this.createProxyDispatch(listenerMainName));
if (!Listener) throw new Error('Init Listener failed');
//实例化NTQQ Listener外包装
const ServiceSubName = /^NodeIKernel(.*?)Listener$/.exec(listenerMainName)![1];
const Service = 'NodeIKernel' + ServiceSubName + 'Service/addKernel' + ServiceSubName + 'Listener';
const addfunc = this.createEventFunction<(listener: T) => number>(Service);
//添加Listener到NTQQ
addfunc!(Listener as T);
this.listenerRefStorage.set(listenerMainName, Listener);
//保存Listener实例
}
return Listener as T;
}
async createEventWithListener<EventType extends (...args: any) => any, ListenerType extends (...args: any) => any>
(
eventName: string,
listenerName: string,
waitTimes = 1,
timeout: number = 3000,
checker: (...args: Parameters<ListenerType>) => boolean,
...eventArg: Parameters<EventType>
) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const ListenerNameList = listenerName.split('/');
const ListenerMainName = ListenerNameList[0];
//const ListenerSubName = ListenerNameList[1];
this.getOrInitListener<ListenerType>(ListenerMainName);
let complete = 0;
const retData: Parameters<ListenerType> | undefined = undefined;
let retEvent: any = {};
const databack = () => {
if (complete == 0) {
reject(new Error('Timeout: NTEvent EventName:' + eventName + ' ListenerName:' + listenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'));
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
}
};
const Timeouter = setTimeout(databack, timeout);
const callback = (...args: Parameters<ListenerType>) => {
if (checker(...args)) {
complete++;
if (complete >= waitTimes) {
clearTimeout(Timeouter);
this.removeListener(listenerName, callback);
databack();
}
}
};
this.on(listenerName, callback);
const EventFunc = this.createEventFunction<EventType>(eventName);
retEvent = await EventFunc!(...(eventArg as any[]));
});
}
private createEventFunction<T extends (...args: any) => any>(eventName: string): T | undefined {
const eventNameArr = eventName.split('/');
type eventType = {
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> }
}
if (eventNameArr.length > 1) {
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '');
const eventName = eventNameArr[1];
//getNodeIKernelGroupListener,GroupService
//console.log('2', eventName);
const services = (this.wrapperSession as unknown as eventType)[serviceName]();
const event = services[eventName]
//重新绑定this
.bind(services);
if (event) {
return event as T;
}
return undefined;
}
}
async callEvent<EventType extends (...args: any[]) => Promise<any> | any>(
EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>((resolve) => {
const EventFunc = this.createEventFunction<EventType>(EventName);
EventFunc!(...args).then((retData: Awaited<ReturnType<EventType>> | PromiseLike<Awaited<ReturnType<EventType>>>) => resolve(retData));
});
}
}
//NTEvent2.0

280
src/common/helper.ts Normal file
View File

@@ -0,0 +1,280 @@
import path from 'node:path';
import fs from 'fs';
import os from 'node:os';
import { Peer, QQLevel } from '@/core';
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
try {
const result = func(...args);
resolve(result);
} catch (e) {
resolve(undefined);
}
});
}
export async function solveAsyncProblem<T extends (...args: any[]) => Promise<any>>(func: T, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>> | undefined> {
return new Promise<Awaited<ReturnType<T>> | undefined>((resolve) => {
func(...args).then((result) => {
resolve(result);
}).catch(() => {
resolve(undefined);
});
});
}
export class FileNapCatOneBotUUID {
static encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = ""): string {
const data = `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}|${fileUUID}`;
//前四个字节塞data长度
const length = Buffer.alloc(4 + data.length);
length.writeUInt32BE(data.length * 2, 0);//储存data的hex长度
length.write(data, 4);
return length.toString('hex') + endString;
}
static decodeModelId(uuid: string): undefined | {
peer: Peer,
modelId: string,
fileId: string,
fileUUID?: string
} {
//前四个字节是data长度
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
//根据length计算需要读取的长度
const dataId = uuid.slice(8, 8 + length);
//hex还原为string
const realData = Buffer.from(dataId, 'hex').toString();
if (!realData.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
const data = realData.split('|');
if (data.length < 6) return undefined; // compatibility requirement
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
return {
peer: {
chatType: +chatType,
peerUid: peerUid,
},
modelId,
fileId,
fileUUID
};
}
static encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", endString: string = ""): string {
const data = `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}|${fileUUID}`;
//前四个字节塞data长度
//一个字节8位 一个ascii字符1字节 一个hex字符4位 表示一个ascii字符需要两个hex字符
const length = Buffer.alloc(4 + data.length);
length.writeUInt32BE(data.length * 2, 0);
length.write(data, 4);
return length.toString('hex') + endString;
}
static decode(uuid: string): undefined | {
peer: Peer,
msgId: string,
elementId: string,
fileUUID?: string
} {
//前四个字节是data长度
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
//根据length计算需要读取的长度
const dataId = uuid.slice(8, 8 + length);
//hex还原为string
const realData = Buffer.from(dataId, 'hex').toString();
if (!realData.startsWith('NapCatOneBot|MsgFile|')) return undefined;
const data = realData.split('|');
if (data.length < 6) return undefined; // compatibility requirement
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
return {
peer: {
chatType: +chatType,
peerUid: peerUid,
},
msgId,
elementId,
fileUUID
};
}
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function PromiseTimer<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeoutPromise = new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('PromiseTimer: Operation timed out')), ms),
);
return Promise.race([promise, timeoutPromise]);
}
export async function runAllWithTimeout<T>(tasks: Promise<T>[], timeout: number): Promise<T[]> {
const wrappedTasks = tasks.map((task) =>
PromiseTimer(task, timeout).then(
(result) => ({ status: 'fulfilled', value: result }),
(error) => ({ status: 'rejected', reason: error }),
),
);
const results = await Promise.all(wrappedTasks);
return results
.filter((result) => result.status === 'fulfilled')
.map((result) => (result as { status: 'fulfilled'; value: T }).value);
}
export function isNull(value: any) {
return value === undefined || value === null;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isEqual(obj1: any, obj2: any) {
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!isEqual(obj1[key], obj2[key])) return false;
}
return true;
}
export function getDefaultQQVersionConfigInfo(): QQVersionConfigType {
if (os.platform() === 'linux') {
return {
baseVersion: '3.2.12.28060',
curVersion: '3.2.12.28060',
prevVersion: '',
onErrorVersions: [],
buildId: '27254',
};
}
if (os.platform() === 'darwin') {
return {
baseVersion: '6.9.53.28060',
curVersion: '6.9.53.28060',
prevVersion: '',
onErrorVersions: [],
buildId: '28060',
};
}
return {
baseVersion: '9.9.15-28131',
curVersion: '9.9.15-28131',
prevVersion: '',
onErrorVersions: [],
buildId: '28131',
};
}
export function getQQPackageInfoPath(exePath: string = '', version?: string): string {
let packagePath;
if (os.platform() === 'darwin') {
packagePath = path.join(path.dirname(exePath), '..', 'Resources', 'app', 'package.json');
} else if (os.platform() === 'linux') {
packagePath = path.join(path.dirname(exePath), './resources/app/package.json');
} else {
packagePath = path.join(path.dirname(exePath), './versions/' + version + '/resources/app/package.json');
}
//下面是老版本兼容 未来去掉
if (!fs.existsSync(packagePath)) {
packagePath = path.join(path.dirname(exePath), './resources/app/versions/' + version + '/package.json');
}
return packagePath;
}
export function getQQVersionConfigPath(exePath: string = ''): string | undefined {
let configVersionInfoPath;
if (os.platform() === 'win32') {
configVersionInfoPath = path.join(path.dirname(exePath), 'versions', 'config.json');
} else if (os.platform() === 'darwin') {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
} else {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './.config/QQ');
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
}
if (typeof configVersionInfoPath !== 'string') {
return undefined;
}
//老版本兼容 未来去掉
if (!fs.existsSync(configVersionInfoPath)) {
configVersionInfoPath = path.join(path.dirname(exePath), './resources/app/versions/config.json');
}
if (!fs.existsSync(configVersionInfoPath)) {
return undefined;
}
return configVersionInfoPath;
}
export function calcQQLevel(level?: QQLevel) {
if (!level) return 0;
const { crownNum, sunNum, moonNum, starNum } = level;
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
}
export function stringifyWithBigInt(obj: any) {
return JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
}
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
const hexSequence = "A4 09 00 00 00 35";
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ""), "hex");
const filePath = path.resolve(nodeMajor);
const fileContent = fs.readFileSync(filePath);
let searchPosition = 0;
while (true) {
const index = fileContent.indexOf(sequenceBytes, searchPosition);
if (index === -1) {
break;
}
const start = index + sequenceBytes.length - 1;
const end = fileContent.indexOf(0x00, start);
if (end === -1) {
break;
}
const content = fileContent.subarray(start, end);
if (!content.every(byte => byte === 0x00)) {
try {
return content.toString("utf-8");
} catch (error) {
break;
}
}
searchPosition = end + 1;
}
return undefined;
}

View File

@@ -1,5 +1,5 @@
import log4js, { Configuration } from 'log4js';
import { truncateString } from '@/common/utils/helper';
import { truncateString } from '@/common/helper';
import path from 'node:path';
import chalk from 'chalk';
import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
@@ -139,9 +139,13 @@ export class LogWrapper {
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
const isSelfSent = msg.senderUin === selfInfo.uin;
this.log(`${
isSelfSent ? '发送 ->' : '接收 <-'
} ${rawMessageToText(msg)}`);
// Intercept grey tip
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
return;
}
this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`);
}
}
@@ -155,7 +159,12 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
if (msg.chatType == ChatType.KCHATTYPEC2C) {
tokens.push(`私聊 (${msg.peerUin})`);
} else if (msg.chatType == ChatType.KCHATTYPEGROUP) {
tokens.push(`群聊 (群 ${msg.peerUin}${msg.senderUin})`);
if (recursiveLevel < 1) {
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
}
if (msg.senderUin !== '0') {
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
}
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备');
} else /* temp */ {
@@ -167,7 +176,8 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
function msgElementToText(element: MessageElement) {
if (element.textElement) {
if (element.textElement.atType === AtType.notAt) {
return element.textElement.content;
const originalContentLines = element.textElement.content.split('\n');
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
} else if (element.textElement.atType === AtType.atAll) {
return `@全体成员`;
} else if (element.textElement.atType === AtType.atUser) {
@@ -179,17 +189,16 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
const recordMsgOrNull = msg.records.find(
record => element.replyElement!.sourceMsgIdInRecords === record.msgId,
);
return `[回复消息 ${
recordMsgOrNull &&
recordMsgOrNull.peerUin != '284840486' // 非转发消息; 否则定位不到
?
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`
return `[回复消息 ${recordMsgOrNull &&
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到
?
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`
}]`;
}
if (element.picElement) {
return `[图片 ${element.picElement.fileName}]`;
return '[图片]';
}
if (element.fileElement) {
@@ -197,7 +206,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
}
if (element.videoElement) {
return `[视频 ${element.videoElement.fileName}]`;
return '[视频]';
}
if (element.pttElement) {
@@ -205,7 +214,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
}
if (element.arkElement) {
return `[卡片消息 ${element.arkElement.bytesData}]`;
return '[卡片消息]';
}
if (element.faceElement) {
@@ -213,19 +222,19 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
}
if (element.marketFaceElement) {
return `[商城表情 ${element.marketFaceElement.faceName}]`;
return element.marketFaceElement.faceName;
}
if (element.markdownElement) {
return `[Markdown ${element.markdownElement.content}]`;
return '[Markdown 消息]';
}
if (element.multiForwardMsgElement) {
return `[转发消息]`;
return '[转发消息]';
}
if (element.elementType === ElementType.GreyTip) {
return `[灰条消息]`; // TODO: resolve the text
return '[灰条消息]';
}
return `[未实现 (ElementType = ${element.elementType})]`;

View File

@@ -1,6 +1,6 @@
export class LRUCache<K, V> {
private capacity: number;
private cache: Map<K, V>;
public cache: Map<K, V>;
constructor(capacity: number) {
this.capacity = capacity;
@@ -24,7 +24,9 @@ export class LRUCache<K, V> {
} else if (this.cache.size >= this.capacity) {
// If the cache is full, remove the least recently used key (the first one in the map)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
}

View File

@@ -23,8 +23,10 @@ export class LimitedHashTable<K, V> {
}
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value;
this.valueToKey.delete(this.keyToValue.get(oldestKey)!);
this.keyToValue.delete(oldestKey);
if (oldestKey !== undefined) {
this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
this.keyToValue.delete(oldestKey);
}
}
}
@@ -91,7 +93,7 @@ class MessageUniqueWrapper {
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined);
}
createMsg(peer: Peer, msgId: string) {
createUniqueMsgId(peer: Peer, msgId: string) {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('md5').update(key).digest();
//设置第一个bit为0 保证shortId为正数

View File

@@ -1,8 +1,7 @@
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
export const napcat_version = '2.2.13';
import os from 'os';
export class NapCatPathWrapper {
binaryPath: string;
@@ -13,17 +12,23 @@ export class NapCatPathWrapper {
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
this.binaryPath = mainPath;
this.logsPath = path.join(this.binaryPath, 'logs');
this.configPath = path.join(this.binaryPath, 'config');
this.cachePath = path.join(this.binaryPath, 'cache');
let writePath: string;
if (os.platform() === 'darwin') {
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
} else {
writePath = this.binaryPath;
}
this.logsPath = path.join(writePath, 'logs');
this.configPath = path.join(writePath, 'config');
this.cachePath = path.join(writePath, 'cache');
this.staticPath = path.join(this.binaryPath, 'static');
if (fs.existsSync(this.logsPath)) {
if (!fs.existsSync(this.logsPath)) {
fs.mkdirSync(this.logsPath, { recursive: true });
}
if (fs.existsSync(this.configPath)) {
if (!fs.existsSync(this.configPath)) {
fs.mkdirSync(this.configPath, { recursive: true });
}
if (fs.existsSync(this.cachePath)) {
if (!fs.existsSync(this.cachePath)) {
fs.mkdirSync(this.cachePath, { recursive: true });
}
}

View File

@@ -1,9 +1,9 @@
import path from 'node:path';
import fs from 'node:fs';
import { systemPlatform } from '@/common/utils/system';
import { getDefaultQQVersionConfigInfo, getQQVersionConfigPath } from './helper';
import { systemPlatform } from '@/common/system';
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
import AppidTable from '@/core/external/appid.json';
import { LogWrapper } from './log';
import { getMajorPath } from '@/core';
export class QQBasicInfoWrapper {
QQMainPath: string | undefined;
@@ -20,14 +20,16 @@ export class QQBasicInfoWrapper {
//基础目录获取
this.context = context;
this.QQMainPath = process.execPath;
this.QQPackageInfoPath = path.join(path.dirname(this.QQMainPath), 'resources', 'app', 'package.json');
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
//基础信息获取 无快更则启用默认模板填充
this.isQuickUpdate = !!this.QQVersionConfigPath;
this.QQVersionConfig = this.isQuickUpdate
? JSON.parse(fs.readFileSync(this.QQVersionConfigPath!).toString())
: getDefaultQQVersionConfigInfo();
this.QQPackageInfoPath = getQQPackageInfoPath(this.QQMainPath, this.QQVersionConfig?.curVersion);
this.QQPackageInfo = JSON.parse(fs.readFileSync(this.QQPackageInfoPath).toString());
const { appid: IQQVersionAppid, qua: IQQVersionQua } = this.getAppidV2();
this.QQVersionAppid = IQQVersionAppid;
@@ -52,28 +54,53 @@ export class QQBasicInfoWrapper {
}
//此方法不要直接使用
getQUAInternal() {
return systemPlatform === 'linux'
? `V1_LNX_NQ_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`
: `V1_WIN_NQ_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`;
getQUAFallback() {
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
win32: `V1_WIN_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
darwin: `V1_MAC_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
linux: `V1_LNX_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
};
return platformMapping[systemPlatform] ?? (platformMapping.win32)!;
}
getAppIdFallback() {
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
win32: '537246092',
darwin: '537246140',
linux: '537246140',
};
return platformMapping[systemPlatform] ?? '537246092';
}
getAppidV2(): { appid: string; qua: string } {
// 通过已有表 性能好
const appidTbale = AppidTable as unknown as QQAppidTableType;
try {
const fullVersion = this.getFullQQVesion();
if (!fullVersion) throw new Error('QQ版本获取失败');
const fullVersion = this.getFullQQVesion();
if (fullVersion) {
const data = appidTbale[fullVersion];
if (data) {
return data;
}
} catch (e) {
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
}
// 以下是兜底措施
this.context.logger.log(
`[QQ版本兼容性检测] ${this.getFullQQVesion()} 版本兼容性不佳,可能会导致一些功能无法正常使用`,
);
return { appid: systemPlatform === 'linux' ? '537240795' : '537240709', qua: this.getQUAInternal() };
// 通过Major拉取 性能差
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版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
}
getAppidV2ByMajor(QQVersion: string) {
const majorPath = getMajorPath(QQVersion);
const appid = parseAppidFromMajor(majorPath);
return appid;
}
}

View File

@@ -61,7 +61,7 @@ export class RequestUtil {
const options = {
hostname: option.hostname,
port: option.port,
path: option.href,
path: option.pathname + option.search,
method: method,
headers: headers,
};

21
src/common/system.ts Normal file
View File

@@ -0,0 +1,21 @@
import os from 'node:os';
import path from 'node:path';
// 缓解Win7设备兼容性问题
let osName: string;
try {
osName = os.hostname();
} catch (e) {
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
}
const homeDir = os.homedir();
export const systemPlatform = os.platform();
export const cpuArch = os.arch();
export const systemVersion = os.release();
export const hostname = osName;
export const downloadsPath = path.join(homeDir, 'Downloads');
export const systemName = os.type();

View File

@@ -1,90 +0,0 @@
import fs from 'fs';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process';
import { LogWrapper } from './log';
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
async function guessDuration(pttPath: string) {
const pttFileInfo = await fsPromise.stat(pttPath);
let duration = pttFileInfo.size / 1024 / 3; // 3kb/s
duration = Math.floor(duration);
duration = Math.max(1, duration);
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
logger.log(`语音文件${filePath}需要转换成silk`);
const _isWav = isWav(file);
const pcmPath = pttPath + '.pcm';
let sampleRate = 0;
const convert = () => {
return new Promise<Buffer>((resolve, reject) => {
// todo: 通过配置文件获取ffmpeg路径
const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
cp.on('error', err => {
logger.log('FFmpeg处理转换出错: ', err.message);
return reject(err);
});
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255];
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000;
const data = fs.readFileSync(pcmPath);
fs.unlink(pcmPath, (err) => {
});
return resolve(data);
}
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
reject(Error('FFmpeg处理转换失败'));
});
});
};
let input: Buffer;
if (!_isWav) {
input = await convert();
} else {
input = file;
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const { fmt } = getWavFileInfo(input);
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
input = await convert();
}
}
const silk = await encode(input, sampleRate);
fs.writeFileSync(pttPath, silk.data);
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return {
converted: true,
path: pttPath,
duration: silk.duration / 1000,
};
} else {
const silk = file;
let duration = 0;
try {
duration = getDuration(silk) / 1000;
} catch (e: any) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack);
duration = await guessDuration(filePath);
}
return {
converted: false,
path: filePath,
duration,
};
}
} catch (error: any) {
logger.logError('convert silk failed', error.stack);
return {};
}
}

View File

@@ -1,154 +0,0 @@
import path from 'node:path';
import fs from 'fs';
import os from 'node:os';
import { QQLevel } from '@/core';
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
try {
const result = func(...args);
resolve(result);
} catch (e) {
resolve(undefined);
}
});
}
export async function solveAsyncProblem<T extends (...args: any[]) => Promise<any>>(func: T, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>> | undefined> {
return new Promise<Awaited<ReturnType<T>> | undefined>((resolve) => {
func(...args).then((result) => {
resolve(result);
}).catch(() => {
resolve(undefined);
});
});
}
//下面这个类是用于将uid+msgid合并的类
export class UUIDConverter {
static encode(highStr: string, lowStr: string): string {
const high = BigInt(highStr);
const low = BigInt(lowStr);
const highHex = high.toString(16).padStart(16, '0');
const lowHex = low.toString(16).padStart(16, '0');
const combinedHex = highHex + lowHex;
return `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(
12,
16,
)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`;
}
static decode(uuid: string): { high: string; low: string } {
const hex = uuid.replace(/-/g, '');
const high = BigInt('0x' + hex.substring(0, 16));
const low = BigInt('0x' + hex.substring(16));
return { high: high.toString(), low: low.toString() };
}
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function PromiseTimer<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeoutPromise = new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('PromiseTimer: Operation timed out')), ms),
);
return Promise.race([promise, timeoutPromise]);
}
export async function runAllWithTimeout<T>(tasks: Promise<T>[], timeout: number): Promise<T[]> {
const wrappedTasks = tasks.map((task) =>
PromiseTimer(task, timeout).then(
(result) => ({ status: 'fulfilled', value: result }),
(error) => ({ status: 'rejected', reason: error }),
),
);
const results = await Promise.all(wrappedTasks);
return results
.filter((result) => result.status === 'fulfilled')
.map((result) => (result as { status: 'fulfilled'; value: T }).value);
}
export function isNull(value: any) {
return value === undefined || value === null;
}
export function isNumeric(str: string) {
return /^\d+$/.test(str);
}
export function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function isEqual(obj1: any, obj2: any) {
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2;
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (const key of keys1) {
if (!isEqual(obj1[key], obj2[key])) return false;
}
return true;
}
export function getDefaultQQVersionConfigInfo(): QQVersionConfigType {
if (os.platform() === 'linux') {
return {
baseVersion: '3.2.12-27254',
curVersion: '3.2.12-27254',
prevVersion: '',
onErrorVersions: [],
buildId: '27254',
};
}
return {
baseVersion: '9.9.15-27391',
curVersion: '9.9.15-27391',
prevVersion: '',
onErrorVersions: [],
buildId: '27391',
};
}
export function getQQVersionConfigPath(exePath: string = ''): string | undefined {
let configVersionInfoPath;
if (os.platform() !== 'linux') {
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json');
} else {
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './.config/QQ');
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
}
if (typeof configVersionInfoPath !== 'string') {
return undefined;
}
if (!fs.existsSync(configVersionInfoPath)) {
return undefined;
}
return configVersionInfoPath;
}
export function calcQQLevel(level: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level;
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
}

View File

@@ -1,73 +0,0 @@
import os from 'node:os';
import path from 'node:path';
import { networkInterfaces } from 'os';
import { randomUUID } from 'crypto';
// 缓解Win7设备兼容性问题
let osName: string;
// 设备ID
let machineId: Promise<string>;
try {
osName = os.hostname();
} catch (e) {
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
}
const invalidMacAddresses = new Set([
'00:00:00:00:00:00',
'ff:ff:ff:ff:ff:ff',
'ac:de:48:00:11:22',
]);
function validateMacAddress(candidate: string): boolean {
// eslint-disable-next-line no-useless-escape
const tempCandidate = candidate.replace(/\-/g, ':').toLowerCase();
return !invalidMacAddresses.has(tempCandidate);
}
export async function getMachineId(): Promise<string> {
if (!machineId) {
machineId = (async () => {
const id = await getMacMachineId();
return id ?? randomUUID(); // fallback, generate a UUID
})();
}
return machineId;
}
export function getMac(): string {
const ifaces = networkInterfaces();
for (const name in ifaces) {
const networkInterface = ifaces[name];
if (networkInterface) {
for (const { mac } of networkInterface) {
if (validateMacAddress(mac)) {
return mac;
}
}
}
}
throw new Error('Unable to retrieve mac address (unexpected format)');
}
async function getMacMachineId(): Promise<string | undefined> {
try {
const crypto = await import('crypto');
const macAddress = getMac();
return crypto.createHash('sha256').update(macAddress, 'utf8').digest('hex');
} catch (err) {
return undefined;
}
}
const homeDir = os.homedir();
export const systemPlatform = os.platform();
export const cpuArch = os.arch();
export const systemVersion = os.release();
export const hostname = osName;
export const downloadsPath = path.join(homeDir, 'Downloads');
export const systemName = os.type();

1
src/common/version.ts Normal file
View File

@@ -0,0 +1 @@
export const napCatVersion = '3.6.2';

View File

@@ -16,7 +16,8 @@ export async function getVideoInfo(filePath: string, logger: LogWrapper) {
filePath: string
}>((resolve, reject) => {
const ffmpegPath = process.env.FFMPEG_PATH;
ffmpegPath && ffmpeg.setFfmpegPath(ffmpegPath);
if (ffmpegPath)
ffmpeg.setFfmpegPath(ffmpegPath);
ffmpeg(filePath).ffprobe((err: any, metadata: ffmpeg.FfprobeData) => {
if (err) {
reject(err);

View File

@@ -1,9 +1,8 @@
import { MsfChangeReasonType, MsfStatusType } from "../entities/adapter";
export class NodeIDependsAdapter {
onMSFStatusChange(arg1: number, arg2: number) {
// console.log(arg1, arg2);
// if (arg1 == 2 && arg2 == 2) {
// log("NapCat丢失网络连接,请检查网络")
// }
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {
}
onMSFSsoError(args: unknown) {

View File

@@ -10,7 +10,7 @@ export class NTQQCollectionApi {
}
async createCollection(authorUin: string, authorUid: string, authorName: string, brief: string, rawData: string) {
const param = {
return this.context.session.getCollectionService().createNewCollectionItem({
commInfo: {
bid: 1,
category: 2,
@@ -43,12 +43,11 @@ export class NTQQCollectionApi {
fileList: [],
},
need_share_url: false,
};
return this.context.session.getCollectionService().createNewCollectionItem(param);
});
}
async getAllCollection(category: number = 0, count: number = 50) {
const param = {
return this.context.session.getCollectionService().getCollectionItemList({
category: category,
groupId: -1,
forceSync: true,
@@ -56,7 +55,6 @@ export class NTQQCollectionApi {
timeStamp: '0',
count: count,
searchDown: true,
};
return this.context.session.getCollectionService().getCollectionItemList(param);
});
}
}

View File

@@ -1,7 +1,4 @@
import {
CacheFileListItem,
CacheFileType,
ChatCacheListItemBasic,
ChatType,
ElementType,
IMAGE_HTTP_HOST,
@@ -9,6 +6,7 @@ import {
Peer,
PicElement,
PicType,
RawMessage,
SendFileElement,
SendPicElement,
SendPttElement,
@@ -17,33 +15,28 @@ import {
import path from 'path';
import fs from 'fs';
import fsPromises from 'fs/promises';
import { InstanceContext, NapCatCore } from '@/core';
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
import * as fileType from 'file-type';
import imageSize from 'image-size';
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { NodeIKernelSearchService } from '../services/NodeIKernelSearchService';
import { RkeyManager } from '../helper/rkey';
import { calculateFileMD5, isGIF } from '@/common/utils/file';
import { calculateFileMD5, isGIF } from '@/common/file';
import pathLib from 'node:path';
import { defaultVideoThumbB64, getVideoInfo } from '@/common/utils/video';
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
import ffmpeg from 'fluent-ffmpeg';
import fsnormal from 'node:fs';
import { encodeSilk } from '@/common/utils/audio';
import { encodeSilk } from '@/common/audio';
import { MessageContext } from '@/onebot/api';
export class NTQQFileApi {
context: InstanceContext;
core: NapCatCore;
rkeyManager: RkeyManager;
packetRkey: Array<{ rkey: string; time: number; type: number; ttl: bigint }> | undefined;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey', this.context.logger);
}
async getFileType(filePath: string) {
return fileType.fileTypeFromFile(filePath);
this.rkeyManager = new RkeyManager(['https://llob.linyuchen.net/rkey', 'http://napcat-sign.wumiao.wang:2082/rkey'], this.context.logger);
}
async copyFile(filePath: string, destPath: string) {
@@ -61,18 +54,15 @@ export class NTQQFileApi {
})).urlResult.domainUrl;
}
// 上传文件到QQ的文件夹
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
// napCatCore.wrapper.util.
const fileMd5 = await calculateFileMD5(filePath);
let ext: string = (await this.getFileType(filePath))?.ext as string || '';
if (ext) {
ext = '.' + ext;
}
const extOrEmpty = (await fileType.fileTypeFromFile(filePath))?.ext;
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf('.') === -1) {
fileName += ext;
}
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
md5HexStr: fileMd5,
fileName: fileName,
@@ -83,7 +73,8 @@ export class NTQQFileApi {
downloadType: 1,
file_uuid: '',
});
await this.copyFile(filePath, mediaPath!);
await this.copyFile(filePath, mediaPath);
const fileSize = await this.getFileSize(filePath);
return {
md5: fileMd5,
@@ -94,11 +85,7 @@ export class NTQQFileApi {
};
}
async createValidSendFileElement(
filePath: string,
fileName: string = '',
folderId: string = '',
): Promise<SendFileElement> {
async createValidSendFileElement(context: MessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
const {
fileName: _fileName,
path,
@@ -107,6 +94,7 @@ export class NTQQFileApi {
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
context.deleteAfterSentFiles.push(path);
return {
elementType: ElementType.FILE,
elementId: '',
@@ -119,73 +107,70 @@ export class NTQQFileApi {
};
}
async createValidSendPicElement(
picPath: string,
summary: string = '',
subType: 0 | 1 = 0,
): Promise<SendPicElement> {
const {
md5,
fileName,
path,
fileSize,
} = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
async createValidSendPicElement(context: MessageContext, picPath: string, summary: string = '', subType: 0 | 1 = 0,): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
const imageSize = await this.core.apis.FileApi.getImageSize(picPath);
const picElement: any = {
md5HexStr: md5,
fileSize: fileSize.toString(),
picWidth: imageSize?.width,
picHeight: imageSize?.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary,
};
context.deleteAfterSentFiles.push(path);
return {
elementType: ElementType.PIC,
elementId: '',
picElement,
picElement: {
md5HexStr: md5,
fileSize: fileSize.toString(),
picWidth: imageSize.width,
picHeight: imageSize.height,
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picSubType: subType,
fileUuid: '',
fileSubId: '',
thumbFileSize: 0,
summary,
} as PicElement,
};
}
async createValidSendVideoElement(
filePath: string,
fileName: string = '',
diyThumbPath: string = '',
): Promise<SendVideoElement> {
async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
const logger = this.core.context.logger;
const {
fileName: _fileName,
path,
fileSize,
md5,
} = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
thumb = pathLib.dirname(thumb);
let videoInfo = {
width: 1920, height: 1080,
time: 15,
format: 'mp4',
size: fileSize,
size: 0,
filePath,
};
try {
videoInfo = await getVideoInfo(path, logger);
videoInfo = await getVideoInfo(filePath, logger);
} catch (e) {
logger.logError('获取视频信息失败', e);
logger.logError.bind(logger)('获取视频信息失败,将使用默认值', e);
}
const createThumb = new Promise<string | undefined>((resolve, reject) => {
let fileExt = 'mp4';
try {
const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
if (tempExt) fileExt = tempExt;
} catch (e) {
this.context.logger.logError.bind(logger)('获取文件类型失败', e);
}
const newFilePath = filePath + '.' + fileExt;
fs.copyFileSync(filePath, newFilePath);
context.deleteAfterSentFiles.push(newFilePath);
filePath = newFilePath;
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
videoInfo.size = fileSize;
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
thumb = pathLib.dirname(thumb);
const thumbPath = new Map();
const _thumbPath = await new Promise<string | undefined>((resolve, reject) => {
const thumbFileName = `${md5}_0.png`;
const thumbPath = pathLib.join(thumb, thumbFileName);
ffmpeg(filePath)
@@ -196,7 +181,7 @@ export class NTQQFileApi {
resolve(thumbPath);
}).catch(reject);
} else {
fsnormal.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
resolve(thumbPath);
}
})
@@ -205,36 +190,21 @@ export class NTQQFileApi {
filename: thumbFileName,
folder: thumb,
size: videoInfo.width + 'x' + videoInfo.height,
}).on('end', () => {
})
.on('end', () => {
resolve(thumbPath);
});
});
const thumbPath = new Map();
const _thumbPath = await createThumb;
const thumbSize = _thumbPath ? (await fsPromises.stat(_thumbPath)).size : 0;
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath);
const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : '';
// "fileElement": {
// "fileMd5": "",
// "fileName": "1.mp4",
// "filePath": "C:\\Users\\nanae\\OneDrive\\Desktop\\1.mp4",
// "fileSize": "1847007",
// "picHeight": 1280,
// "picWidth": 720,
// "picThumbPath": {},
// "file10MMd5": "",
// "fileSha": "",
// "fileSha3": "",
// "fileUuid": "",
// "fileSubId": "",
// "thumbFileSize": 750
// }
context.deleteAfterSentFiles.push(path);
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith('.' + fileExt.toLocaleLowerCase()) ? (fileName || _fileName) : (fileName || _fileName) + '.' + fileExt;
return {
elementType: ElementType.VIDEO,
elementId: '',
videoElement: {
fileName: fileName || _fileName,
fileName: uploadName,
filePath: path,
videoMd5: md5,
thumbMd5,
@@ -244,28 +214,12 @@ export class NTQQFileApi {
thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height,
fileSize: '' + fileSize,
// fileFormat: videotype
// fileUuid: "",
// transferStatus: 0,
// progress: 0,
// invalidState: 0,
// fileSubId: "",
// fileBizId: null,
// originVideoMd5: "",
// fileFormat: 2,
// import_rich_media_context: null,
// sourceVideoCodecFormat: 2
},
};
}
async createValidSendPttElement(pttPath: string): Promise<SendPttElement> {
const {
converted,
path: silkPath,
duration,
} = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
// 生成语音 Path: silkPath Time: duration
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
if (!silkPath) {
throw new Error('语音转换失败, 请检查语音文件是否正常');
}
@@ -274,7 +228,9 @@ export class NTQQFileApi {
throw new Error('文件异常大小为0');
}
if (converted) {
fsPromises.unlink(silkPath);
fsPromises.unlink(silkPath).then().catch(
(e) => this.context.logger.logError.bind(this.context.logger)('删除临时文件失败', e)
);
}
return {
elementType: ElementType.PTT,
@@ -283,8 +239,7 @@ export class NTQQFileApi {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
// duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算
fileSize: fileSize.toString(),
duration: duration ?? 1,
formatType: 1,
voiceType: 1,
@@ -300,12 +255,67 @@ export class NTQQFileApi {
};
}
async downloadMediaByUuid() {
//napCatCore.session.getRichMediaService().downloadFileForFileUuid();
async downloadFileForModelId(peer: Peer, modelId: string, unknown: string, timeout = 1000 * 60 * 2) {
const [, fileTransNotifyInfo] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelRichMediaService/downloadFileForModelId',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
[peer, [modelId], unknown],
() => true,
(arg) => arg?.commonFileInfo?.fileModelId === modelId,
1,
timeout,
);
return fileTransNotifyInfo.filePath;
}
async downloadRawMsgMedia(msg: RawMessage[]) {
const res = await Promise.all(
msg.map(m =>
Promise.all(
m.elements
.filter(element =>
element.elementType === ElementType.PIC ||
element.elementType === ElementType.VIDEO ||
element.elementType === ElementType.PTT ||
element.elementType === ElementType.FILE
)
.map(element =>
this.downloadMedia(m.msgId, m.chatType, m.peerUid, element.elementId, '', '', 1000 * 60 * 2, true)
)
)
)
);
msg.forEach((m, msgIndex) => {
const elementResults = res[msgIndex];
let elementIndex = 0;
m.elements.forEach(element => {
if (
element.elementType === ElementType.PIC ||
element.elementType === ElementType.VIDEO ||
element.elementType === ElementType.PTT ||
element.elementType === ElementType.FILE
) {
switch (element.elementType) {
case ElementType.PIC:
element.picElement!.sourcePath = elementResults[elementIndex];
break;
case ElementType.VIDEO:
element.videoElement!.filePath = elementResults[elementIndex];
break;
case ElementType.PTT:
element.pttElement!.filePath = elementResults[elementIndex];
break;
case ElementType.FILE:
element.fileElement!.filePath = elementResults[elementIndex];
break;
}
elementIndex++;
}
});
});
}
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
//logDebug('receive downloadMedia task', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force);
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) {
if (force) {
@@ -318,7 +328,7 @@ export class NTQQFileApi {
return sourcePath;
}
}
const [, fileTransNotifyInfo] = await this.core.eventWrapper.callNormalEventV2(
const [, completeRetData] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
[{
@@ -334,200 +344,131 @@ export class NTQQFileApi {
filePath: thumbPath,
}],
() => true,
(arg) => arg.msgId === msgId,
(arg) => arg.msgElementId === elementId && arg.msgId === msgId,
1,
timeout,
);
const msg = await this.core.apis.MsgApi.getMsgsByMsgId({
guildId: '',
chatType: chatType,
peerUid: peerUid,
}, [msgId]);
if (msg.msgList.length === 0) {
return fileTransNotifyInfo.filePath;
}
//获取原始消息
const FileElements = msg?.msgList[0]?.elements?.find(e => e.elementId === elementId);
if (!FileElements) {
//失败则就乱来 Todo
return fileTransNotifyInfo.filePath;
}
//从原始消息获取文件路径
return FileElements?.fileElement?.filePath ??
FileElements?.pttElement?.filePath ??
FileElements?.videoElement?.filePath ??
FileElements?.picElement?.sourcePath;
return completeRetData.filePath;
}
async getImageSize(filePath: string): Promise<ISizeCalculationResult | undefined> {
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
return new Promise((resolve, reject) => {
imageSize(filePath, (err, dimensions) => {
if (err) {
reject(err);
} else {
resolve(dimensions);
if (!dimensions) {
reject(new Error('获取图片尺寸失败'));
} else {
resolve(dimensions);
}
}
});
});
}
async addFileCache(peer: Peer, msgId: string, msgSeq: string, senderUid: string, elemId: string, elemType: string, fileSize: string, fileName: string) {
let GroupData;
let BuddyData;
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
GroupData =
[{
groupCode: peer.peerUid,
isConf: false,
hasModifyConfGroupFace: true,
hasModifyConfGroupName: true,
groupName: 'NapCat.Cached',
remark: 'NapCat.Cached',
}];
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
BuddyData = [{
category_name: 'NapCat.Cached',
peerUid: peer.peerUid,
peerUin: peer.peerUid,
remark: 'NapCat.Cached',
}];
} else {
return undefined;
}
return this.context.session.getSearchService().addSearchHistory({
type: 4,
contactList: [],
id: -1,
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: peer.chatType,
buddyChatInfo: BuddyData || [],
discussChatInfo: [],
groupChatInfo: GroupData || [],
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: msgId,
msgSeq: msgSeq,
msgTime: Math.floor(Date.now() / 1000).toString(),
senderUid: senderUid,
senderNick: 'NapCat.Cached',
senderRemark: 'NapCat.Cached',
senderCard: 'NapCat.Cached',
elemId: elemId,
elemType: elemType,
fileSize: fileSize,
filePath: '',
fileName: fileName,
hits: [{
start: 12,
end: 14,
}],
},
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
const randomResultId = 100000 + Math.floor(Math.random() * 10000);
let searchId = 0;
const [, searchResult] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelFileAssistantService/searchFile',
'NodeIKernelFileAssistantListener/onFileSearch',
[
keys,
{ resultType: 2, pageLimit: 1 },
randomResultId,
],
});
(ret) => {
searchId = ret;
return true;
},
result => result.searchId === searchId && result.resultId === randomResultId,
);
return searchResult.resultItems[0];
}
async searchfile(keys: string[]) {
const Event = this.core.eventWrapper.createEventFunction('NodeIKernelSearchService/searchFileWithKeywords');
const id = await Event!(keys, 12);
const Listener = this.core.eventWrapper.registerListen(
'NodeIKernelSearchListener/onSearchFileKeywordsResult',
async downloadFileById(
fileId: string,
fileSize: number = 1024576,
estimatedTime: number = (fileSize * 1000 / 1024576) + 5000,
) {
const [, fileData] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelFileAssistantService/downloadFile',
'NodeIKernelFileAssistantListener/onFileStatusChanged',
[[fileId]],
ret => ret.result === 0,
status => status.fileStatus === 2 && status.fileProgress === '0',
1,
20000,
(params) => id !== '' && params.searchId == id,
estimatedTime, // estimate 1MB/s
);
const [ret] = (await Listener);
return ret;
return fileData.filePath!;
}
async getImageUrl(element: PicElement) {
if (!element) {
return '';
}
const url: string = element.originImageUrl!; // 没有域名
const url: string = element.originImageUrl ?? '';
const md5HexStr = element.md5HexStr;
const fileMd5 = element.md5HexStr;
// const fileUuid = element.fileUuid;
if (url) {
const UrlParse = new URL(IMAGE_HTTP_HOST + url);//临时解析拼接
const imageAppid = UrlParse.searchParams.get('appid');
const isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid);
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey');
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url;
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
const urlRkey = parsedUrl.searchParams.get('rkey');
const imageAppid = parsedUrl.searchParams.get('appid');
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
const imageFileId = parsedUrl.searchParams.get('fileid');
const rkeyData = {
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
online_rkey: false
};
try {
if (this.core.apis.PacketApi.available) {
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
if (rkey_expired_private || rkey_expired_group) {
this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket();
}
if (this.packetRkey && this.packetRkey.length > 0) {
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
rkeyData.online_rkey = true;
}
}
const rkeyData = await this.rkeyManager.getRkey();
UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`;
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url;
} catch (error: any) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
}
} else if (fileMd5 || md5HexStr) {
// 没有url需要自己拼接
if (!rkeyData.online_rkey) {
try {
const tempRkeyData = await this.rkeyManager.getRkey();
rkeyData.group_rkey = tempRkeyData.group_rkey;
rkeyData.private_rkey = tempRkeyData.private_rkey;
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
} catch (e) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
}
}
if (isNTV2 && urlRkey) {
return IMAGE_HTTP_HOST_NT + urlRkey;
} else if (isNTV2 && rkeyData.online_rkey) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `&rkey=${rkey}`;
} else if (isNTV2 && imageFileId) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
}
}
//到这里说明可能是旧客户端
if (fileMd5 || md5HexStr) {
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
}
this.context.logger.logDebug('图片url获取失败', element);
return '';
}
}
export class NTQQFileCacheApi {
context: InstanceContext;
core: NapCatCore;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
async setCacheSilentScan(isSilent: boolean = true) {
return '';
}
getCacheSessionPathList() {
return '';
}
clearCache(cacheKeys: Array<string> = ['tmp', 'hotUpdate']) {
// 参数未验证
return this.context.session.getStorageCleanService().clearCacheDataByKeys(cacheKeys);
}
addCacheScannedPaths(pathMap: object = {}) {
return this.context.session.getStorageCleanService().addCacheScanedPaths(pathMap);
}
scanCache() {
//return (await this.context.session.getStorageCleanService().scanCache()).size;
}
getHotUpdateCachePath() {
// 未实现
return '';
}
getDesktopTmpPath() {
// 未实现
return '';
}
getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) {
return this.context.session.getStorageCleanService().getChatCacheInfo(type, pageSize, 1, pageIndex);
}
getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) {
// const _lastRecord = lastRecord ? lastRecord : { fileType: fileType };
// 需要五个参数
// return napCatCore.session.getStorageCleanService().getFileCacheInfo();
}
async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) {
return this.context.session.getStorageCleanService().clearChatCacheInfo(chats, fileKeys);
}
}

View File

@@ -1,62 +1,60 @@
import { FriendV2 } from '@/core/entities';
import { BuddyListReqType, InstanceContext, NapCatCore, NodeIKernelProfileService } from '@/core';
import { LimitedHashTable } from '@/common/utils/message-unique';
import { BuddyListReqType, InstanceContext, NapCatCore } from '@/core';
import { LimitedHashTable } from '@/common/message-unique';
export class NTQQFriendApi {
context: InstanceContext;
core: NapCatCore;
// friends: Map<string, Friend> = new Map<string, FriendV2>();
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
// if (!this.context.basicInfoWrapper.requireMinNTQQBuild('26702')) {
// this.getFriends(true);
// }
}
async setBuddyRemark(uid: string, remark: string) {
return this.context.session.getBuddyService().setBuddyRemark({ uid, remark });
}
async getBuddyV2SimpleInfoMap(refresh = false) {
const buddyService = this.context.session.getBuddyService();
const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
return await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo',
'nodeStore',
uids,
);
}
async getBuddyV2(refresh = false): Promise<FriendV2[]> {
const uids: string[] = [];
const buddyService = this.context.session.getBuddyService();
const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids));
const data = await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo', 'nodeStore', uids,
);
return Array.from(data.values());
return Array.from((await this.getBuddyV2SimpleInfoMap(refresh)).values());
}
async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
const uids: string[] = [];
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000);
const buddyService = this.context.session.getBuddyService();
const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
uids.push(...buddyListV2.data.flatMap(item => item.buddyUids));
const data = await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo', 'nodeStore', uids,
);
data.forEach((value) => {
retMap.set(value.uin!, value.uid!);
});
//console.log('getBuddyIdMap', retMap.getValue);
const data = await this.getBuddyV2SimpleInfoMap(refresh);
data.forEach((value) => retMap.set(value.uin!, value.uid!));
return retMap;
}
async delBuudy(uid: string, tempBlock = false, tempBothDel = false) {
return this.context.session.getBuddyService().delBuddy({
friendUid: uid,
tempBlock: tempBlock,
tempBothDel: tempBothDel
});
}
async getBuddyV2ExWithCate(refresh = false) {
const uids: string[] = [];
const categoryMap: Map<string, any> = new Map();
const buddyService = this.context.session.getBuddyService();
const buddyListV2 = refresh ? (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data : (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
uids.push(
...buddyListV2.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categoryName: item.categroyName });
});
return item.buddyUids;
}));
const uids = buddyListV2.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categoryName: item.categroyName });
});
return item.buddyUids;
});
const data = await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo', 'nodeStore', uids,
'NodeIKernelProfileService/getCoreAndBaseInfo',
'nodeStore',
uids,
);
return buddyListV2.map(category => ({
categoryId: category.categoryId,

View File

@@ -1,5 +1,4 @@
import {
ChatType,
GeneralCallResult,
Group,
GroupMember,
@@ -9,9 +8,10 @@ import {
KickMemberV2Req,
MemberExtSourceType,
NapCatCore,
NodeIKernelGroupService,
} from '@/core';
import { isNumeric, runAllWithTimeout, sleep } from '@/common/utils/helper';
import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique';
import { NTEventWrapper } from '@/common/event';
export class NTQQGroupApi {
context: InstanceContext;
@@ -19,23 +19,47 @@ export class NTQQGroupApi {
groupCache: Map<string, Group> = new Map<string, Group>();
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
groups: Group[] = [];
essenceLRU = new LimitedHashTable<number, string>(1000);
session: any;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
sleep(1000).then(() => {
this.initCache().then().catch(context.logger.logError);
});
this.initCache().then().catch(context.logger.logError.bind(context.logger));
}
async initCache() {
this.groups = await this.getGroups();
for (const group of this.groups) {
this.groupCache.set(group.groupCode, group);
const data = await this.getGroupMembers(group.groupCode, 3000);
this.groupMemberCache.set(group.groupCode, data);
}
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
// process.pid 调试点
}
async getCoreAndBaseInfo(uids: string[]) {
return await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo',
'nodeStore',
uids,
);
}
async fetchGroupEssenceList(groupCode: string) {
const pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().fetchGroupEssenceList({
groupCode: groupCode,
pageStart: 0,
pageLimit: 300,
}, pskey);
}
async getGroupShutUpMemberList(groupCode: string) {
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) {
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
}
async setGroupAvatar(gc: string, filePath: string) {
@@ -50,9 +74,10 @@ export class NTQQGroupApi {
);
return groupList;
}
async getGroupExtFE0Info(GroupCode: string[], forced = true) {
async getGroupExtFE0Info(groupCode: string[], forced = true) {
return this.context.session.getGroupService().getGroupExt0xEF0Info(
GroupCode,
groupCode,
[],
{
bindGuildId: 1,
@@ -86,11 +111,12 @@ export class NTQQGroupApi {
showPlayTogetherSwitch: 1,
starId: 1,
todoSeq: 1,
viewedMsgDisappearTime: 1
viewedMsgDisappearTime: 1,
},
forced
forced,
);
}
async getGroup(groupCode: string, forced = false) {
let group = this.groupCache.get(groupCode.toString());
if (!group) {
@@ -109,64 +135,8 @@ export class NTQQGroupApi {
return group;
}
async getGroupMemberLatestSendTimeCache(GroupCode: string, uids: string[]) {
return this.getGroupMemberLatestSendTime(GroupCode, uids);
}
/**
* 通过QQ自带数据库获取群成员最后发言时间(仅返回有效数据 且消耗延迟大 需要进行缓存)
* @param GroupCode 群号
* @param uids QQ号
* @returns Map<string, string> key: uin value: sendTime
* @example
* let ret = await NTQQGroupApi.getGroupMemberLastestSendTime('123456');
* for (let [uin, sendTime] of ret) {
* console.log(uin, sendTime);
* }
*/
async getGroupMemberLatestSendTime(GroupCode: string, uids: string[]) {
const getdata = async (uid: string) => {
const NTRet = await this.getLatestMsgByUids(GroupCode, [uid]);
if (NTRet.result != 0 && NTRet.msgList.length < 1) {
return undefined;
}
return { sendUin: NTRet.msgList[0].senderUin, sendTime: NTRet.msgList[0].msgTime };
};
const PromiseData: Promise<({
sendUin: string;
sendTime: string;
} | undefined)>[] = [];
const ret: Map<string, string> = new Map();
for (const uid of uids) {
PromiseData.push(getdata(uid).catch(() => undefined));
}
const allRet = await runAllWithTimeout(PromiseData, 2500);
for (const PromiseDo of allRet) {
if (PromiseDo) {
ret.set(PromiseDo.sendUin, PromiseDo.sendTime);
}
}
return ret;
}
async getLatestMsgByUids(GroupCode: string, uids: string[]) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: {
peerUid: GroupCode,
chatType: ChatType.KCHATTYPEGROUP,
},
filterMsgType: [],
filterSendersUid: uids,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
}
async getGroupMemberAll(GroupCode: string, forced = false) {
return this.context.session.getGroupService().getAllMemberList(GroupCode, forced);
async getGroupMemberAll(groupCode: string, forced = false) {
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
}
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
@@ -202,31 +172,8 @@ export class NTQQGroupApi {
return member;
}
async getLatestMsg(GroupCode: string, uins: string[]) {
const uids: Array<string> = [];
for (const uin of uins) {
const uid = await this.core.apis.UserApi.getUidByUinV2(uin);
if (uid) {
uids.push(uid);
}
}
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: {
peerUid: GroupCode,
chatType: ChatType.KCHATTYPEGROUP,
},
filterMsgType: [],
filterSendersUid: uids,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
}
async getGroupRecommendContactArkJson(GroupCode: string) {
return this.context.session.getGroupService().getGroupRecommendContactArkJson(GroupCode);
async getGroupRecommendContactArkJson(groupCode: string) {
return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode);
}
async CreatGroupFileFolder(groupCode: string, folderName: string) {
@@ -242,8 +189,6 @@ export class NTQQGroupApi {
}
async addGroupEssence(GroupCode: string, msgId: string) {
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2,
guildId: '',
@@ -254,7 +199,6 @@ export class NTQQGroupApi {
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return this.context.session.getGroupService().addGroupEssence(param);
}
@@ -263,8 +207,8 @@ export class NTQQGroupApi {
}
async deleteGroupBulletin(GroupCode: string, noticeId: string) {
const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, _Pskey, noticeId);
const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, psKey, noticeId);
}
async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) {
@@ -272,13 +216,19 @@ export class NTQQGroupApi {
groupCode: GroupCode,
needDeleteLocalMsg: needDeleteLocalMsg,
};
//应该是直接返回不需要Listener的 未经测试 需测试再发布
return this.context.session.getGroupService().quitGroupV2(param);
}
async removeGroupEssenceBySeq(GroupCode: string, msgRandom: string, msgSeq: string) {
const param = {
groupCode: GroupCode,
msgRandom: parseInt(msgRandom),
msgSeq: parseInt(msgSeq),
};
return this.context.session.getGroupService().removeGroupEssence(param);
}
async removeGroupEssence(GroupCode: string, msgId: string) {
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2,
guildId: '',
@@ -289,16 +239,15 @@ export class NTQQGroupApi {
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return this.context.session.getGroupService().removeGroupEssence(param);
}
async getSingleScreenNotifies(num: number) {
async getSingleScreenNotifies(doubt: boolean, num: number) {
const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getSingleScreenNotifies',
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
[
false,
doubt,
'',
num,
],
@@ -307,12 +256,11 @@ export class NTQQGroupApi {
}
async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
type EventType = NodeIKernelGroupService['getMemberInfo'];
const Listener = this.core.eventWrapper.registerListen(
'NodeIKernelGroupListener/onMemberInfoChange',
1,
forced ? 5000 : 250,
(params) => params === GroupCode,
(params, _, members) => params === GroupCode && members.size > 0,
);
const retData = await (
this.core.eventWrapper
@@ -330,6 +278,118 @@ export class NTQQGroupApi {
return member;
}
async searchGroup(groupCode: string) {
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelSearchService/searchGroup',
'NodeIKernelSearchListener/onSearchGroupResult',
[{
keyWords: groupCode,
groupNum: 25,
exactSearch: false,
penetrate: ''
}],
(ret) => ret.result === 0,
(params) => !!params.groupInfos.find(g => g.groupCode === groupCode),
1,
5000
);
return ret.groupInfos.find(g => g.groupCode === groupCode);
}
async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) {
const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => {
return eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getMemberInfo',
'NodeIKernelGroupListener/onMemberInfoChange',
[GroupCode, [uid], forced],
(ret) => ret.result === 0,
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
1,
forced ? 2500 : 250
);
}, this.core.eventWrapper, GroupCode, uid, forced);
if (data && data[3] instanceof Map && data[3].has(uid)) {
return data[3].get(uid);
}
if (retry > 0) {
const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined;
if (trydata) return trydata;
}
return undefined;
}
async tryGetGroupMembersV2(modeListener = false, groupQQ: string, num = 30, timeout = 100): Promise<{
infos: Map<string, GroupMember>;
finish: boolean;
hasNext: boolean | undefined;
}> {
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 0, timeout, (params) => params.sceneId === sceneId)
.catch(() => { });
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
let resMode2;
if (modeListener) {
const ret = (await once)?.[0];
if (ret) {
resMode2 = ret;
}
}
this.context.session.getGroupService().destroyMemberListScene(sceneId);
return {
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
finish: result.result.finish,
hasNext: resMode2?.hasNext,
};
}
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
infos: Map<string, GroupMember>;
finish: boolean;
hasNext: boolean | undefined;
listenerMode: boolean;
}> {
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 0, timeout, (params) => params.sceneId === sceneId)
.catch(() => { });
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
let resMode2;
if (result.result.finish && result.result.infos.size === 0) {
const ret = (await once)?.[0];
if (ret) {
resMode2 = ret;
}
}
this.context.session.getGroupService().destroyMemberListScene(sceneId);
//console.log('GetGroupMembersV3 len :', result.result.infos.size, resMode2?.infos.size, groupQQ);
return {
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
finish: result.result.finish,
hasNext: resMode2?.hasNext,
listenerMode: resMode2?.hasNext !== undefined ? true : false
};
}
async getGroupMembersV2(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
//console.log('getGroupMembers -->', groupQQ);
let res = await this.GetGroupMembersV3(groupQQ, num);
let ret = res.infos;
if (res.infos.size === 0 && !res.listenerMode) {
res = await this.GetGroupMembersV3(groupQQ, num);
ret = res.infos;
}
if (res.infos.size === 0) {
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
}
//console.log("<---------------")
return ret;
}
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const groupService = this.context.session.getGroupService();
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
@@ -337,31 +397,14 @@ export class NTQQGroupApi {
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`); //, Array.from(result.result.infos.values()));
this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`);
return result.result.infos;
/*
console.log(sceneId);
const result = await napCatCore.getGroupService().getNextMemberList(sceneId, num);
console.log(result);
return result;
*/
}
async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
}
async GetGroupFileCount(Gids: Array<string>) {
async getGroupFileCount(Gids: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(Gids);
}
async getGroupIgnoreNotifies() {
}
async getArkJsonGroupShare(GroupCode: string) {
const ret = await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelGroupService/getGroupRecommendContactArkJson',
@@ -385,12 +428,12 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().operateSysNotify(
false,
{
'operateType': operateType, // 2 拒绝
'targetMsg': {
'seq': seq, // 通知序列号
'type': type,
'groupCode': groupCode,
'postscript': reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
operateType: operateType, // 2 拒绝
targetMsg: {
seq: seq, // 通知序列号
type: type,
groupCode: groupCode,
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
},
});
}
@@ -424,19 +467,12 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false);
}
// 头衔不可用
/*
async setGroupTitle(groupQQ: string, uid: string, title: string) {
}
*/
async publishGroupBulletin(groupQQ: string, content: string, picInfo: {
id: string,
width: number,
height: number
} | undefined = undefined, pinned: number = 0, confirmRequired: number = 0) {
const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com');
const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com');
//text是content内容url编码
const data = {
text: encodeURI(content),
@@ -445,15 +481,14 @@ export class NTQQGroupApi {
pinned: pinned,
confirmRequired: confirmRequired,
};
return this.context.session.getGroupService().publishGroupBulletin(groupQQ, _Pskey!, data);
return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data);
}
async getGroupRemainAtTimes(GroupCode: string) {
this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
}
async getMemberExtInfo(groupCode: string, uin: string) {
// 仅NTQQ 9.9.11 24568测试 容易炸开谨慎使用
return this.context.session.getGroupService().getMemberExtInfo(
{
groupCode: groupCode,

View File

@@ -3,6 +3,13 @@ import { InstanceContext, NapCatCore } from '@/core';
import { GeneralCallResult } from '@/core/services/common';
export class NTQQMsgApi {
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
}
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
context: InstanceContext;
core: NapCatCore;
@@ -11,32 +18,22 @@ export class NTQQMsgApi {
this.core = core;
}
async FetchLongMsg(peer: Peer, msgId: string) {
return this.context.session.getMsgService().fetchLongMsg(peer, msgId);
async getAioFirstViewLatestMsgs(peer: Peer, MsgCount: number) {
return this.context.session.getMsgService().getAioFirstViewLatestMsgs(peer, MsgCount);
}
async sendShowInputStatusReq(peer: Peer, eventType: number) {
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
}
async getSourceOfReplyMsgV2(peer: Peer, clientSeq: string, time: string) {
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
}
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
//console.log(peer, msgSeq, emojiId, emojiType, count);
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged M likiowa
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
}
// napCatCore: NapCatCore | null = null;
// enum BaseEmojiType {
// NORMAL_EMOJI,
// SUPER_EMOJI,
// RANDOM_SUPER_EMOJI,
// CHAIN_SUPER_EMOJI,
// EMOJI_EMOJI
// }
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
emojiId = emojiId.toString();
return this.context.session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set);
}
@@ -51,23 +48,10 @@ export class NTQQMsgApi {
return this.context.session.getMsgService().forwardMsg(msgIds, peer, [peer], new Map());
}
async getLastestMsgByUids(peer: Peer, count: number = 20, isReverseOrder: boolean = false) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: isReverseOrder,//此参数有点离谱 注意不是本次查询的排序 而是全部消历史信息的排序 默认false 从新消息拉取到旧消息
isIncludeCurrent: true,
pageLimit: count,
});
}
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
if (!peer) throw new Error('peer is not allowed');
if (!msgIds) throw new Error('msgIds is not allowed');
//Mlikiowa 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得
//MliKiowa: 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得
return await this.context.session.getMsgService().getMsgsByMsgId(peer, msgIds);
}
@@ -81,7 +65,7 @@ export class NTQQMsgApi {
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
@@ -91,11 +75,46 @@ export class NTQQMsgApi {
pageLimit: 1,
});
}
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
async queryMsgsWithFilterExWithSeqV2(peer: Peer, msgSeq: string, MsgTime: string, SendersUid: string[]) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
filterMsgToTime: MsgTime,
filterMsgFromTime: MsgTime,
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
}
async queryMsgsWithFilterExWithSeqV3(peer: Peer, msgSeq: string, SendersUid: string[]) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
}
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 1,
});
}
// 客户端还在用别慌
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
}
async getMsgExBySeq(peer: Peer, msgSeq: string) {
const DateNow = Math.floor(Date.now() / 1000);
const filterMsgFromTime = (DateNow - 300).toString();
@@ -125,10 +144,7 @@ export class NTQQMsgApi {
params,
],
() => true,
( /* groupFileListResult: GroupFileInfoUpdateParamType */) => {
//Developer Mlikiowa Todo: 此处有问题 无法判断是否成功
return true;
},
() => true, // Todo: 应当通过 groupFileListResult 判断
1,
5000,
);
@@ -140,22 +156,19 @@ export class NTQQMsgApi {
return this.context.session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder);
}
async recallMsg(peer: Peer, msgIds: string[]) {
await this.context.session.getMsgService().recallMsg({
chatType: peer.chatType,
peerUid: peer.peerUid,
}, msgIds);
async recallMsg(peer: Peer, msgId: string) {
await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelMsgService/recallMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
[peer, [msgId]],
() => true,
(updatedList) => updatedList.find(m => m.msgId === msgId && m.recallTime !== '0') !== undefined,
1,
1000,
);
}
async PrepareTempChat(toUserUid: string, GroupCode: string, nickname: string) {
//By Jadx/Ida Mlikiowa
const TempGameSession = {
nickname: '',
gameAppId: '',
selfTinyId: '',
peerRoleId: '',
peerOpenId: '',
};
return this.context.session.getMsgService().prepareTempChat({
chatType: ChatType.KCHATTYPETEMPC2CFROMGROUP,
peerUid: toUserUid,
@@ -164,7 +177,13 @@ export class NTQQMsgApi {
sig: '',
selfPhone: '',
selfUid: this.core.selfInfo.uid,
gameSession: TempGameSession,
gameSession: {
nickname: '',
gameAppId: '',
selfTinyId: '',
peerRoleId: '',
peerOpenId: '',
},
});
}
@@ -173,14 +192,14 @@ export class NTQQMsgApi {
}
async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
//唉? !我有个想法
//唉?!我有个想法
if (peer.chatType === ChatType.KCHATTYPETEMPC2CFROMGROUP && peer.guildId && peer.guildId !== '') {
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid!);
if (member) {
await this.PrepareTempChat(peer.peerUid, peer.guildId, member.nick);
}
}
const msgId = await this.generateMsgUniqueId(peer.chatType, await this.getServerTime());
const msgId = await this.generateMsgUniqueId(peer.chatType);
peer.guildId = msgId;
const [, msgList] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelMsgService/sendMsg',
@@ -191,7 +210,7 @@ export class NTQQMsgApi {
msgElements,
new Map(),
],
() => true,
(ret) => ret.result === 0,
msgRecords => {
for (const msgRecord of msgRecords) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === SendStatusType.KSEND_STATUS_SUCCESS) {
@@ -203,19 +222,11 @@ export class NTQQMsgApi {
1,
timeout,
);
return msgList.find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true;
}
});
return msgList.find(msgRecord => msgRecord.guildId === msgId);
}
async generateMsgUniqueId(chatType: number, time: string) {
return this.context.session.getMsgService().generateMsgUniqueId(chatType, time);
}
async getServerTime() {
return this.context.session.getMSFService().getServerTime();
async generateMsgUniqueId(chatType: number) {
return this.context.session.getMsgService().generateMsgUniqueId(chatType, this.context.session.getMSFService().getServerTime());
}
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
@@ -237,14 +248,10 @@ export class NTQQMsgApi {
new Map(),
],
() => true,
(msgRecords) => {
for (const msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == this.core.selfInfo.uid) {
return true;
}
}
return false;
},
(msgRecords) => msgRecords.some(
msgRecord => msgRecord.peerUid === destPeer.peerUid
&& msgRecord.senderUid === this.core.selfInfo.uid
),
);
for (const msg of msgList) {
const arkElement = msg.elements.find(ele => ele.arkElement);
@@ -262,7 +269,7 @@ export class NTQQMsgApi {
throw new Error('转发消息超时');
}
async markallMsgAsRead() {
async markAllMsgAsRead() {
return this.context.session.getMsgService().setAllC2CAndGroupMsgRead();
}
}

242
src/core/apis/packet.ts Normal file
View File

@@ -0,0 +1,242 @@
import * as crypto from 'crypto';
import * as os from 'os';
import { ChatType, InstanceContext, NapCatCore } from '..';
import offset from '@/core/external/offset.json';
import { PacketSession } from "@/core/packet/session";
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { NapProtoMsg, NapProtoEncodeStructType, NapProtoDecodeStructType } from "@napneko/nap-proto-core";
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
import { LogWrapper } from "@/common/log";
import { SendLongMsgResp } from "@/core/packet/proto/message/action";
import { PacketMsg } from "@/core/packet/message/message";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp";
import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
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";
import { napCatVersion } from "@/common/version";
interface OffsetType {
[key: string]: {
recv: string;
send: string;
};
}
const typedOffset: OffsetType = offset;
export class NTQQPacketApi {
context: InstanceContext;
core: NapCatCore;
logger: LogWrapper;
qqVersion: string | undefined;
packetSession: PacketSession | undefined;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.logger = core.context.logger;
this.packetSession = undefined;
this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
.then()
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
}
get available(): boolean {
return this.packetSession?.client.available ?? false;
}
async InitSendPacket(qqversion: string) {
this.qqVersion = qqversion;
const table = typedOffset[qqversion + '-' + os.arch()];
if (!table) {
this.logger.logError(`[Core] [Packet] PacketBackend 不支持当前QQ版本架构${qqversion}-${os.arch()}
请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本`);
return false;
}
if (this.core.configLoader.configData.packetBackend === 'disable') {
this.logger.logWarn('[Core] [Packet] 已禁用PacketBackendNapCat.Packet将不会加载');
return false;
}
this.packetSession = new PacketSession(this.core);
const cb = () => {
if (this.packetSession && this.packetSession.client) {
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
}
};
await this.packetSession.client.connect(cb);
return true;
}
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
return this.packetSession!.client.sendPacket(cmd, data, rsp);
}
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp);
}
async sendPokePacket(peer: number, group?: number) {
const data = this.packetSession?.packer.packPokePacket(peer, group);
await this.sendOidbPacket(data!, false);
}
async sendRkeyPacket() {
const packet = this.packetSession?.packer.packRkeyPacket();
const ret = await this.sendOidbPacket(packet!, true);
if (!ret?.hex_data) return [];
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
return retData.data.rkeyList;
}
async sendGroupSignPacket(groupCode: string) {
const packet = this.packetSession?.packer.packGroupSignReq(this.core.selfInfo.uin, groupCode);
await this.sendOidbPacket(packet!, true);
}
async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
let status = 0;
try {
const packet = this.packetSession?.packer.packStatusPacket(uin);
const ret = await this.sendOidbPacket(packet!, true);
const data = Buffer.from(ret.hex_data, 'hex');
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
// ext & 0xff00 + ext >> 16 & 0xff
const extBigInt = BigInt(ext); // 转换为 BigInt
if (extBigInt <= 10n) {
return { status: Number(extBigInt) * 10, ext_status: 0 };
}
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); // 使用 BigInt 操作符
return { status: 10, ext_status: status };
} catch (error) {
return undefined;
}
}
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
await this.sendOidbPacket(data!, true);
}
// TODO: can simplify this
async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
const reqList = [];
for (const m of msg) {
for (const e of m.msg) {
if (e instanceof PacketMsgPicElement) {
reqList.push(this.packetSession?.highwaySession.uploadImage({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgVideoElement) {
reqList.push(this.packetSession?.highwaySession.uploadVideo({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgPttElement) {
reqList.push(this.packetSession?.highwaySession.uploadPtt({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgFileElement) {
reqList.push(this.packetSession?.highwaySession.uploadFile({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
}
}
const res = await Promise.allSettled(reqList);
this.logger.log(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.logger.logError(`上传第${index + 1}个资源失败:${result.reason}`);
}
});
}
async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
await this.uploadResources(msg, groupUin);
const data = await this.packetSession?.packer.packUploadForwardMsg(this.core.selfInfo.uid, msg, groupUin);
const ret = await this.sendPacket('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', data!, true);
this.logger.logDebug('sendUploadForwardMsg', ret);
const resp = new NapProtoMsg(SendLongMsgResp).decode(Buffer.from(ret.hex_data, 'hex'));
return resp.result.resId;
}
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
if (resp.download.retCode !== 0) {
throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`);
}
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
}
async 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

@@ -1,4 +1,4 @@
import { RequestUtil } from '@/common/utils/request';
import { RequestUtil } from '@/common/request';
import { MiniAppLuaJsonType } from '@/core';
import { InstanceContext, NapCatCore } from '..';
@@ -214,11 +214,6 @@ export class NTQQMusicSignApi {
//console.log(MusicReal);
return { ...MusicReal.data, mid: signedMid };
}
async CreateMusicThirdWay1(id: string = '', mid: string = '') {
}
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM

View File

@@ -1,4 +1,4 @@
import { GeneralCallResult, InstanceContext, NapCatCore } from '@/core';
import { InstanceContext, NapCatCore } from '@/core';
export class NTQQSystemApi {
context: InstanceContext;
@@ -13,7 +13,7 @@ export class NTQQSystemApi {
return this.core.util.hasOtherRunningQQProcess();
}
async ORCImage(filePath: string) {
async ocrImage(filePath: string) {
return this.context.session.getNodeMiscService().wantWinScreenOCR(filePath);
}
@@ -21,20 +21,16 @@ export class NTQQSystemApi {
return this.context.session.getRichMediaService().translateEnWordToZn(words);
}
//调用会超时 没灯用 (好像是通知listener的) onLineDev
async getOnlineDev() {
return this.context.session.getMsgService().getOnLineDev();
this.context.session.getMsgService().getOnLineDev();
}
//1-2-162b9b42-65b9-4405-a8ed-2e256ec8aa50
async getArkJsonCollection(cid: string) {
return await this.core.eventWrapper.callNoListenerEvent('NodeIKernelCollectionService/collectionArkShare', '1717662698058');
}
async BootMiniApp(appfile: string, params: string) {
async bootMiniApp(appFile: string, params: string) {
await this.context.session.getNodeMiscService().setMiniAppVersion('2.16.4');
// const c = await this.context.session.getNodeMiscService().getMiniAppPath();
return this.context.session.getNodeMiscService().startNewMiniApp(appfile, params);
return this.context.session.getNodeMiscService().startNewMiniApp(appFile, params);
}
}

View File

@@ -1,8 +1,7 @@
import type { ModifyProfileParams, User, UserDetailInfoByUinV2 } from '@/core/entities';
import { RequestUtil } from '@/common/utils/request';
import { ProfileBizType, UserDetailSource } from '@/core/services';
import { InstanceContext, NapCatCore } from '..';
import { solveAsyncProblem } from '@/common/utils/helper';
import { ModifyProfileParams, User, UserDetailSource } from '@/core/entities';
import { RequestUtil } from '@/common/request';
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
import { solveAsyncProblem } from '@/common/helper';
export class NTQQUserApi {
context: InstanceContext;
@@ -12,22 +11,37 @@ export class NTQQUserApi {
this.context = context;
this.core = core;
}
async getProfileLike(uid: string) {
//self_tind格式
async createUidFromTinyId(tinyId: string) {
return this.context.session.getMsgService().createUidFromTinyId(this.core.selfInfo.uin, tinyId);
}
async getStatusByUid(uid: string) {
return this.context.session.getProfileService().getStatus(uid);
}
async getProfileLike(uid: string, start: number, count: number) {
return this.context.session.getProfileLikeService().getBuddyProfileLike({
friendUids: [
uid,
],
friendUids: [uid],
basic: 1,
vote: 1,
favorite: 0,
userProfile: 1,
type: 2,
start: start,
limit: count,
});
}
async fetchOtherProfileLike(uid: string) {
return this.context.session.getProfileLikeService().getBuddyProfileLike({
friendUids: [uid],
basic: 1,
vote: 1,
favorite: 0,
userProfile: 0,
type: 1,
start: 0,
limit: 20,
});
}
async setLongNick(longNick: string) {
return this.context.session.getProfileService().setLongNick(longNick);
}
@@ -54,8 +68,7 @@ export class NTQQUserApi {
}
async setQQAvatar(filePath: string) {
type setQQAvatarRet = { result: number, errMsg: string };
const ret = await this.context.session.getProfileService().setHeader(filePath) as setQQAvatarRet;
const ret = await this.context.session.getProfileService().setHeader(filePath);
return { result: ret?.result, errMsg: ret?.errMsg };
}
@@ -63,41 +76,6 @@ export class NTQQUserApi {
return this.context.session.getGroupService().setHeader(gc, filePath);
}
async fetchUserDetailInfos(uids: string[]) {
// TODO: 26702 以上使用新接口 .Dev MliKiowa
const retData: User[] = [];
const [_retData, _retListener] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
[
'BuddyProfileStore',
uids,
UserDetailSource.KSERVER,
[ProfileBizType.KALL],
],
() => true,
(profile) => {
if (uids.includes(profile.uid)) {
const RetUser: User = {
...profile.simpleInfo.coreInfo,
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
...profile.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel,
pendantId: '',
};
retData.push(RetUser);
return true;
}
return false;
},
uids.length,
);
return retData;
}
async fetchUserDetailInfo(uid: string, mode: UserDetailSource = UserDetailSource.KDB) {
const [_retData, profile] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelProfileService/fetchUserDetailInfo',
@@ -112,7 +90,6 @@ export class NTQQUserApi {
(profile) => profile.uid === uid,
);
const RetUser: User = {
...profile.simpleInfo.coreInfo,
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
@@ -120,29 +97,42 @@ export class NTQQUserApi {
qqLevel: profile.commonExt?.qqLevel,
age: profile.simpleInfo.baseInfo.age,
pendantId: '',
...profile.simpleInfo.coreInfo
};
return RetUser;
}
async getUserDetailInfo(uid: string): Promise<User> {
const retUser = await solveAsyncProblem(async (uid) => this.fetchUserDetailInfo(uid, UserDetailSource.KDB), uid);
let retUser = await solveAsyncProblem(async (uid) => this.fetchUserDetailInfo(uid, UserDetailSource.KDB), uid);
if (retUser && retUser.uin !== '0') {
return retUser;
}
this.context.logger.logDebug('[NapCat] [Mark] getUserDetailInfo Mode1 Failed.');
return this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER);
retUser = await this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER);
if (retUser && retUser.uin === '0') {
retUser.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
}
return retUser;
}
async modifySelfProfile(param: ModifyProfileParams) {
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
}
//需要异常处理
async getCookies(domain: string) {
const ClientKeyData = await this.forceFetchClientKey();
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + this.core.selfInfo.uin +
'&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + this.core.selfInfo.uin + '%2Finfocenter&keyindex=19%27';
return await RequestUtil.HttpsGetCookies(requestUrl);
const data = await RequestUtil.HttpsGetCookies(requestUrl);
if (!data.p_skey || data.p_skey.length == 0) {
try {
const pskey = (await this.getPSkey([domain])).domainPskeyMap.get(domain);
if (pskey) data.p_skey = pskey;
} catch {
return data;
}
}
return data;
}
async getPSkey(domainList: string[]) {
@@ -156,7 +146,6 @@ export class NTQQUserApi {
version: 0,
aioKeywordVersion: 0,
});
// console.log(robotUinRanges?.response?.robotUinRanges);
return robotUinRanges?.response?.robotUinRanges;
}
@@ -170,7 +159,7 @@ export class NTQQUserApi {
//需要异常处理
async getSkey(): Promise<string | undefined> {
async getSKey(): Promise<string | undefined> {
const ClientKeyData = await this.forceFetchClientKey();
if (ClientKeyData.result !== 0) {
throw new Error('getClientKey Error');
@@ -181,7 +170,7 @@ export class NTQQUserApi {
const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl);
const skey = cookies['skey'];
if (!skey) {
throw new Error('getSkey Skey is Empty');
throw new Error('SKey is Empty');
}
return skey;
}
@@ -194,8 +183,8 @@ export class NTQQUserApi {
if (uid) return uid;
uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
if (uid) return uid;
const unveifyUid = (await this.getUserDetailInfoByUinV2(Uin)).detail.uid;//从QQ Native 特殊转换
if (unveifyUid.indexOf('*') == -1) uid = unveifyUid;
const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换
if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid;
//if (uid) return uid;
return uid;
}
@@ -230,8 +219,11 @@ export class NTQQUserApi {
return await this.context.session.getRecentContactService().getRecentContactList();
}
async getUserDetailInfoByUinV2(Uin: string) {
return await this.core.eventWrapper.callNoListenerEvent('NodeIKernelProfileService/getUserDetailInfoByUin', Uin);
async getUserDetailInfoByUin(Uin: string) {
return await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getUserDetailInfoByUin',
Uin
);
}
async forceFetchClientKey() {

View File

@@ -1,4 +1,4 @@
import { RequestUtil } from '@/common/utils/request';
import { RequestUtil } from '@/common/request';
import {
GroupEssenceMsgRet,
InstanceContext,
@@ -26,35 +26,42 @@ export class NTQQWebApi {
msg_seq: msgSeq,
msg_random: msgRandom,
target_group_code: targetGroupCode,
}).toString()
}`;
}).toString()}`;
try {
return RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': this.cookieToString(cookieObject) });
} catch (e) {
return undefined;
}
}
async getGroupEssenceMsg(GroupCode: string, page_start: string) {
async getGroupEssenceMsgAll(GroupCode: string) {
const ret: GroupEssenceMsgRet[] = [];
for (let i = 0; i < 20; i++) {
const data = await this.getGroupEssenceMsg(GroupCode, i, 50);
if (!data) break;
ret.push(data);
if (data.data.is_end) break;
}
return ret;
}
async getGroupEssenceMsg(GroupCode: string, page_start: number = 0, page_limit: number = 50) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
page_start: page_start.toString(),
page_limit: page_limit.toString(),
group_code: GroupCode,
page_start,
page_limit: '20',
}).toString()
}`;
let ret;
}).toString()}`;
try {
ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>
(url, 'GET', '', { 'Cookie': this.cookieToString(cookieObject) });
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
url,
'GET',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
return ret.retcode === 0 ? ret : undefined;
} catch {
return undefined;
}
if (ret.retcode !== 0) {
return undefined;
}
return ret;
}
async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
@@ -62,15 +69,18 @@ export class NTQQWebApi {
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const retList: Promise<WebApiGroupMemberRet>[] = [];
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>
(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()
}`, 'POST', '', { 'Cookie': this.cookieToString(cookieObject) });
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: '0',
end: '40',
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
return [];
} else {
@@ -82,15 +92,18 @@ export class NTQQWebApi {
const PageNum = Math.ceil(fastRet.count / 40);
//遍历批量请求
for (let i = 2; i <= PageNum; i++) {
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>
(`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()
}`, 'POST', '', { 'Cookie': this.cookieToString(cookieObject) });
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
st: ((i - 1) * 40).toString(),
end: (i * 40).toString(),
sort: '1',
gc: GroupCode,
bkn: this.getBknFromCookie(cookieObject),
}).toString()}`,
'POST',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
retList.push(ret);
}
//批量等待
@@ -118,20 +131,56 @@ export class NTQQWebApi {
// return await res.json();
// }
async setGroupNotice(GroupCode: string, Content: string) {
async setGroupNotice(
GroupCode: string,
Content: string,
pinned: number = 0,
type: number = 1,
is_show_edit_card: number = 1,
tip_window_type: number = 1,
confirm_required: number = 1,
picId: string = '',
imgWidth: number = 540,
imgHeight: number = 300,
) {
interface SetNoticeRetSuccess {
ec: number;
em: string;
id: number;
ltsm: number;
new_fid: string;
read_only: number;
role: number;
srv_code: number;
}
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
let ret: any = undefined;
try {
ret = await RequestUtil.HttpGetJson<any>
(`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: '0',
type: '1',
settings: '{"is_show_edit_card":1,"tip_window_type":1,"confirm_required":1}',
}).toString()
}`, 'GET', '', { 'Cookie': this.cookieToString(cookieObject) });
const settings = JSON.stringify({
is_show_edit_card: is_show_edit_card,
tip_window_type: tip_window_type,
confirm_required: confirm_required
});
const externalParam = {
pic: picId,
imgWidth: imgWidth.toString(),
imgHeight: imgHeight.toString(),
};
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
text: Content,
pinned: pinned.toString(),
type: type.toString(),
settings: settings,
...(picId === '' ? {} : externalParam)
}).toString()}`,
'POST',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
return ret;
} catch (e) {
return undefined;
@@ -140,16 +189,24 @@ export class NTQQWebApi {
async getGroupNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
let ret: WebApiGroupNoticeRet | undefined = undefined;
try {
const url = 'https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=' +
this.getBknFromCookie(cookieObject) + '&qid=' + GroupCode + '&ft=23&ni=1&n=1&i=1&log_read=1&platform=1&s=-1&n=20';
ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(url, 'GET', '', { 'Cookie': this.cookieToString(cookieObject) });
if (ret?.ec !== 0) {
return undefined;
}
return ret;
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
bkn: this.getBknFromCookie(cookieObject),
qid: GroupCode,
ft: '23',
ni: '1',
n: '1',
i: '1',
log_read: '1',
platform: '1',
s: '-1',
}).toString()}&n=20`,
'GET',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
return ret?.ec === 0 ? ret : undefined;
} catch (e) {
return undefined;
}
@@ -158,14 +215,17 @@ export class NTQQWebApi {
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
const url = `https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: Internal_groupCode,
type: Internal_type.toString(),
}).toString()
}`;
let resJson;
try {
const res = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': this.cookieToString(cookieObject) });
const res = await RequestUtil.HttpGetText(
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: Internal_groupCode,
type: Internal_type.toString(),
}).toString()}`,
'GET',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
if (match) {
resJson = JSON.parse(match[1].trim());
@@ -176,7 +236,7 @@ export class NTQQWebApi {
return resJson?.actorList;
}
} catch (e) {
this.context.logger.logDebug('获取当前群荣耀失败', url, e);
this.context.logger.logDebug('获取当前群荣耀失败', e);
}
return undefined;
};
@@ -184,11 +244,8 @@ export class NTQQWebApi {
const HonorInfo: any = { group_id: groupCode };
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
try {
const RetInternal = await getDataInternal(groupCode, 1);
if (!RetInternal) {
throw new Error('获取龙王信息失败');
}
const RetInternal = await getDataInternal(groupCode, 1);
if (RetInternal) {
HonorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
avatar: RetInternal[0]?.avatar,
@@ -206,16 +263,13 @@ export class NTQQWebApi {
nickname: talkative_ele?.name,
});
}
} catch (e) {
this.context.logger.logDebug(e);
} else {
this.context.logger.logError.bind(this.context.logger)('获取龙王信息失败');
}
}
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
try {
const RetInternal = await getDataInternal(groupCode, 2);
if (!RetInternal) {
throw new Error('获取群聊之火失败');
}
const RetInternal = await getDataInternal(groupCode, 2);
if (RetInternal) {
HonorInfo.performer_list = [];
for (const performer_ele of RetInternal) {
HonorInfo.performer_list.push({
@@ -225,16 +279,13 @@ export class NTQQWebApi {
description: performer_ele?.desc,
});
}
} catch (e) {
this.context.logger.logDebug(e);
} else {
this.context.logger.logError.bind(this.context.logger)('获取群聊之火失败');
}
}
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
try {
const RetInternal = await getDataInternal(groupCode, 3);
if (!RetInternal) {
throw new Error('获取群聊炽焰失败');
}
if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) {
const RetInternal = await getDataInternal(groupCode, 3);
if (RetInternal) {
HonorInfo.legend_list = [];
for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({
@@ -244,16 +295,13 @@ export class NTQQWebApi {
desc: legend_ele?.description,
});
}
} catch (e) {
this.context.logger.logDebug('获取群聊炽焰失败', e);
} else {
this.context.logger.logError.bind(this.context.logger)('获取群聊炽焰失败');
}
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
try {
const RetInternal = await getDataInternal(groupCode, 6);
if (!RetInternal) {
throw new Error('获取快乐源泉失败');
}
const RetInternal = await getDataInternal(groupCode, 6);
if (RetInternal) {
HonorInfo.emotion_list = [];
for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({
@@ -263,14 +311,16 @@ export class NTQQWebApi {
desc: emotion_ele.description,
});
}
} catch (e) {
this.context.logger.logDebug('获取快乐源泉失败', e);
} else {
this.context.logger.logError.bind(this.context.logger)('获取快乐源泉失败');
}
}
//冒尖小春笋好像已经被tx扬了
// 冒尖小春笋好像已经被tx扬了 R.I.P.
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
HonorInfo.strong_newbie_list = [];
}
return HonorInfo;
}
@@ -288,4 +338,12 @@ export class NTQQWebApi {
}
return (hash & 0x7FFFFFFF).toString();
}
public getBknFromSKey(sKey: string) {
let hash = 5381;
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
}
return (hash & 0x7FFFFFFF).toString();
}
}

View File

@@ -1,206 +0,0 @@
import { NodeQQNTWrapperUtil, StableNTApiWrapper, WrapperNodeApi } from '@/core/wrapper';
import path from 'node:path';
import fs from 'node:fs';
import { InstanceContext } from './wrapper';
import { proxiedListenerOf } from '@/common/utils/proxy-handler';
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from './listeners';
import { DataSource, GroupMember, SelfInfo } from './entities';
import { LegacyNTEventWrapper } from '@/common/framework/event-legacy';
import { NTQQFileApi, NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, NTQQSystemApi, NTQQUserApi, NTQQWebApi } from './apis';
import os from 'node:os';
import { NTQQCollectionApi } from './apis/collection';
import { NapCatConfigLoader } from './helper/config';
import { LogLevel } from '@/common/utils/log';
export enum NapCatCoreWorkingEnv {
Unknown = 0,
Shell = 1,
Framework = 2,
}
export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
let wrapperNodePath = path.resolve(path.dirname(process.execPath), './resources/app/wrapper.node');
if (!fs.existsSync(wrapperNodePath)) {
wrapperNodePath = path.join(path.dirname(process.execPath), `resources/app/versions/${QQVersion}/wrapper.node`);
}
const nativemodule: any = { exports: {} };
process.dlopen(nativemodule, wrapperNodePath);
return nativemodule.exports;
}
export class NapCatCore {
readonly context: InstanceContext;
readonly apis: StableNTApiWrapper;
readonly eventWrapper: LegacyNTEventWrapper;
// readonly eventChannel: NTEventChannel;
NapCatDataPath: string;
NapCatTempPath: string;
// runtime info, not readonly
selfInfo: SelfInfo;
util: NodeQQNTWrapperUtil;
configLoader: NapCatConfigLoader;
// 通过构造器递过去的 runtime info 应该尽量少
constructor(context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new LegacyNTEventWrapper(context.session);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),
SystemApi: new NTQQSystemApi(this.context, this),
CollectionApi: new NTQQCollectionApi(this.context, this),
WebApi: new NTQQWebApi(this.context, this),
FriendApi: new NTQQFriendApi(this.context, this),
MsgApi: new NTQQMsgApi(this.context, this),
UserApi: new NTQQUserApi(this.context, this),
GroupApi: new NTQQGroupApi(this.context, this),
};
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
// 创建临时目录
if (!fs.existsSync(this.NapCatTempPath)) {
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
}
this.initNapCatCoreListeners().then().catch(this.context.logger.logError);
this.context.logger.setFileLogEnabled(
this.configLoader.configData.fileLog,
);
this.context.logger.setConsoleLogEnabled(
this.configLoader.configData.consoleLog,
);
this.context.logger.setFileAndConsoleLogLevel(
this.configLoader.configData.fileLogLevel as LogLevel,
this.configLoader.configData.consoleLogLevel as LogLevel,
);
}
get dataPath(): string {
let result = this.context.wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
if (!result) {
result = path.resolve(os.homedir(), './.config/QQ');
fs.mkdirSync(result, { recursive: true });
}
return result;
}
// Renamed from 'InitDataListener'
async initNapCatCoreListeners() {
const msgListener = new NodeIKernelMsgListener();
msgListener.onRecvMsg = (msgs) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
};
msgListener.onAddSendMsg = (msg) => {
this.context.logger.logMessage(msg, this.selfInfo);
};
//await sleep(2500);
this.context.session.getMsgService().addKernelMsgListener(
proxiedListenerOf(msgListener, this.context.logger) as any,
);
const profileListener = new NodeIKernelProfileListener();
profileListener.onProfileDetailInfoChanged = (profile) => {
if (profile.uid === this.selfInfo.uid) {
Object.assign(this.selfInfo, profile);
}
};
profileListener.onSelfStatusChanged = (/* Info: SelfStatusInfo */) => {
// if (Info.status == 20) {
// log("账号状态变更为离线")
// }
};
this.context.session.getProfileService().addKernelProfileListener(
proxiedListenerOf(profileListener, this.context.logger),
);
// 群相关
const groupListener = new NodeIKernelGroupListener();
groupListener.onGroupListUpdate = (updateType, groupList) => {
// console.log("onGroupListUpdate", updateType, groupList)
groupList.map(g => {
const existGroup = this.apis.GroupApi.groupCache.get(g.groupCode);
//群成员数量变化 应该刷新缓存
if (existGroup && g.memberCount === existGroup.memberCount) {
Object.assign(existGroup, g);
} else {
this.apis.GroupApi.groupCache.set(g.groupCode, g);
// 获取群成员
}
const sceneId = this.context.session.getGroupService().createMemberListScene(g.groupCode, 'groupMemberList_MainWindow');
this.context.session.getGroupService().getNextMemberList(sceneId!, undefined, 3000).then( /* r => {
// console.log(`get group ${g.groupCode} members`, r);
// r.result.infos.forEach(member => {
// });
// groupMembers.set(g.groupCode, r.result.infos);
} */);
});
};
groupListener.onMemberListChange = (arg) => {
// todo: 应该加一个内部自己维护的成员变动callback用于判断成员变化通知
const groupCode = arg.sceneId.split('_')[0];
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
arg.infos.forEach((member, uid) => {
//console.log('onMemberListChange', member);
const existMember = existMembers.get(uid);
if (existMember) {
Object.assign(existMember, member);
} else {
existMembers!.set(uid, member);
}
//移除成员
if (member.isDelete) {
existMembers.delete(uid);
}
});
} else {
this.apis.GroupApi.groupMemberCache.set(groupCode, arg.infos);
}
// console.log('onMemberListChange', groupCode, arg);
};
groupListener.onMemberInfoChange = (groupCode, dataSource, members) => {
//console.log('onMemberInfoChange', groupCode, changeType, members);
if (dataSource === DataSource.LOCAL && members.get(this.selfInfo.uid)?.isDelete) {
// 自身退群或者被踢退群 5s用于Api操作 之后不再出现
setTimeout(() => {
this.apis.GroupApi.groupCache.delete(groupCode);
}, 5000);
}
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode);
if (existMembers) {
members.forEach((member, uid) => {
const existMember = existMembers.get(uid);
if (existMember) {
// 检查管理变动
member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember);
// 更新成员信息
Object.assign(existMember, member);
} else {
existMembers.set(uid, member);
}
//移除成员
if (member.isDelete) {
existMembers.delete(uid);
}
});
} else {
this.apis.GroupApi.groupMemberCache.set(groupCode, members);
}
};
this.context.session.getGroupService().addKernelGroupListener(
proxiedListenerOf(groupListener, this.context.logger) as any,
);
}
checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean {
if (memberNew.role !== memberOld?.role) {
this.context.logger.logDebug(`${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,11 @@
export enum MsfStatusType {
KUNKNOWN,
KDISCONNECTED,
KCONNECTED
}
export enum MsfChangeReasonType {
KUNKNOWN,
KUSERLOGININ,
KUSERLOGINOUT,
KAUTO
}

View File

@@ -0,0 +1,12 @@
export interface FSABRecentContactParams {
anchorPointContact: {
contactId: string;
sortField: string;
pos: number;
};
relativeMoveCount: number;
listType: number;
count: number;
fetchOld: boolean;
}

View File

@@ -117,6 +117,7 @@ export enum GroupMemberRole {
}
export interface GroupMember {
memberRealLevel: string | undefined;
memberSpecialTitle?: string;
avatarPath: string;
cardName: string;

View File

@@ -22,98 +22,75 @@ export interface GetFileListParam {
startIndex: number;
sortOrder: number;
showOnlinedocFolder: number;
folderId?: string;
}
export enum ElementType {
UNKNOWN = 0,
TEXT = 1,
PIC = 2,
FILE = 3,
PTT = 4,
VIDEO = 5,
FACE = 6,
REPLY = 7,
GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等
WALLET = 9,
/**
* “小灰条”,包括拍一拍 (Poke)、撤回提示等
*/
GreyTip = 8,
ARK = 10,
MFACE = 11,
LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
}
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
type ElementBase<
K extends keyof ElementFullBase,
S extends Partial<{ [P in K]: keyof NonNullable<ElementFullBase[P]> | Array<keyof NonNullable<ElementFullBase[P]>> }> = object
> = {
[P in K]:
S[P] extends Array<infer U>
? Pick<NonNullable<ElementFullBase[P]>, U & keyof NonNullable<ElementFullBase[P]>>
: S[P] extends keyof NonNullable<ElementFullBase[P]>
? Pick<NonNullable<ElementFullBase[P]>, S[P]>
: NonNullable<ElementFullBase[P]>;
};
export interface SendElementBase<ET extends ElementType> {
elementType: ET;
elementId: string;
extBufForUI?: string;
}
export interface ActionBarElement {
rows: InlineKeyboardRow[];
botAppid: string;
}
export interface SendActionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: ActionBarElement;
}
export interface RecommendedMsgElement {
rows: InlineKeyboardRow[];
botAppid: string;
}
export interface SendRecommendedMsgElement {
elementType: ElementType.RECOMMENDEDMSG;
elementId: string;
recommendedMsgElement: RecommendedMsgElement;
}
export type SendRecommendedMsgElement = SendElementBase<ElementType.RECOMMENDEDMSG> & ElementBase<'recommendedMsgElement'>;
export interface InlineKeyboardButton {
id: string;
@@ -170,11 +147,7 @@ export enum NTMsgType {
KMSGTYPEWALLET = 10
}
export interface SendTaskTopMsgElement {
elementType: ElementType.TASKTOPMSG;
elementId: string;
taskTopMsgElement: TaskTopMsgElement;
}
export type SendTaskTopMsgElement = SendElementBase<ElementType.TASKTOPMSG> & ElementBase<'taskTopMsgElement'>;
export interface TofuRecordElement {
type: number;
@@ -193,11 +166,7 @@ export interface TofuRecordElement {
onscreennotify: boolean;
}
export interface SendTofuRecordElement {
elementType: ElementType.TOFURECORD;
elementId: string;
tofuRecordElement: TofuRecordElement;
}
export type SendTofuRecordElement = SendElementBase<ElementType.TOFURECORD> & ElementBase<'tofuRecordElement'>;
export interface FaceBubbleElement {
faceCount: number;
@@ -215,12 +184,7 @@ export interface FaceBubbleElement {
};
}
export interface SendFaceBubbleElement {
elementType: ElementType.FACEBUBBLE;
elementId: string;
faceBubbleElement: FaceBubbleElement;
}
export type SendFaceBubbleElement = SendElementBase<ElementType.FACEBUBBLE> & ElementBase<'faceBubbleElement'>;
export interface AvRecordElement {
type: number;
@@ -231,11 +195,7 @@ export interface AvRecordElement {
extraType: number;
}
export interface SendavRecordElement {
elementType: ElementType.AVRECORD;
elementId: string;
avRecordElement: AvRecordElement;
}
export type SendAvRecordElement = SendElementBase<ElementType.AVRECORD> & ElementBase<'avRecordElement'>;
export interface YoloUserInfo {
uid: string;
@@ -244,24 +204,13 @@ export interface YoloUserInfo {
bizId: string;
}
export interface SendInlineKeyboardElement {
elementType: ElementType.INLINEKEYBOARD;
elementId: string;
inlineKeyboardElement: {
rows: number;
botAppid: string;
};
}
export type SendInlineKeyboardElement = SendElementBase<ElementType.INLINEKEYBOARD> & ElementBase<'inlineKeyboardElement'>;
export interface YoloGameResultElement {
UserInfo: YoloUserInfo[];
}
export interface SendYoloGameResultElement {
elementType: ElementType.YOLOGAMERESULT;
yoloGameResultElement: YoloGameResultElement;
}
export type SendYoloGameResultElement = SendElementBase<ElementType.YOLOGAMERESULT> & ElementBase<'yoloGameResultElement'>;
export interface GiphyElement {
id: string;
@@ -270,17 +219,9 @@ export interface GiphyElement {
height: number;
}
export interface SendGiphyElement {
elementType: ElementType.GIPHY;
elementId: string;
giphyElement: GiphyElement;
}
export type SendGiphyElement = SendElementBase<ElementType.GIPHY> & ElementBase<'giphyElement'>;
export interface SendWalletElement {
elementType: ElementType.UNKNOWN;//不做 设置位置
elementId: string;
walletElement: Record<string, never>;
}
export type SendWalletElement = SendElementBase<ElementType.UNKNOWN> & ElementBase<'walletElement'>;
export interface CalendarElement {
summary: string;
@@ -290,49 +231,16 @@ export interface CalendarElement {
schema: string;
}
export interface SendCalendarElement {
elementType: ElementType.CALENDAR;
elementId: string;
calendarElement: CalendarElement;
}
export type SendCalendarElement = SendElementBase<ElementType.CALENDAR> & ElementBase<'calendarElement'>;
export interface SendliveGiftElement {
elementType: ElementType.LIVEGIFT;
elementId: string;
liveGiftElement: Record<string, never>;
}
export type SendLiveGiftElement = SendElementBase<ElementType.LIVEGIFT> & ElementBase<'liveGiftElement'>;
export interface SendTextElement {
elementType: ElementType.TEXT;
elementId: string;
textElement: {
content: string;
atType: number;
atUid: string;
atTinyId: string;
atNtUid: string;
};
}
export type SendTextElement = SendElementBase<ElementType.TEXT> & ElementBase<'textElement'>;
export interface SendPttElement {
elementType: ElementType.PTT;
elementId: string;
pttElement: {
fileName: string;
filePath: string;
md5HexStr: string;
fileSize: number;
duration: number; // 单位是秒
formatType: number;
voiceType: number;
voiceChangeType: number;
canConvert2Text: boolean;
waveAmplitudes: number[];
fileSubId: string;
playState: number;
autoConvertText: number;
};
}
export type SendPttElement = SendElementBase<ElementType.PTT> & ElementBase<'pttElement', {
pttElement: ['fileName', 'filePath', 'md5HexStr', 'fileSize', 'duration', 'formatType', 'voiceType',
'voiceChangeType', 'canConvert2Text', 'waveAmplitudes', 'fileSubId', 'playState', 'autoConvertText']
}>;
export enum PicType {
gif = 2000,
@@ -358,67 +266,39 @@ export enum NTMsgAtType {
ATTYPEUNKNOWN = 0
}
export interface SendPicElement {
elementType: ElementType.PIC;
elementId: string;
picElement: PicElement;
}
export type SendPicElement = SendElementBase<ElementType.PIC> & ElementBase<'picElement'>;
export interface ReplyElement {
sourceMsgIdInRecords?: string;
replayMsgSeq: string;
replayMsgId: string;
senderUin: string;
senderUinStr: string;
senderUidStr?: string;
replyMsgTime?: string;
replyMsgClientSeq?: string;
}
export interface SendReplyElement {
elementType: ElementType.REPLY;
elementId: string;
replyElement: ReplyElement;
}
export type SendReplyElement = SendElementBase<ElementType.REPLY> & ElementBase<'replyElement'>;
export interface SendFaceElement {
elementType: ElementType.FACE;
elementId: string;
faceElement: FaceElement;
}
export type SendFaceElement = SendElementBase<ElementType.FACE> & ElementBase<'faceElement'>;
export interface SendMarketFaceElement {
elementType: ElementType.MFACE;
marketFaceElement: MarketFaceElement;
}
export type SendMarketFaceElement = SendElementBase<ElementType.MFACE> & ElementBase<'marketFaceElement'>;
export interface SendstructLongMsgElement {
elementType: ElementType.STRUCTLONGMSG;
elementId: string;
structLongMsgElement: StructLongMsgElement;
}
export type SendStructLongMsgElement = SendElementBase<ElementType.STRUCTLONGMSG> & ElementBase<'structLongMsgElement'>;
export interface StructLongMsgElement {
xmlContent: string;
resId: string;
}
export interface SendactionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: number;
botAppid: string;
};
}
export type SendActionBarElement = SendElementBase<ElementType.ACTIONBAR> & ElementBase<'actionBarElement'>;
export interface ShareLocationElement {
text: string;
ext: string;
}
export interface SendShareLocationElement {
elementType: ElementType.SHARELOCATION;
elementId: string;
shareLocationElement?: ShareLocationElement;
}
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
export interface FileElement {
fileMd5?: string;
@@ -438,29 +318,13 @@ export interface FileElement {
fileBizId?: number;
}
export interface SendFileElement {
elementType: ElementType.FILE;
elementId: string;
fileElement: FileElement;
}
export type SendFileElement = SendElementBase<ElementType.FILE> & ElementBase<'fileElement'>;
export interface SendVideoElement {
elementType: ElementType.VIDEO;
elementId: string;
videoElement: VideoElement;
}
export type SendVideoElement = SendElementBase<ElementType.VIDEO> & ElementBase<'videoElement'>;
export interface SendArkElement {
elementType: ElementType.ARK;
elementId: string;
arkElement: ArkElement;
}
export type SendArkElement = SendElementBase<ElementType.ARK> & ElementBase<'arkElement'>;
export interface SendMarkdownElement {
elementType: ElementType.MARKDOWN;
elementId: string;
markdownElement: MarkdownElement;
}
export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & ElementBase<'markdownElement'>;
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
@@ -477,7 +341,7 @@ export interface TextElement {
export interface MessageElement {
elementType: ElementType,
elementId: string,
extBufForUI: string,//"0x",
extBufForUI?: string, //"0x",
textElement?: TextElement;
faceElement?: FaceElement,
marketFaceElement?: MarketFaceElement,
@@ -506,7 +370,6 @@ export interface MessageElement {
taskTopMsgElement?: TaskTopMsgElement,
recommendedMsgElement?: RecommendedMsgElement,
actionBarElement?: ActionBarElement
}
export enum AtType {
@@ -514,6 +377,12 @@ export enum AtType {
atAll = 1,
atUser = 2
}
export enum MsgSourceType {
K_DOWN_SOURCETYPE_AIOINNER = 1,
K_DOWN_SOURCETYPE_BIGSCREEN = 2,
K_DOWN_SOURCETYPE_HISTORY = 3,
K_DOWN_SOURCETYPE_UNKNOWN = 0
}
// 来自Android分析
export enum ChatType {
@@ -569,7 +438,7 @@ export interface PttElement {
fileSize: string; // "4261"
fileSubId: string; // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1
formatType: number; // 1
invalidState: number; // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0
@@ -580,6 +449,7 @@ export interface PttElement {
voiceChangeType: number; // 0
voiceType: number; // 0
waveAmplitudes: number[];
autoConvertText: number;
}
export interface ArkElement {
@@ -593,6 +463,7 @@ export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn';
export interface PicElement {
md5HexStr?: string;
filePath?: string;
fileSize: number | string;//number
picWidth: number;
picHeight: number;
@@ -656,7 +527,8 @@ export interface GrayTipElement {
export enum FaceType {
normal = 1, // 小黄脸
normal2 = 2, // 新小黄脸, 从faceIndex 222开始
dice = 3 // 骰子
dice = 3, // 骰子
poke = 5 // 拍一拍
}
export enum FaceIndex {
@@ -783,7 +655,8 @@ export interface InlineKeyboardElementRowButton {
export interface InlineKeyboardElement {
rows: [{
buttons: InlineKeyboardElementRowButton[]
}];
}],
botAppid: string;
}
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
@@ -869,6 +742,8 @@ export interface RawMessage {
/**
* 扩展字段,与 Ob11 msg ID 有关
*/
id?: number;
guildId: string;
@@ -905,11 +780,26 @@ export interface RawMessage {
*/
peerUin: string;
/**
* 好友备注(如果是好友消息)
*/
remark?: string;
/**
* 群名(如果是群消息)
*/
peerName: string;
/**
* 发送者昵称(如果是好友消息)
*/
sendNickName: string;
/**
* 发送者好友备注(如果是群消息并且有发送者好友)
*/
sendRemarkName: string;
/**
* 发送者群名片(如果是群消息)
*/
@@ -930,4 +820,48 @@ export interface RawMessage {
records: RawMessage[];
elements: MessageElement[];
sourceType: MsgSourceType;
isOnlineMsg: boolean;
}
export interface QueryMsgsParams {
chatInfo: Peer;
filterMsgType: [];
filterSendersUid: string[];
filterMsgFromTime: string;
filterMsgToTime: string;
pageLimit: number;
isReverseOrder: boolean;
isIncludeCurrent: boolean;
}
export interface TmpChatInfoApi {
errMsg: string;
result: number;
tmpChatInfo?: TmpChatInfo;
}
export interface TmpChatInfo {
chatType: number;
fromNick: string;
groupCode: string;
peerUid: string;
sessionType: number;
sig: string;
}
export interface MsgReqType {
peer: Peer,
byType: number,
msgId: string,
msgSeq: string,
msgTime: string,
clientSeq: string,
cnt: number,
queryOrder: boolean,
includeSelf: boolean,
includeDeleteMsg: boolean,
extraCnt: number
}
//getMsgsIncludeSelf Peer必须 byType 1
//getMsgsWithMsgTimeAndClientSeqForC2C Peer必须 byType 3

View File

@@ -43,6 +43,50 @@ export enum GroupInviteType {
BYGROUPMEMBER,
BYDISCUSSMEMBER
}
export interface ShutUpGroupHonor {
[key: string]: number;
}
export interface ShutUpGroupMember {
uid: string;
qid: string;
uin: string;
nick: string;
remark: string;
cardType: number;
cardName: string;
role: number;
avatarPath: string;
shutUpTime: number;
isDelete: boolean;
isSpecialConcerned: boolean;
isSpecialShield: boolean;
isRobot: boolean;
groupHonor: ShutUpGroupHonor;
memberRealLevel: number;
memberLevel: number;
globalGroupLevel: number;
globalGroupPoint: number;
memberTitleId: number;
memberSpecialTitle: string;
specialTitleExpireTime: string;
userShowFlag: number;
userShowFlagNew: number;
richFlag: number;
mssVipType: number;
bigClubLevel: number;
bigClubFlag: number;
autoRemark: string;
creditLevel: number;
joinTime: number;
lastSpeakTime: number;
memberFlag: number;
memberFlagExt: number;
memberMobileFlag: number;
memberFlagExt2: number;
isSpecialShielded: boolean;
cardNameId: number;
}
export interface GroupNotify {
seq: string; // 通知序列号

View File

@@ -1,14 +1,15 @@
export interface IdMusicSignPostData {
type: 'qq' | '163',
type: 'qq' | '163' | 'kugou' | 'migu' | 'kuwo',
id: string | number,
}
export interface CustomMusicSignPostData {
type: 'custom',
type: 'qq' | '163' | 'kugou' | 'migu' | 'kuwo' | 'custom',
id: undefined,
url: string,
audio: string,
title: string,
image?: string,
audio?: string,
title?: string,
image: string,
singer?: string
}

View File

@@ -153,7 +153,10 @@ interface CommonExt {
labels: any[];
qqLevel: QQLevel;
}
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
interface Pic {
picId: string;
picTime: number;
@@ -213,7 +216,7 @@ export interface BuddyProfileLikeReq {
userProfile: number;
type: number;
start: number;
limit: number;
limit?: number;
}
export interface QQLevel {
@@ -285,16 +288,16 @@ export interface User {
export interface SelfInfo extends User {
online?: boolean;
}
export type Friend = User;
export interface Friend extends User {
}
// 本来是 Friend extends User 现在用不到
export enum BizKey {
KPRIVILEGEICON,
KPHOTOWALL
}
export interface UserDetailInfoByUinV2 {
export interface UserDetailInfoByUin {
result: number,
errMsg: string,
detail: {
@@ -305,62 +308,15 @@ export interface UserDetailInfoByUinV2 {
photoWall: null
}
}
export interface UserDetailInfoByUin {
result: number,
errMsg: string,
info: {
uid: string,//这个没办法用
qid: string,
uin: string,
nick: string,
remark: string,
longNick: string,
avatarUrl: string,
birthday_year: number,
birthday_month: number,
birthday_day: number,
sex: number,//0
topTime: string,
constellation: number,
shengXiao: number,
kBloodType: number,
homeTown: string,
makeFriendCareer: number,
pos: string,
eMail: string,
phoneNum: string,
college: string,
country: string,
province: string,
city: string,
postCode: string,
address: string,
isBlock: boolean,
isSpecialCareOpen: boolean,
isSpecialCareZone: boolean,
ringId: string,
regTime: number,
interest: string,
termType: number,
labels: any[],
qqLevel: { crownNum: number, sunNum: number, moonNum: number, starNum: number },
isHideQQLevel: number,
privilegeIcon: { jumpUrl: string, openIconList: any[], closeIconList: any[] },
isHidePrivilegeIcon: number,
photoWall: { picList: any[] },
vipFlag: boolean,
yearVipFlag: boolean,
svipFlag: boolean,
vipLevel: number,
status: number,
qidianMasterFlag: number,
qidianCrewFlag: number,
qidianCrewFlag2: number,
extStatus: number,
recommendImgFlag: number,
disableEmojiShortCuts: number,
pendantId: string,
vipNameColorId: string
}
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}

View File

@@ -1,34 +1,78 @@
{
"3.2.12-27187": {
"appid": 537240645,
"qua": "V1_LNX_NQ_3.2.12_27187_GW_B"
"9.9.15-28060": {
"appid": 537246092,
"qua": "V1_WIN_NQ_9.9.15_28060_GW_B"
},
"3.2.12-27206": {
"appid": 537240645,
"qua": "V1_LNX_NQ_3.2.12_27206_GW_B"
"9.9.15-28131": {
"appid": 537246092,
"qua": "V1_WIN_NQ_9.9.15_28131_GW_B"
},
"3.2.12-27254": {
"appid": 537240795,
"qua": "V1_LNX_NQ_3.2.12_27254_GW_B"
"3.2.12-28060": {
"appid": 537246140,
"qua": "V1_LNX_NQ_3.2.12_28060_GW_B"
},
"9.9.15-27187": {
"appid": 537240610,
"qua": "V1_WIN_NQ_9.9.15_27187_GW_B"
"3.2.12-28131": {
"appid": 537246140,
"qua": "V1_LNX_NQ_3.2.12_28131_GW_B"
},
"9.9.15-27206": {
"appid": 537240610,
"qua": "V1_WIN_NQ_9.9.15_27206_GW_B"
"6.9.55-28131": {
"appid": 537246115,
"qua": "V1_MAC_NQ_6.9.55_28131_GW_B"
},
"9.9.15-27254": {
"appid": 537240709,
"qua": "V1_WIN_NQ_9.9.15_27254_GW_B"
"9.9.15-28327": {
"appid": 537249321,
"qua": "V1_WIN_NQ_9.9.15_28327_GW_B"
},
"9.9.15-27333": {
"appid": 537240709,
"qua": "V1_WIN_NQ_9.9.15_27333_GW_B"
"3.2.12-28327": {
"appid": 537249393,
"qua": "V1_LNX_NQ_3.2.12_28327_GW_B"
},
"9.9.15-27391": {
"appid": 537240709,
"qua": "V1_WIN_NQ_9.9.15_27333_GW_B"
"9.9.15-28418": {
"appid": 537249321,
"qua": "V1_WIN_NQ_9.9.15_28418_GW_B"
},
"3.2.12-28418": {
"appid": 537249393,
"qua": "V1_LNX_NQ_3.2.12_28418_GW_B"
},
"6.9.56-28418": {
"appid": 537249367,
"qua": "V1_MAC_NQ_6.9.56_28418_GW_B"
},
"9.9.15-28498": {
"appid": 537249321,
"qua": "V1_WIN_NQ_9.9.15_28498_GW_B"
},
"3.2.13-28788": {
"appid": 537249787,
"qua": "V1_LNX_NQ_3.2.13_28788_GW_B"
},
"9.9.16-28788": {
"appid": 537249739,
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
},
"9.9.16-28971": {
"appid": 537249775,
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
},
"3.2.13-28971": {
"appid": 537249848,
"qua": "V1_LNX_NQ_3.2.13_28971_GW_B"
},
"6.9.58-28971": {
"appid": 537249826,
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
},
"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

@@ -2,5 +2,7 @@
"fileLog": true,
"consoleLog": true,
"fileLogLevel": "debug",
"consoleLogLevel": "info"
"consoleLogLevel": "info",
"packetBackend": "auto",
"packetServer": ""
}

58
src/core/external/offset.json vendored Normal file
View File

@@ -0,0 +1,58 @@
{
"6.9.56-28418-arm64": {
"send": "4471360",
"recv": "4473BCC"
},
"3.2.12-28418-x64": {
"recv": "A0723E0",
"send": "A06EAE0"
},
"9.9.15-28418-x64": {
"recv": "37A9004",
"send": "37A4BD0"
},
"9.9.15-28498-x64": {
"recv": "37A9004",
"send": "37A4BD0"
},
"9.9.16-28788-x64": {
"send": "38076D0",
"recv": "380BB04"
},
"3.2.13-28788-x64": {
"send": "A0CEC20",
"recv": "A0D2520"
},
"3.2.13-28788-arm64": {
"send": "6E91018",
"recv": "6E94850"
},
"9.9.16-28971-x64": {
"send": "38079F0",
"recv": "380BE24"
},
"3.2.13-28971-x64": {
"send": "A0CEF60",
"recv": "A0D2860"
},
"3.2.12-28971-arm64": {
"send": "6E91318",
"recv": "6E94B50"
},
"6.9.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

@@ -0,0 +1,31 @@
syntax = 'proto3';
package SysMessage;
message EmojiLikeToOthersWrapper1 {
EmojiLikeToOthersWrapper2 wrapper = 1;
}
message EmojiLikeToOthersWrapper2 {
EmojiLikeToOthersWrapper3 body = 1;
}
message EmojiLikeToOthersWrapper3 {
EmojiLikeToOthersMsgSpec msgSpec = 2;
EmojiLikeToOthersAttributes attributes = 3;
}
message EmojiLikeToOthersMsgSpec {
uint32 msgSeq = 1;
}
message EmojiLikeToOthersAttributes {
enum Operation {
FALLBACK = 0;
LIKE = 1;
UNLIKE = 2;
}
string emojiId = 1;
string senderUid = 4;
Operation operation = 5;
}

View File

@@ -0,0 +1,9 @@
syntax = 'proto3';
package SysMessage;
message GreyTipWrapper {
uint32 subTypeId = 1;
uint32 groupCode = 4;
uint32 subTypeIdMinusOne = 13;
bytes rest = 44;
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
package SysMessage;
message likeDetail {
string txt = 1;
int64 uin = 3;
string nickname = 5;
}
message likeMsg {
int32 times = 1;
int32 time = 2;
likeDetail detail = 3;
}
message profileLikeTip {
likeMsg msg = 14;
}

View File

@@ -0,0 +1,36 @@
syntax = 'proto3';
package SysMessage;
message SysMessage {
repeated SysMessageHeader header = 1;
repeated SysMessageMsgSpec msgSpec = 2;
SysMessageBodyWrapper bodyWrapper = 3;
}
message SysMessageHeader {
uint32 PeerNumber = 1;
string PeerString = 2;
uint32 Uin = 5;
optional string Uid = 6;
}
message SysMessageMsgSpec {
uint32 msgType = 1;
uint32 subType = 2;
uint32 subSubType = 3;
uint32 msgSeq = 5;
uint32 time = 6;
uint64 msgId = 12;
uint32 other = 13;
}
message SysMessageBodyWrapper {
bytes wrappedBody = 2;
// Find the first [08], or ignore the first 7 bytes?
// And it becomes another ProtoBuf message.
}
message KeyValuePair {
string key = 1;
string value = 2;
}

View File

@@ -1,11 +1,9 @@
import { ConfigBase } from '@/common/utils/config-base';
import { ConfigBase } from '@/common/config-base';
import napCatDefaultConfig from '@/core/external/napcat.json';
import { NapCatCore } from '@/core';
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export type NapCatConfig = typeof napCatDefaultConfig;
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class NapCatConfigLoader extends ConfigBase<NapCatConfig> {
constructor(core: NapCatCore, configPath: string) {
super('napcat', core, configPath);

View File

@@ -1,5 +1,5 @@
import { LogWrapper } from '@/common/utils/log';
import { RequestUtil } from '@/common/utils/request';
import { LogWrapper } from '@/common/log';
import { RequestUtil } from '@/common/request';
interface ServerRkeyData {
group_rkey: string;
@@ -8,7 +8,7 @@ interface ServerRkeyData {
}
export class RkeyManager {
serverUrl: string = '';
serverUrl: string[] = [];
logger: LogWrapper;
private rkeyData: ServerRkeyData = {
group_rkey: '',
@@ -16,7 +16,7 @@ export class RkeyManager {
expired_time: 0,
};
constructor(serverUrl: string, logger: LogWrapper) {
constructor(serverUrl: string[], logger: LogWrapper) {
this.logger = logger;
this.serverUrl = serverUrl;
}
@@ -26,7 +26,8 @@ export class RkeyManager {
try {
await this.refreshRkey();
} catch (e) {
this.logger.logError('获取rkey失败', e);
throw new Error(`获取rkey失败: ${e}`);//外抛
//this.logger.logError.bind(this.logger)('获取rkey失败', e);
}
}
return this.rkeyData;
@@ -40,6 +41,22 @@ export class RkeyManager {
async refreshRkey(): Promise<any> {
//刷新rkey
this.rkeyData = await RequestUtil.HttpGetJson<ServerRkeyData>(this.serverUrl, 'GET');
for (const url of this.serverUrl) {
try {
const temp = await RequestUtil.HttpGetJson<ServerRkeyData>(url, 'GET');
this.rkeyData = {
group_rkey: temp.group_rkey.slice(6),
private_rkey: temp.private_rkey.slice(6),
expired_time: temp.expired_time
};
} catch (e) {
this.logger.logError.bind(this.logger)(`[Rkey] Get Rkey ${url} Error `, e);
//是否为最后一个url
if (url === this.serverUrl[this.serverUrl.length - 1]) {
throw new Error(`获取rkey失败: ${e}`);//外抛
}
}
}
}
}

View File

@@ -1,5 +1,353 @@
export * from './core';
import {
NTQQFileApi,
NTQQFriendApi,
NTQQGroupApi,
NTQQMsgApi,
NTQQSystemApi,
NTQQUserApi,
NTQQWebApi,
} from '@/core/apis';
import { NTQQCollectionApi } from '@/core/apis/collection';
import {
NodeIQQNTWrapperSession,
NodeQQNTWrapperUtil,
PlatformType,
VendorType,
WrapperNodeApi,
WrapperSessionInitConfig,
} from '@/core/wrapper';
import { LogLevel, LogWrapper } from '@/common/log';
import { NodeIKernelLoginService } from '@/core/services';
import { QQBasicInfoWrapper } from '@/common/qq-basic-info';
import { NapCatPathWrapper } from '@/common/path';
import path from 'node:path';
import fs from 'node:fs';
import { hostname, systemName, systemVersion } from '@/common/system';
import { NTEventWrapper } from '@/common/event';
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/entities';
import { NapCatConfigLoader } from '@/core/helper/config';
import os from 'node:os';
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
export * from './wrapper';
export * from './entities';
export * from './services';
export * from './listeners';
export enum NapCatCoreWorkingEnv {
Unknown = 0,
Shell = 1,
Framework = 2,
}
export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
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 wrapperNodePath = path.resolve(appPath, 'wrapper.node');
if (!fs.existsSync(wrapperNodePath)) {
wrapperNodePath = path.join(appPath, `./resources/app/wrapper.node`);
}
//老版本兼容 未来去掉
if (!fs.existsSync(wrapperNodePath)) {
wrapperNodePath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/wrapper.node`);
}
const nativemodule: any = { exports: {} };
process.dlopen(nativemodule, wrapperNodePath);
return nativemodule.exports;
}
export function getMajorPath(QQVersion: string): string {
// major.node
let appPath;
if (os.platform() === 'darwin') {
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
} else if (os.platform() === 'linux') {
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
} else {
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
}
let majorPath = path.resolve(appPath, 'major.node');
if (!fs.existsSync(majorPath)) {
majorPath = path.join(appPath, `./resources/app/major.node`);
}
//老版本兼容 未来去掉
if (!fs.existsSync(majorPath)) {
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
}
return majorPath;
}
export class NapCatCore {
readonly context: InstanceContext;
readonly apis: StableNTApiWrapper;
readonly eventWrapper: NTEventWrapper;
// readonly eventChannel: NTEventChannel;
NapCatDataPath: string;
NapCatTempPath: string;
// runtime info, not readonly
selfInfo: SelfInfo;
util: NodeQQNTWrapperUtil;
configLoader: NapCatConfigLoader;
// 通过构造器递过去的 runtime info 应该尽量少
constructor(context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session);
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),
SystemApi: new NTQQSystemApi(this.context, this),
CollectionApi: new NTQQCollectionApi(this.context, this),
PacketApi: new NTQQPacketApi(this.context, this),
WebApi: new NTQQWebApi(this.context, this),
FriendApi: new NTQQFriendApi(this.context, this),
MsgApi: new NTQQMsgApi(this.context, this),
UserApi: new NTQQUserApi(this.context, this),
GroupApi: new NTQQGroupApi(this.context, this),
};
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
// 创建临时目录
if (!fs.existsSync(this.NapCatTempPath)) {
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
}
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
this.context.logger.setFileLogEnabled(
this.configLoader.configData.fileLog,
);
this.context.logger.setConsoleLogEnabled(
this.configLoader.configData.consoleLog,
);
this.context.logger.setFileAndConsoleLogLevel(
this.configLoader.configData.fileLogLevel as LogLevel,
this.configLoader.configData.consoleLogLevel as LogLevel,
);
}
get dataPath(): string {
let result = this.context.wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
if (!result) {
result = path.resolve(os.homedir(), './.config/QQ');
fs.mkdirSync(result, { recursive: true });
}
return result;
}
// Renamed from 'InitDataListener'
async initNapCatCoreListeners() {
const msgListener = new NodeIKernelMsgListener();
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
this.context.logger.logError.bind(this.context.logger)('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.selfInfo.online = false;
};
msgListener.onRecvMsg = (msgs) => {
msgs.forEach(msg => this.context.logger.logMessage(msg, this.selfInfo));
};
msgListener.onAddSendMsg = (msg) => {
this.context.logger.logMessage(msg, this.selfInfo);
};
//await sleep(2500);
this.context.session.getMsgService().addKernelMsgListener(
proxiedListenerOf(msgListener, this.context.logger),
);
const profileListener = new NodeIKernelProfileListener();
profileListener.onProfileDetailInfoChanged = (profile) => {
if (profile.uid === this.selfInfo.uid) {
Object.assign(this.selfInfo, profile);
}
};
profileListener.onSelfStatusChanged = (Info: SelfStatusInfo) => {
if (Info.status == 20) {
this.selfInfo.online = false;
this.context.logger.log("账号状态变更为离线");
} else {
this.selfInfo.online = true;
}
};
this.context.session.getProfileService().addKernelProfileListener(
proxiedListenerOf(profileListener, this.context.logger),
);
// 群相关
const groupListener = new NodeIKernelGroupListener();
groupListener.onGroupListUpdate = (updateType, groupList) => {
// console.log("onGroupListUpdate", updateType, groupList)
groupList.map(g => {
const existGroup = this.apis.GroupApi.groupCache.get(g.groupCode);
//群成员数量变化 应该刷新缓存
if (existGroup && g.memberCount === existGroup.memberCount) {
Object.assign(existGroup, g);
} else {
this.apis.GroupApi.groupCache.set(g.groupCode, g);
// 获取群成员
}
const sceneId = this.context.session.getGroupService().createMemberListScene(g.groupCode, 'groupMemberList_MainWindow');
this.context.session.getGroupService().getNextMemberList(sceneId, undefined, 3000).then( /* r => {
// console.log(`get group ${g.groupCode} members`, r);
// r.result.infos.forEach(member => {
// });
// groupMembers.set(g.groupCode, r.result.infos);
} */);
this.context.session.getGroupService().destroyMemberListScene(sceneId);
});
};
groupListener.onMemberListChange = (arg) => {
// todo: 应该加一个内部自己维护的成员变动callback用于判断成员变化通知
const groupCode = arg.sceneId.split('_')[0];
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
arg.infos.forEach((member, uid) => {
//console.log('onMemberListChange', member);
const existMember = existMembers.get(uid);
if (existMember) {
Object.assign(existMember, member);
} else {
existMembers!.set(uid, member);
}
//移除成员
if (member.isDelete) {
existMembers.delete(uid);
}
});
} else {
this.apis.GroupApi.groupMemberCache.set(groupCode, arg.infos);
}
};
groupListener.onMemberInfoChange = (groupCode, dataSource, members) => {
if (dataSource === DataSource.LOCAL && members.get(this.selfInfo.uid)?.isDelete) {
// 自身退群或者被踢退群 5s用于Api操作 之后不再出现
setTimeout(() => {
this.apis.GroupApi.groupCache.delete(groupCode);
}, 5000);
}
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode);
if (existMembers) {
members.forEach((member, uid) => {
const existMember = existMembers.get(uid);
if (existMember) {
// 检查管理变动
member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember);
// 更新成员信息
Object.assign(existMember, member);
} else {
existMembers.set(uid, member);
}
//移除成员
if (member.isDelete) {
existMembers.delete(uid);
}
});
} else {
this.apis.GroupApi.groupMemberCache.set(groupCode, members);
}
};
this.context.session.getGroupService().addKernelGroupListener(
proxiedListenerOf(groupListener, this.context.logger),
);
}
checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean {
if (memberNew.role !== memberOld?.role) {
this.context.logger.logDebug(`${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`);
return true;
}
return false;
}
}
export async function genSessionConfig(
guid: string,
QQVersionAppid: string,
QQVersion: string,
selfUin: string,
selfUid: string,
account_path: string
): Promise<WrapperSessionInitConfig> {
const downloadPath = path.join(account_path, 'NapCat', 'temp');
fs.mkdirSync(downloadPath, { recursive: true });
const platformMapping: Partial<Record<NodeJS.Platform, PlatformType>> = {
win32: PlatformType.KWINDOWS,
darwin: PlatformType.KMAC,
linux: PlatformType.KLINUX,
};
const systemPlatform = platformMapping[os.platform()] ?? PlatformType.KWINDOWS;
return {
selfUin,
selfUid,
desktopPathConfig: {
account_path, // 可以通过NodeQQNTWrapperUtil().getNTUserDataInfoConfig()获取
},
clientVer: QQVersion,
a2: '',
d2: '',
d2Key: '',
machineId: '',
platform: systemPlatform, // 3是Windows?
platVer: systemVersion, // 系统版本号, 应该可以固定
appid: QQVersionAppid,
rdeliveryConfig: {
appKey: '',
systemId: 0,
appId: '',
logicEnvironment: '',
platform: systemPlatform,
language: '',
sdkVersion: '',
userId: '',
appVersion: '',
osVersion: '',
bundleId: '',
serverUrl: '',
fixedAfterHitKeys: [''],
},
defaultFileDownloadPath: downloadPath,
deviceInfo: {
guid,
buildVer: QQVersion,
localId: 2052,
devName: hostname,
devType: systemName,
vendorName: '',
osVer: systemVersion,
vendorOsName: systemName,
setMute: false,
vendorType: VendorType.KNOSETONIOS,
},
deviceConfig: '{"appearance":{"isSplitViewMode":true},"msg":{}}',
};
}
export interface InstanceContext {
readonly workingEnv: NapCatCoreWorkingEnv;
readonly wrapper: WrapperNodeApi;
readonly session: NodeIQQNTWrapperSession;
readonly logger: LogWrapper;
readonly loginService: NodeIKernelLoginService;
readonly basicInfoWrapper: QQBasicInfoWrapper;
readonly pathWrapper: NapCatPathWrapper;
}
export interface StableNTApiWrapper {
FileApi: NTQQFileApi,
SystemApi: NTQQSystemApi,
PacketApi: NTQQPacketApi,
CollectionApi: NTQQCollectionApi,
WebApi: NTQQWebApi,
FriendApi: NTQQFriendApi,
MsgApi: NTQQMsgApi,
UserApi: NTQQUserApi,
GroupApi: NTQQGroupApi
}

View File

@@ -1,5 +1,13 @@
export class NodeIKernelFileAssistantListener {
onFileStatusChanged(...args: unknown[]) {
onFileStatusChanged(fileStatus: {
id: string,
fileStatus: number,
fileProgress: `${number}`,
fileSize: `${number}`,
fileSpeed: number,
thumbPath: string | null,
filePath: string | null,
}) {
}
onSessionListChanged(...args: unknown[]) {
@@ -11,6 +19,42 @@ export class NodeIKernelFileAssistantListener {
onFileListChanged(...args: unknown[]) {
}
onFileSearch(...args: unknown[]) {
onFileSearch(searchResult: SearchResultWrapper) {
}
}
export type SearchResultWrapper = {
searchId: number,
resultId: number,
hasMore: boolean,
resultItems: SearchResultItem[],
};
export type SearchResultItem = {
id: string,
fileName: string,
fileNameHits: string[],
fileStatus: number,
fileSize: string,
isSend: boolean,
source: number,
fileTime: string,
expTime: string,
session: {
context: null,
uid: string,
nick: string,
remark: string,
memberCard: string,
groupCode: string,
groupName: string,
groupRemark: string,
count: number,
},
thumbPath: string,
filePath: string,
msgId: string,
chatType: number,
peerUid: string,
fileType: number,
};

View File

@@ -1,6 +1,7 @@
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/core/entities';
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/entities';
export class NodeIKernelGroupListener {
onGroupListInited(listEmpty: boolean): void { }
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void {
@@ -70,7 +71,8 @@ export class NodeIKernelGroupListener {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean,
hasPrev: boolean,
hasNext: boolean,
hasRobot: boolean
}) {
}
@@ -78,6 +80,6 @@ export class NodeIKernelGroupListener {
onSearchMemberChange(...args: unknown[]) {
}
onShutUpMemberListChanged(...args: unknown[]) {
onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>) {
}
}

View File

@@ -1,4 +1,5 @@
import { ChatType, RawMessage } from '@/core/entities';
import { CommonFileInfo } from '@/core';
export interface OnRichMediaDownloadCompleteParams {
fileModelId: string,
@@ -15,7 +16,7 @@ export interface OnRichMediaDownloadCompleteParams {
totalSize: string,
trasferStatus: number,
step: number,
commonFileInfo: unknown | null,
commonFileInfo?: CommonFileInfo,
fileSrvErrCode: string,
clientMsg: string,
businessId: number,
@@ -28,7 +29,47 @@ export interface GroupFileInfoUpdateParamType {
retMsg: string;
clientWording: string;
isEnd: boolean;
item: Array<any>;
item: Array<{
peerId: string;
type: number;
folderInfo?: {
folderId: string;
parentFolderId: string;
folderName: string;
createTime: number;
modifyTime: number;
createUin: string;
creatorName: string;
totalFileCount: number;
modifyUin: string;
modifyName: string;
usedSpace: string;
},
fileInfo?: {
fileModelId: string;
fileId: string;
fileName: string;
fileSize: string;
busId: number;
uploadedSize: string;
uploadTime: number;
deadTime: number;
modifyTime: number;
downloadTimes: number;
sha: string;
sha3: string;
md5: string;
uploaderLocalPath: string;
uploaderName: string;
uploaderUin: string;
parentFolderId: string;
localPath: string;
transStatus: number;
transType: number;
elementId: string;
isFolder: boolean;
},
}>;
allFileCount: string;
nextIndex: string;
reqId: string;

View File

@@ -0,0 +1,97 @@
import { ChatType } from '@/core';
export interface SearchGroupInfo {
groupCode: string;
ownerUid: string;
groupFlag: number;
groupFlagExt: number;
maxMemberNum: number;
memberNum: number;
groupOption: number;
classExt: number;
groupName: string;
fingerMemo: string;
groupQuestion: string;
certType: number;
shutUpAllTimestamp: number;
shutUpMeTimestamp: number;
groupTypeFlag: number;
privilegeFlag: number;
groupSecLevel: number;
groupFlagExt3: number;
isConfGroup: number;
isModifyConfGroupFace: number;
isModifyConfGroupName: number;
noFigerOpenFlag: number;
noCodeFingerOpenFlag: number;
groupFlagExt4: number;
groupMemo: string;
cmdUinMsgSeq: number;
cmdUinJoinTime: number;
cmdUinUinFlag: number;
cmdUinMsgMask: number;
groupSecLevelInfo: number;
cmdUinPrivilege: number;
cmdUinFlagEx2: number;
appealDeadline: number;
remarkName: string;
isTop: boolean;
richFingerMemo: string;
groupAnswer: string;
joinGroupAuth: string;
isAllowModifyConfGroupName: number;
}
export interface GroupInfo {
groupCode: string;
searchGroupInfo: SearchGroupInfo;
privilege: number;
}
export interface GroupSearchResult {
keyWord: string;
errorCode: number;
groupInfos: GroupInfo[];
penetrate: string;
isEnd: boolean;
nextPos: number;
}
export interface NodeIKernelSearchListener {
onSearchGroupResult(params: GroupSearchResult): void;
onSearchFileKeywordsResult(params: {
searchId: string,
hasMore: boolean,
resultItems: {
chatType: ChatType,
buddyChatInfo: any[],
discussChatInfo: any[],
groupChatInfo: {
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}[],
dataLineChatInfo: any[],
tmpChatInfo: any[],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: number,
fileSize: string,
filePath: string,
fileName: string,
hits: {
start: number,
end: number
}[]
}[]
}): void;
}

View File

@@ -1,39 +0,0 @@
import { ChatType } from '@/core';
export interface NodeIKernelSearchListener_Polyfill {
onSearchFileKeywordsResult(params: {
searchId: string,
hasMore: boolean,
resultItems: {
chatType: ChatType,
buddyChatInfo: any[],
discussChatInfo: any[],
groupChatInfo: {
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}[],
dataLineChatInfo: any[],
tmpChatInfo: any[],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: number,
fileSize: string,
filePath: string,
fileName: string,
hits: {
start: number,
end: number
}[]
}[]
}): void;
}

View File

@@ -0,0 +1,5 @@
export class NodeIO3MiscListener {
getOnAmgomDataPiece(...arg: unknown[]) {
}
}

View File

@@ -9,7 +9,7 @@ export * from './NodeIKernelProfileListener';
export * from './NodeIKernelTicketListener';
export * from './NodeIKernelStorageCleanListener';
export * from './NodeIKernelFileAssistantListener';
export * from './NodeIKernelSearchListener_Polyfill';
export * from './NodeIKernelSearchListener';
import type {
NodeIKernelBuddyListener,
@@ -22,8 +22,8 @@ import type {
NodeIKernelSessionListener,
NodeIKernelStorageCleanListener,
NodeIKernelTicketListener,
NodeIKernelSearchListener_Polyfill,
} from '.';
import { NodeIKernelSearchListener } from './NodeIKernelSearchListener';
export type ListenerNamingMapping = {
NodeIKernelSessionListener: NodeIKernelSessionListener;
@@ -36,5 +36,5 @@ export type ListenerNamingMapping = {
NodeIKernelTicketListener: NodeIKernelTicketListener;
NodeIKernelStorageCleanListener: NodeIKernelStorageCleanListener;
NodeIKernelFileAssistantListener: NodeIKernelFileAssistantListener;
NodeIKernelSearchListener: NodeIKernelSearchListener_Polyfill;
NodeIKernelSearchListener: NodeIKernelSearchListener;
};

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) => {
const timeoutHandle = setTimeout(() => {
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
}, timeout);
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
sendcb(json);
if (!rsp) {
clearTimeout(timeoutHandle);
resolve(json);
}
});
if (rsp) {
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
clearTimeout(timeoutHandle);
resolve(json);
});
}
this.sendCommandImpl(cmd, data, trace_id);
});
}
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

@@ -0,0 +1,72 @@
import * as stream from 'node:stream';
import { ReadStream } from "node:fs";
import { PacketHighwaySig } from "@/core/packet/highway/session";
import { HighwayHttpUploader, HighwayTcpUploader } from "@/core/packet/highway/uploader";
import { LogWrapper } from "@/common/log";
export interface PacketHighwayTrans {
uin: string;
cmd: number;
command: string;
data: stream.Readable;
sum: Uint8Array;
size: number;
ticket: Uint8Array;
loginSig?: Uint8Array;
ext: Uint8Array;
encrypt: boolean;
timeout?: number;
server: string;
port: number;
}
export class PacketHighwayClient {
sig: PacketHighwaySig;
server: string = 'htdata3.qq.com';
port: number = 80;
logger: LogWrapper;
constructor(sig: PacketHighwaySig, logger: LogWrapper, server: string = 'htdata3.qq.com', port: number = 80) {
this.sig = sig;
this.logger = logger;
}
changeServer(server: string, port: number) {
this.server = server;
this.port = port;
}
private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 1200): PacketHighwayTrans {
return {
uin: this.sig.uin,
cmd: cmd,
command: 'PicUp.DataUp',
data: data,
sum: md5,
size: fileSize,
ticket: this.sig.sigSession!,
ext: extendInfo,
encrypt: false,
timeout: timeout,
server: this.server,
port: this.port,
} as PacketHighwayTrans;
}
async upload(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array): Promise<void> {
const trans = this.buildDataUpTrans(cmd, data, fileSize, md5, extendInfo);
try {
const tcpUploader = new HighwayTcpUploader(trans, this.logger);
await tcpUploader.upload();
} catch (e) {
this.logger.logError(`[Highway] upload failed: ${e}, fallback to http upload`);
try {
const httpUploader = new HighwayHttpUploader(trans, this.logger);
await httpUploader.upload();
} catch (e) {
this.logger.logError(`[Highway] http upload failed: ${e}`);
throw e;
}
}
}
}

View File

@@ -0,0 +1,23 @@
import assert from "node:assert";
export class Frame{
static pack(head: Buffer, body: Buffer): Buffer {
const totalLength = 9 + head.length + body.length + 1;
const buffer = Buffer.allocUnsafe(totalLength);
buffer[0] = 0x28;
buffer.writeUInt32BE(head.length, 1);
buffer.writeUInt32BE(body.length, 5);
head.copy(buffer, 9);
body.copy(buffer, 9 + head.length);
buffer[totalLength - 1] = 0x29;
return buffer;
}
static unpack(frame: Buffer): [Buffer, Buffer] {
assert(frame[0] === 0x28 && frame[frame.length - 1] === 0x29, 'Invalid frame!');
const headLen = frame.readUInt32BE(1);
const bodyLen = frame.readUInt32BE(5);
// assert(frame.length === 9 + headLen + bodyLen + 1, `Frame ${frame.toString('hex')} length does not match head and body lengths!`);
return [frame.subarray(9, 9 + headLen), frame.subarray(9 + headLen, 9 + headLen + bodyLen)];
}
}

View File

@@ -0,0 +1,570 @@
import * as fs from "node:fs";
import { ChatType, Peer } from "@/core";
import { LogWrapper } from "@/common/log";
import { PacketPacker } from "@/core/packet/packer";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
import { PacketHighwayClient } from "@/core/packet/highway/client";
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase";
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { FileUploadExt, NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway";
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
import { PacketClient } from "@/core/packet/client/client";
export const BlockSize = 1024 * 1024;
interface HighwayServerAddr {
ip: string
port: number
}
export interface PacketHighwaySig {
uin: string;
uid: string;
sigSession: Uint8Array | null
sessionKey: Uint8Array | null
serverAddr: HighwayServerAddr[]
}
export class PacketHighwaySession {
protected packetClient: PacketClient;
protected packetHighwayClient: PacketHighwayClient;
protected sig: PacketHighwaySig;
protected logger: LogWrapper;
protected packer: PacketPacker;
private cachedPrepareReq: Promise<void> | null = null;
constructor(logger: LogWrapper, client: PacketClient, packer: PacketPacker) {
this.packetClient = client;
this.logger = logger;
this.sig = {
uin: this.packetClient.napCatCore.selfInfo.uin,
uid: this.packetClient.napCatCore.selfInfo.uid,
sigSession: null,
sessionKey: null,
serverAddr: [],
};
this.packer = packer;
this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger);
}
private async checkAvailable() {
if (!this.packetClient.available) {
throw new Error('packetBackend不可用请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置');
}
if (this.sig.sigSession === null || this.sig.sessionKey === null) {
this.logger.logWarn('[Highway] sigSession or sessionKey not available!');
if (this.cachedPrepareReq === null) {
this.cachedPrepareReq = this.prepareUpload().finally(() => {
this.cachedPrepareReq = null;
});
}
await this.cachedPrepareReq;
}
}
private async prepareUpload(): Promise<void> {
const packet = this.packer.packHttp0x6ff_501();
const req = await this.packetClient.sendPacket('HttpConn.0x6ff_501', packet, true);
const rsp = new NapProtoMsg(HttpConn0x6ff_501Response).decode(
Buffer.from(req.hex_data, 'hex')
);
this.sig.sigSession = rsp.httpConn.sigSession;
this.sig.sessionKey = rsp.httpConn.sessionKey;
for (const info of rsp.httpConn.serverInfos) {
if (info.serviceType !== 1) continue;
for (const addr of info.serverAddrs) {
this.logger.logDebug(`[Highway PrepareUpload] server addr add: ${int32ip2str(addr.ip)}:${addr.port}`);
this.sig.serverAddr.push({
ip: int32ip2str(addr.ip),
port: addr.port
});
}
}
}
async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> {
await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupImageReq(+peer.peerUid, img);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CImageReq(peer.peerUid, img);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
}
async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise<void> {
await this.checkAvailable();
if (+(video.fileSize ?? 0) > 1024 * 1024 * 100) {
throw new Error(`[Highway] 视频文件过大: ${(+(video.fileSize ?? 0) / (1024 * 1024)).toFixed(2)} MB > 100 MB请使用文件上传`);
}
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupVideoReq(+peer.peerUid, video);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CVideoReq(peer.peerUid, video);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
}
async uploadPtt(peer: Peer, ptt: PacketMsgPttElement): Promise<void> {
await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupPttReq(+peer.peerUid, ptt);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CPttReq(peer.peerUid, ptt);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
}
async uploadFile(peer: Peer, file: PacketMsgFileElement): Promise<void> {
await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupFileReq(+peer.peerUid, file);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CFileReq(peer.peerUid, file);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
}
private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const preReq = await this.packer.packUploadGroupImgReq(groupUin, img);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
1004,
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
img.size,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`);
}
img.msgInfo = preRespData.upload.msgInfo;
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
}
private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const preReq = await this.packer.packUploadC2CImgReq(peerUid, img);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
1003,
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
img.size,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
}
img.msgInfo = preRespData.upload.msgInfo;
}
private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<void> {
if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty");
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
const preReq = await this.packer.packUploadGroupVideoReq(groupUin, video);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: await calculateSha1StreamBytes(video.filePath!)
}
});
await this.packetHighwayClient.upload(
1005,
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
+video.fileSize!,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
}
const subFile = preRespData.upload.subFileInfos[0];
if (subFile.uKey && subFile.uKey != "") {
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: subFile.uKey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
1006,
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
+video.thumbSize!,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
}
video.msgInfo = preRespData.upload.msgInfo;
}
private async uploadC2CVideoReq(peerUid: string, video: PacketMsgVideoElement): Promise<void> {
if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty");
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
const preReq = await this.packer.packUploadC2CVideoReq(peerUid, video);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: await calculateSha1StreamBytes(video.filePath!)
}
});
await this.packetHighwayClient.upload(
1001,
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
+video.fileSize!,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
}
const subFile = preRespData.upload.subFileInfos[0];
if (subFile.uKey && subFile.uKey != "") {
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: subFile.uKey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
1002,
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
+video.thumbSize!,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
}
video.msgInfo = preRespData.upload.msgInfo;
}
private async uploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise<void> {
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
const preReq = await this.packer.packUploadGroupPttReq(groupUin, ptt);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
1008,
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
ptt.fileSize,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupPttReq get upload invalid ukey ${ukey}, don't need upload!`);
}
ptt.msgInfo = preRespData.upload.msgInfo;
}
private async uploadC2CPttReq(peerUid: string, ptt: PacketMsgPttElement): Promise<void> {
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
const preReq = await this.packer.packUploadC2CPttReq(peerUid, ptt);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
hash: {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
1007,
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
ptt.fileSize,
md5,
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CPttReq get upload invalid ukey ${ukey}, don't need upload!`);
}
ptt.msgInfo = preRespData.upload.msgInfo;
}
private async uploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise<void> {
file.isGroupFile = true;
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
file.fileSha1 = await calculateSha1(file.filePath);
const preReq = await this.packer.packUploadGroupFileReq(groupUin, file);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(preResp.body);
if (!preRespData?.upload?.boolFileExist) {
this.logger.logDebug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
const ext = new NapProtoMsg(FileUploadExt).encode({
unknown1: 100,
unknown2: 1,
entry: {
busiBuff: {
senderUin: BigInt(this.sig.uin),
receiverUin: BigInt(groupUin),
groupCode: BigInt(groupUin),
},
fileEntry: {
fileSize: BigInt(file.fileSize),
md5: file.fileMd5,
md5S2: file.fileMd5,
checkKey: preRespData.upload.checkKey,
fileId: preRespData.upload.fileId,
uploadKey: preRespData.upload.fileKey,
},
clientInfo: {
clientType: 3,
appId: "100",
terminalType: 3,
clientVer: "1.1.1",
unknown: 4
},
fileNameInfo: {
fileName: file.fileName
},
host: {
hosts: [
{
url: {
host: preRespData.upload.uploadIp,
unknown: 1,
},
port: preRespData.upload.uploadPort,
}
]
}
},
unknown200: 0,
});
await this.packetHighwayClient.upload(
71,
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
file.fileSize,
file.fileMd5,
ext
);
} else {
this.logger.logDebug(`[Highway] uploadGroupFileReq file exist, don't need upload!`);
}
file.fileUuid = preRespData.upload.fileId;
}
private async uploadC2CFileReq(peerUid: string, file: PacketMsgFileElement): Promise<void> {
file.isGroupFile = false;
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
file.fileSha1 = await calculateSha1(file.filePath);
const preReq = await this.packer.packUploadC2CFileReq(this.sig.uid, peerUid, file);
const preRespRaw = await this.packetClient.sendOidbPacket( preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0XE37Response).decode(preResp.body);
if (!preRespData.upload?.boolFileExist) {
this.logger.logDebug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
const ext = new NapProtoMsg(FileUploadExt).encode({
unknown1: 100,
unknown2: 1,
entry: {
busiBuff: {
senderUin: BigInt(this.sig.uin),
},
fileEntry: {
fileSize: BigInt(file.fileSize),
md5: file.fileMd5,
md5S2: file.fileMd5,
checkKey: file.fileSha1,
fileId: preRespData.upload?.uuid,
uploadKey: preRespData.upload?.mediaPlatformUploadKey,
},
clientInfo: {
clientType: 3,
appId: "100",
terminalType: 3,
clientVer: "1.1.1",
unknown: 4
},
fileNameInfo: {
fileName: file.fileName
},
host: {
hosts: [
{
url: {
host: preRespData.upload?.uploadIp,
unknown: 1,
},
port: preRespData.upload?.uploadPort,
}
]
}
},
unknown200: 1,
unknown3: 0
});
await this.packetHighwayClient.upload(
95,
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
file.fileSize,
file.fileMd5,
ext
);
}
file.fileUuid = preRespData.upload?.uuid;
file.fileHash = preRespData.upload?.fileAddon;
const FetchExistFileReq = this.packer.packOfflineFileDownloadReq(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
const resp = await this.packetClient.sendOidbPacket(FetchExistFileReq, true);
const oidb_resp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(resp.hex_data, 'hex'));
file._e37_800_rsp = new NapProtoMsg(OidbSvcTrpcTcp0XE37_800Response).decode(oidb_resp.body);
file._private_send_uid = this.sig.uid;
file._private_recv_uid = peerUid;
}
}

View File

@@ -0,0 +1,215 @@
import * as net from "node:net";
import * as crypto from "node:crypto";
import * as http from "node:http";
import * as stream from "node:stream";
import { LogWrapper } from "@/common/log";
import * as tea from "@/core/packet/utils/crypto/tea";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { ReqDataHighwayHead, RespDataHighwayHead } from "@/core/packet/proto/highway/highway";
import { BlockSize } from "@/core/packet/highway/session";
import { PacketHighwayTrans } from "@/core/packet/highway/client";
import { Frame } from "@/core/packet/highway/frame";
abstract class HighwayUploader {
readonly trans: PacketHighwayTrans;
readonly logger: LogWrapper;
constructor(trans: PacketHighwayTrans, logger: LogWrapper) {
this.trans = trans;
this.logger = logger;
}
private encryptTransExt(key: Uint8Array) {
if (!this.trans.encrypt) return;
this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key));
}
protected timeout(): Promise<void> {
return new Promise<void>((_, reject) => {
setTimeout(() => {
reject(new Error(`[Highway] timeout after ${this.trans.timeout}s`));
}, (this.trans.timeout ?? Infinity) * 1000
);
});
}
buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array {
return new NapProtoMsg(ReqDataHighwayHead).encode({
msgBaseHead: {
version: 1,
uin: this.trans.uin,
command: "PicUp.DataUp",
seq: 0,
retryTimes: 0,
appId: 1600001604,
dataFlag: 16,
commandId: this.trans.cmd,
},
msgSegHead: {
serviceId: 0,
filesize: BigInt(this.trans.size),
dataOffset: BigInt(offset),
dataLength: bodyLength,
serviceTicket: this.trans.ticket,
md5: bodyMd5,
fileMd5: this.trans.sum,
cacheAddr: 0,
cachePort: 0,
},
bytesReqExtendInfo: this.trans.ext,
timestamp: BigInt(0),
msgLoginSigHead: {
uint32LoginSigType: 8,
appId: 1600001604,
}
});
}
abstract upload(): Promise<void>;
}
class HighwayTcpUploaderTransform extends stream.Transform {
uploader: HighwayTcpUploader;
offset: number;
constructor(uploader: HighwayTcpUploader) {
super();
this.uploader = uploader;
this.offset = 0;
}
_transform(data: Buffer, _: BufferEncoding, callback: stream.TransformCallback) {
let chunkOffset = 0;
while (chunkOffset < data.length) {
const chunkSize = Math.min(BlockSize, data.length - chunkOffset);
const chunk = data.subarray(chunkOffset, chunkOffset + chunkSize);
const chunkMd5 = crypto.createHash('md5').update(chunk).digest();
const head = this.uploader.buildPicUpHead(this.offset, chunk.length, chunkMd5);
chunkOffset += chunk.length;
this.offset += chunk.length;
this.push(Frame.pack(Buffer.from(head), chunk));
}
callback(null);
}
}
export class HighwayTcpUploader extends HighwayUploader {
async upload(): Promise<void> {
const controller = new AbortController();
const { signal } = controller;
const upload = new Promise<void>((resolve, reject) => {
const highwayTransForm = new HighwayTcpUploaderTransform(this);
const socket = net.connect(this.trans.port, this.trans.server, () => {
this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false });
});
const handleRspHeader = (header: Buffer) => {
const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header);
if (rsp.errorCode !== 0) {
socket.end();
reject(new Error(`[Highway] tcpUpload failed (code=${rsp.errorCode})`));
}
const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2);
this.logger.logDebug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`);
if (Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength) >= Number(rsp.msgSegHead?.filesize)) {
this.logger.logDebug('[Highway] tcpUpload finished.');
socket.end();
resolve();
}
};
socket.on('data', (chunk: Buffer) => {
if (signal.aborted) {
socket.end();
reject(new Error('Upload aborted due to timeout'));
}
const [head, _] = Frame.unpack(chunk);
handleRspHeader(head);
});
socket.on('close', () => {
this.logger.logDebug('[Highway] tcpUpload socket closed.');
resolve();
});
socket.on('error', (err) => {
socket.end();
reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`));
});
this.trans.data.on('error', (err) => {
socket.end();
reject(new Error(`[Highway] tcpUpload readable error: ${err}`));
});
});
const timeout = this.timeout().catch((err) => {
controller.abort();
throw new Error(err.message);
});
await Promise.race([upload, timeout]);
}
}
export class HighwayHttpUploader extends HighwayUploader {
async upload(): Promise<void> {
const controller = new AbortController();
const { signal } = controller;
const upload = (async () => {
let offset = 0;
for await (const chunk of this.trans.data) {
if (signal.aborted) {
throw new Error('Upload aborted due to timeout');
}
const block = chunk as Buffer;
try {
await this.uploadBlock(block, offset);
} catch (err) {
throw new Error(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`);
}
offset += block.length;
}
})();
const timeout = this.timeout().catch((err) => {
controller.abort();
throw new Error(err.message);
});
await Promise.race([upload, timeout]);
}
private async uploadBlock(block: Buffer, offset: number): Promise<void> {
const chunkMD5 = crypto.createHash('md5').update(block).digest();
const payload = this.buildPicUpHead(offset, block.length, chunkMD5);
const frame = Frame.pack(Buffer.from(payload), block);
const resp = await this.httpPostHighwayContent(frame, `http://${this.trans.server}:${this.trans.port}/cgi-bin/httpconn?htcmd=0x6FF0087&uin=${this.trans.uin}`);
const [head, body] = Frame.unpack(resp);
const headData = new NapProtoMsg(RespDataHighwayHead).decode(head);
this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`);
if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
}
private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
try {
const options: http.RequestOptions = {
method: 'POST',
headers: {
'Connection': 'keep-alive',
'Accept-Encoding': 'identity',
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)',
'Content-Length': frame.length.toString(),
},
};
const req = http.request(serverURL, options, (res) => {
const data: Buffer[] = [];
res.on('data', (chunk) => {
data.push(chunk);
});
res.on('end', () => {
resolve(Buffer.concat(data));
});
});
req.write(frame);
req.on('error', (error) => {
reject(error);
});
} catch (error) {
reject(error);
}
});
}
}

View File

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

View File

@@ -0,0 +1,74 @@
import * as crypto from "crypto";
import { PushMsgBody } from "@/core/packet/proto/message/message";
import { NapProtoEncodeStructType } from "@napneko/nap-proto-core";
import { LogWrapper } from "@/common/log";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/element";
import { SendTextElement } from "@/core";
export class PacketMsgBuilder {
private logger: LogWrapper;
constructor(logger: LogWrapper) {
this.logger = logger;
}
protected static failBackText = new PacketMsgTextElement(
{
textElement: { content: "[该消息类型暂不支持查看]" }!
} as SendTextElement
);
buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`;
const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => {
return acc !== undefined ? acc : msg.buildContent();
}, undefined);
const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []);
if (!msgContent && !msgElement.length) {
this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement`);
msgElement.push(PacketMsgBuilder.failBackText.buildElement());
}
return {
responseHead: {
fromUid: "",
fromUin: node.senderUin,
toUid: node.groupId ? undefined : selfUid,
forward: node.groupId ? undefined : {
friendName: node.senderName,
},
grp: node.groupId ? {
groupUin: node.groupId,
memberName: node.senderName,
unknown5: 2
} : undefined,
},
contentHead: {
type: node.groupId ? 82 : 9,
subType: node.groupId ? undefined : 4,
divSeq: node.groupId ? undefined : 4,
msgId: crypto.randomBytes(4).readUInt32LE(0),
sequence: crypto.randomBytes(4).readUInt32LE(0),
timeStamp: +node.time.toString().substring(0, 10),
field7: BigInt(1),
field8: 0,
field9: 0,
forward: {
field1: 0,
field2: 0,
field3: node.groupId ? 0 : 2,
unknownBase64: avatar,
avatar: avatar
}
},
body: {
richText: {
elems: msgElement
},
msgContent: msgContent,
}
};
});
}
}

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