Compare commits

...

296 Commits

Author SHA1 Message Date
手瓜一十雪
1070278eaf release: 1.8.0 2024-08-06 09:13:03 +08:00
手瓜一十雪
16b1a6b153 release: 1.7.9 2024-08-05 21:04:49 +08:00
手瓜一十雪
096a7534e0 build: 1.7.9-test 2024-08-05 20:36:08 +08:00
手瓜一十雪
b0897187d2 feat: fetch_emoji_like 2024-08-05 20:33:24 +08:00
手瓜一十雪
885d94882d remove: 😢意外加入的文件 2024-08-05 18:40:39 +08:00
手瓜一十雪
11a3341e13 feat: GetRecentContact 私有标准 2024-08-05 18:39:02 +08:00
手瓜一十雪
231890f78a refactor: msgRandom控制 2024-08-05 17:42:07 +08:00
手瓜一十雪
a272feda6a release: 1.7.8 2024-08-05 13:52:34 +08:00
手瓜一十雪
bc936a0ca7 build: 178发言时间与加入时间完全兜底 2024-08-05 13:45:07 +08:00
手瓜一十雪
28030c2d13 chore: test 2024-08-05 13:01:26 +08:00
手瓜一十雪
7fe9176286 feat: richmsg failed 2024-08-05 12:45:06 +08:00
手瓜一十雪
1105f9b8d6 docs: 补充 2024-08-05 01:11:11 +08:00
手瓜一十雪
e4653defa8 LICENSE: break 2024-08-05 00:48:41 +08:00
Alen
1c62a1e839 Merge pull request #208 from cnxysoft/main
BUG修复
2024-08-05 00:20:17 +08:00
Alen
3a3bbfe201 BUG修复
修复(群聊/私聊)转发合并消息错误的问题
2024-08-05 00:18:42 +08:00
手瓜一十雪
1c38833998 fix: 177 2024-08-04 23:26:51 +08:00
手瓜一十雪
38894177ee fix: get_stranger_info 2024-08-04 22:21:24 +08:00
手瓜一十雪
dce8416942 fix: 177 2024-08-04 21:40:44 +08:00
手瓜一十雪
14219e9b42 release: v1.7.7 2024-08-04 21:23:55 +08:00
手瓜一十雪
7b459e7502 refactor: get_group_member_list 2024-08-04 21:21:36 +08:00
手瓜一十雪
31824c0504 refactor: get_group_list 2024-08-04 20:56:49 +08:00
手瓜一十雪
e203abae85 refactor: /get_group_member_info 2024-08-04 20:53:23 +08:00
手瓜一十雪
faf83b680b 《NTQQ参数全解》 2024-08-04 20:19:34 +08:00
手瓜一十雪
67dcbcb842 refactor: 开始重构群成员信息获取 2024-08-04 20:10:21 +08:00
手瓜一十雪
6533a25404 chore: 移除调试代码 2024-08-04 19:12:18 +08:00
手瓜一十雪
4dc760b0e9 feat: shareDigest 2024-08-04 19:11:44 +08:00
手瓜一十雪
25933b9043 chore: 移除旧代码 2024-08-04 18:50:40 +08:00
手瓜一十雪
a53aaa456e fix: 一处很久很久的看错的的问题 2024-08-04 18:49:40 +08:00
手瓜一十雪
e8a7ea07a5 refactor: Id转换 2024-08-04 18:45:00 +08:00
手瓜一十雪
8817dc6b10 refactor: Uid/Uin转换V2版本 2024-08-04 18:01:31 +08:00
手瓜一十雪
491ec04b46 fix: 准备第二次重构uid/uin 2024-08-04 16:50:23 +08:00
手瓜一十雪
8a5d4a683b feat: getBuddyV2ExWithCate 2024-08-04 16:45:14 +08:00
手瓜一十雪
dfc7c7357a Refactor Api: GetFriendsWithCategory 2024-08-04 16:37:15 +08:00
手瓜一十雪
690a2f7d34 refctor: getBuddyV2 支持分类 2024-08-04 16:27:25 +08:00
手瓜一十雪
58f22b24e4 refactor: api getbuddyv2 2024-08-04 16:05:07 +08:00
手瓜一十雪
3cce9f528b build: 1.7.7 2024-08-04 15:16:45 +08:00
手瓜一十雪
20fd5ac8cb chore: Todo 2024-08-04 15:12:55 +08:00
手瓜一十雪
9e05e086eb fix: 初始化问题 2024-08-04 15:01:43 +08:00
手瓜一十雪
056e0adddf build: 1.7.7-refactor 2024-08-04 14:39:45 +08:00
手瓜一十雪
b36388200d Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-08-04 14:35:47 +08:00
手瓜一十雪
9793e5741a refactor: Appid的获取 2024-08-04 14:35:26 +08:00
手瓜一十雪
143380c012 Merge pull request #206 from pohgxz/main
修复webui快捷登录失败
2024-08-04 14:15:53 +08:00
手瓜一十雪
4b92254945 chore: 订正类型 2024-08-04 14:15:40 +08:00
手瓜一十雪
f9c1d8b4a6 build: 1.7.7 refactor 2024-08-04 14:06:30 +08:00
手瓜一十雪
c0c469339b refactor: versionGet 2024-08-04 14:03:28 +08:00
Nepenthe
0ca6343ed7 修复webui快捷登录失败 2024-08-04 13:04:31 +08:00
手瓜一十雪
3db74c3427 refactor: BuddyList 2024-08-04 12:26:55 +08:00
手瓜一十雪
48d5cb53bd release: 1.7.6 2024-08-03 22:45:36 +08:00
手瓜一十雪
fd7d2dbf53 release: 1.7.5 2024-08-03 20:48:13 +08:00
手瓜一十雪
6609697752 release: 1.7.5 2024-08-03 20:47:05 +08:00
手瓜一十雪
dcd6e1973e build:1.7.5For9.9.15 2024-08-03 16:23:55 +08:00
手瓜一十雪
3614a6e932 chore: 9.9.15 support 2024-08-03 16:08:23 +08:00
手瓜一十雪
931a0210e5 chore: 兼容9.915 信息获取 2024-08-03 15:36:56 +08:00
手瓜一十雪
f9e7de4b42 build: 1.7.5For9.9.15 2024-08-03 15:07:51 +08:00
手瓜一十雪
8e0b79594e style: lint 2024-08-03 15:06:02 +08:00
手瓜一十雪
17122c4360 feat: 精简历史获取 2024-08-03 15:03:41 +08:00
手瓜一十雪
154f7b6a30 chore: 清除老旧代码 2024-08-03 14:57:24 +08:00
手瓜一十雪
52e5543d0b chore: queryEmoticonMsgs 2024-08-03 14:44:48 +08:00
手瓜一十雪
3c304bd2ae feat: 补全类型 开始对9.9.15针对优化 2024-08-03 14:25:26 +08:00
手瓜一十雪
26609bb8fd chore: 9.9.15兼容sendmsg 2024-08-03 13:11:25 +08:00
手瓜一十雪
de3fa9aaa4 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-08-03 12:28:41 +08:00
手瓜一十雪
788665f84c chore: support win 9.9.15 2024-08-03 12:28:25 +08:00
手瓜一十雪
3943782971 Merge pull request #201 from idranme/main
feat: at segment add name
2024-08-03 07:08:25 +08:00
idranme
8f899c40f2 chore 2024-08-02 15:49:01 +00:00
idranme
a1f582399e feat: at segment add name 2024-08-02 15:45:37 +00:00
手瓜一十雪
440b63f662 release: v1.7.4 2024-08-01 23:35:35 +08:00
手瓜一十雪
7d2cc3b56b fix: 多次上报自身消息 2024-08-01 22:00:40 +08:00
手瓜一十雪
5fe3422469 #176 revert 2024-08-01 21:48:17 +08:00
手瓜一十雪
6c02cedb1e build: 1.7.4 2024-08-01 19:44:28 +08:00
手瓜一十雪
3cc2f1dcad fix #183 2024-08-01 19:43:29 +08:00
手瓜一十雪
773cdc5877 build: test 2024-08-01 17:14:15 +08:00
手瓜一十雪
361a7329d7 Revert "build(deps-dev): bump @typescript-eslint/eslint-plugin"
This reverts commit 2562a38fa1.
2024-08-01 17:13:54 +08:00
手瓜一十雪
29910f1236 build: test api 2024-08-01 17:12:56 +08:00
手瓜一十雪
4a164016f5 chore: test 2024-08-01 17:08:22 +08:00
手瓜一十雪
cebd3e62a4 Merge pull request #192 from NapNeko/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.0.0
build(deps-dev): bump @typescript-eslint/eslint-plugin from 7.18.0 to 8.0.0
2024-08-01 16:26:50 +08:00
dependabot[bot]
2562a38fa1 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.0.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.0.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-01 08:22:34 +00:00
手瓜一十雪
d46c922bbf Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-08-01 13:11:19 +08:00
手瓜一十雪
66b59982f7 补充类型 移除无用代码 2024-08-01 13:11:09 +08:00
手瓜一十雪
ad397ccf7f Merge pull request #188 from Fripine/feat/FriendAddNoticeEvent
feat: support FriendAddNoticeEvent
2024-08-01 08:46:50 +08:00
Fripine
bdef80ede7 feat: support FriendAddNoticeEvent 2024-08-01 01:03:33 +08:00
手瓜一十雪
385dcbc75a fix #186 2024-07-31 22:23:12 +08:00
手瓜一十雪
74cf501c8f release: 1.7.3 2024-07-31 22:21:23 +08:00
手瓜一十雪
0c200d6748 build: 1.7.2 beta0 2024-07-31 22:20:39 +08:00
手瓜一十雪
e65a36c517 chore: Ver2SendMsg 2024-07-31 22:19:35 +08:00
手瓜一十雪
126b54ad40 fix #186 2024-07-31 21:46:12 +08:00
手瓜一十雪
78637751af fix #187 2024-07-31 21:41:31 +08:00
手瓜一十雪
f96526ee3a fix #184 2024-07-31 21:36:13 +08:00
手瓜一十雪
b3c7a91f3d refactor: getfile 2024-07-31 16:40:34 +08:00
手瓜一十雪
b8daeef0c4 fix #173 2024-07-31 16:02:08 +08:00
手瓜一十雪
2b662944cf typo fix #178 2024-07-31 15:58:27 +08:00
手瓜一十雪
3d516df01e try fix #183 2024-07-31 15:52:28 +08:00
手瓜一十雪
26b4a9b15b chore: 兼容wt 2024-07-31 14:21:05 +08:00
手瓜一十雪
0f8af273ae release: 1.7.2 2024-07-31 10:51:12 +08:00
手瓜一十雪
fa29a31da9 release: 1.7.2 2024-07-31 10:46:23 +08:00
手瓜一十雪
9e0d2606d8 build: 1.7.2 no log 2024-07-31 10:35:53 +08:00
手瓜一十雪
338dedd6e0 build: 1.7.1 双消息队列 2024-07-31 10:33:38 +08:00
手瓜一十雪
1f893b1393 build: 1.7.2 消息队列重构 2024-07-31 10:25:46 +08:00
手瓜一十雪
b783d6f928 refatcor: sendmsg 2024-07-31 01:12:46 +08:00
手瓜一十雪
8ca0d40f05 fix: error 2024-07-30 23:34:29 +08:00
手瓜一十雪
2f40a80434 fix: error 2024-07-30 23:33:32 +08:00
手瓜一十雪
812d8eb5bb feat: 对msgId兜底 2024-07-30 23:31:42 +08:00
手瓜一十雪
bf5f548349 fix: msghash性能问题 2024-07-30 23:06:58 +08:00
手瓜一十雪
ebf90e72b9 Merge pull request #180 from cnxysoft/main
BW5启动脚本修改
2024-07-30 22:30:04 +08:00
Alen
31d7d42edf BW5启动脚本修复
修改脚本为 UTF-8 with BOM 格式,解决添加注释后执行报错的问题
2024-07-30 19:56:17 +08:00
Alen
833875b42f Merge remote-tracking branch 'upstream/main' 2024-07-30 17:56:48 +08:00
Alen
b901c10f3c 修改BW5启动脚本
支持脚本自动提权
自动覆盖dbghelp.dll
修改默认启用UTF8
修改不再从新窗口运行NC
2024-07-30 17:56:28 +08:00
手瓜一十雪
8ab678bd97 chore: remove log 2024-07-30 16:45:03 +08:00
手瓜一十雪
55d5072f46 fix: setMsg 2024-07-30 16:34:33 +08:00
手瓜一十雪
a379ffd0f2 Merge pull request #175 from pohgxz/main
修复Way05无法带参
2024-07-30 09:12:15 +08:00
Nepenthe
4501d73134 修复Way05无法带参 2024-07-29 23:32:34 +08:00
手瓜一十雪
5f15774ec7 fix: script 2024-07-29 20:58:27 +08:00
手瓜一十雪
c9e057599e fix: error 2024-07-29 20:49:35 +08:00
手瓜一十雪
66851f5625 fix 2024-07-29 20:39:01 +08:00
手瓜一十雪
b33c235b4d release: 1.7.1 2024-07-29 20:13:09 +08:00
手瓜一十雪
d6693b6114 Merge pull request #171 from NapNeko/dependabot/npm_and_yarn/types/node-22.0.0
build(deps-dev): bump @types/node from 20.14.13 to 22.0.0
2024-07-29 17:23:02 +08:00
dependabot[bot]
36b4d26c78 build(deps-dev): bump @types/node from 20.14.13 to 22.0.0
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.14.13 to 22.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-29 08:23:55 +00:00
手瓜一十雪
617592d90a fix: 多次post 2024-07-29 11:32:55 +08:00
手瓜一十雪
15a77b8070 fix: bug 2024-07-29 10:24:02 +08:00
手瓜一十雪
606eccd22b refactor: 精简逻辑 2024-07-29 10:12:41 +08:00
手瓜一十雪
5613450313 refactor: 回复 2024-07-29 10:07:59 +08:00
手瓜一十雪
c59b5564af release: 1.7.0 2024-07-29 09:24:17 +08:00
手瓜一十雪
330b086b8b fix #98 2024-07-29 09:10:52 +08:00
手瓜一十雪
9837ef4f36 fix #125 2024-07-29 08:59:26 +08:00
手瓜一十雪
add46b3251 chore: 落地标准化 2024-07-29 08:43:37 +08:00
手瓜一十雪
e169199107 feat: #162 2024-07-29 08:29:24 +08:00
手瓜一十雪
92fe654850 fix #157 2024-07-29 08:12:49 +08:00
手瓜一十雪
b257486404 fix #158 2024-07-29 08:02:18 +08:00
手瓜一十雪
bdf2e33f40 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-29 07:51:28 +08:00
手瓜一十雪
224d361923 fix #167 2024-07-29 07:51:18 +08:00
手瓜一十雪
3452fa56df build: 😭版本满天飞版本 2024-07-29 00:39:34 +08:00
手瓜一十雪
cd256235da refactor: selfMsgPost 2024-07-29 00:16:24 +08:00
手瓜一十雪
361a164f2a fix: typo 2024-07-28 23:54:50 +08:00
手瓜一十雪
0e60d4b198 fix: 延迟调整 2024-07-28 23:53:45 +08:00
手瓜一十雪
67b47e39b4 fix: typo 2024-07-28 23:49:04 +08:00
手瓜一十雪
8f54310f63 refactor: reply 2024-07-28 23:35:58 +08:00
手瓜一十雪
c7a7494d7e fix: 保证NC回复的消息一致性 2024-07-28 22:33:50 +08:00
手瓜一十雪
af88b3166d build: 169-test 2024-07-28 17:55:34 +08:00
手瓜一十雪
b7837b2a14 chore:test 2024-07-28 17:38:48 +08:00
手瓜一十雪
950ddc749e chore: 🥹LocalMsg不会写入数据库 2024-07-28 16:23:56 +08:00
手瓜一十雪
df081ef0cf chore: 移除调试代码 2024-07-28 15:40:44 +08:00
手瓜一十雪
7b24f90d9f rector: 离线文件重构初步完成 2024-07-28 15:37:34 +08:00
手瓜一十雪
f2e4579fd8 feat: 缓存文件 2024-07-28 15:34:08 +08:00
手瓜一十雪
97cb351827 fix: typo 2024-07-28 14:55:30 +08:00
手瓜一十雪
c1ec53fdbb refactor: fileget 2024-07-28 14:25:13 +08:00
手瓜一十雪
98214aa429 refactor: video element 2024-07-28 13:36:03 +08:00
手瓜一十雪
ce7deac2dd refactor: video element 2024-07-28 13:28:21 +08:00
手瓜一十雪
612092b867 build: 170 re 2024-07-28 13:15:14 +08:00
手瓜一十雪
92579d5949 Merge pull request #163 from cnxysoft/main
修复提交疏漏
2024-07-28 13:00:12 +08:00
手瓜一十雪
9ab07060ae fix: default 2024-07-28 12:59:20 +08:00
手瓜一十雪
0d45125d79 fix: uid && latestMsg 2024-07-28 09:56:00 +08:00
手瓜一十雪
9ced152778 fix: 修复uid转换异常问题 2024-07-28 09:24:03 +08:00
Alen
3685ab2e3e Merge remote-tracking branch 'upstream/main' 2024-07-27 15:01:23 +08:00
Alen
be605f11f2 修复提交疏漏
修改在查询群历史消息时,如未提供msg_seq,则返回最新消息
2024-07-27 15:01:08 +08:00
手瓜一十雪
8cca8df976 Merge pull request #159 from cnxysoft/main
bug修复和标准兼容
2024-07-27 06:46:25 +08:00
Alen
990a31e961 标准兼容
根据GOCQ标准将获取群历史消息中的msg_seq改为非必要参数,默认为0
2024-07-27 04:10:01 +08:00
Alen
5db201c342 BUG修复
修复创建reply消息体时向NTQQMsgApi.getMsgsByMsgId提交空值查询会导致QQ崩溃的BUG
2024-07-27 04:01:22 +08:00
手瓜一十雪
a625e30dd4 fix: search file 2024-07-26 17:37:04 +08:00
手瓜一十雪
b236cdd060 refactor: search file 2024-07-26 17:15:28 +08:00
手瓜一十雪
2db9899184 chore: type 2024-07-26 16:26:47 +08:00
手瓜一十雪
fe5d6db986 chore: wait release 1.7.0 2024-07-26 16:10:05 +08:00
手瓜一十雪
7c7bf8fecf chore: 类型+++++ 2024-07-26 16:08:28 +08:00
手瓜一十雪
76e3a46378 fix: type 2024-07-26 16:02:23 +08:00
手瓜一十雪
16f3897fec chore: 类型补全计划 2024-07-26 15:55:05 +08:00
手瓜一十雪
045e120854 refactor: type 2024-07-26 15:38:43 +08:00
Version
2b7fcce9b2 chore:version change 2024-07-26 05:25:49 +00:00
手瓜一十雪
9685931694 fix: uint 2024-07-26 13:25:19 +08:00
手瓜一十雪
1dc844435a Merge pull request #153 from Guation/main
feat: WebUI支持放置到二级目录中
2024-07-26 13:04:26 +08:00
手瓜一十雪
18892379de fix: search file 2024-07-26 12:27:02 +08:00
手瓜一十雪
620d61c8dc docs: v1.6.8 2024-07-26 11:59:18 +08:00
手瓜一十雪
9f91398875 build: 再次优化发送速度 2024-07-26 11:30:04 +08:00
挂神
a29b1154a9 feat: WebUI支持放置到二级目录中 2024-07-26 11:12:09 +08:00
手瓜一十雪
34d19a471a refactor: 回滚 2024-07-26 10:58:56 +08:00
手瓜一十雪
2ef6477d7c build: log info 2024-07-25 20:22:03 +08:00
手瓜一十雪
26e6800836 fix: 退群推送 2024-07-25 18:08:49 +08:00
手瓜一十雪
9dbbcf3872 build: 1.6.8 - parse appid 2024-07-25 17:57:39 +08:00
手瓜一十雪
6b3343e1e4 build: 1.6.8 beta6 2024-07-25 10:59:06 +08:00
手瓜一十雪
ef48f754a5 docs: 整理当前进度 2024-07-25 10:44:53 +08:00
手瓜一十雪
3be1ede847 refactor: sendtime/join time 2024-07-25 10:32:44 +08:00
手瓜一十雪
7bff1b61e8 refactor: SendTime 2024-07-25 10:02:16 +08:00
手瓜一十雪
6affd0eb68 feat: GetSendTime 2024-07-24 17:49:03 +08:00
手瓜一十雪
0a112d15e0 fix: typo 2024-07-24 15:37:23 +08:00
手瓜一十雪
4e03f582bb fix: richmeida name 2024-07-24 14:43:10 +08:00
手瓜一十雪
8f186c1c5e chore: action clean 2024-07-24 14:37:48 +08:00
手瓜一十雪
cd1bae9a1f fix: setGroupAvatar 2024-07-24 14:35:12 +08:00
手瓜一十雪
60796c26ca Merge pull request #147 from serfend/default-config
fix[config]support overwrite by user #145
2024-07-24 14:28:42 +08:00
汉广
28927f950d fix[config]support overwrite by user 2024-07-24 14:25:58 +08:00
手瓜一十雪
95f16ebc8c Merge pull request #144 from Guation/main
feat: http与ws允许监听同一端口,快速登录允许自动选择QQ号,允许禁用webUI
2024-07-24 14:09:19 +08:00
挂神
25bca8385d feat: http与ws共站支持热重载 2024-07-24 13:14:35 +08:00
手瓜一十雪
965c7f23b4 feat: 群头像设置 2024-07-24 11:37:12 +08:00
手瓜一十雪
33082af9cc feat: searchFile 2024-07-24 11:23:27 +08:00
手瓜一十雪
1f7f3565b0 build: 1.6.8 beta05 2024-07-24 10:45:11 +08:00
手瓜一十雪
f784363696 refactor: UUID 2024-07-24 10:44:55 +08:00
手瓜一十雪
37bd51e138 refactor: getUserInfo 2024-07-24 10:42:22 +08:00
手瓜一十雪
c367728c43 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-24 10:23:48 +08:00
手瓜一十雪
61f0f5d884 refactor: 改造接口调用 2024-07-24 10:23:41 +08:00
手瓜一十雪
5f2ebeead7 docs: update 2024-07-23 18:32:07 +08:00
手瓜一十雪
7646037fc7 docs: 砍掉 2024-07-23 18:21:19 +08:00
手瓜一十雪
eac6d285ff chore: debug 2024-07-23 17:39:00 +08:00
手瓜一十雪
b921d5e734 refactor: downloadMedia 2024-07-23 16:15:23 +08:00
手瓜一十雪
831d808e63 chore: remove 2024-07-23 16:03:06 +08:00
手瓜一十雪
451b88d7e3 refactor: video type 2024-07-23 15:51:57 +08:00
手瓜一十雪
f916682a71 build: 1.6.8 beta07 2024-07-23 15:38:41 +08:00
手瓜一十雪
2d76bcf0cf refactor: message id 2024-07-23 15:10:39 +08:00
手瓜一十雪
8db294efe6 refactor: 转发消息修复 2024-07-23 14:54:05 +08:00
手瓜一十雪
5cc5149aed fix: 合并转发 2024-07-23 14:19:26 +08:00
手瓜一十雪
7ecb01dc9f docs: 规划 2024-07-23 12:34:20 +08:00
手瓜一十雪
8bf1a545d9 chore: remove debug 2024-07-23 10:12:20 +08:00
手瓜一十雪
efb2be2f94 fix: timeout 2024-07-23 09:50:31 +08:00
手瓜一十雪
bd2edda494 refactor: sendMsg 2024-07-23 09:45:00 +08:00
手瓜一十雪
991172eae4 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-23 09:21:36 +08:00
手瓜一十雪
fc7631f9aa refactor: sendmsg 2024-07-23 09:21:22 +08:00
手瓜一十雪
a21efb7d2f Merge pull request #145 from serfend/default-config
fix[default-config]config name check #138
2024-07-22 21:36:54 +08:00
汉广
03bc844ad0 fix[default-config]config name check 2024-07-22 20:12:24 +08:00
挂神
f7bdc35ed6 feat: http与ws允许监听同一端口,快速登录允许自动选择QQ号,允许禁用webUI 2024-07-22 19:47:23 +08:00
手瓜一十雪
0efdffd857 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-22 18:49:36 +08:00
手瓜一十雪
6a16a42d0c feat: refactor send 2024-07-22 18:49:25 +08:00
手瓜一十雪
e9c00c72b1 Merge pull request #138 from serfend/main
feat[config]support use default-template
2024-07-22 18:22:49 +08:00
手瓜一十雪
ab22f36b8a refactor: NTEvent Checker 2024-07-22 18:21:29 +08:00
手瓜一十雪
c57497cd91 feat: remove debug 2024-07-22 18:17:33 +08:00
手瓜一十雪
ba123236e5 feat:msgid generate 2024-07-22 18:17:03 +08:00
手瓜一十雪
338b6e4607 fix: QRCode 2024-07-22 15:46:48 +08:00
手瓜一十雪
f88c717560 build: 1.6.8-beta03 2024-07-22 15:40:41 +08:00
手瓜一十雪
f8ffc92db5 feat: remove LineDev&&Protobuf 2024-07-22 15:40:23 +08:00
手瓜一十雪
98c23c172c build: 1.6.8-无数据库版本 2024-07-22 15:13:38 +08:00
手瓜一十雪
781c107d8c feat: 拉取重启消息 2024-07-22 15:12:25 +08:00
手瓜一十雪
186668c075 fix: Login 2024-07-22 14:18:04 +08:00
手瓜一十雪
cf9f785193 style: lint 2024-07-22 14:12:03 +08:00
手瓜一十雪
72d2d3f224 feat: 破坏file/db相关接口 2024-07-22 14:09:37 +08:00
手瓜一十雪
087c76b394 refactor: msgId stage-2 2024-07-22 11:34:18 +08:00
手瓜一十雪
4f9fb2c8c3 Merge pull request #141 from cnxysoft/main
修复提交疏漏
2024-07-22 11:15:28 +08:00
手瓜一十雪
334e43e764 refactor: MsgId 2024-07-22 11:15:01 +08:00
Alen
7843256402 修复提交疏漏
修复变量类型未断言的问题
2024-07-22 11:07:33 +08:00
手瓜一十雪
0522ba35fe refactor: jest test 2024-07-22 10:24:55 +08:00
手瓜一十雪
24d3b52e0b refactor: Message Unique 2024-07-22 09:56:08 +08:00
手瓜一十雪
3177110f0f feat: RecentContact 2024-07-22 09:24:16 +08:00
手瓜一十雪
e1b8243a67 Merge pull request #140 from cnxysoft/main
BUG修复
2024-07-22 08:40:21 +08:00
Alen
b1c6ce3885 BUG修复
1.尝试让所有人能收到group_admin事件
2.修复请求API: delete_msg(POST请求网址传参)将负数判定为文本导致无法调用的问题
2024-07-22 01:22:38 +08:00
手瓜一十雪
0b4b25a11e feat: LineDev for Develop-0 2024-07-21 19:31:13 +08:00
手瓜一十雪
1176fe984a add: RecentListener 2024-07-21 19:01:47 +08:00
汉广
6ca768c3ee feat[config]support use default-template 2024-07-20 23:43:32 +08:00
手瓜一十雪
3da1659c8d fix: buddylike 2024-07-20 20:16:45 +08:00
手瓜一十雪
9aa4cd319c release: 1.6.7 2024-07-20 19:52:39 +08:00
手瓜一十雪
5af866cdca fix: error 2024-07-20 19:31:59 +08:00
手瓜一十雪
2b421fa447 release: 1.6.7 2024-07-20 17:43:56 +08:00
手瓜一十雪
30ec964325 feat: new api 2024-07-20 17:33:26 +08:00
手瓜一十雪
714d7d72eb feat: raw api add 2024-07-20 17:09:38 +08:00
手瓜一十雪
687aa0f363 chore: remove debug 2024-07-20 16:37:16 +08:00
手瓜一十雪
8363ab07a7 feat: 支持精华消息 2024-07-20 16:17:02 +08:00
手瓜一十雪
c46a757339 fix: error 2024-07-20 16:14:02 +08:00
手瓜一十雪
557864395b feat: support essence 2024-07-20 16:09:44 +08:00
手瓜一十雪
3f7a85d80b feat: essence get_sender 2024-07-20 16:00:01 +08:00
手瓜一十雪
8d18d2ce1f refactor: 标准化 2024-07-20 15:55:26 +08:00
手瓜一十雪
7141ba1587 refactor: essence and together listener 2024-07-20 15:53:39 +08:00
手瓜一十雪
44d350a225 docs: todo 2024-07-20 15:37:57 +08:00
手瓜一十雪
239b8e72d9 Merge pull request #134 from serfend/main
fix[group]handle_request reason empty
2024-07-20 15:24:39 +08:00
手瓜一十雪
279bdb6fb1 Merge pull request #135 from pohgxz/main
群戳一戳增加原始信息
2024-07-20 15:24:27 +08:00
Nepenthe
a0cea819da 群戳一戳增加原始信息
群消息log增加视频解析
2024-07-20 14:51:50 +08:00
汉广
9ab7f60544 fix[group]handle_request reason empty 2024-07-20 14:16:07 +08:00
手瓜一十雪
aaf7191bf3 build: 1.6.7-beta03 2024-07-20 10:45:26 +08:00
手瓜一十雪
628c9be0c8 feat: 2401 for 群精华设置 2024-07-20 10:44:57 +08:00
手瓜一十雪
733052720c build: 1.6.6-build02 2024-07-20 10:28:47 +08:00
手瓜一十雪
a10f007194 build: 1.6.7-beta0 2024-07-20 10:21:16 +08:00
手瓜一十雪
6fa50c58d3 feat: 优化接口转换速度 避免频繁读写 2024-07-17 15:03:10 +08:00
手瓜一十雪
c54a58d6e4 release: v1.6.6 2024-07-16 15:55:25 +08:00
手瓜一十雪
34cb1ea3fd Merge pull request #126 from cnxysoft/main
修复戳一戳
2024-07-16 15:52:39 +08:00
Alen
f640b0ca91 修复戳一戳 2024-07-16 15:47:23 +08:00
手瓜一十雪
60e16da42e Merge pull request #121 from pohgxz/main
增加winQQ-9912一键启动脚本
2024-07-14 08:44:55 +08:00
Nepenthe
0cdceb95d6 增加winQQ-9912一键启动脚本 2024-07-13 16:09:14 +00:00
手瓜一十雪
70bd22d925 fix: typo 2024-07-13 20:27:18 +08:00
手瓜一十雪
82462dd647 docs: 规划 2024-07-13 20:21:48 +08:00
手瓜一十雪
c0466e943d build: test 2024-07-13 19:37:39 +08:00
手瓜一十雪
b187b4695d refactor: uin<->uid 2024-07-13 19:37:02 +08:00
手瓜一十雪
b1956d2a37 refactor: poke 2024-07-13 19:10:47 +08:00
手瓜一十雪
590b622e5f build: test 2024-07-13 18:58:52 +08:00
手瓜一十雪
3d8174396a feat: LinuxQQ版本25765 2024-07-13 18:58:29 +08:00
手瓜一十雪
b8dc6e9bd9 feat: 再次提升版本 25765 2024-07-13 18:56:42 +08:00
手瓜一十雪
8ee99109dc chore: 整理代码 2024-07-13 18:20:44 +08:00
手瓜一十雪
902041d4ee refactor: 新增启动脚本 2024-07-13 18:15:00 +08:00
手瓜一十雪
cc34aef47e style: code lint 2024-07-13 18:12:38 +08:00
手瓜一十雪
0afbbe7c7a refactor: 废弃部分代码 2024-07-13 18:10:41 +08:00
手瓜一十雪
8004553ba7 refactor: groupNotifies 2024-07-13 18:04:55 +08:00
手瓜一十雪
0023b2846a feat: 第二次大致整理 2024-07-13 17:23:05 +08:00
手瓜一十雪
34775c1816 fix: 整理常量 2024-07-12 18:08:45 +08:00
手瓜一十雪
e0759e704b feat:大部分消息元素 2024-07-12 18:01:48 +08:00
手瓜一十雪
0aa225ca78 fix: typo 2024-07-12 17:04:28 +08:00
手瓜一十雪
b43b4ee5c0 feat: test code 2024-07-12 16:59:08 +08:00
手瓜一十雪
0cdb8cecbf feat: 群精华 代码未测试 2024-07-12 11:02:10 +08:00
手瓜一十雪
fd6a306742 feat: 懒得写了 2024-07-12 10:54:01 +08:00
手瓜一十雪
7f3b3d2277 feat: 群精华 2024-07-12 10:46:57 +08:00
手瓜一十雪
8be5b977bf Merge pull request #117 from po-lan/main
对缓存进一步优化
2024-07-12 09:52:17 +08:00
po-lan
d7ddb15f9c 对缓存进一步优化
LRUCache 将所有被移除的缓存数据作为事件参数传递给事件处理程序。

在数据库操作部分,优化了读写流程,以确保每个群组至多执行三次数据库操作:

读取:先判断缓存中是否存在用户记录,若不存在则读取数据库。
创建:如果用户记录在数据库中不存在,则新增记录。
修改:如果用户记录在数据库中存在,则进行修改。
即使单个群组内有大量用户,每种操作也只会执行一次。
2024-07-12 00:46:03 +08:00
手瓜一十雪
9a6a1798d0 build: poke能用25493 2024-07-11 12:44:42 +08:00
手瓜一十雪
14196fd349 build: 移除调试代码 2024-07-11 12:31:00 +08:00
手瓜一十雪
941b89a523 feat: uin转换优化&poke支持重写 2024-07-11 12:28:11 +08:00
手瓜一十雪
a5f9e5f8c0 Merge pull request #113 from idranme/main
perf: audio
2024-07-11 09:49:56 +08:00
idranme
80c3356c8f perf: audio 2024-07-10 17:44:17 +00:00
150 changed files with 5137 additions and 2453 deletions

3
.gitignore vendored
View File

@@ -14,4 +14,5 @@ dist/
# Build # Build
*.db *.db
checkVersion.sh checkVersion.sh
bun.lockb

386
LICENSE
View File

@@ -1,21 +1,373 @@
MIT License Mozilla Public License Version 2.0
==================================
Copyright (c) 2024 NapCatQQ 1. Definitions
--------------
Permission is hereby granted, free of charge, to any person obtaining a copy 1.1. "Contributor"
of this software and associated documentation files (the "Software"), to deal means each individual or legal entity that creates, contributes to
in the Software without restriction, including without limitation the rights the creation of, or owns Covered Software.
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all 1.2. "Contributor Version"
copies or substantial portions of the Software. means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1.3. "Contribution"
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, means Covered Software of a particular Contributor.
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1.4. "Covered Software"
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, means Source Code Form to which the initial Contributor has attached
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE the notice in Exhibit A, the Executable Form of such Source Code
SOFTWARE. Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -0,0 +1,21 @@
# v1.8.0
QQ Version: Windows 9.9.15-26702 / Linux 3.2.12-26702
## 启动的方式
Way03/Way05
## 新增与调整
1. 消息ID映射到UINT32空间
2. 回复ID验证与修复
3. 新API /fetch_emoji_like
```json5
{
"message_id": 1557274996,//消息ID
"emojiType": "1",//可以从event事件获取 一般为1或者2
"emojiId": "76"//可以从event事件获取 一个表情对应一个ID
}
```
新增的 API 详细见[API文档](https://napneko.github.io/zh-CN/develop/extends_api)

View File

@@ -0,0 +1,17 @@
# v1.6.6
QQ Version: Windows 9.9.12-26000 / Linux 3.2.9-26000
## 使用前警告
1. 在最近版本由于QQ本体大幅变动为了保证NapCat可用性NapCat近期启动与安装方式将将大幅变动请关注文档和社群获取。
2. 在Core上完全执行开源请不要用于违法用途如此可能造成NapCat完全停止更新。
3. 针对原启动方式的围堵NapCat研发了多种方式除此其余理论与扩展的分析和思路将部分展示于Docs以便各位参与开发与维护NapCat。
## 其余·备注
启动方式: WayBoot.03 Electron Main进程为Node 直接注入代码 同理项目: LiteLoader
## 修复与优化
1. 修复了一些问题
## 新增与调整
没有哦
新增的 API 详细见[API文档](https://napneko.github.io/zh-CN/develop/extends_api)

View File

@@ -1,2 +0,0 @@
# 开始
jadx 跳转于 `com.tencent.qqnt.kernel.*`

View File

@@ -1,42 +0,0 @@
# Android
```java
GroupMemberExtReq groupMemberExtReq = new GroupMemberExtReq();
groupMemberExtReq.sourceType = MemberExtSourceType.TITLETYPE.ordinal();
groupMemberExtReq.groupCode = longOrNull.longValue();
groupMemberExtReq.beginUin = "0";
groupMemberExtReq.dataTime = "0";
Long[] lArr = new Long[1];
AppInterface a2 = dVar.a();
lArr[0] = Long.valueOf(a2 != null ? a2.getLongAccountUin() : 0L);
arrayListOf = CollectionsKt__CollectionsKt.arrayListOf(lArr);
groupMemberExtReq.uinList = arrayListOf;
MemberExtInfoFilter memberExtInfoFilter = new MemberExtInfoFilter();
memberExtInfoFilter.memberLevelInfoUin = 1;
memberExtInfoFilter.memberLevelInfoPoint = 1;
memberExtInfoFilter.memberLevelInfoActiveDay = 1;
memberExtInfoFilter.memberLevelInfoLevel = 1;
memberExtInfoFilter.levelName = 1;
memberExtInfoFilter.dataTime = 1;
memberExtInfoFilter.sysShowFlag = 1;
memberExtInfoFilter.userShowFlag = 1;
memberExtInfoFilter.userShowFlagNew = 1;
memberExtInfoFilter.levelNameNew = 1;
Unit unit = Unit.INSTANCE;
groupMemberExtReq.memberExtFilter = memberExtInfoFilter;
troopLevelFrequencyControl.f(troopUin, new TroopListRepo$fetchTroopLevelInfo$2(b2, groupMemberExtReq, troopUin, new com.tencent.qqnt.troopmemberlist.report.c("fetchTroopLevelInfo")));
```
# Win
参数解析位于 sub_181456A10(24108) -> wrapper.node(24108)+1456A10
IGroupService.GetMemberExt(param: object);
param展开如下
```
groupCode string
beginUin string
dataTime string
uinList Array<string>
uinNum string
groupType string
richCardNameVer string
sourceType number
memberExtFilter object// 参数解析位于 sub_18145A6D0(24108) -> wrapper.node(24108)+145A6D0
```

View File

@@ -1,3 +1,4 @@
public static final int C2C_PIC_DOWNLOAD = 1004; public static final int C2C_PIC_DOWNLOAD = 1004;
public static final String C2C_PIC_DOWNLOAD_DOMAIN = "c2cpicdw.qpic.cn"; public static final String C2C_PIC_DOWNLOAD_DOMAIN = "c2cpicdw.qpic.cn";
public static final String C2C_PIC_DOWNLOAD_QUIC_DOMAIN = "c2cpicdw.quic.qpic.cn"; public static final String C2C_PIC_DOWNLOAD_QUIC_DOMAIN = "c2cpicdw.quic.qpic.cn";

View File

@@ -0,0 +1,16 @@
# 开发方向
方向一 NativeCall/Hook:
1. 崩溃检测机制的实现
2. Api_Caller 的Hook 可以拿到Event/Handler 进一步提升NC 即时的拦截与处理一些事件比如ReCall拦截
3. Node包装层 进一步分析拿到脱离自带Listener/Adapter可以拿到一些更加底层的数据变动 或许包括更多二进制数据
方向二 全新的无头启动 Way01
1. 基于Node启动原理借助导出符号获取函数地址 再次还原NodeMain
方向三 发包与收包
1. 参考 方向一/3 大概可以收包
2. 发包 (暂时没有计划)
方向四 版本控制
1. 根据不同版本进行逻辑控制
2. 某些参数的自动提取

View File

@@ -1,24 +0,0 @@
# 前排提示
由于Core未处于开源非组织人员无法参与Core开发此处为Core开发提示
# 准备工具
frida ida-pro jadx x64dbg ce 内部调试脚本
## ida-pro
1. 用于快速分析入参和返回类型
2. 通过静态QLog推测语义
3. 提取Listener与Service (常用)
## frida
1. 用于动态获取QLog推测语义
2. 捕捉Native函数 实际入参与数据 分析中间流程
## jadx
1. 通过其它平台实现 静态获取QLog推测语义
2. 提供部分未调用代码 参考
## x64dbg
1. 验证IDA的Hook点
## 内部脚本
1. 提取Listener与Service (不调用无类型 不推荐)
2. 获取NT调用流程

View File

@@ -1 +1,8 @@
# Api方向
## getMsgUniqueId √ 已应用
getMsgUniqueId 传入时间 产出一个唯一ID 发送消息作为一个参数 getMsgUniqueId 传入时间 产出一个唯一ID 发送消息作为一个参数
# Native方向
## magic_load
## api_caller
## NodeMain

View File

@@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "1.6.5", "version": "1.8.0",
"scripts": { "scripts": {
"watch:dev": "vite --mode development", "watch:dev": "vite --mode development",
"watch:prod": "vite --mode production", "watch:prod": "vite --mode production",
@@ -19,10 +19,9 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.7", "@babel/core": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"vite-plugin-babel": "^1.2.0",
"@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.7", "@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@log4js-node/log4js-api": "^1.0.2", "@log4js-node/log4js-api": "^1.0.2",
"@protobuf-ts/plugin": "^2.9.4", "@protobuf-ts/plugin": "^2.9.4",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
@@ -31,9 +30,9 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/figlet": "^1.5.8", "@types/figlet": "^1.5.8",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^20.11.30", "@types/jest": "^29.5.12",
"@types/node": "^22.0.0",
"@types/qrcode-terminal": "^0.12.2", "@types/qrcode-terminal": "^0.12.2",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0", "@typescript-eslint/parser": "^7.4.0",
@@ -47,6 +46,7 @@
"rollup-plugin-obfuscator": "^1.1.0", "rollup-plugin-obfuscator": "^1.1.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.2.6", "vite": "^5.2.6",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-cp": "^4.0.8", "vite-plugin-cp": "^4.0.8",
"vite-plugin-dts": "^3.8.2", "vite-plugin-dts": "^3.8.2",
"vite-tsconfig-paths": "^4.3.2" "vite-tsconfig-paths": "^4.3.2"
@@ -64,9 +64,7 @@
"json-schema-to-ts": "^3.1.0", "json-schema-to-ts": "^3.1.0",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.3.4", "silk-wasm": "^3.6.1",
"sqlite3": "^5.1.7",
"uuid": "^10.0.0",
"ws": "^8.16.0" "ws": "^8.16.0"
} }
} }

View File

@@ -42,4 +42,4 @@ if (!(Test-Path $QQpath)) {
$Bootfile = Join-Path $PSScriptRoot "napcat.mjs" $Bootfile = Join-Path $PSScriptRoot "napcat.mjs"
$env:ELECTRON_RUN_AS_NODE = 1 $env:ELECTRON_RUN_AS_NODE = 1
$commandInfo = Get-Command $QQpath -ErrorAction Stop $commandInfo = Get-Command $QQpath -ErrorAction Stop
Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& chcp 65001;& '$($commandInfo.Path)' --enable-logging $params}" Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& chcp 65001;& '$($commandInfo.Path)' --enable-logging }"

123
script/BootWay05.ps1 Normal file
View File

@@ -0,0 +1,123 @@
# 检查当前会话是否具有管理员权限
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..."
}

3
script/NapCat.Way01.bat Normal file
View File

@@ -0,0 +1,3 @@
REM 全新启动脚本 基于 Hook Native 预计版本1.6.0左右发布
@echo off
pause

BIN
script/dbghelp.dll Normal file

Binary file not shown.

View File

@@ -0,0 +1,18 @@
@echo off
setlocal enabledelayedexpansion
chcp 65001
: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!"
cd /d !QQPath!
echo !QQPath!
QQ.exe --enable-logging %*

View File

@@ -0,0 +1,41 @@
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) + "\"
}
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"
}
Set-Location -Path $QQpath
Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& chcp 65001;& ./QQ.exe --enable-logging $params}"

17
script/napcat-9912.bat Normal file
View File

@@ -0,0 +1,17 @@
@echo off
setlocal enabledelayedexpansion
: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!
cd /d !QQPath!
echo !QQPath!
QQ.exe --enable-logging %*

41
script/napcat-9912.ps1 Normal file
View File

@@ -0,0 +1,41 @@
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) + "\"
}
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"
}
Set-Location -Path $QQpath
Start-Process powershell -ArgumentList "-noexit", "-noprofile", "-command &{& ./QQ.exe --enable-logging $params}"

View File

@@ -9,7 +9,15 @@ type RegisterHandler = (res: Response, payload: any) => Promise<any>
export abstract class HttpServerBase { export abstract class HttpServerBase {
name: string = 'NapCatQQ'; name: string = 'NapCatQQ';
private readonly expressAPP: Express; private readonly expressAPP: Express;
private server: http.Server | null = null; private _server: http.Server | null = null;
public get server(): http.Server | null {
return this._server;
}
private set server(value: http.Server | null) {
this._server = value;
}
constructor() { constructor() {
this.expressAPP = express(); this.expressAPP = express();
@@ -78,7 +86,7 @@ export abstract class HttpServerBase {
this.start(port, host); this.start(port, host);
} }
abstract handleFailed(res: Response, payload: any, err: any): void abstract handleFailed(res: Response, payload: any, err: Error): void
registerRouter(method: 'post' | 'get' | string, url: string, handler: RegisterHandler) { registerRouter(method: 'post' | 'get' | string, url: string, handler: RegisterHandler) {
if (!url.startsWith('/')) { if (!url.startsWith('/')) {
@@ -103,7 +111,7 @@ export abstract class HttpServerBase {
try { try {
res.send(await handler(res, payload)); res.send(await handler(res, payload));
} catch (e: any) { } catch (e: any) {
this.handleFailed(res, payload, e.stack.toString()); this.handleFailed(res, payload, e);
} }
}); });
} }

View File

@@ -1,4 +1,5 @@
import { WebSocket, WebSocketServer } from 'ws'; import { WebSocket, WebSocketServer } from 'ws';
import http from 'http';
import urlParse from 'url'; import urlParse from 'url';
import { IncomingMessage } from 'node:http'; import { IncomingMessage } from 'node:http';
import { log } from '@/common/utils/log'; import { log } from '@/common/utils/log';
@@ -27,17 +28,36 @@ export class WebsocketServerBase {
constructor() { constructor() {
} }
start(port: number, host: string = '') { start(port: number | http.Server, host: string = '') {
try { if (port instanceof http.Server) {
this.ws = new WebSocketServer({ try {
port, const wss = new WebSocketServer({
host: '', noServer: true,
maxPayload: 1024 * 1024 * 1024 maxPayload: 1024 * 1024 * 1024
}).on('error', () => { }).on('error', () => {
}); });
log(`ws服务启动成功, ${host}:${port}`); this.ws = wss;
} catch (e: any) { port.on('upgrade', function upgrade(request, socket, head) {
throw Error('ws服务启动失败, 请检查监听的ip和端口' + e.toString()); wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request);
});
});
log('ws服务启动成功, 绑定到HTTP服务');
} catch (e: any) {
throw Error('ws服务启动失败, 可能是绑定的HTTP服务异常' + e.toString());
}
} else {
try {
this.ws = new WebSocketServer({
port,
host: '',
maxPayload: 1024 * 1024 * 1024
}).on('error', () => {
});
log(`ws服务启动成功, ${host}:${port}`);
} catch (e: any) {
throw Error('ws服务启动失败, 请检查监听的ip和端口' + e.toString());
}
} }
this.ws.on('connection', (wsClient, req) => { this.ws.on('connection', (wsClient, req) => {
const url: string = req.url!.split('?').shift() || '/'; const url: string = req.url!.split('?').shift() || '/';
@@ -50,10 +70,12 @@ export class WebsocketServerBase {
} }
stop() { stop() {
this.ws && this.ws.close((err) => { if (this.ws) {
log('ws server close failed!', err); this.ws.close((err) => {
}); if (err) log('ws server close failed!', err);
this.ws = null; });
this.ws = null;
}
} }
restart(port: number) { restart(port: number) {

View File

@@ -1,36 +0,0 @@
import { sleep } from '@/common/utils/helper';
import { logError } from './log';
type AsyncQueueTask = (() => void) | (()=>Promise<void>);
export class AsyncQueue {
private tasks: (AsyncQueueTask)[] = [];
public addTask(task: AsyncQueueTask) {
this.tasks.push(task);
// console.log('addTask', this.tasks.length);
if (this.tasks.length === 1) {
this.runQueue().then().catch(()=>{});
}
}
private async runQueue() {
// console.log('runQueue', this.tasks.length);
while (this.tasks.length > 0) {
const task = this.tasks[0];
// console.log('typeof task', typeof task);
try {
const taskRet = task();
// console.log('type of taskRet', typeof taskRet, taskRet);
if (taskRet instanceof Promise) {
await taskRet;
}
} catch (e) {
// console.error(e);
logError(e);
}
this.tasks.shift();
await sleep(100);
}
}
}

View File

@@ -3,6 +3,7 @@ import fs from 'node:fs';
import { log, logDebug, logError } from '@/common/utils/log'; import { log, logDebug, logError } from '@/common/utils/log';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { selfInfo } from '@/core/data';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -12,8 +13,9 @@ const configDir = path.resolve(__dirname, 'config');
fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(configDir, { recursive: true });
export class ConfigBase<T>{ export class ConfigBase<T> {
public name: string = 'default_config';
private pathName: string | null = null; // 本次读取的文件路径
constructor() { constructor() {
} }
@@ -22,19 +24,28 @@ export class ConfigBase<T>{
return null; return null;
} }
getConfigDir(){ getConfigDir() {
const configDir = path.resolve(__dirname, 'config'); const configDir = path.resolve(__dirname, 'config');
fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(configDir, { recursive: true });
return configDir; return configDir;
} }
getConfigPath(): string { getConfigPath(pathName: string | null): string {
throw new Error('Method not implemented.'); const suffix = pathName ? `_${pathName}` : '';
const filename = `${this.name}${suffix}.json`;
return path.join(this.getConfigDir(), filename);
} }
read() { read() {
const configPath = this.getConfigPath(); // 尝试加载当前账号配置
if (this.read_from_file(selfInfo.uin, false)) return this;
// 尝试加载默认配置
return this.read_from_file('', true);
}
read_from_file(pathName: string, createIfNotExist: boolean) {
const configPath = this.getConfigPath(pathName);
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
try{ if (!createIfNotExist) return null;
this.pathName = pathName; // 记录有效的设置文件
try {
fs.writeFileSync(configPath, JSON.stringify(this, this.getKeys(), 2)); fs.writeFileSync(configPath, JSON.stringify(this, this.getKeys(), 2));
log(`配置文件${configPath}已创建\n如果修改此文件后需要重启 NapCat 生效`); log(`配置文件${configPath}已创建\n如果修改此文件后需要重启 NapCat 生效`);
} }
@@ -43,6 +54,7 @@ export class ConfigBase<T>{
} }
return this; return this;
} }
try { try {
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')); const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
logDebug(`配置文件${configPath}已加载`, data); logDebug(`配置文件${configPath}已加载`, data);
@@ -61,9 +73,13 @@ export class ConfigBase<T>{
} }
} }
save(config: T) { save(config: T, overwrite: boolean = false) {
Object.assign(this, config); Object.assign(this, config);
const configPath = this.getConfigPath(); if (overwrite) {
// 用户要求强制写入,则变更当前文件为目标文件
this.pathName = `${selfInfo.uin}`;
}
const configPath = this.getConfigPath(this.pathName);
try { try {
fs.writeFileSync(configPath, JSON.stringify(this, this.getKeys(), 2)); fs.writeFileSync(configPath, JSON.stringify(this, this.getKeys(), 2));
} catch (e: any) { } catch (e: any) {

View File

@@ -5,6 +5,7 @@ interface Internal_MapKey {
timeout: number, timeout: number,
createtime: number, createtime: number,
func: (...arg: any[]) => any, func: (...arg: any[]) => any,
checker: ((...args: any[]) => boolean) | undefined,
} }
export class ListenerClassBase { export class ListenerClassBase {
@@ -83,17 +84,19 @@ export class NTEventWrapper {
} }
//统一回调清理事件 //统一回调清理事件
async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) { async DispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
//console.log(ListenerMainName, this.EventTask.get(ListenerMainName), ListenerSubName, this.EventTask.get(ListenerMainName)?.get(ListenerSubName)); //console.log("[EventDispatcher]",ListenerMainName, ListenerSubName, ...args);
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => { this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.forEach((task, uuid) => {
//console.log(task.func, uuid, task.createtime, task.timeout); //console.log(task.func, uuid, task.createtime, task.timeout);
if (task.createtime + task.timeout < Date.now()) { if (task.createtime + task.timeout < Date.now()) {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid); this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid);
return; return;
} }
task.func(...args); if (task.checker && task.checker(...args)) {
task.func(...args);
}
}); });
} }
async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any>,>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) { async CallNoListenerEvent<EventType extends (...args: any[]) => Promise<any> | any>(EventName = '', timeout: number = 3000, ...args: Parameters<EventType>) {
return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => { return new Promise<Awaited<ReturnType<EventType>>>(async (resolve, reject) => {
const EventFunc = this.CreatEventFunction<EventType>(EventName); const EventFunc = this.CreatEventFunction<EventType>(EventName);
let complete = false; let complete = false;
@@ -107,27 +110,71 @@ export class NTEventWrapper {
resolve(retData); resolve(retData);
}); });
} }
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, ...args: Parameters<EventType>) { async RegisterListen<ListenerType extends (...args: any[]) => void>(ListenerName = '', waitTimes = 1, timeout = 5000, checker: (...args: Parameters<ListenerType>) => boolean) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = ListenerName.split('/');
const ListenerMainName = ListenerNameList[0];
const ListenerSubName = ListenerNameList[1];
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined;
const databack = () => {
if (complete == 0) {
reject(new Error(' ListenerName:' + ListenerName + ' timeout'));
} else {
resolve(retData!);
}
};
const Timeouter = setTimeout(databack, timeout);
const eventCallbak = {
timeout: timeout,
createtime: Date.now(),
checker: checker,
func: (...args: Parameters<ListenerType>) => {
complete++;
retData = args;
if (complete >= waitTimes) {
clearTimeout(Timeouter);
databack();
}
}
};
if (!this.EventTask.get(ListenerMainName)) {
this.EventTask.set(ListenerMainName, new Map());
}
if (!(this.EventTask.get(ListenerMainName)?.get(ListenerSubName))) {
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak);
this.CreatListenerFunction(ListenerMainName);
});
}
async CallNormalEvent<EventType extends (...args: any[]) => Promise<any>, ListenerType extends (...args: any[]) => void>
(EventName = '', ListenerName = '', waitTimes = 1, timeout: number = 3000, checker: (...args: Parameters<ListenerType>) => boolean, ...args: Parameters<EventType>) {
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => { return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(async (resolve, reject) => {
const id = randomUUID(); const id = randomUUID();
let complete = 0; let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined; let retData: Parameters<ListenerType> | undefined = undefined;
let retEvent: any = {}; let retEvent: any = {};
const databack = () => { const databack = () => {
if (complete < waitTimes) { if (complete == 0) {
reject(new Error('NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' timeout')); reject(new Error('Timeout: NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' EventRet:\n' + JSON.stringify(retEvent, null, 4) + '\n'));
} else { } else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]); resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
} }
}; };
const Timeouter = setTimeout(databack, timeout);
const ListenerNameList = ListenerName.split('/'); const ListenerNameList = ListenerName.split('/');
const ListenerMainName = ListenerNameList[0]; const ListenerMainName = ListenerNameList[0];
const ListenerSubName = ListenerNameList[1]; const ListenerSubName = ListenerNameList[1];
const Timeouter = setTimeout(databack, timeout);
const eventCallbak = { const eventCallbak = {
timeout: timeout, timeout: timeout,
createtime: Date.now(), createtime: Date.now(),
checker: checker,
func: (...args: any[]) => { func: (...args: any[]) => {
complete++; complete++;
//console.log('func', ...args); //console.log('func', ...args);
@@ -147,7 +194,6 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak); this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallbak);
this.CreatListenerFunction(ListenerMainName); this.CreatListenerFunction(ListenerMainName);
const EventFunc = this.CreatEventFunction<EventType>(EventName); const EventFunc = this.CreatEventFunction<EventType>(EventName);
//console.log("测试打点", args);
retEvent = await EventFunc!(...(args as any[])); retEvent = await EventFunc!(...(args as any[]));
}); });
} }

View File

@@ -1,165 +0,0 @@
import { logError, logDebug } from "@/common/utils/log";
type group_id = number;
type user_id = number;
class cacheNode<T> {
value: T;
groupId: group_id;
userId: user_id;
prev: cacheNode<T> | null;
next: cacheNode<T> | null;
timestamp: number;
constructor(groupId: group_id, userId: user_id, value: T) {
this.groupId = groupId;
this.userId = userId;
this.value = value;
this.prev = null;
this.next = null;
this.timestamp = Date.now();
}
}
type cache<T> = { [key: group_id]: { [key: user_id]: cacheNode<T> } };
class LRU<T> {
private maxAge: number;
private maxSize: number;
private currentSize: number;
private cache: cache<T>;
private head: cacheNode<T> | null = null;
private tail: cacheNode<T> | null = null;
private onFuncs: ((node: cacheNode<T>) => void)[] = [];
constructor(maxAge: number = 6e4, maxSize: number = 5e3) {
this.maxAge = maxAge;
this.maxSize = maxSize;
this.cache = Object.create(null);
this.currentSize = 0;
if (maxSize == 0) return;
setInterval(() => this.removeExpired(), this.maxAge);
}
// 移除LRU节点
private removeLRUNode(node: cacheNode<T>) {
logDebug(
"removeLRUNode",
node.groupId,
node.userId,
node.value,
this.currentSize
);
node.prev = node.next = null;
delete this.cache[node.groupId][node.userId];
this.removeNode(node);
this.onFuncs.forEach((func) => func(node));
this.currentSize--;
}
public on(func: (node: cacheNode<T>) => void) {
this.onFuncs.push(func);
}
private removeExpired() {
const now = Date.now();
let current = this.tail;
const nodesToRemove: cacheNode<T>[] = [];
let removedCount = 0;
// 收集需要删除的节点
while (current && now - current.timestamp > this.maxAge) {
nodesToRemove.push(current);
current = current.prev;
removedCount++;
if (removedCount >= 100) break;
}
// 更新链表指向
if (nodesToRemove.length > 0) {
const newTail = nodesToRemove[nodesToRemove.length - 1].prev;
if (newTail) {
newTail.next = null;
} else {
this.head = null;
}
this.tail = newTail;
}
nodesToRemove.forEach((node) => {
node.prev = node.next = null;
delete this.cache[node.groupId][node.userId];
this.currentSize--;
this.onFuncs.forEach((func) => func(node));
});
}
private addNode(node: cacheNode<T>) {
node.next = this.head;
if (this.head) this.head.prev = node;
if (!this.tail) this.tail = node;
this.head = node;
}
private removeNode(node: cacheNode<T>) {
if (node.prev) node.prev.next = node.next;
if (node.next) node.next.prev = node.prev;
if (node === this.head) this.head = node.next;
if (node === this.tail) this.tail = node.prev;
}
private moveToHead(node: cacheNode<T>) {
if (this.head === node) return;
this.removeNode(node);
this.addNode(node);
node.prev = null;
}
public set(groupId: group_id, userId: user_id, value: T) {
if (!this.cache[groupId]) {
this.cache[groupId] = Object.create(null);
}
const groupObject = this.cache[groupId];
if (groupObject[userId]) {
const node = groupObject[userId];
node.value = value;
node.timestamp = Date.now();
this.moveToHead(node);
} else {
const node = new cacheNode(groupId, userId, value);
groupObject[userId] = node;
this.currentSize++;
this.addNode(node);
if (this.currentSize > this.maxSize) {
const tail = this.tail!;
this.removeLRUNode(tail);
}
}
}
public get(groupId: group_id): { userId: user_id; value: T }[];
public get(groupId: group_id, userId: user_id): null | { userId: user_id; value: T };
public get(groupId: group_id, userId?: user_id): any {
const groupObject = this.cache[groupId];
if(!groupObject) return userId === undefined ? [] : null;
if (userId === undefined) {
return Object.entries(groupObject).map(([userId, { value }]) => ({
userId: Number(userId),
value,
}));
}
if (groupObject[userId]) {
return { userId, value: groupObject[userId].value };
}
return null;
}
}
export default LRU;

View File

@@ -1,20 +1,40 @@
import crypto from 'crypto'; import { Peer } from '@/core';
import crypto, { randomInt, randomUUID } from 'crypto';
import { logError } from './log';
class LimitedHashTable<K, V> { export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map(); private keyToValue: Map<K, V> = new Map();
private valueToKey: Map<V, K> = new Map(); private valueToKey: Map<V, K> = new Map();
private maxSize: number; private maxSize: number;
private KeyQueneList: K[] = [];
private ValueQueneList: V[] = [];
constructor(maxSize: number) { constructor(maxSize: number) {
this.maxSize = maxSize; this.maxSize = maxSize;
} }
resize(count: number) {
this.maxSize = count;
}
set(key: K, value: V): void { set(key: K, value: V): void {
// const isExist = this.keyToValue.get(key);
// if (isExist && isExist === value) {
// return;
// }
this.keyToValue.set(key, value); this.keyToValue.set(key, value);
this.valueToKey.set(value, key); this.valueToKey.set(value, key);
if (this.KeyQueneList.length >= this.maxSize || this.ValueQueneList.length >= this.maxSize) { while (this.keyToValue.size !== this.valueToKey.size) {
this.KeyQueneList.shift(); console.log('keyToValue.size !== valueToKey.size Error Atom');
this.ValueQueneList.shift(); this.keyToValue.clear();
this.valueToKey.clear();
}
// console.log('---------------');
// console.log(this.keyToValue);
// console.log(this.valueToKey);
// console.log('---------------');
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
//console.log(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);
} }
} }
@@ -26,28 +46,99 @@ class LimitedHashTable<K, V> {
return this.valueToKey.get(value); return this.valueToKey.get(value);
} }
delete(key: K): void { deleteByValue(value: V): void {
const key = this.valueToKey.get(value);
if (key !== undefined) {
this.keyToValue.delete(key);
this.valueToKey.delete(value);
}
}
deleteByKey(key: K): void {
const value = this.keyToValue.get(key); const value = this.keyToValue.get(key);
if (value !== undefined) { if (value !== undefined) {
this.keyToValue.delete(key); this.keyToValue.delete(key);
this.valueToKey.delete(value); this.valueToKey.delete(value);
} }
} }
getKeyList(): K[] {
return Array.from(this.keyToValue.keys());
}
//获取最近刚写入的几个值
getHeads(size: number): { key: K; value: V }[] | undefined {
const keyList = this.getKeyList();
if (keyList.length === 0) {
return undefined;
}
const result: { key: K; value: V }[] = [];
const listSize = Math.min(size, keyList.length);
for (let i = 0; i < listSize; i++) {
const key = keyList[listSize - i];
result.push({ key, value: this.keyToValue.get(key)! });
}
return result;
}
} }
class MessageUniqueWrapper { class MessageUniqueWrapper {
private msgIdMap: LimitedHashTable<number, string> = new LimitedHashTable(1000); private msgDataMap: LimitedHashTable<string, number>;
createMsg(MsgId: string) { private msgIdMap: LimitedHashTable<string, number>;
const ShortId = parseInt(crypto.createHash('sha1').update('2345').digest('hex').slice(0, 8), 16); constructor(maxMap: number = 1000) {
this.msgIdMap.set(ShortId, MsgId); this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
return ShortId; this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
} }
getMsgIdByShortId(ShortId: number) { getRecentMsgIds(Peer: Peer, size: number): string[] {
return this.msgIdMap.getValue(ShortId); const heads = this.msgIdMap.getHeads(size);
if (!heads) {
return [];
}
const data = heads.map((t) => MessageUnique.getMsgIdAndPeerByShortId(t.value));
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid);
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined);
} }
getShortIdByMsgId(MsgId: string) { createMsg(peer: Peer, msgId: string): number | undefined {
return this.msgIdMap.getKey(MsgId); const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('md5').update(key).digest();
//设置第一个bit为0 保证shortId为正数
hash[0] &= 0x7f;
const shortId = hash.readInt32BE(0);
//减少性能损耗
// const isExist = this.msgIdMap.getKey(shortId);
// if (isExist && isExist === msgId) {
// return shortId;
// }
this.msgIdMap.set(msgId, shortId);
this.msgDataMap.set(key, shortId);
return shortId;
}
getMsgIdAndPeerByShortId(shortId: number): { MsgId: string; Peer: Peer } | undefined {
const data = this.msgDataMap.getKey(shortId);
if (data) {
const [msgId, chatTypeStr, peerUid] = data.split('|');
const peer: Peer = {
chatType: parseInt(chatTypeStr),
peerUid,
guildId: '',
};
return { MsgId: msgId, Peer: peer };
}
return undefined;
}
getShortIdByMsgId(msgId: string): number | undefined {
return this.msgIdMap.getValue(msgId);
}
getPeerByMsgId(msgId: string) {
const shortId = this.msgIdMap.getValue(msgId);
if (!shortId) return undefined;
return this.getMsgIdAndPeerByShortId(shortId);
}
resize(maxSize: number): void {
this.msgIdMap.resize(maxSize);
this.msgDataMap.resize(maxSize);
} }
} }
export const MessageUnique = new MessageUniqueWrapper(); export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();

View File

@@ -1,77 +1,56 @@
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os';
import { systemPlatform } from '@/common/utils/system'; import { systemPlatform } from '@/common/utils/system';
import { logError } from '@/common/utils/log'; import { getDefaultQQVersionConfigInfo, getQQVersionConfigPath } from './helper';
import AppidTable from '@/core/external/appid.json';
import { log } from './log';
export const exePath = process.execPath; //基础目录获取
export let QQMainPath = process.execPath;
export let QQPackageInfoPath: string = path.join(path.dirname(QQMainPath), 'resources', 'app', 'package.json');
export let QQVersionConfigPath: string | undefined = getQQVersionConfigPath(QQMainPath);
export const pkgInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'package.json'); //基础信息获取 无快更则启用默认模板填充
let configVersionInfoPath; export let isQuickUpdate: boolean = !!QQVersionConfigPath;
export let QQVersionConfig: QQVersionConfigType = isQuickUpdate ? JSON.parse(fs.readFileSync(QQVersionConfigPath!).toString()) : getDefaultQQVersionConfigInfo();
export let QQPackageInfo: QQPackageInfoType = JSON.parse(fs.readFileSync(QQPackageInfoPath).toString());
export let { appid: QQVersionAppid, qua: QQVersionQua } = getAppidV2();
if (os.platform() !== 'linux') { //基础函数
configVersionInfoPath = path.join(path.dirname(exePath), 'resources', 'app', 'versions', 'config.json'); export function getQQBuildStr() {
} else { return isQuickUpdate ? QQVersionConfig.buildId : QQPackageInfo.buildVersion;
const userPath = os.homedir();
const appDataPath = path.resolve(userPath, './.config/QQ');
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
} }
export function getFullQQVesion() {
if (typeof configVersionInfoPath !== 'string') { return isQuickUpdate ? QQVersionConfig.curVersion : QQPackageInfo.version;
throw new Error('Something went wrong when load QQ info path');
} }
export function requireMinNTQQBuild(buildStr: string) {
export { configVersionInfoPath }; return parseInt(getQQBuildStr()) >= parseInt(buildStr);
type QQPkgInfo = {
version: string;
buildVersion: string;
platform: string;
eleArch: string;
} }
type QQVersionConfigInfo = { //此方法不要直接使用
baseVersion: string; export function getQUAInternal() {
curVersion: string; return systemPlatform === 'linux' ? `V1_LNX_NQ_${getFullQQVesion()}_${getQQBuildStr()}_GW_B` : `V1_WIN_NQ_${getFullQQVesion()}_${getQQBuildStr()}_GW_B`;
prevVersion: string;
onErrorVersions: Array<any>;
buildId: string;
} }
export function getAppidV2(): { appid: string, qua: string } {
let _qqVersionConfigInfo: QQVersionConfigInfo = { let appidTbale = AppidTable as unknown as QQAppidTableType;
'baseVersion': '9.9.11-24568',
'curVersion': '9.9.11-24568',
'prevVersion': '',
'onErrorVersions': [],
'buildId': '24568'
};
if (fs.existsSync(configVersionInfoPath)) {
try { try {
const _ =JSON.parse(fs.readFileSync(configVersionInfoPath).toString()); let data = appidTbale[getFullQQVesion()];
_qqVersionConfigInfo = Object.assign(_qqVersionConfigInfo, _); if (data) {
} catch (e) { return data;
logError('Load QQ version config info failed, Use default version', e); }
} }
catch (e) {
log('[QQ版本兼容性检测] 版本兼容性不佳,可能会导致一些功能无法正常使用', e);
}
// 以下是兜底措施
return { appid: systemPlatform === 'linux' ? '537237950' : '537237765', qua: getQUAInternal() };
} }
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo;
//V1_WIN_NQ_9.9.11_24568_GW_B
export const qqPkgInfo: QQPkgInfo = JSON.parse(fs.readFileSync(pkgInfoPath).toString());
// platform_type: 3, // platform_type: 3,
// app_type: 4, // app_type: 4,
// app_version: '9.9.9-23159', // app_version: '9.9.12-25765',
// qua: 'V1_WIN_NQ_9.9.9_23159_GW_B', // qua: 'V1_WIN_NQ_9.9.12_25765_GW_B',
// appid: '537213764', // appid: '537234702',
// platVer: '10.0.26100', // platVer: '10.0.26100',
// clientVer: '9.9.9-23159', // clientVer: '9.9.9-25765',
// Linux // Linux
// app_version: '3.2.9-24568', // app_version: '3.2.9-25765',
// qua: 'V1_LNX_NQ_3.2.9_24568_GW_B', // qua: 'V1_LNX_NQ_3.2.10_25765_GW_B',
let _appid: string = '537226369'; // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') {
_appid = '537226441';
}
// todo: mac 平台的 appid
export const appid = _appid;

View File

@@ -1,9 +1,9 @@
import fs from 'fs'; import fs from 'fs';
import { encode, getDuration, getWavFileInfo, isWav } from 'silk-wasm'; import { encode, getDuration, getWavFileInfo, isWav, isSilk } from 'silk-wasm';
import fsPromise from 'fs/promises'; import fsPromise from 'fs/promises';
import { log, logError } from './log'; import { log, logError } from './log';
import path from 'node:path'; import path from 'node:path';
import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { getTempDir } from '@/common/utils/file'; import { getTempDir } from '@/common/utils/file';
@@ -63,10 +63,11 @@ export async function encodeSilk(filePath: string) {
// } // }
try { try {
const pttPath = path.join(TEMP_DIR, uuidv4()); const file = await fsPromise.readFile(filePath);
if (getFileHeader(filePath) !== '02232153494c4b') { const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
log(`语音文件${filePath}需要转换成silk`); log(`语音文件${filePath}需要转换成silk`);
const _isWav = await isWavFile(filePath); const _isWav = isWav(file);
const pcmPath = pttPath + '.pcm'; const pcmPath = pttPath + '.pcm';
let sampleRate = 0; let sampleRate = 0;
const convert = () => { const convert = () => {
@@ -96,7 +97,7 @@ export async function encodeSilk(filePath: string) {
if (!_isWav) { if (!_isWav) {
input = await convert(); input = await convert();
} else { } else {
input = fs.readFileSync(filePath); input = file;
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const { fmt } = getWavFileInfo(input); const { fmt } = getWavFileInfo(input);
// log(`wav文件信息`, fmt) // log(`wav文件信息`, fmt)
@@ -113,7 +114,7 @@ export async function encodeSilk(filePath: string) {
duration: silk.duration / 1000 duration: silk.duration / 1000
}; };
} else { } else {
const silk = fs.readFileSync(filePath); const silk = file;
let duration = 0; let duration = 0;
try { try {
duration = getDuration(silk) / 1000; duration = getDuration(silk) / 1000;

View File

@@ -1,451 +0,0 @@
import { ElementType, FileElement, PicElement, PttElement, RawMessage, VideoElement } from '../../core/src/entities';
import sqlite3 from 'sqlite3';
import { log, logDebug, logError } from '@/common/utils/log';
import { NTQQMsgApi } from '@/core';
import LRU from '@/common/utils/LRUCache';
export interface IRember {
last_sent_time: number;
join_time: number;
user_id: number;
}
type DBMsg = {
id: number,
shortId: number,
longId: string,
seq: number,
peerUid: string,
chatType: number,
}
type DBFile = {
name: string; // 文件名
path: string;
url: string;
size: number;
uuid: string;
msgId: string;
elementId: string;
element: PicElement | VideoElement | FileElement | PttElement;
elementType: ElementType.PIC | ElementType.VIDEO | ElementType.FILE | ElementType.PTT;
}
class DBUtilBase {
protected db: sqlite3.Database | undefined;
async init(dbPath: string) {
if (this.db) {
return;
}
return new Promise<void>((resolve, reject) => {
this.db = new sqlite3.Database(dbPath, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => {
if (err) {
logError('Could not connect to database', err);
reject(err);
return;
}
this.createTable();
resolve();
});
});
}
protected createTable() {
throw new Error('Method not implemented.');
}
close() {
this.db?.close();
}
}
class DBUtil extends DBUtilBase {
private msgCache: Map<string | number, RawMessage> = new Map<string | number, RawMessage>();
private globalMsgShortId = -2147483640;
private groupIds: number[] = [];
private LURCache = new LRU<number>();
private LastSentCache = new (class {
private cache: { gid: number; uid: number }[] = [];
private maxSize: number;
constructor(maxSize: number = 5000) {
this.maxSize = maxSize;
}
get(gid: number, uid: number): boolean {
const exists = this.cache.some(
(entry) => entry.gid === gid && entry.uid === uid
);
if (!exists) {
this.cache.push({ gid, uid });
if (this.cache.length > this.maxSize) {
this.cache.shift();
}
}
return exists;
}
})();
constructor() {
super();
const interval = 1000 * 60 * 10; // 10分钟清理一次缓存
setInterval(() => {
logDebug('清理消息缓存');
this.msgCache.forEach((msg, key) => {
if ((Date.now() - parseInt(msg.msgTime) * 1000) > interval) {
this.msgCache.delete(key);
}
});
}, interval);
}
async init(dbPath: string) {
await super.init(dbPath);
this.globalMsgShortId = await this.getCurrentMaxShortId();
// 初始化群缓存列表
this.db!.serialize(() => {
const sql = 'SELECT * FROM sqlite_master WHERE type=\'table\'';
this.db!.all(sql, [], (err, rows: { name: string }[]) => {
if (err) return logError(err);
rows.forEach((row) => this.groupIds.push(parseInt(row.name)));
//logDebug(`已加载 ${groupIds.length} 个群`);
});
});
this.LURCache.on(async (node) => {
const { value: time, groupId, userId } = node;
logDebug('插入发言时间', userId, groupId);
await this.createGroupInfoTimeTableIfNotExist(groupId);
const method = await this.getDataSetMethod(groupId, userId);
logDebug('插入发言时间方法判断', userId, groupId, method);
const sql =
method == 'update'
? `UPDATE "${groupId}" SET last_sent_time = ? WHERE user_id = ?`
: `INSERT INTO "${groupId}" (last_sent_time, user_id) VALUES (?, ?)`;
this.db!.all(sql, [time, userId], (err) => {
if (err) {
return logError('插入/更新发言时间失败', userId, groupId);
}
logDebug('插入/更新发言时间成功', userId, groupId);
});
});
}
async getDataSetMethod(groupId: number, userId: number) {
// 缓存记录
if (this.LastSentCache.get(groupId, userId)) {
logDebug('缓存命中', userId, groupId);
return 'update';
}
// 数据库判断
return new Promise<'insert' | 'update'>((resolve, reject) => {
this.db!.all(
`SELECT * FROM "${groupId}" WHERE user_id = ?`,
[userId],
(err, rows) => {
if (err) {
logError('查询发言时间存在失败', userId, groupId, err);
return logError('插入发言时间失败', userId, groupId, err);
}
if (rows.length === 0) {
logDebug('查询发言时间不存在', userId, groupId);
return resolve('insert');
}
logDebug('查询发言时间存在', userId, groupId);
resolve('update');
}
);
});
}
async createGroupInfoTimeTableIfNotExist(groupId: number) {
const createTableSQL = (groupId: number) =>
`CREATE TABLE IF NOT EXISTS "${groupId}" (
user_id INTEGER,
last_sent_time INTEGER,
join_time INTEGER,
PRIMARY KEY (user_id)
);`;
if (this.groupIds.includes(groupId)) {
return;
}
return new Promise((resolve, reject) => {
const sql = createTableSQL(groupId);
this.db!.all(sql, (err) => {
if (err) {
reject(err);
return;
}
this.groupIds.push(groupId);
resolve(true);
});
});
}
protected createTable() {
// 消息记录
const createTableSQL = `
CREATE TABLE IF NOT EXISTS msgs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
shortId INTEGER NOT NULL UNIQUE,
longId TEXT NOT NULL UNIQUE,
seq INTEGER NOT NULL,
peerUid TEXT NOT NULL,
chatType INTEGER NOT NULL
)`;
this.db!.run(createTableSQL, function (err) {
if (err) {
logError('Could not create table msgs', err.stack);
}
});
// 文件缓存
const createFileTableSQL = `
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
path TEXT NOT NULL,
url TEXT,
size INTEGER NOT NULL,
uuid TEXT,
elementType INTEGER,
element TEXT NOT NULL,
elementId TEXT NOT NULL,
msgId TEXT NOT NULL
)`;
this.db!.run(createFileTableSQL, function (err) {
if (err) {
logError('Could not create table files', err);
}
});
}
private async getCurrentMaxShortId() {
return new Promise<number>((resolve, reject) => {
this.db!.get('SELECT MAX(shortId) as maxId FROM msgs', (err, row: { maxId: number }) => {
if (err) {
logDebug('Could not get max short id, Use default -2147483640', err);
return resolve(-2147483640);
}
logDebug('数据库中消息最大短id', row?.maxId);
resolve(row?.maxId ?? -2147483640);
});
});
}
private async getMsg(query: string, params: any[]) {
const stmt = this.db!.prepare(query);
return new Promise<RawMessage | null>((resolve, reject) => {
stmt.get(...params, (err: any, row: DBMsg) => {
// log("getMsg", row, err);
if (err) {
logError('Could not get msg', err, query, params);
return resolve(null);
}
if (!row) {
// logDebug('不存在数据库中的消息,不进行处理', query, params);
resolve(null);
return;
}
const msgId = row.longId;
NTQQMsgApi.getMsgsByMsgId({ peerUid: row.peerUid, chatType: row.chatType }, [msgId]).then(res => {
const msg = res.msgList[0];
if (!msg) {
resolve(null);
return;
}
msg.id = row.shortId;
resolve(msg);
}).catch(e => {
resolve(null);
});
});
});
}
async getMsgByShortId(shortId: number): Promise<RawMessage | null> {
if (this.msgCache.has(shortId)) {
return this.msgCache.get(shortId)!;
}
const getStmt = 'SELECT * FROM msgs WHERE shortId = ?';
return this.getMsg(getStmt, [shortId]);
}
async getMsgByLongId(longId: string): Promise<RawMessage | null> {
if (this.msgCache.has(longId)) {
return this.msgCache.get(longId)!;
}
return this.getMsg('SELECT * FROM msgs WHERE longId = ?', [longId]);
}
async getMsgBySeq(peerUid: string, seq: string): Promise<RawMessage | null> {
const stmt = 'SELECT * FROM msgs WHERE peerUid = ? AND seq = ?';
return this.getMsg(stmt, [peerUid, seq]);
}
async addMsg(msg: RawMessage, update = true): Promise<number> {
const existMsg = await this.getMsgByLongId(msg.msgId);
if (existMsg) {
// logDebug('消息已存在,更新数据库', msg.msgId);
if (update) this.updateMsg(msg).then();
return existMsg.id!;
}
const stmt = this.db!.prepare('INSERT INTO msgs (shortId, longId, seq, peerUid, chatType) VALUES (?, ?, ?, ?, ?)');
// const runAsync = promisify(stmt.run.bind(stmt));
const shortId = ++this.globalMsgShortId;
msg.id = shortId;
//logDebug(`记录消息到数据库, 消息长id: ${msg.msgId}, 短id: ${msg.id}`);
this.msgCache.set(shortId, msg);
this.msgCache.set(msg.msgId, msg);
stmt.run(this.globalMsgShortId, msg.msgId, msg.msgSeq.toString(), msg.peerUid, msg.chatType, (err: any) => {
if (err) {
if (err.errno === 19) {
this.getMsgByLongId(msg.msgId).then((msg: RawMessage | null) => {
if (msg) {
this.msgCache.set(shortId, msg);
this.msgCache.set(msg.msgId, msg);
// logDebug('获取消息短id成功', msg.id);
} else {
logError('db could not get msg by long id', err);
}
}).catch(e => logError('db getMsgByLongId error', e));
} else {
logError('db could not add msg', err);
}
}
});
return shortId;
}
async updateMsg(msg: RawMessage) {
const existMsg = this.msgCache.get(msg.msgId);
if (existMsg) {
Object.assign(existMsg, msg);
}
//logDebug(`更新消息, shortId:${msg.id}, seq: ${msg.msgSeq}, msgId: ${msg.msgId}`);
const stmt = this.db!.prepare('UPDATE msgs SET seq=? WHERE longId=?');
stmt.run(msg.msgSeq, msg.msgId, (err: any) => {
if (err) {
logError('updateMsg db error', err);
}
});
}
async addFileCache(file: DBFile) {
const stmt = this.db!.prepare('INSERT INTO files (name, path, url, size, uuid, elementType ,element, elementId, msgId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
return new Promise((resolve, reject) => {
stmt.run(file.name, file.path, file.url, file.size, file.uuid,
file.elementType,
JSON.stringify(file.element),
file.elementId,
file.msgId,
function (err: any) {
if (err) {
logError('db could not add file', err);
reject(err);
}
resolve(null);
});
});
}
private async getFileCache(query: string, params: any[]) {
const stmt = this.db!.prepare(query);
return new Promise<DBFile | null>((resolve, reject) => {
stmt.get(...params, (err: any, row: DBFile & { element: string }) => {
if (err) {
logError('db could not get file cache', err);
reject(err);
}
if (row) {
row.element = JSON.parse(row.element);
}
resolve(row);
});
});
}
async getFileCacheByName(name: string): Promise<DBFile | null> {
return this.getFileCache('SELECT * FROM files WHERE name = ?', [name]);
}
async getFileCacheByUuid(uuid: string): Promise<DBFile | null> {
return this.getFileCache('SELECT * FROM files WHERE uuid = ?', [uuid]);
}
// todo: 是否所有的文件都有uuid语音消息有没有uuid
async updateFileCache(file: DBFile) {
const stmt = this.db!.prepare('UPDATE files SET path = ?, url = ? WHERE uuid = ?');
return new Promise((resolve, reject) => {
stmt.run(file.path, file.url, file.uuid, function (err: any) {
if (err) {
logError('db could not update file cache', err);
reject(err);
}
resolve(null);
});
});
}
async getLastSentTimeAndJoinTime(
groupId: number
): Promise<IRember[]> {
logDebug('读取发言时间', groupId);
return new Promise<IRember[]>((resolve, reject) => {
this.db!.all(`SELECT * FROM "${groupId}" `, (err, rows: IRember[]) => {
const cache = this.LURCache.get(groupId).map(e=>({user_id:e.userId, last_sent_time:e.value}));
if (err) {
logError('查询发言时间失败', groupId);
return resolve(cache.map(e=>({...e, join_time:0})));
}
Object.assign(rows, cache)
logDebug('查询发言时间成功', groupId, rows);
resolve(rows);
});
});
}
insertLastSentTime(
groupId: number,
userId: number,
time: number
) {
this.LURCache.set(groupId, userId, time);
}
async insertJoinTime(
groupId: number,
userId: number,
time: number
) {
await this.createGroupInfoTimeTableIfNotExist(groupId);
this.db!.all(
`INSERT OR REPLACE INTO "${groupId}" (user_id, last_sent_time, join_time) VALUES (?,?,?)`,
[userId, time, time],
(err) => {
if (err)
logError(err),
Promise.reject(),
logError('插入入群时间失败', userId, groupId);
}
);
}
}
export const dbUtil = new DBUtil();

View File

@@ -4,9 +4,8 @@ import crypto from 'crypto';
import util from 'util'; import util from 'util';
import path from 'node:path'; import path from 'node:path';
import { log, logError } from './log'; import { log, logError } from './log';
import { dbUtil } from '@/common/utils/db';
import * as fileType from 'file-type'; import * as fileType from 'file-type';
import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'crypto';
import { napCatCore } from '@/core'; import { napCatCore } from '@/core';
export const getNapCatDir = () => { export const getNapCatDir = () => {
@@ -192,7 +191,7 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
isLocal: false isLocal: false
}; };
if (!fileName) { if (!fileName) {
fileName = uuidv4(); fileName = randomUUID();
} }
let filePath = path.join(getTempDir(), fileName); let filePath = path.join(getTempDir(), fileName);
let url = null; let url = null;
@@ -235,7 +234,7 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
} }
fileName = fileName.replace(/[/\\:*?"<>|]/g, '_'); fileName = fileName.replace(/[/\\:*?"<>|]/g, '_');
res.fileName = fileName; res.fileName = fileName;
filePath = path.join(getTempDir(), uuidv4() + fileName); filePath = path.join(getTempDir(), randomUUID() + fileName);
fs.writeFileSync(filePath, buffer); fs.writeFileSync(filePath, buffer);
} catch (e: any) { } catch (e: any) {
res.errMsg = `${url}下载失败,` + e.toString(); res.errMsg = `${url}下载失败,` + e.toString();
@@ -251,13 +250,14 @@ export async function uri2local(uri: string, fileName: string | null = null): Pr
} else { } else {
filePath = pathname; filePath = pathname;
} }
} else { }
const cache = await dbUtil.getFileCacheByName(uri); else {
if (cache) { // const cache = await dbUtil.getFileCacheByName(uri);
filePath = cache.path; // if (cache) {
} else { // filePath = cache.path;
filePath = uri; // } else {
} // filePath = uri;
// }
} }
res.isLocal = true; res.isLocal = true;

View File

@@ -1,17 +1,58 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import path from 'node:path'; import path from 'node:path';
import fs from 'fs/promises'; import fs from 'fs';
import { log, logDebug } from './log'; import { log, logDebug } from './log';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import * as fsPromise from 'node:fs/promises'; import * as fsPromise from 'node:fs/promises';
import os from 'node:os';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
//下面这个类是用于将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;
const uuid = `${combinedHex.substring(0, 8)}-${combinedHex.substring(8, 12)}-${combinedHex.substring(12, 16)}-${combinedHex.substring(16, 20)}-${combinedHex.substring(20)}`;
return uuid;
}
static decode(uuid: string): { high: string, low: string } {
const hex = uuid.replace(/-/g, '');
const high = BigInt('0x' + hex.substring(0, 16));
const low = BigInt('0x' + hex.substring(16));
return { high: high.toString(), low: low.toString() };
}
}
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); 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 getMd5(s: string) { export function getMd5(s: string) {
const h = crypto.createHash('md5'); const h = crypto.createHash('md5');
@@ -89,7 +130,36 @@ export function CacheClassFuncAsync(ttl: number = 3600 * 1000, customKey: string
} }
return logExecutionTime; return logExecutionTime;
} }
export function CacheClassFuncAsyncExtend(ttl: number = 3600 * 1000, customKey: string = '', checker: any = (...data: any[]) => { return true; }) {
//console.log('CacheClassFuncAsync', ttl, customKey);
function logExecutionTime(target: any, methodName: string, descriptor: PropertyDescriptor) {
//console.log('logExecutionTime', target, methodName, descriptor);
const cache = new Map<string, { expiry: number; value: any }>();
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const key = `${customKey}${String(methodName)}.(${args.map(arg => JSON.stringify(arg)).join(', ')})`;
cache.forEach((value, key) => {
if (value.expiry < Date.now()) {
cache.delete(key);
}
});
const cachedValue = cache.get(key);
if (cachedValue && cachedValue.expiry > Date.now()) {
return cachedValue.value;
}
// const start = Date.now();
const result = await originalMethod.apply(this, args);
if (!checker(...args, result)) {
return result;//丢弃缓存
}
// const end = Date.now();
// console.log(`Method ${methodName} executed in ${end - start} ms.`);
cache.set(key, { expiry: Date.now() + ttl, value: result });
return result;
};
}
return logExecutionTime;
}
// export function CacheClassFuncAsync(ttl: number = 3600 * 1000, customKey: string = ''): any { // export function CacheClassFuncAsync(ttl: number = 3600 * 1000, customKey: string = ''): any {
// const cache = new Map<string, { expiry: number; value: any }>(); // const cache = new Map<string, { expiry: number; value: any }>();
@@ -235,14 +305,14 @@ export function migrateConfig(oldConfig: any) {
} }
// 升级旧的配置到新的 // 升级旧的配置到新的
export async function UpdateConfig() { export async function UpdateConfig() {
const configFiles = await fs.readdir(path.join(__dirname, 'config')); const configFiles = await fsPromise.readdir(path.join(__dirname, 'config'));
for (const file of configFiles) { for (const file of configFiles) {
if (file.match(/^onebot11_\d+.json$/)) { if (file.match(/^onebot11_\d+.json$/)) {
const CurrentConfig = JSON.parse(await fs.readFile(path.join(__dirname, 'config', file), 'utf8')); const CurrentConfig = JSON.parse(await fsPromise.readFile(path.join(__dirname, 'config', file), 'utf8'));
if (isValidOldConfig(CurrentConfig)) { if (isValidOldConfig(CurrentConfig)) {
log('正在迁移旧配置到新配置 File:', file); log('正在迁移旧配置到新配置 File:', file);
const NewConfig = migrateConfig(CurrentConfig); const NewConfig = migrateConfig(CurrentConfig);
await fs.writeFile(path.join(__dirname, 'config', file), JSON.stringify(NewConfig, null, 2)); await fsPromise.writeFile(path.join(__dirname, 'config', file), JSON.stringify(NewConfig, null, 2));
} }
} }
} }
@@ -262,7 +332,56 @@ export function isEqual(obj1: any, obj2: any) {
} }
return true; return true;
} }
export function getDefaultQQVersionConfigInfo(): QQVersionConfigType {
if (os.platform() === 'linux') {
return {
baseVersion: '3.2.12-26702',
curVersion: '3.2.12-26702',
prevVersion: '',
onErrorVersions: [],
buildId: '26702'
};
}
return {
baseVersion: '9.9.15-26702',
curVersion: '9.9.15-26702',
prevVersion: '',
onErrorVersions: [],
buildId: '26702'
};
}
export async function promisePipeline(promises: Promise<any>[], callback: (result: any) => boolean): Promise<void> {
let callbackCalled = false;
for (const promise of promises) {
if (callbackCalled) break;
try {
const result = await promise;
if (!callbackCalled) {
callbackCalled = callback(result);
}
} catch (error) {
console.error("Error in promise pipeline:", error);
}
}
}
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 async function deleteOldFiles(directoryPath: string, daysThreshold: number) { export async function deleteOldFiles(directoryPath: string, daysThreshold: number) {
try { try {
const files = await fsPromise.readdir(directoryPath); const files = await fsPromise.readdir(directoryPath);

View File

@@ -91,8 +91,10 @@ export function enableConsoleLog(enable: boolean) {
function formatMsg(msg: any[]) { function formatMsg(msg: any[]) {
let logMsg = ''; let logMsg = '';
for (const msgItem of msg) { for (const msgItem of msg) {
// 判断是否是对象 if (msgItem instanceof Error) { // 判断是否是错误
if (typeof msgItem === 'object') { logMsg += msgItem.stack + ' ';
continue;
} else if (typeof msgItem === 'object') { // 判断是否是对象
const obj = JSON.parse(JSON.stringify(msgItem, null, 2)); const obj = JSON.parse(JSON.stringify(msgItem, null, 2));
logMsg += JSON.stringify(truncateString(obj)) + ' '; logMsg += JSON.stringify(truncateString(obj)) + ' ';
continue; continue;

View File

@@ -7,7 +7,7 @@ export class RequestUtil {
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> { static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
const client = url.startsWith('https') ? https : http; const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let req = client.get(url, (res) => { const req = client.get(url, (res) => {
let cookies: { [key: string]: string } = {}; let cookies: { [key: string]: string } = {};
const handleRedirect = (res: http.IncomingMessage) => { const handleRedirect = (res: http.IncomingMessage) => {
//console.log(res.headers.location); //console.log(res.headers.location);
@@ -44,7 +44,7 @@ export class RequestUtil {
}); });
} }
}); });
req.on('error', (error: any) => { req.on('error', (error: any) => {
reject(error); reject(error);
}); });
}); });

View File

@@ -1,7 +1,7 @@
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { networkInterfaces } from 'os'; import { networkInterfaces } from 'os';
import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'crypto';
// 缓解Win7设备兼容性问题 // 缓解Win7设备兼容性问题
let osName: string; let osName: string;
@@ -30,7 +30,7 @@ export async function getMachineId(): Promise<string> {
if (!machineId) { if (!machineId) {
machineId = (async () => { machineId = (async () => {
const id = await getMacMachineId(); const id = await getMacMachineId();
return id || uuidv4(); // fallback, generate a UUID return id || randomUUID(); // fallback, generate a UUID
})(); })();
} }

View File

@@ -1,31 +1,17 @@
/** //QQVersionType
* 运行时类型转换与检查类 type QQPackageInfoType = {
*/ version: string;
export class TypeCheck { buildVersion: string;
static isEmpty(value: any): boolean { platform: string;
return value === null || value === undefined || value === '' || eleArch: string;
(Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0);
}
} }
type QQVersionConfigType = {
export class TypeConvert { baseVersion: string;
static toNumber(value: any): number { curVersion: string;
const num = Number(value); prevVersion: string;
if (isNaN(num)) { onErrorVersions: Array<any>;
throw new Error(`无法将输入转换为数字: ${value}`); buildId: string;
} }
return num; type QQAppidTableType = {
} [key: string]: { appid: string, qua: string };
static toString(value: any): string {
return String(value);
}
static toBoolean(value: any): boolean {
return Boolean(value);
}
static toArray(value: any): any[] {
return Array.isArray(value) ? value : [value];
}
} }

View File

@@ -4,10 +4,10 @@ export async function checkVersion(): Promise<string> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const MirrorList = const MirrorList =
[ [
'https://jsd.cdn.zzko.cn/gh/NapNeko/NapCatQQ@main/package.json',
'https://fastly.jsdelivr.net/gh/NapNeko/NapCatQQ@main/package.json', 'https://fastly.jsdelivr.net/gh/NapNeko/NapCatQQ@main/package.json',
'https://gcore.jsdelivr.net/gh/NapNeko/NapCatQQ@main/package.json', 'https://gcore.jsdelivr.net/gh/NapNeko/NapCatQQ@main/package.json',
'https://cdn.jsdelivr.us/gh/NapNeko/NapCatQQ@main/package.json', 'https://cdn.jsdelivr.net/gh/NapNeko/NapCatQQ@main/package.json'
'https://jsd.cdn.zzko.cn/gh/NapNeko/NapCatQQ@main/package.json'
]; ];
let version = undefined; let version = undefined;
for (const url of MirrorList) { for (const url of MirrorList) {

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,9 @@
import { import {
CacheFileList,
CacheFileListItem, CacheFileListItem,
CacheFileType, CacheFileType,
CacheScanResult,
ChatCacheList,
ChatCacheListItemBasic, ChatCacheListItemBasic,
ChatType, ChatType,
ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, RawMessage ElementType, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, Peer, PicElement, RawMessage
} from '@/core/entities'; } from '@/core/entities';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
@@ -15,31 +12,15 @@ import { log, logDebug, logError } from '@/common/utils/log';
import { GeneralCallResult, napCatCore, OnRichMediaDownloadCompleteParams } from '@/core'; import { GeneralCallResult, napCatCore, OnRichMediaDownloadCompleteParams } from '@/core';
import { calculateFileMD5 } from '@/common/utils/file'; import { calculateFileMD5 } from '@/common/utils/file';
import * as fileType from 'file-type'; import * as fileType from 'file-type';
import { MsgListener } from '@/core/listeners';
import imageSize from 'image-size'; import imageSize from 'image-size';
import { ISizeCalculationResult } from 'image-size/dist/types/interface'; import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { sessionConfig } from '@/core/sessionConfig'; import { sessionConfig } from '@/core/sessionConfig';
import { randomUUID } from 'crypto';
import { rkeyManager } from '../utils/rkey'; import { rkeyManager } from '../utils/rkey';
import { AsyncQueue } from '@/common/utils/AsyncQueue'; import { NTEventDispatch } from '@/common/utils/EventTask';
// const rkeyExpireTime = 1000; import { NodeIKernelSearchService } from '../services/NodeIKernelSearchService';
import { selfInfo } from '../data';
const getRKeyTaskQueue = new AsyncQueue();
const downloadMediaTasks: Map<string, (arg: OnRichMediaDownloadCompleteParams) => void> = new Map<string, (arg: OnRichMediaDownloadCompleteParams) => void>();
const downloadMediaListener = new MsgListener();
downloadMediaListener.onRichMediaDownloadComplete = arg => {
for (const [uuid, cb] of downloadMediaTasks) {
cb(arg);
downloadMediaTasks.delete(uuid);
}
};
setTimeout(() => {
napCatCore.onLoginSuccess(() => {
napCatCore.addListener(downloadMediaListener);
});
}, 100);
export class NTQQFileApi { export class NTQQFileApi {
static async getFileType(filePath: string) { static async getFileType(filePath: string) {
return fileType.fileTypeFromFile(filePath); return fileType.fileTypeFromFile(filePath);
@@ -52,12 +33,8 @@ export class NTQQFileApi {
static async getFileSize(filePath: string): Promise<number> { static async getFileSize(filePath: string): Promise<number> {
return await napCatCore.util.getFileSize(filePath); return await napCatCore.util.getFileSize(filePath);
} }
static async getVideoUrl(msg: RawMessage, element: any) { static async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
return (await napCatCore.session.getRichMediaService().getVideoPlayUrlV2({ return (await napCatCore.session.getRichMediaService().getVideoPlayUrlV2(peer, msgId, elementId, 0, { downSourceType: 1, triggerType: 1 })).urlResult.domainUrl;
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '0'
}, msg.msgId, element.elementId, 0, { downSourceType: 1, triggerType: 1 })).urlResult.domainUrl[0].url;
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) { static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
@@ -91,7 +68,9 @@ export class NTQQFileApi {
ext ext
}; };
} }
static async downloadMediaByUuid() {
//napCatCore.session.getRichMediaService().downloadFileForFileUuid();
}
static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) { static 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); //logDebug('receive downloadMedia task', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force);
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
@@ -106,31 +85,33 @@ export class NTQQFileApi {
return sourcePath; return sourcePath;
} }
} }
//logDebug('start downloadMedia', msgId, chatType, peerUid, elementId, thumbPath, sourcePath, timeout, force); let data = await NTEventDispatch.CallNormalEvent<
return new Promise<string>((resolve, reject) => { (
let completed = false; params: {
const cb = (arg: OnRichMediaDownloadCompleteParams) => { fileModelId: string,
//logDebug('downloadMedia complete', arg, msgId); downloadSourceType: number,
triggerType: number,
msgId: string,
chatType: ChatType,
peerUid: string,
elementId: string,
thumbSize: number,
downloadType: number,
filePath: string
}) => Promise<unknown>,
(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) => void
>(
'NodeIKernelMsgService/downloadRichMedia',
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
1,
timeout,
(arg: OnRichMediaDownloadCompleteParams) => {
if (arg.msgId === msgId) { if (arg.msgId === msgId) {
completed = true; return true;
let filePath = arg.filePath;
if (filePath.startsWith('\\')) {
// log('filePath start with \\');
const downloadPath = sessionConfig.defaultFileDownloadPath;
//logDebug('downloadPath', downloadPath);
filePath = path.join(downloadPath, filePath);
// 下载路径是下载文件夹的相对路径
}
resolve(filePath);
} }
}; return false;
downloadMediaTasks.set(randomUUID(), cb); },
setTimeout(() => { {
if (!completed) {
reject('下载超时');
}
}, timeout);
napCatCore.session.getMsgService().downloadRichMedia({
fileModelId: '0', fileModelId: '0',
downloadSourceType: 0, downloadSourceType: 0,
triggerType: 1, triggerType: 1,
@@ -140,9 +121,18 @@ export class NTQQFileApi {
elementId: elementId, elementId: elementId,
thumbSize: 0, thumbSize: 0,
downloadType: 1, downloadType: 1,
filePath: thumbPath, filePath: thumbPath
}); }
}); );
let filePath = data[1].filePath;
if (filePath.startsWith('\\')) {
// log('filePath start with \\');
const downloadPath = sessionConfig.defaultFileDownloadPath;
//logDebug('downloadPath', downloadPath);
filePath = path.join(downloadPath, filePath);
// 下载路径是下载文件夹的相对路径
}
return filePath;
} }
static async getImageSize(filePath: string): Promise<ISizeCalculationResult | undefined> { static async getImageSize(filePath: string): Promise<ISizeCalculationResult | undefined> {
@@ -156,27 +146,137 @@ export class NTQQFileApi {
}); });
}); });
} }
static 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.group) {
GroupData =
[{
groupCode: peer.peerUid,
isConf: false,
hasModifyConfGroupFace: true,
hasModifyConfGroupName: true,
groupName: "NapCat.Cached",
remark: "NapCat.Cached"
}];
} else if (peer.chatType === ChatType.friend) {
BuddyData = [{
category_name: 'NapCat.Cached',
peerUid: peer.peerUid,
peerUin: peer.peerUid,
remark: 'NapCat.Cached'
}];
} else {
return undefined;
}
static async getImageUrl(element: { originImageUrl: any; md5HexStr?: any; fileUuid: any; }, isPrivateImage: boolean) { return napCatCore.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
}]
}
]
});
}
static async searchfile(keys: string[]) {
type EventType = NodeIKernelSearchService['searchFileWithKeywords'];
interface OnListener {
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
}[]
}[]
};
const Event = await NTEventDispatch.CreatEventFunction<EventType>('NodeIKernelSearchService/searchFileWithKeywords');
let id = '';
const Listener = NTEventDispatch.RegisterListen<(params: OnListener) => void>('NodeIKernelSearchListener/onSearchFileKeywordsResult', 1, 20000, (params) => {
if (id !== '' && params.searchId == id) {
return true
}
return false;
});
id = await Event!(keys, 12);
let [ret] = (await Listener);
return ret;
}
static async getImageUrl(element: PicElement) {
if (!element) { if (!element) {
return ''; return '';
} }
const url = element.originImageUrl; // 没有域名 const url: string = element.originImageUrl!; // 没有域名
const md5HexStr = element.md5HexStr; const md5HexStr = element.md5HexStr;
const fileMd5 = element.md5HexStr; const fileMd5 = element.md5HexStr;
const fileUuid = element.fileUuid; const fileUuid = element.fileUuid;
if (url) { if (url) {
if (url.startsWith('/download')) { let UrlParse = new URL(IMAGE_HTTP_HOST + url);//临时解析拼接
if (url.includes('&rkey=')) { let imageAppid = UrlParse.searchParams.get('appid');
let isNewPic = imageAppid && ['1406', '1407'].includes(imageAppid);
if (isNewPic) {
let UrlRkey = UrlParse.searchParams.get('rkey');
if (UrlRkey) {
return IMAGE_HTTP_HOST_NT + url; return IMAGE_HTTP_HOST_NT + url;
} }
const rkeyData = await rkeyManager.getRkey(); const rkeyData = await rkeyManager.getRkey();
UrlRkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
const existsRKey = isPrivateImage ? rkeyData.private_rkey : rkeyData.group_rkey; return IMAGE_HTTP_HOST_NT + url + `${UrlRkey}`;
return IMAGE_HTTP_HOST_NT + url + `${existsRKey}`;
} else { } else {
// 老的图片url不需要rkey // 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url; return IMAGE_HTTP_HOST + url;

View File

@@ -1,11 +1,66 @@
import { FriendRequest, User } from '@/core/entities'; import { FriendRequest, FriendV2, SimpleInfo, User } from '@/core/entities';
import { napCatCore, OnBuddyChangeParams } from '@/core'; import { BuddyListReqType, napCatCore, NodeIKernelBuddyListener, NodeIKernelProfileService, OnBuddyChangeParams } from '@/core';
import { NTEventDispatch } from '@/common/utils/EventTask'; import { NTEventDispatch } from '@/common/utils/EventTask';
import { LimitedHashTable } from '@/common/utils/MessageUnique';
import { CacheClassFuncAsyncExtend } from '@/common/utils/helper';
export class NTQQFriendApi { export class NTQQFriendApi {
static async getBuddyV2(refresh = false): Promise<FriendV2[]> {
let uids: string[] = [];
const buddyService = napCatCore.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 NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
);
return Array.from(data.values());
}
@CacheClassFuncAsyncExtend(3600 * 1000, 'getBuddyIdMap', () => true)
static async getBuddyIdMapCache(refresh = false): Promise<LimitedHashTable<string, string>> {
return await NTQQFriendApi.getBuddyIdMap(refresh);
}
static async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
let uids: string[] = [];
let retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000);
const buddyService = napCatCore.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 NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
);
data.forEach((value, key) => {
retMap.set(value.uin!, value.uid!);
});
//console.log('getBuddyIdMap', retMap.getValue);
return retMap;
}
static async getBuddyV2ExWithCate(refresh = false) {
let uids: string[] = [];
let categoryMap: Map<string, any> = new Map();
const buddyService = napCatCore.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, categroyName: item.categroyName });
});
return item.buddyUids
}));
const data = await NTEventDispatch.CallNoListenerEvent<NodeIKernelProfileService['getCoreAndBaseInfo']>(
'NodeIKernelProfileService/getCoreAndBaseInfo', 5000, 'nodeStore', uids
);
return Array.from(data).map(([key, value]) => {
const category = categoryMap.get(key);
return category ? { ...value, categoryId: category.categoryId, categroyName: category.categroyName } : value;
});
}
static async isBuddy(uid: string) { static async isBuddy(uid: string) {
return napCatCore.session.getBuddyService().isBuddy(uid); return napCatCore.session.getBuddyService().isBuddy(uid);
} }
/**
* @deprecated
* @param forced
* @returns
*/
static async getFriends(forced = false): Promise<User[]> { static async getFriends(forced = false): Promise<User[]> {
let [_retData, _BuddyArg] = await NTEventDispatch.CallNormalEvent let [_retData, _BuddyArg] = await NTEventDispatch.CallNormalEvent
<(force: boolean) => Promise<any>, (arg: OnBuddyChangeParams) => void> <(force: boolean) => Promise<any>, (arg: OnBuddyChangeParams) => void>
@@ -14,6 +69,7 @@ export class NTQQFriendApi {
'NodeIKernelBuddyListener/onBuddyListChange', 'NodeIKernelBuddyListener/onBuddyListChange',
1, 1,
5000, 5000,
() => true,
forced forced
); );
const friends: User[] = []; const friends: User[] = [];

View File

@@ -1,24 +1,112 @@
import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType } from '../entities'; import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType, GroupNotifyTypes, ChatType, Peer, GroupListUpdateType } from '../entities';
import { GeneralCallResult, NTQQUserApi, napCatCore } from '@/core'; import { GeneralCallResult, NTQQUserApi, NodeIKernelGroupListener, NodeIKernelGroupService, napCatCore } from '@/core';
import { NTEventDispatch } from '@/common/utils/EventTask'; import { NTEventDispatch } from '@/common/utils/EventTask';
import { logDebug } from '@/common/utils/log'; import { log } from '@/common/utils/log';
// console.log(process.pid); import { groupMembers } from '../data';
// setTimeout(async () => { import { CacheClassFuncAsyncExtend, runAllWithTimeout } from '@/common/utils/helper';
// console.log(JSON.stringify(await NTQQGroupApi.getMemberExtInfo(), null, 2));
// }, 20000);
export class NTQQGroupApi { export class NTQQGroupApi {
static async setGroupAvatar(gc: string, filePath: string) {
return napCatCore.session.getGroupService().setHeader(gc, filePath);
}
static async getGroups(forced = false) { static async getGroups(forced = false) {
type ListenerType = NodeIKernelGroupListener['onGroupListUpdate'];
let [_retData, _updateType, groupList] = await NTEventDispatch.CallNormalEvent let [_retData, _updateType, groupList] = await NTEventDispatch.CallNormalEvent
<(force: boolean) => Promise<any>, (updateType: number, groupList: Group[]) => void> <(force: boolean) => Promise<any>, ListenerType>
( (
'NodeIKernelGroupService/getGroupList', 'NodeIKernelGroupService/getGroupList',
'NodeIKernelGroupListener/onGroupListUpdate', 'NodeIKernelGroupListener/onGroupListUpdate',
1, 1,
5000, 5000,
(updateType) => true,
forced forced
); );
return groupList; return groupList;
} }
@CacheClassFuncAsyncExtend(3600 * 1000, "LastestSendTime", () => true)
static async getGroupMemberLastestSendTimeCache(GroupCode: string) {
return NTQQGroupApi.getGroupMemberLastestSendTime(GroupCode);
}
/**
* 通过QQ自带数据库获取群成员最后发言时间(仅返回有效数据 且消耗延迟大 需要进行缓存)
* @param GroupCode 群号
* @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);
* }
*/
static async getGroupMemberLastestSendTime(GroupCode: string) {
async function getdata(uid: string) {
let NTRet = await NTQQGroupApi.getLastestMsgByUids(GroupCode, [uid]);
if (NTRet.result != 0 && NTRet.msgList.length < 1) {
return undefined;
}
return { sendUin: NTRet.msgList[0].senderUin, sendTime: NTRet.msgList[0].msgTime }
}
let currentGroupMembers = groupMembers.get(GroupCode);
let PromiseData: Promise<({
sendUin: string;
sendTime: string;
} | undefined)>[] = [];
let ret: Map<string, string> = new Map();
if (!currentGroupMembers) {
return ret;
}
for (let member of currentGroupMembers.values()) {
PromiseData.push(getdata(member.uid).catch(() => undefined));
}
let allRet = await runAllWithTimeout(PromiseData, 2500);
for (let PromiseDo of allRet) {
if (PromiseDo) {
ret.set(PromiseDo.sendUin, PromiseDo.sendTime);
}
}
return ret;
}
static async getLastestMsgByUids(GroupCode: string, uids: string[]) {
let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: {
peerUid: GroupCode,
chatType: ChatType.group,
},
filterMsgType: [],
filterSendersUid: uids,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
return ret;
}
static async getGroupMemberAll(GroupCode: string, forced = false) {
return napCatCore.session.getGroupService().getAllMemberList(GroupCode, forced);
}
static async getLastestMsg(GroupCode: string, uins: string[]) {
let uids: Array<string> = [];
for (let uin of uins) {
let uid = await NTQQUserApi.getUidByUin(uin)
if (uid) {
uids.push(uid);
}
}
let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: {
peerUid: GroupCode,
chatType: ChatType.group,
},
filterMsgType: [],
filterSendersUid: uids,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
return ret;
}
static async getGroupRecommendContactArkJson(GroupCode: string) { static async getGroupRecommendContactArkJson(GroupCode: string) {
return napCatCore.session.getGroupService().getGroupRecommendContactArkJson(GroupCode); return napCatCore.session.getGroupService().getGroupRecommendContactArkJson(GroupCode);
} }
@@ -31,6 +119,30 @@ export class NTQQGroupApi {
static async DelGroupFileFolder(groupCode: string, folderId: string) { static async DelGroupFileFolder(groupCode: string, folderId: string) {
return napCatCore.session.getRichMediaService().deleteGroupFolder(groupCode, folderId); return napCatCore.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
} }
static async addGroupEssence(GroupCode: string, msgId: string) {
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await napCatCore.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return napCatCore.session.getGroupService().addGroupEssence(param);
}
static async removeGroupEssence(GroupCode: string, msgId: string) {
// 代码没测过
// 需要 ob11msgid->msgId + (peer) -> msgSeq + msgRandom
let MsgData = await napCatCore.session.getMsgService().getMsgsIncludeSelf({ chatType: 2, guildId: '', peerUid: GroupCode }, msgId, 1, false);
let param = {
groupCode: GroupCode,
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
msgSeq: parseInt(MsgData.msgList[0].msgSeq)
};
// GetMsgByShoretID(ShoretID); -> MsgService.getMsgs(Peer,MsgId,1,false); -> 组出参数
return napCatCore.session.getGroupService().removeGroupEssence(param);
}
static async getSingleScreenNotifies(num: number) { static async getSingleScreenNotifies(num: number) {
let [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent let [_retData, _doubt, _seq, notifies] = await NTEventDispatch.CallNormalEvent
<(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void> <(arg1: boolean, arg2: string, arg3: number) => Promise<any>, (doubt: boolean, seq: string, notifies: GroupNotify[]) => void>
@@ -39,12 +151,32 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onGroupSingleScreenNotifies', 'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
1, 1,
5000, 5000,
() => true,
false, false,
'', '',
num num
); );
return notifies; return notifies;
} }
static async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
type ListenerType = NodeIKernelGroupListener['onMemberInfoChange'];
type EventType = NodeIKernelGroupService['getMemberInfo'];
// NTEventDispatch.CreatListenerFunction('NodeIKernelGroupListener/onGroupMemberInfoUpdate',
//return napCatCore.session.getGroupService().getMemberInfo(GroupCode, [uid], forced);
const [ret, _groupCode, _changeType, _members] = await NTEventDispatch.CallNormalEvent
<EventType, ListenerType>
(
'NodeIKernelGroupService/getMemberInfo',
'NodeIKernelGroupListener/onMemberInfoChange',
1,
5000,
(groupCode: string, changeType: number, members: Map<string, GroupMember>) => {
return groupCode == GroupCode && members.has(uid);
},
GroupCode, [uid], forced
);
return _members.get(uid);
}
static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> { static async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const groupService = napCatCore.session.getGroupService(); const groupService = napCatCore.session.getGroupService();
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow'); const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
@@ -52,6 +184,7 @@ export class NTQQGroupApi {
if (result.errCode !== 0) { if (result.errCode !== 0) {
throw ('获取群成员列表出错,' + result.errMsg); throw ('获取群成员列表出错,' + result.errMsg);
} }
//logDebug(`获取群(${groupQQ})成员列表结果:`, `finish: ${result.result.finish}`); //, Array.from(result.result.infos.values())); //logDebug(`获取群(${groupQQ})成员列表结果:`, `finish: ${result.result.finish}`); //, Array.from(result.result.infos.values()));
return result.result.infos; return result.result.infos;
/* /*
@@ -87,16 +220,21 @@ export class NTQQGroupApi {
const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!; const _Pskey = (await NTQQUserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
return napCatCore.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl); return napCatCore.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl);
} }
static async handleGroupRequest(notify: GroupNotify, operateType: GroupRequestOperateTypes, reason?: string) { static async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
let flagitem = flag.split('|');
let groupCode = flagitem[0];
let seq = flagitem[1];
let type = parseInt(flagitem[2]);
return napCatCore.session.getGroupService().operateSysNotify( return napCatCore.session.getGroupService().operateSysNotify(
false, false,
{ {
'operateType': operateType, // 2 拒绝 'operateType': operateType, // 2 拒绝
'targetMsg': { 'targetMsg': {
'seq': notify.seq, // 通知序列号 'seq': seq, // 通知序列号
'type': notify.type, 'type': type,
'groupCode': notify.group.groupCode, 'groupCode': groupCode,
'postscript': reason || '' 'postscript': reason || ' ' // 仅传空值可能导致处理失败,故默认给个空格
} }
}); });
} }

View File

@@ -1,83 +1,84 @@
import { GetFileListParam, Peer, RawMessage, SendMessageElement } from '@/core/entities'; import { GetFileListParam, Peer, RawMessage, SendMessageElement, SendMsgElementConstructor } from '@/core/entities';
import { selfInfo } from '@/core/data'; import { friends, groups, selfInfo } from '@/core/data';
import { log, logError } from '@/common/utils/log'; import { log, logWarn } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper'; import { sleep } from '@/common/utils/helper';
import { napCatCore } from '@/core'; import { napCatCore, NTQQUserApi } from '@/core';
import { MsgListener, onGroupFileInfoUpdateParamType } from '@/core/listeners'; import { onGroupFileInfoUpdateParamType } from '@/core/listeners';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
import { randomUUID } from 'crypto'; import { MessageUnique } from '../../../common/utils/MessageUnique';
import { NTEventDispatch } from '@/common/utils/EventTask';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
async function LoadMessageIdList(Peer: Peer, msgId: string) {
const sendMessagePool: Record<string, ((sendSuccessMsg: RawMessage) => void | Promise<void>) | null> = {};// peerUid: callbackFunc let msgList = await NTQQMsgApi.getMsgHistory(Peer, msgId, 50);
for (let j = 0; j < msgList.msgList.length; j++) {
const sendSuccessCBMap: Record<string, ((sendSuccessMsg: RawMessage) => boolean | Promise<boolean>) | null> = {};// uuid: callbackFunc let shortId = MessageUnique.createMsg(Peer, msgList.msgList[j].msgId);
const GroupFileInfoUpdateTasks: Map<string, ((groupFileListResult: onGroupFileInfoUpdateParamType) => void)> = new Map();
const sentMsgTasks: Map<string, (msg: RawMessage) => void> = new Map();
const msgListener = new MsgListener();
msgListener.onGroupFileInfoUpdate = (groupFileListResult: onGroupFileInfoUpdateParamType) => {
for (const [uuid, cb] of GroupFileInfoUpdateTasks) {
cb(groupFileListResult);
GroupFileInfoUpdateTasks.delete(uuid);
} }
}; }
async function loadMessageUnique() {
msgListener.onAddSendMsg = (msgRecord: RawMessage) => { if (groups.size > 100) {
// console.log("sent msg", msgRecord, sendMessagePool); logWarn('[性能检测] 群数量大于100可能会导致性能问题');
for (const [uuid, cb] of sentMsgTasks) {
cb(msgRecord);
sentMsgTasks.delete(uuid);
} }
if (sendMessagePool[msgRecord.peerUid]) { let predict = (groups.size + friends.size / 2) / 5;
const r = sendMessagePool[msgRecord.peerUid]?.(msgRecord); predict = predict < 20 ? 20 : predict;
if (r instanceof Promise) { predict = predict > 50 ? 50 : predict;
r.then().catch(logError); //let waitpromise: Array<Promise<{ msgList: RawMessage[]; }>> = [];
predict = Math.floor(predict * 50);
MessageUnique.resize(predict);
let RecentContact = await NTQQUserApi.getRecentContactListSnapShot(predict);
let LoadMessageIdDo: Array<Promise<void>> = new Array<Promise<void>>();
if (RecentContact?.info?.changedList && RecentContact?.info?.changedList?.length > 0) {
for (let i = 0; i < RecentContact.info.changedList.length; i++) {
let Peer: Peer = { chatType: RecentContact.info.changedList[i].chatType, peerUid: RecentContact.info.changedList[i].peerUid, guildId: '' };
LoadMessageIdDo.push(LoadMessageIdList(Peer, RecentContact.info.changedList[i].msgId));
} }
} }
}; await Promise.all(LoadMessageIdDo).then(() => {
log(`[消息序列] 加载 ${predict} 条历史消息记录完成`);
msgListener.onMsgInfoListUpdate = (msgInfoList: RawMessage[]) => {
msgInfoList.forEach(msg => {
new Promise((resolve, reject) => {
for (const cbId in sendSuccessCBMap) {
const cb = sendSuccessCBMap[cbId]!;
const cbResult = cb(msg);
const checkResult = (result: boolean) => {
if (result) {
delete sendSuccessCBMap[cbId];
}
};
if (cbResult instanceof Promise) {
cbResult.then(checkResult);
} else {
checkResult(cbResult);
}
}
}).then().catch(log);
}); });
}; }
setTimeout(() => { setTimeout(() => {
napCatCore.onLoginSuccess(() => { napCatCore.onLoginSuccess(async () => {
napCatCore.addListener(msgListener); await sleep(100);
// NTQQMsgApi.CheckSendMode().then().catch();
loadMessageUnique().then().catch();
//let data = await napCatCore.session.getMsgService().sendSsoCmdReqByContend("LightAppSvc.mini_app_growguard.ReportExecute","1124343");
//console.log(data);
}); });
}, 100); }, 100);
//歇菜LocalMsg压根不写Db
// setTimeout(async () => {
// let ele: MessageElement = { extBufForUI: '0x', ...SendMsgElementConstructor.text('测试消息') };
// let MsgId = await NTQQMsgApi.getMsgUniqueEx();
// let peer = { chatType: 2, peerUid: '', guildId: '' };
// console.log(await napCatCore.session.getTestPerformanceService().insertMsg(
// {
// peer: peer,
// msgTime: Math.floor(Date.now() / 1000).toString(),
// msgId: MsgId,
// msgSeq: '56564',
// batchNums: 1,
// timesPerBatch: 1,
// numPerTime: 1
// }, [ele]
// ));
// console.log(await NTQQMsgApi.multiForwardMsg(peer, peer, [MsgId]));
// }, 25000)
export class NTQQMsgApi { export class NTQQMsgApi {
static async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
//console.log(peer, msgSeq, emojiId, emojiType, count);
return napCatCore.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, "", false, 20)
}
// static napCatCore: NapCatCore | null = null; // static napCatCore: NapCatCore | null = null;
// enum BaseEmojiType { // enum BaseEmojiType {
// NORMAL_EMOJI, // NORMAL_EMOJI,
// SUPER_EMOJI, // SUPER_EMOJI,
// RANDOM_SUPER_EMOJI, // RANDOM_SUPER_EMOJI,
// CHAIN_SUPER_EMOJI, // CHAIN_SUPER_EMOJI,
// EMOJI_EMOJI // EMOJI_EMOJI
// } // }
static async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) { static 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//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid // nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
@@ -90,142 +91,208 @@ export class NTQQMsgApi {
} | undefined> { } | undefined> {
return napCatCore.session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId); return napCatCore.session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId);
} }
static async getLastestMsgByUids(peer: Peer, count: number = 20) {
let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
return ret;
}
static async getMsgsByMsgId(peer: Peer, msgIds: string[]) { static async getMsgsByMsgId(peer: Peer, msgIds: string[]) {
return await napCatCore.session.getMsgService().getMsgsByMsgId(peer, msgIds); return await napCatCore.session.getMsgService().getMsgsByMsgId(peer, msgIds);
} }
static async getSingleMsg(peer: Peer, seq: string) {
return await napCatCore.session.getMsgService().getSingleMsg(peer, seq);
}
static async fetchFavEmojiList(num: number) {
return napCatCore.session.getMsgService().fetchFavEmojiList("", num, true, true)
}
static async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
let ret = await napCatCore.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
return ret;
}
static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) { static async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
return await napCatCore.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z); return await napCatCore.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
} }
static async activateChat(peer: Peer) {
// await this.fetchRecentContact();
// await sleep(500);
}
static async activateChatAndGetHistory(peer: Peer) {
}
static async setMsgRead(peer: Peer) { static async setMsgRead(peer: Peer) {
return napCatCore.session.getMsgService().setMsgRead(peer); return napCatCore.session.getMsgService().setMsgRead(peer);
} }
static async getGroupFileList(GroupCode: string, params: GetFileListParam) { static async getGroupFileList(GroupCode: string, params: GetFileListParam) {
return new Promise<Array<any>>(async (resolve, reject) => { let data = await NTEventDispatch.CallNormalEvent<
let complete = false; (GroupCode: string, params: GetFileListParam) => Promise<unknown>,
setTimeout(() => { (groupFileListResult: onGroupFileInfoUpdateParamType) => void
if (!complete) { >(
reject('获取群文件列表超时'); 'NodeIKernelRichMediaService/getGroupFileList',
} 'NodeIKernelMsgListener/onGroupFileInfoUpdate',
}, 5000); 1,
const GroupFileInfoUpdateCB = (groupFileListResult: onGroupFileInfoUpdateParamType) => { 5000,
complete = true; (groupFileListResult: onGroupFileInfoUpdateParamType) => {
resolve(groupFileListResult.item); return true;
}; },
GroupFileInfoUpdateTasks.set(randomUUID(), GroupFileInfoUpdateCB); GroupCode,
await napCatCore.session.getRichMediaService().getGroupFileList(GroupCode, params); params
}); );
return data[1].item;
} }
static async getMsgHistory(peer: Peer, msgId: string, count: number) { static async getMsgHistory(peer: Peer, msgId: string, count: number) {
// 消息时间从旧到新 // 消息时间从旧到新
return napCatCore.session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, true); return napCatCore.session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, true);
} }
static async fetchRecentContact() {
}
static async recallMsg(peer: Peer, msgIds: string[]) { static async recallMsg(peer: Peer, msgIds: string[]) {
await napCatCore.session.getMsgService().recallMsg({ await napCatCore.session.getMsgService().recallMsg({
chatType: peer.chatType, chatType: peer.chatType,
peerUid: peer.peerUid peerUid: peer.peerUid
}, msgIds); }, msgIds);
} }
static async sendMsgV2(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000): Promise<RawMessage> { // function generateMsgId() {
const peerUid = peer.peerUid; // const timestamp = Math.floor(Date.now() / 1000);
// 等待上一个相同的peer发送完 // const random = Math.floor(Math.random() * Math.pow(2, 32));
let checkLastSendUsingTime = 0; // const buffer = Buffer.alloc(8);
const waitLastSend: () => Promise<void> = async () => { // buffer.writeUInt32BE(timestamp, 0);
if (checkLastSendUsingTime > timeout) { // buffer.writeUInt32BE(random, 4);
throw ('发送超时'); // const msgId = BigInt("0x" + buffer.toString('hex')).toString();
} // return msgId;
const lastSending = sendMessagePool[peer.peerUid]; // }
if (lastSending) { // 此处有采用Hack方法 利用数据返回正确得到对应消息
// log("有正在发送的消息,等待中...") // 与之前 Peer队列 MsgSeq队列 真正的MsgId并发不同
await sleep(500); // 谨慎采用 目前测试暂无问题 Developer.Mlikiowa
checkLastSendUsingTime += 500; let msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime());
return await waitLastSend(); let data = await NTEventDispatch.CallNormalEvent<
} else { (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
return; (msgList: RawMessage[]) => void
} >(
}; 'NodeIKernelMsgService/sendMsg',
await waitLastSend(); 'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
return new Promise((resolve, reject) => { timeout,
let completed = false; (msgRecords: RawMessage[]) => {
let sentMessage: RawMessage | null = null; for (let msgRecord of msgRecords) {
const sendSuccessCBId = randomUUID() as string; if (msgRecord.msgId === msgId && msgRecord.sendStatus === 2) {
sendSuccessCBMap[sendSuccessCBId] = (msgRecord: RawMessage) => {
if (msgRecord.msgId === sentMessage?.msgId) {
if (msgRecord.sendStatus === 2) {
delete sendSuccessCBMap[sendSuccessCBId];
completed = true;
resolve(msgRecord);
return true; return true;
} }
return false;
} }
return false; return false;
}; },
sendMessagePool[peerUid] = async (rawMessage: RawMessage) => { msgId,
// console.log('收到sent 消息', rawMessage.msgId); peer,
delete sendMessagePool[peerUid]; msgElements,
sentMessage = rawMessage; new Map()
}; );
setTimeout(() => { let retMsg = data[1].find(msgRecord => {
if (completed) return; if (msgRecord.msgId === msgId) {
delete sendMessagePool[peerUid]; return true;
delete sendSuccessCBMap[sendSuccessCBId]; }
reject('发送超时');
}, timeout);
const result = napCatCore.session.getMsgService().sendMsg('0', peer, msgElements, new Map());
}); });
return retMsg;
}
static sendMsgEx(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
//return NTQQMsgApi.sendMsgV1(peer, msgElements, waitComplete, timeout);
}
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
//唉? !我有个想法
let msgId = await NTQQMsgApi.getMsgUnique(peer.chatType, await NTQQMsgApi.getServerTime());
peer.guildId = msgId;
let data = await NTEventDispatch.CallNormalEvent<
(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
(msgList: RawMessage[]) => void
>(
'NodeIKernelMsgService/sendMsg',
'NodeIKernelMsgListener/onMsgInfoListUpdate',
1,
timeout,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.guildId === msgId && msgRecord.sendStatus === 2) {
return true;
}
}
return false;
},
"0",
peer,
msgElements,
new Map()
);
let retMsg = data[1].find(msgRecord => {
if (msgRecord.guildId === msgId) {
return true;
}
});
return retMsg;
}
static async getMsgUnique(chatType: number, time: string) {
if (requireMinNTQQBuild('26702')) {
return napCatCore.session.getMsgService().generateMsgUniqueId(chatType, time);
}
return napCatCore.session.getMsgService().getMsgUniqueId(time);
}
static async getServerTime() {
return napCatCore.session.getMSFService().getServerTime();
}
static async getServerTimeV2() {
return NTEventDispatch.CallNoListenerEvent<() => string>('NodeIKernelMsgService/getServerTime', 5000);
} }
static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) { static async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
return napCatCore.session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], new Map()); return napCatCore.session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], new Map());
} }
static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> { static async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
const msgInfos = msgIds.map(id => { const msgInfos = msgIds.map(id => {
return { msgId: id, senderShowName: selfInfo.nick }; return { msgId: id, senderShowName: selfInfo.nick };
}); });
let data = await NTEventDispatch.CallNormalEvent<
return new Promise((resolve, reject) => { (msgInfo: typeof msgInfos, srcPeer: Peer, destPeer: Peer, comment: Array<any>, attr: Map<any, any>,) => Promise<unknown>,
let complete = false; (msgList: RawMessage[]) => void
const onSentCB = (msg: RawMessage) => { >(
const arkElement = msg.elements.find(ele => ele.arkElement); 'NodeIKernelMsgService/multiForwardMsgWithComment',
if (!arkElement) { 'NodeIKernelMsgListener/onMsgInfoListUpdate',
// log("收到的不是转发消息") 1,
return; 5000,
(msgRecords: RawMessage[]) => {
for (let msgRecord of msgRecords) {
if (msgRecord.peerUid == destPeer.peerUid && msgRecord.senderUid == selfInfo.uid) {
return true;
}
} }
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData); return false;
if (forwardData.app != 'com.tencent.multimsg') { },
return; msgInfos,
} srcPeer,
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) { destPeer,
complete = true; [],
resolve(msg); new Map()
}
};
sentMsgTasks.set(randomUUID(), onSentCB);
setTimeout(() => {
if (!complete) {
reject('转发消息超时');
}
}, 5000);
napCatCore.session.getMsgService().multiForwardMsgWithComment(msgInfos, srcPeer, destPeer, [], new Map());
}
); );
for (let msg of data[1]) {
const arkElement = msg.elements.find(ele => ele.arkElement);
if (!arkElement) {
continue;
}
const forwardData: any = JSON.parse(arkElement.arkElement.bytesData);
if (forwardData.app != 'com.tencent.multimsg') {
continue;
}
if (msg.peerUid == destPeer.peerUid && msg.senderUid == selfInfo.uid) {
return msg;
}
}
throw new Error('转发消息超时');
}
static async markallMsgAsRead() {
return napCatCore.session.getMsgService().setAllC2CAndGroupMsgRead();
} }
} }

View File

@@ -19,6 +19,43 @@ export interface CustomMusicSignPostData {
image?: string, image?: string,
singer?: string singer?: string
} }
// let t = await napCatCore.session.getGroupService().shareDigest({
// appId: "100497308",
// appType: 1,
// msgStyle: 0,
// recvUin: "726067488",
// sendType: 1,
// clientInfo: {
// platform: 1
// },
// richMsg: {
// usingArk: true,
// title: "Bot测试title",
// summary: "Bot测试summary",
// url: "https://www.bilibili.com",
// pictureUrl: "https://y.qq.com/music/photo_new/T002R300x300M0000035DC6W4ZpSqf_1.jpg?max_age=2592000",
// brief: "Bot测试brief",
// }
// });
// {
// errCode: 0,
// errMsg: '',
// rsp: {
// sendType: 1,
// recvUin: '726067488',
// recvOpenId: '',
// errCode: 901501,
// errMsg: 'imagent service_error:150_OIDB_NO_PRIV',
// extInfo: {
// wording: '消息下发失败(错误码901501)',
// jumpResult: 0,
// jumpUrl: '',
// level: 0,
// subLevel: 0,
// developMsg: 'imagent error'
// }
// }
// }
// export class MusicSign { // export class MusicSign {
// private readonly url: string; // private readonly url: string;

View File

@@ -1,11 +1,7 @@
import { NTEventDispatch } from '@/common/utils/EventTask'; import { NTEventDispatch } from '@/common/utils/EventTask';
import { GeneralCallResult, NTQQUserApi, napCatCore } from '@/core'; import { GeneralCallResult, NTQQFileApi, NTQQUserApi, napCatCore } from '@/core';
// setTimeout(async () => {
// let ret = await NTQQSystemApi.getArkJsonCollection('1-2-162b9b42-65b9-4405-a8ed-2e256ec8aa50');
// console.log(ret);
// }, 20000)
export class NTQQSystemApi { export class NTQQSystemApi {
static async hasOtherRunningQQProcess() { static async hasOtherRunningQQProcess() {
return napCatCore.util.hasOtherRunningQQProcess(); return napCatCore.util.hasOtherRunningQQProcess();
@@ -16,7 +12,7 @@ export class NTQQSystemApi {
static async translateEnWordToZn(words: string[]) { static async translateEnWordToZn(words: string[]) {
return napCatCore.session.getRichMediaService().translateEnWordToZn(words); return napCatCore.session.getRichMediaService().translateEnWordToZn(words);
} }
//调用会超时 没灯用 //调用会超时 没灯用 (好像是通知listener的) onLineDev
static async getOnlineDev() { static async getOnlineDev() {
return napCatCore.session.getMsgService().getOnLineDev(); return napCatCore.session.getMsgService().getOnLineDev();
} }
@@ -33,7 +29,7 @@ export class NTQQSystemApi {
static async BootMiniApp(appfile: string, params: string) { static async BootMiniApp(appfile: string, params: string) {
await napCatCore.session.getNodeMiscService().setMiniAppVersion('2.16.4'); await napCatCore.session.getNodeMiscService().setMiniAppVersion('2.16.4');
let c = await napCatCore.session.getNodeMiscService().getMiniAppPath(); let c = await napCatCore.session.getNodeMiscService().getMiniAppPath();
console.log(c);
return napCatCore.session.getNodeMiscService().startNewMiniApp(appfile, params); return napCatCore.session.getNodeMiscService().startNewMiniApp(appfile, params);
} }
} }

View File

@@ -1,40 +1,29 @@
import { ModifyProfileParams, SelfInfo, User, UserDetailInfoByUin } from '@/core/entities'; import { ModifyProfileParams, SelfInfo, User, UserDetailInfoByUin, UserDetailInfoByUinV2 } from '@/core/entities';
import { friends, selfInfo } from '@/core/data'; import { friends, groupMembers, selfInfo } from '@/core/data';
import { CacheClassFuncAsync } from '@/common/utils/helper'; import { CacheClassFuncAsync, CacheClassFuncAsyncExtend } from '@/common/utils/helper';
import { GeneralCallResult, napCatCore, NTQQFriendApi } from '@/core'; import { napCatCore, NTQQFriendApi } from '@/core';
import { ProfileListener } from '@/core/listeners'; import { NodeIKernelProfileListener, ProfileListener } from '@/core/listeners';
import { rejects } from 'assert';
import { randomUUID } from 'crypto';
import { RequestUtil } from '@/common/utils/request'; import { RequestUtil } from '@/common/utils/request';
import { logDebug, logError } from '@/common/utils/log'; import { logWarn } from '@/common/utils/log';
import { NTEventDispatch } from '@/common/utils/EventTask'; import { NTEventDispatch } from '@/common/utils/EventTask';
const userInfoCache: Record<string, User> = {}; // uid: User import { NodeIKernelProfileService, ProfileBizType, UserDetailSource } from '@/core/services';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
const profileListener = new ProfileListener();
const userDetailHandlers: Map<string, ((profile: User) => void)> = new Map();
profileListener.onProfileDetailInfoChanged = (profile) => {
userInfoCache[profile.uid] = profile;
userDetailHandlers.forEach(handler => handler(profile));
};
setTimeout(() => {
napCatCore.onLoginSuccess(() => {
napCatCore.addListener(profileListener);
});
}, 100);
// 老版本逻辑现已移除
// console.log('onProfileDetailInfoChanged', profile);
// recevCount++;
// firstProfile = profile;
// if (recevCount === 2) {
// profileService.removeKernelProfileListener(listenerId);
// // if (!completed) {
// completed = true;
// resolve(profile);
// // }
// }
// };
export class NTQQUserApi { export class NTQQUserApi {
static async getProfileLike(uid: string) {
return napCatCore.session.getProfileLikeService().getBuddyProfileLike({
friendUids: [
uid
],
basic: 1,
vote: 1,
favorite: 0,
userProfile: 1,
type: 2,
start: 0,
limit: 20
});
}
static async setLongNick(longNick: string) { static async setLongNick(longNick: string) {
return napCatCore.session.getProfileService().setLongNick(longNick); return napCatCore.session.getProfileService().setLongNick(longNick);
} }
@@ -58,60 +47,110 @@ export class NTQQUserApi {
const ret = await napCatCore.session.getProfileService().setHeader(filePath) as setQQAvatarRet; const ret = await napCatCore.session.getProfileService().setHeader(filePath) as setQQAvatarRet;
return { result: ret?.result, errMsg: ret?.errMsg }; return { result: ret?.result, errMsg: ret?.errMsg };
} }
static async setGroupAvatar(gc: string, filePath: string) {
static async getSelfInfo() { return napCatCore.session.getGroupService().setHeader(gc, filePath);
} }
static async getUserInfo(uid: string) { static async fetchUserDetailInfos(uids: string[]) {
//26702 以上使用新接口 .Dev Mlikiowa
type EventService = NodeIKernelProfileService['fetchUserDetailInfo'];
type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged'];
let retData: User[] = [];
let [_retData, _retListener] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
uids.length,
5000,
(profile) => {
if (uids.includes(profile.uid)) {
let 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;
},
"BuddyProfileStore",
uids,
UserDetailSource.KSERVER,
[
ProfileBizType.KALL
]
);
return retData;
} }
// enum ProfileBizType { static async fetchUserDetailInfo(uid: string) {
// KALL, type EventService = NodeIKernelProfileService['fetchUserDetailInfo'];
// KBASEEXTEND, type EventListener = NodeIKernelProfileListener['onUserDetailInfoChanged'];
// KVAS, let [_retData, profile] = await NTEventDispatch.CallNormalEvent
// KQZONE, <EventService, EventListener>
// KOTHER (
// } 'NodeIKernelProfileService/fetchUserDetailInfo',
static async getUserDetailInfo(uid: string): Promise<User> { 'NodeIKernelProfileListener/onUserDetailInfoChanged',
// const existUser = userInfoCache[uid]; 1,
// if (existUser) { 5000,
// return existUser; (profile) => {
// } if (profile.uid === uid) {
const profileService = napCatCore.session.getProfileService(); return true;
// console.log('getUserDetailInfo', result);
return new Promise((resolve, reject) => {
const uuid = randomUUID();
let completed = false;
let retData: User | undefined = undefined;
let isFirst = true;
// 不管返回几次 超时有数据就该返回 兼容就好了
setTimeout(() => {
if (!completed) {
if (retData) {
resolve(retData);
} else {
reject('getUserDetailInfo timeout');
} }
} return false;
userDetailHandlers.delete(uuid); },
}, 5000); "BuddyProfileStore",
userDetailHandlers.set(uuid, (profile) => { [
if (profile.uid === uid) { uid
if (isFirst) { ],
retData = profile; UserDetailSource.KSERVER,
isFirst = false; [
// console.log('getUserDetailInfo', profile); ProfileBizType.KALL
} else { ]
completed = true; );
resolve(profile); let RetUser: User = {
...profile.simpleInfo.coreInfo,
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
...profile.simpleInfo.baseInfo,
qqLevel: profile.commonExt.qqLevel,
pendantId: ""
};
return RetUser;
}
static async getUserDetailInfo(uid: string) {
if (requireMinNTQQBuild('26702')) {
return this.fetchUserDetailInfo(uid);
}
return this.getUserDetailInfoOld(uid);
}
static async getUserDetailInfoOld(uid: string) {
type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'];
type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'];
let [_retData, profile] = await NTEventDispatch.CallNormalEvent
<EventService, EventListener>
(
'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
'NodeIKernelProfileListener/onProfileDetailInfoChanged',
2,
5000,
(profile: User) => {
if (profile.uid === uid) {
return true;
} }
} return false;
}); },
profileService.getUserDetailInfoWithBizInfo(uid, [0]).then(result => { uid,
// console.log('getUserDetailInfo', result); [0]
}); );
}); return profile;
} }
static async modifySelfProfile(param: ModifyProfileParams) { static async modifySelfProfile(param: ModifyProfileParams) {
return napCatCore.session.getProfileService().modifyDesktopMiniProfile(param); return napCatCore.session.getProfileService().modifyDesktopMiniProfile(param);
@@ -163,29 +202,89 @@ export class NTQQUserApi {
} }
return skey; return skey;
} }
@CacheClassFuncAsyncExtend(3600 * 1000, 'Uin2Uid', (Uin: string, Uid: string | undefined) => {
if (Uid && Uid.indexOf('u_') != -1) {
return true
}
logWarn("uin转换到uid时异常", Uin, Uid);
return false;
})
static async getUidByUin(Uin: string) { static async getUidByUin(Uin: string) {
let ret = await NTEventDispatch.CallNoListenerEvent //此代码仅临时使用,后期会被废弃
<(Uin: string[]) => Promise<{ uidInfo: Map<string, string> }>>( if (requireMinNTQQBuild('26702')) {
'NodeIKernelUixConvertService/getUid', return await NTQQUserApi.getUidByUinV2(Uin);
5000, }
[Uin] return await NTQQUserApi.getUidByUinV1(Uin);
); }
let uid = ret.uidInfo.get(Uin); //通过QQ默认方式转换 @CacheClassFuncAsyncExtend(3600 * 1000, 'Uid2Uin', (Uid: string | undefined, Uin: number | undefined) => {
if (Uin && Uin != 0 && !isNaN(Uin)) {
return true
}
logWarn("uid转换到uin时异常", Uid, Uin);
return false;
})
static async getUinByUid(Uid: string) {
//此代码仅临时使用,后期会被废弃
if (requireMinNTQQBuild('26702')) {
return await NTQQUserApi.getUinByUidV2(Uid);
}
return await NTQQUserApi.getUinByUidV1(Uid);
}
//后期改成流水线处理
static async getUidByUinV2(Uin: string) {
let uid = (await napCatCore.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin);
if (uid) return uid;
uid = (await napCatCore.session.getGroupService().getUidByUins([Uin])).uids.get(Uin);
if (uid) return uid;
uid = (await napCatCore.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
if (uid) return uid;
console.log((await NTQQFriendApi.getBuddyIdMapCache(true)));
uid = (await NTQQFriendApi.getBuddyIdMapCache(true)).getValue(Uin);//从Buddy缓存获取Uid
if (uid) return uid;
uid = (await NTQQFriendApi.getBuddyIdMap(true)).getValue(Uin);
if (uid) return uid;
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUinV2(Uin)).detail.uid;//从QQ Native 特殊转换
if (unveifyUid.indexOf("*") == -1) uid = unveifyUid;
//if (uid) return uid;
return uid;
}
//后期改成流水线处理
static async getUinByUidV2(Uid: string) {
let uin = (await napCatCore.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid);
if (uin) return uin;
uin = (await napCatCore.session.getGroupService().getUinByUids([Uid])).uins.get(Uid);
if (uin) return uin;
uin = (await napCatCore.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid);
if (uin) return uin;
uin = (await NTQQFriendApi.getBuddyIdMapCache(true)).getKey(Uid);//从Buddy缓存获取Uin
if (uin) return uin;
uin = (await NTQQFriendApi.getBuddyIdMap(true)).getKey(Uid);
if (uin) return uin;
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
return uin;
}
static async getUidByUinV1(Uin: string) {
// 通用转换开始尝试
let uid = (await napCatCore.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
// Uid 好友转
if (!uid) { if (!uid) {
Array.from(friends.values()).forEach((t) => { Array.from(friends.values()).forEach((t) => {
if (t.uin == Uin) { if (t.uin == Uin) {
//logDebug('getUidByUin', t.uid, t.uin, Uin);
uid = t.uid; uid = t.uid;
} }
//console.log(t.uid, t.uin, Uin);
}); });
//uid = Array.from(friends.values()).find((t) => { t.uin == Uin })?.uid; // 从NC维护的QQ Buddy缓存 转换
} }
//Uid 群友列表转
if (!uid) { if (!uid) {
uid = (await NTQQFriendApi.getFriends(false)).find((t) => { t.uin == Uin })?.uid; //从QQ Native 缓存转换 方法一 for (let groupMembersList of groupMembers.values()) {
} for (let GroupMember of groupMembersList.values()) {
if (!uid) { if (GroupMember.uin == Uin) {
uid = (await NTQQFriendApi.getFriends(true)).find((t) => { t.uin == Uin })?.uid; //从QQ Native 非缓存转换 方法二 uid = GroupMember.uid;
}
}
}
} }
if (!uid) { if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三 let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
@@ -195,17 +294,53 @@ export class NTQQUserApi {
} }
return uid; return uid;
} }
static async getUinByUid(Uid: string | undefined) { static async getUinByUidV1(Uid: string) {
if (!Uid) {
return '';
}
let ret = await NTEventDispatch.CallNoListenerEvent let ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>( <(Uin: string[]) => Promise<{ uinInfo: Map<string, string> }>>(
'NodeIKernelUixConvertService/getUin', 'NodeIKernelUixConvertService/getUin',
5000, 5000,
[Uid] [Uid]
); );
return ret.uinInfo.get(Uid); let uin = ret.uinInfo.get(Uid);
if (!uin) {
//从Buddy缓存获取Uin
Array.from(friends.values()).forEach((t) => {
if (t.uid == Uid) {
uin = t.uin;
}
})
}
if (!uin) {
uin = (await NTQQUserApi.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
}
// if (!uin) {
// uin = (await NTQQFriendApi.getFriends(false)).find((t) => { t.uid == Uid })?.uin; //从QQ Native 缓存转换
// }
// if (!uin) {
// uin = (await NTQQFriendApi.getFriends(true)).find((t) => { t.uid == Uid })?.uin; //从QQ Native 非缓存转换
// }
return uin;
}
static async getRecentContactListSnapShot(count: number) {
return await napCatCore.session.getRecentContactService().getRecentContactListSnapShot(count);
}
static async getRecentContactListSyncLimit(count: number) {
return await napCatCore.session.getRecentContactService().getRecentContactListSyncLimit(count);
}
static async getRecentContactListSync() {
return await napCatCore.session.getRecentContactService().getRecentContactListSync();
}
static async getRecentContactList() {
return await napCatCore.session.getRecentContactService().getRecentContactList();
}
static async getUserDetailInfoByUinV2(Uin: string) {
return await NTEventDispatch.CallNoListenerEvent
<(Uin: string) => Promise<UserDetailInfoByUinV2>>(
'NodeIKernelProfileService/getUserDetailInfoByUin',
5000,
Uin
);
} }
static async getUserDetailInfoByUin(Uin: string) { static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent return NTEventDispatch.CallNoListenerEvent

View File

@@ -114,6 +114,22 @@ export interface GroupEssenceMsgRet {
} }
} }
export class WebApi { export class WebApi {
static async shareDigest(groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com');
const CookieValue = Object.entries(CookiesObject).map(([key, value]) => `${key}=${value}`).join('; ');
const Bkn = WebApi.genBkn(CookiesObject.skey);
let ret: any = undefined;
const data = 'group_code=' + groupCode + '&msg_seq=' + msgSeq + '&msg_random=' + msgRandom + '&target_group_code=' + targetGroupCode;
const url = 'https://qun.qq.com/cgi-bin/group_digest/share_digest?bkn=' + Bkn + "&" + data;
//console.log(url);
try {
ret = await RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': CookieValue });
return ret;
} catch (e) {
return undefined;
}
return undefined;
}
@CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members') @CacheClassFuncAsync(3600 * 1000, 'webapi_get_group_members')
static async getGroupEssenceMsg(GroupCode: string, page_start: string) { static async getGroupEssenceMsg(GroupCode: string, page_start: string) {
const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com'); const CookiesObject = await NTQQUserApi.getCookies('qun.qq.com');

View File

@@ -1,4 +1,5 @@
import QQWrapper, { NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, NodeQQNTWrapperUtil } from '@/core/wrapper'; import QQWrapper, { NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, NodeQQNTWrapperUtil } from '@/core/wrapper';
import { DeviceList } from '@/onebot11/main';
import { import {
NodeIKernelLoginService, NodeIKernelLoginService,
NodeIKernelBuddyService, NodeIKernelBuddyService,
@@ -14,14 +15,13 @@ import { DependsAdapter, DispatcherAdapter, GlobalAdapter, NodeIGlobalAdapter }
import path from 'node:path'; import path from 'node:path';
import os from 'node:os'; import os from 'node:os';
import fs from 'node:fs'; import fs from 'node:fs';
import { appid, qqVersionConfigInfo } from '@/common/utils/QQBasicInfo'; import { getFullQQVesion, QQVersionAppid, QQVersionQua, requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
import { hostname, systemVersion } from '@/common/utils/system'; import { hostname, systemVersion } from '@/common/utils/system';
import { genSessionConfig } from '@/core/sessionConfig'; import { genSessionConfig } from '@/core/sessionConfig';
import { dbUtil } from '@/common/utils/db';
import { sleep } from '@/common/utils/helper'; import { sleep } from '@/common/utils/helper';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { rawFriends, friends, groupMembers, groups, selfInfo, stat } from '@/core/data'; import { groupMembers, groups, selfInfo, stat } from '@/core/data';
import { RawMessage } from '@/core/entities'; import { GroupMember, RawMessage } from '@/core/entities';
import { NTEventDispatch } from '@/common/utils/EventTask'; import { NTEventDispatch } from '@/common/utils/EventTask';
import { import {
enableConsoleLog, enableConsoleLog,
@@ -33,6 +33,7 @@ import {
setLogSelfInfo setLogSelfInfo
} from '@/common/utils/log'; } from '@/common/utils/log';
import { napCatConfig } from '@/core/utils/config'; import { napCatConfig } from '@/core/utils/config';
import { NTQQFriendApi } from './apis';
export interface OnLoginSuccess { export interface OnLoginSuccess {
(uin: string, uid: string): void | Promise<void>; (uin: string, uid: string): void | Promise<void>;
@@ -84,20 +85,15 @@ export class NapCatCore {
const dataPath = path.resolve(this.dataPath, './NapCat/data'); const dataPath = path.resolve(this.dataPath, './NapCat/data');
fs.mkdirSync(dataPath, { recursive: true }); fs.mkdirSync(dataPath, { recursive: true });
logDebug('本账号数据/缓存目录:', dataPath); logDebug('本账号数据/缓存目录:', dataPath);
dbUtil.init(path.resolve(dataPath, `./${arg.uin}-v2.db`)).then(() => { this.initDataListener();
this.initDataListener(); this.onLoginSuccessFuncList.map(cb => {
this.onLoginSuccessFuncList.map(cb => { new Promise((resolve, reject) => {
new Promise((resolve, reject) => { const result = cb(arg.uin, arg.uid);
const result = cb(arg.uin, arg.uid); if (result instanceof Promise) {
if (result instanceof Promise) { result.then(resolve).catch(reject);
result.then(resolve).catch(reject); }
} }).then();
}).then();
});
}).catch((e) => {
logError('数据库初始化失败', e);
}); });
// this.initDataListener();
}).catch((e) => { }).catch((e) => {
logError('initSession failed', e); logError('initSession failed', e);
throw new Error(`启动失败: ${JSON.stringify(e)}`); throw new Error(`启动失败: ${JSON.stringify(e)}`);
@@ -108,8 +104,8 @@ export class NapCatCore {
logError('登录失败(onQRCodeSessionFailed)', errMsg); logError('登录失败(onQRCodeSessionFailed)', errMsg);
if (errType == 1 && errCode == 3) { if (errType == 1 && errCode == 3) {
// 二维码过期刷新 // 二维码过期刷新
this.loginService.getQRCodePicture();
} }
this.loginService.getQRCodePicture();
}; };
this.loginListener.onLoginFailed = (args) => { this.loginListener.onLoginFailed = (args) => {
logError('登录失败(onLoginFailed)', args); logError('登录失败(onLoginFailed)', args);
@@ -138,10 +134,10 @@ export class NapCatCore {
base_path_prefix: '', base_path_prefix: '',
platform_type: 3, platform_type: 3,
app_type: 4, app_type: 4,
app_version: qqVersionConfigInfo.curVersion, app_version: getFullQQVesion(),
os_version: 'Windows 10 Pro', os_version: 'Windows 10 Pro',
use_xlog: true, use_xlog: true,
qua: `V1_WIN_NQ_${qqVersionConfigInfo.curVersion.replace('-', '_')}_GW_B`, qua: QQVersionQua,
global_path_config: { global_path_config: {
desktopGlobalPath: this.dataPathGlobal, desktopGlobalPath: this.dataPathGlobal,
}, },
@@ -149,10 +145,10 @@ export class NapCatCore {
}, new QQWrapper.NodeIGlobalAdapter(new GlobalAdapter())); }, new QQWrapper.NodeIGlobalAdapter(new GlobalAdapter()));
this.loginService.initConfig({ this.loginService.initConfig({
machineId: '', machineId: '',
appid, appid: QQVersionAppid,
platVer: systemVersion, platVer: systemVersion,
commonPath: this.dataPathGlobal, commonPath: this.dataPathGlobal,
clientVer: qqVersionConfigInfo.curVersion, clientVer: getFullQQVesion(),
hostName: hostname hostName: hostname
}); });
} }
@@ -189,7 +185,6 @@ export class NapCatCore {
} }
}); });
} }
private initDataListener() { private initDataListener() {
// 消息相关 // 消息相关
interface LineDevice { interface LineDevice {
@@ -208,10 +203,17 @@ export class NapCatCore {
} }
const msgListener = new MsgListener(); const msgListener = new MsgListener();
msgListener.onLineDev = (Devices: LineDevice[]) => { msgListener.onLineDev = (Devices: LineDevice[]) => {
DeviceList.splice(0, DeviceList.length);
Devices.map((Device: LineDevice) => { Devices.map((Device: LineDevice) => {
if (Device.clientType === 2) { let DeviceData = {
log('账号设备(' + Device.devUid + ') 在线状态变更'); app_id: Device.devUid,
} device_name: Device.clientType.toString(),
device_kind: Device.clientType.toString(),
};
DeviceList.push(DeviceData);
// if (Device.clientType === 2) {
// log('账号设备(' + Device.devUid + ') 在线状态变更');
// }
}); });
}; };
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => { msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
@@ -242,6 +244,7 @@ export class NapCatCore {
stat.last_message_time = Math.floor(Date.now() / 1000); stat.last_message_time = Math.floor(Date.now() / 1000);
}; };
msgListener.onRecvMsg = (msgList: RawMessage[]) => { msgListener.onRecvMsg = (msgList: RawMessage[]) => {
// console.log(JSON.stringify(msgList[0],null,2));
stat.packet_received += 1; stat.packet_received += 1;
stat.message_received += msgList.length; stat.message_received += msgList.length;
stat.last_message_time = Math.floor(Date.now() / 1000); stat.last_message_time = Math.floor(Date.now() / 1000);
@@ -250,31 +253,23 @@ export class NapCatCore {
stat.packet_received += 1; stat.packet_received += 1;
}; };
this.addListener(msgListener); this.addListener(msgListener);
// 好友相关 // 好友相关
const buddyListener = new BuddyListener(); const buddyListener = new BuddyListener();
buddyListener.onBuddyListChange = arg => {
rawFriends.length = 0;
rawFriends.push(...arg);
// console.log('onBuddyListChange', arg);
for (const categoryItem of arg) {
for (const friend of categoryItem.buddyList) {
// console.log("onBuddyListChange", friend)
const existFriend = friends.get(friend.uid);
if (existFriend) {
Object.assign(existFriend, friend);
}
else {
friends.set(friend.uid, friend);
}
}
// console.log("onBuddyListChange", friend)
}
};
this.addListener(buddyListener); this.addListener(buddyListener);
// 刷新一次好友列表 // 刷新一次好友列表 26702版本以下需要手动刷新一次获取 高版本NTQQ自带缓存
this.session.getBuddyService().getBuddyList(true).then(arg => { if (!requireMinNTQQBuild('26702')) {
// console.log('getBuddyList', arg); this.session.getBuddyService().getBuddyList(true).then(arg => {
}); // console.log('getBuddyList', arg);
});
} else {
// NTQQFriendApi.getBuddyV2(true).then((res) => {
// res.forEach((item) => {
// CachedIdMap.set(item.uid!, item.uin!);
// });
// }).catch();
}
interface SelfStatusInfo { interface SelfStatusInfo {
uid: string uid: string
status: number status: number
@@ -327,6 +322,7 @@ export class NapCatCore {
if (groupMembers.has(groupCode)) { if (groupMembers.has(groupCode)) {
const existMembers = groupMembers.get(groupCode)!; const existMembers = groupMembers.get(groupCode)!;
arg.infos.forEach((member, uid) => { arg.infos.forEach((member, uid) => {
//console.log('onMemberListChange', member);
const existMember = existMembers.get(uid); const existMember = existMembers.get(uid);
if (existMember) { if (existMember) {
Object.assign(existMember, member); Object.assign(existMember, member);
@@ -346,7 +342,7 @@ export class NapCatCore {
// console.log('onMemberListChange', groupCode, arg); // console.log('onMemberListChange', groupCode, arg);
}; };
groupListener.onMemberInfoChange = (groupCode, changeType, members) => { groupListener.onMemberInfoChange = (groupCode, changeType, members) => {
// console.log('onMemberInfoChange', arg); //console.log('onMemberInfoChange', groupCode, changeType, members);
if (changeType === 0 && members.get(selfInfo.uid)?.isDelete) { if (changeType === 0 && members.get(selfInfo.uid)?.isDelete) {
// 自身退群或者被踢退群 5s用于Api操作 之后不再出现 // 自身退群或者被踢退群 5s用于Api操作 之后不再出现
setTimeout(() => { setTimeout(() => {
@@ -359,6 +355,9 @@ export class NapCatCore {
members.forEach((member, uid) => { members.forEach((member, uid) => {
const existMember = existMembers.get(uid); const existMember = existMembers.get(uid);
if (existMember) { if (existMember) {
// 检查管理变动
member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember);
// 更新成员信息
Object.assign(existMember, member); Object.assign(existMember, member);
} }
else { else {
@@ -471,6 +470,13 @@ export class NapCatCore {
const loginList = await this.loginService.getLoginList(); const loginList = await this.loginService.getLoginList();
return loginList; return loginList;
} }
checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean {
if (memberNew.role !== memberOld?.role) {
log(`${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`);
return true;
}
return false;
}
} }
export const napCatCore = new NapCatCore(); export const napCatCore = new NapCatCore();

View File

@@ -1,11 +1,13 @@
import { import {
type Friend, type Friend,
type Group, type Group,
type GroupMember, GroupNotify, type GroupMember,
type SelfInfo, type SelfInfo,
BuddyCategoryType type BuddyCategoryType,
FriendV2
} from './entities'; } from './entities';
import { isNumeric } from '@/common/utils/helper'; import { isNumeric } from '@/common/utils/helper';
import { LimitedHashTable } from '@/common/utils/MessageUnique';
import { NTQQGroupApi } from '@/core/apis'; import { NTQQGroupApi } from '@/core/apis';
export const selfInfo: SelfInfo = { export const selfInfo: SelfInfo = {
@@ -14,8 +16,6 @@ export const selfInfo: SelfInfo = {
nick: '', nick: '',
online: true online: true
}; };
// 未来只在此处保留 selfInfo stat
// groupCode -> Group
export const groups: Map<string, Group> = new Map<string, Group>(); export const groups: Map<string, Group> = new Map<string, Group>();
export function deleteGroup(groupQQ: string) { export function deleteGroup(groupQQ: string) {
@@ -26,11 +26,11 @@ export function deleteGroup(groupQQ: string) {
// 群号 -> 群成员map(uid=>GroupMember) // 群号 -> 群成员map(uid=>GroupMember)
export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>(); export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
// uid -> Friend 下面这俩个准备移除 QQ里面自带缓存
export const friends: Map<string, Friend> = new Map<string, Friend>(); export const friends: Map<string, Friend> = new Map<string, Friend>();
export const rawFriends: Array<BuddyCategoryType> = []; // 带分组的好友列表
export const groupNotifies: Record<string, GroupNotify> = {}; // flag->GroupNotify //转换列表
//export const CachedIdMap = new LimitedHashTable<string, string>(1000);
export async function getGroup(qq: string | number): Promise<Group | undefined> { export async function getGroup(qq: string | number): Promise<Group | undefined> {
let group = groups.get(qq.toString()); let group = groups.get(qq.toString());
if (!group) { if (!group) {

View File

@@ -8,8 +8,10 @@ import {
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
SendReplyElement, SendReplyElement,
sendShareLocationElement,
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement,
viedo_type
} from './index'; } from './index';
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import ffmpeg from 'fluent-ffmpeg'; import ffmpeg from 'fluent-ffmpeg';
@@ -27,6 +29,16 @@ export const mFaceCache = new Map<string, string>(); // emojiId -> faceName
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
static location(): sendShareLocationElement {
return {
elementType: ElementType.SHARELOCATION,
elementId: '',
shareLocationElement: {
text: "测试",
ext: ""
}
}
}
static text(content: string): SendTextElement { static text(content: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
@@ -116,7 +128,7 @@ export class SendMsgElementConstructor {
return element; return element;
} }
static async video(filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> { static async video(filePath: string, fileName: string = '', diyThumbPath: string = '', videotype: viedo_type = viedo_type.VIDEO_FORMAT_MP4): Promise<SendVideoElement> {
const { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); const { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO);
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0'; throw '文件异常大小为0';
@@ -184,6 +196,7 @@ export class SendMsgElementConstructor {
thumbWidth: videoInfo.width, thumbWidth: videoInfo.width,
thumbHeight: videoInfo.height, thumbHeight: videoInfo.height,
fileSize: '' + fileSize, fileSize: '' + fileSize,
//fileFormat: videotype
// fileUuid: "", // fileUuid: "",
// transferStatus: 0, // transferStatus: 0,
// progress: 0, // progress: 0,
@@ -235,7 +248,28 @@ export class SendMsgElementConstructor {
} }
}; };
} }
// NodeIQQNTWrapperSession sendMsg [
// "0",
// {
// "peerUid": "u_e_RIxgTs2NaJ68h0PwOPSg",
// "chatType": 1,
// "guildId": ""
// },
// [
// {
// "elementId": "0",
// "elementType": 6,
// "faceElement": {
// "faceIndex": 0,
// "faceType": 5,
// "msgType": 0,
// "pokeType": 1,
// "pokeStrength": 0
// }
// }
// ],
// {}
// ]
static face(faceId: number): SendFaceElement { static face(faceId: number): SendFaceElement {
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface; const sysFaces = faceConfig.sysface;

View File

@@ -1,12 +1,18 @@
import { QQLevel, Sex } from './user'; import { QQLevel, Sex } from './user';
export enum GroupListUpdateType {
REFRESHALL,
GETALL,
MODIFIED,
REMOVE
}
export interface Group { export interface Group {
groupCode: string, groupCode: string,
createTime?: string,//高版本才有
maxMember: number, maxMember: number,
memberCount: number, memberCount: number,
groupName: string, groupName: string,
groupStatus: 0, groupStatus: number,
memberRole: 2, memberRole: number,
isTop: boolean, isTop: boolean,
toppedTimestamp: string, toppedTimestamp: string,
privilegeFlag: number, //65760 privilegeFlag: number, //65760
@@ -52,4 +58,7 @@ export interface GroupMember {
isRobot: boolean; isRobot: boolean;
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean;
joinTime: string;
lastSpeakTime: string;
} }

View File

@@ -23,6 +23,7 @@ export interface GetFileListParam {
showOnlinedocFolder: number showOnlinedocFolder: number
} }
export enum ElementType { export enum ElementType {
UNKNOWN = 0,
TEXT = 1, TEXT = 1,
PIC = 2, PIC = 2,
FILE = 3, FILE = 3,
@@ -30,11 +31,192 @@ export enum ElementType {
VIDEO = 5, VIDEO = 5,
FACE = 6, FACE = 6,
REPLY = 7, REPLY = 7,
WALLET = 9,
GreyTip = 8,//Poke别叫戳一搓了 官方名字拍一拍 戳一戳是另一个名字
ARK = 10, ARK = 10,
MFACE = 11, MFACE = 11,
MARKDOWN = 14 LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
}
export interface 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 interface InlineKeyboardButton {
id: string;
label: string;
visitedLabel: string;
unsupportTips: string;
data: string;
specifyRoleIds: string[];
specifyTinyids: string[];
style: number;
type: number;
clickLimit: number;
atBotShowChannelList: boolean;
permissionType: number;
}
export interface InlineKeyboardRow {
buttons: InlineKeyboardButton[];
}
export interface TofuElementContent {
color: string;
tittle: string;
}
export interface TaskTopMsgElement {
msgTitle: string;
msgSummary: string;
iconUrl: string;
topMsgType: number;
}
export interface SendTaskTopMsgElement {
elementType: ElementType.TASKTOPMSG;
elementId: string;
taskTopMsgElement: TaskTopMsgElement;
}
export interface TofuRecordElement {
type: number;
busiid: string;
busiuuid: string;
descriptionContent: string;
contentlist: TofuElementContent[],
background: string;
icon: string;
uinlist: string[],
uidlist: string[],
busiExtra: string;
updateTime: string;
dependedmsgid: string;
msgtime: string;
onscreennotify: boolean;
}
export interface SendTofuRecordElement {
elementType: ElementType.TOFURECORD;
elementId: string;
tofuRecordElement: TofuRecordElement;
} }
export interface FaceBubbleElement {
faceCount: number;
faceSummary: string;
faceFlag: number;
content: string;
oldVersionStr: string;
faceType: number;
others: string;
yellowFaceInfo: {
index: number;
buf: string;
compatibleText: string;
text: string;
}
}
export interface SendFaceBubbleElement {
elementType: ElementType.FACEBUBBLE;
elementId: string;
faceBubbleElement: FaceBubbleElement;
}
export interface AvRecordElement {
type: number;
time: string;
text: string;
mainType: number;
hasRead: boolean;
extraType: number;
}
export interface SendavRecordElement {
elementType: ElementType.AVRECORD;
elementId: string;
avRecordElement: AvRecordElement;
}
export interface YoloUserInfo {
uid: string;
result: number;
rank: number;
bizId: string
}
export interface SendInlineKeyboardElement {
elementType: ElementType.INLINEKEYBOARD;
elementId: string;
inlineKeyboardElement: {
rows: number;
botAppid: string;
}
}
export interface YoloGameResultElement {
UserInfo: YoloUserInfo[];
}
export interface SendYoloGameResultElement {
elementType: ElementType.YOLOGAMERESULT;
yoloGameResultElement: YoloGameResultElement
}
export interface GiphyElement {
id: string;
isClip: boolean;
width: number;
height: number;
}
export interface SendGiphyElement {
elementType: ElementType.GIPHY;
elementId: string;
giphyElement: GiphyElement;
}
export interface SendWalletElement {
elementType: ElementType.UNKNOWN;//不做 设置位置
elementId: string;
walletElement: {}
}
export interface CalendarElement {
summary: string;
msg: string;
expireTimeMs: string;
schemaType: number;
schema: string
}
export interface SendCalendarElement {
elementType: ElementType.CALENDAR;
elementId: string;
calendarElement: CalendarElement;
}
export interface SendliveGiftElement {
elementType: ElementType.LIVEGIFT;
elementId: string;
liveGiftElement: {}
}
export interface SendTextElement { export interface SendTextElement {
elementType: ElementType.TEXT; elementType: ElementType.TEXT;
elementId: string; elementId: string;
@@ -80,32 +262,18 @@ export enum PicSubType {
export interface SendPicElement { export interface SendPicElement {
elementType: ElementType.PIC; elementType: ElementType.PIC;
elementId: string; elementId: string;
picElement: { picElement:PicElement
md5HexStr: string; }
fileSize: number | string; export interface ReplyElement {
picWidth: number; replayMsgSeq: string;
picHeight: number; replayMsgId: string;
fileName: string; senderUin: string;
sourcePath: string; senderUinStr: string;
original: boolean;
picType: PicType;
picSubType: PicSubType;
fileUuid: string;
fileSubId: string;
thumbFileSize: number;
summary: string;
};
} }
export interface SendReplyElement { export interface SendReplyElement {
elementType: ElementType.REPLY; elementType: ElementType.REPLY;
elementId: string; elementId: string;
replyElement: { replyElement: ReplyElement
replayMsgSeq: string;
replayMsgId: string;
senderUin: string;
senderUinStr: string;
}
} }
export interface SendFaceElement { export interface SendFaceElement {
@@ -118,6 +286,32 @@ export interface SendMarketFaceElement {
elementType: ElementType.MFACE; elementType: ElementType.MFACE;
marketFaceElement: MarketFaceElement; marketFaceElement: MarketFaceElement;
} }
export interface SendstructLongMsgElement {
elementType: ElementType.STRUCTLONGMSG;
elementId: string;
structLongMsgElement: StructLongMsgElement;
}
export interface StructLongMsgElement {
xmlContent: string;
resId: string;
}
export interface SendactionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: number;
botAppid: string;
}
}
export interface ShareLocationElement {
text: string;
ext: string;
}
export interface sendShareLocationElement {
elementType: ElementType.SHARELOCATION;
elementId: string;
shareLocationElement?: ShareLocationElement;
}
export interface FileElement { export interface FileElement {
fileMd5?: string; fileMd5?: string;
@@ -160,10 +354,51 @@ export interface SendMarkdownElement {
elementId: string; elementId: string;
markdownElement: MarkdownElement; markdownElement: MarkdownElement;
} }
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement | SendVideoElement | SendArkElement | SendMarkdownElement SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
SendVideoElement | SendArkElement | SendMarkdownElement | sendShareLocationElement;
export interface TextElement {
content: string;
atType: number;
atUid: string;
atTinyId: string;
atNtUid: string;
}
export interface MessageElement {
elementType: ElementType,
elementId: string,
extBufForUI: string,//"0x",
textElement?: TextElement;
faceElement?: FaceElement,
marketFaceElement?: MarkdownElement,
replyElement?: ReplyElement,
picElement?: PicElement,
pttElement?: PttElement,
videoElement?: VideoElement,
grayTipElement?: GrayTipElement,
arkElement?: ArkElement,
fileElement?: FileElement,
liveGiftElement?: null,
markdownElement?: MarkdownElement,
structLongMsgElement?: StructLongMsgElement,
multiForwardMsgElement?: MultiForwardMsgElement,
giphyElement?: GiphyElement,
walletElement?: null,
inlineKeyboardElement?: InlineKeyboardElement,
textGiftElement?: null,//????
calendarElement?: CalendarElement,
yoloGameResultElement?: YoloGameResultElement,
avRecordElement?: AvRecordElement,
structMsgElement?: null,
faceBubbleElement?: FaceBubbleElement,
shareLocationElement?: ShareLocationElement,
tofuRecordElement?: TofuRecordElement,
taskTopMsgElement?: TaskTopMsgElement,
recommendedMsgElement?: RecommendedMsgElement,
actionBarElement?:ActionBarElement
}
export enum AtType { export enum AtType {
notAt = 0, notAt = 0,
atAll = 1, atAll = 1,
@@ -254,18 +489,23 @@ export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn';
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn'; export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn';
export interface PicElement { export interface PicElement {
picSubType?: number; md5HexStr?: string;
originImageUrl: string; // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn fileSize: number | string;//number
originImageMd5?: string;
sourcePath: string; // 图片本地路径
thumbPath: Map<number, string>;
picWidth: number; picWidth: number;
picHeight: number; picHeight: number;
fileSize: number;
fileName: string; fileName: string;
sourcePath: string;
original: boolean;
picType: PicType;
picSubType?: PicSubType;
fileUuid: string; fileUuid: string;
md5HexStr?: string; fileSubId: string;
} thumbFileSize: number;
summary: string;
thumbPath: Map<number, string>;
originImageMd5?: string;
originImageUrl?: string; // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
};
export enum GrayTipElementSubType { export enum GrayTipElementSubType {
INVITE_NEW_MEMBER = 12, INVITE_NEW_MEMBER = 12,
@@ -289,6 +529,7 @@ export interface GrayTipElement {
templId: string; templId: string;
}; };
jsonGrayTipElement: { jsonGrayTipElement: {
busiId?: number;
jsonStr: string; jsonStr: string;
}; };
} }
@@ -329,7 +570,7 @@ export interface VideoElement {
thumbMd5?: string thumbMd5?: string
fileTime?: number; // second fileTime?: number; // second
thumbSize?: number; // byte thumbSize?: number; // byte
fileFormat?: number; // 2表示mp4 参考下面条目 fileFormat?: viedo_type; // 2表示mp4 参考下面条目
fileSize?: string; // byte fileSize?: string; // byte
thumbWidth?: number; thumbWidth?: number;
thumbHeight?: number; thumbHeight?: number;
@@ -490,6 +731,8 @@ export interface MultiForwardMsgElement {
} }
export interface RawMessage { export interface RawMessage {
guildId: string;
msgRandom: string;
// int32, 自己维护的消息id // int32, 自己维护的消息id
id?: number; id?: number;
@@ -510,10 +753,12 @@ export interface RawMessage {
chatType: ChatType; chatType: ChatType;
sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送 sendStatus?: number; // 消息状态别人发的2是已撤回自己发的2是已发送
recallTime: string; // 撤回时间, "0"是没有撤回 recallTime: string; // 撤回时间, "0"是没有撤回
records: RawMessage[];
elements: { elements: {
elementId: string; elementId: string;
elementType: ElementType; elementType: ElementType;
replyElement: { replyElement: {
sourceMsgIdInRecords: string;
senderUid: string; // 原消息发送者QQ号 senderUid: string; // 原消息发送者QQ号
sourceMsgIsIncPic: boolean; // 原消息是否有图片 sourceMsgIsIncPic: boolean; // 原消息是否有图片
sourceMsgText: string; sourceMsgText: string;

View File

@@ -62,6 +62,7 @@ export enum BuddyReqType {
KMEINITIATORWAITPEERCONFIRM KMEINITIATORWAITPEERCONFIRM
} }
export interface FriendRequest { export interface FriendRequest {
isInitiator?: boolean;
isDecide: boolean; isDecide: boolean;
friendUid: string; friendUid: string;
reqType: BuddyReqType, reqType: BuddyReqType,

View File

@@ -9,7 +9,181 @@ export interface BuddyCategoryType {
categroyMbCount: number; categroyMbCount: number;
buddyList: User[]; buddyList: User[];
} }
export interface CoreInfo {
uid: string;
uin: string;
nick: string;
remark: string;
}
export interface BaseInfo {
qid: string;
longNick: string;
birthday_year: number;
birthday_month: number;
birthday_day: number;
age: number;
sex: number;
eMail: string;
phoneNum: string;
categoryId: number;
richTime: number;
richBuffer: string;
}
interface MusicInfo {
buf: string;
}
interface VideoBizInfo {
cid: string;
tvUrl: string;
synchType: string;
}
interface VideoInfo {
name: string;
}
interface ExtOnlineBusinessInfo {
buf: string;
customStatus: any;
videoBizInfo: VideoBizInfo;
videoInfo: VideoInfo;
}
interface ExtBuffer {
buf: string;
}
interface UserStatus {
uid: string;
uin: string;
status: number;
extStatus: number;
batteryStatus: number;
termType: number;
netType: number;
iconType: number;
customStatus: any;
setTime: string;
specialFlag: number;
abiFlag: number;
eNetworkType: number;
showName: string;
termDesc: string;
musicInfo: MusicInfo;
extOnlineBusinessInfo: ExtOnlineBusinessInfo;
extBuffer: ExtBuffer;
}
interface PrivilegeIcon {
jumpUrl: string;
openIconList: any[];
closeIconList: any[];
}
interface VasInfo {
vipFlag: boolean;
yearVipFlag: boolean;
svipFlag: boolean;
vipLevel: number;
bigClub: boolean;
bigClubLevel: number;
nameplateVipType: number;
grayNameplateFlag: number;
superVipTemplateId: number;
diyFontId: number;
pendantId: number;
pendantDiyId: number;
faceId: number;
vipFont: number;
vipFontType: number;
magicFont: number;
fontEffect: number;
newLoverDiamondFlag: number;
extendNameplateId: number;
diyNameplateIDs: any[];
vipStartFlag: number;
vipDataFlag: number;
gameNameplateId: string;
gameLastLoginTime: string;
gameRank: number;
gameIconShowFlag: boolean;
gameCardId: string;
vipNameColorId: string;
privilegeIcon: PrivilegeIcon;
}
interface RelationFlags {
topTime: string;
isBlock: boolean;
isMsgDisturb: boolean;
isSpecialCareOpen: boolean;
isSpecialCareZone: boolean;
ringId: string;
isBlocked: boolean;
recommendImgFlag: number;
disableEmojiShortCuts: number;
qidianMasterFlag: number;
qidianCrewFlag: number;
qidianCrewFlag2: number;
isHideQQLevel: number;
isHidePrivilegeIcon: number;
}
interface CommonExt {
constellation: number;
shengXiao: number;
kBloodType: number;
homeTown: string;
makeFriendCareer: number;
pos: string;
college: string;
country: string;
province: string;
city: string;
postCode: string;
address: string;
regTime: number;
interest: string;
labels: any[];
qqLevel: QQLevel;
}
interface Pic {
picId: string;
picTime: number;
picUrlMap: Record<string, string>;
}
interface PhotoWall {
picList: Pic[];
}
export interface SimpleInfo {
uid?: string;
uin?: string;
coreInfo: CoreInfo;
baseInfo: BaseInfo;
status: UserStatus | null;
vasInfo: VasInfo | null;
relationFlags: RelationFlags | null;
otherFlags: any | null;
intimate: any | null;
}
export interface FriendV2 extends SimpleInfo {
categoryId?: number;
categroyName?: string;
}
export interface UserDetailInfoListenerArg {
uid: string;
uin: string;
simpleInfo: SimpleInfo;
commonExt: CommonExt;
photoWall: PhotoWall;
}
export interface ModifyProfileParams { export interface ModifyProfileParams {
nick: string, nick: string,
longNick: string, longNick: string,
@@ -101,7 +275,18 @@ export interface Friend extends User { }
export enum BizKey { export enum BizKey {
KPRIVILEGEICON, KPRIVILEGEICON,
KPHOTOWALL KPHOTOWALL
} }
export interface UserDetailInfoByUinV2 {
result: number,
errMsg: string,
detail: {
uid: string,
uin: string,
simpleInfo: SimpleInfo,
commonExt: CommonExt,
photoWall: null
}
}
export interface UserDetailInfoByUin { export interface UserDetailInfoByUin {
result: number, result: number,
errMsg: string, errMsg: string,

42
src/core/src/external/appid.json vendored Normal file
View File

@@ -0,0 +1,42 @@
{
"3.1.2-13107": {
"appid": 537146866,
"qua": "V1_LNX_NQ_3.1.2-13107_RDM_B"
},
"3.2.10-25765": {
"appid": 537234773,
"qua": "V1_LNX_NQ_3.2.10_25765_GW_B"
},
"3.2.12-26702": {
"appid": 537237950,
"qua": "V1_LNX_NQ_3.2.12_26702_GW_B"
},
"9.9.11-24815": {
"appid": 537226656,
"qua": "V1_WIN_NQ_9.9.11_24815_GW_B"
},
"9.9.12-25493": {
"appid": 537231759,
"qua": "V1_WIN_NQ_9.9.12_25493_GW_B"
},
"9.9.12-25765": {
"appid": 537234702,
"qua": "V1_WIN_NQ_9.9.12_25765_GW_B"
},
"9.9.12-26299": {
"appid": 537234826,
"qua": "V1_WIN_NQ_9.9.12_26299_GW_B"
},
"9.9.12-26339": {
"appid": 537234826,
"qua": "V1_WIN_NQ_9.9.12_26339_GW_B"
},
"9.9.12-26466": {
"appid": 537234826,
"qua": "V1_WIN_NQ_9.9.12_26466_GW_B"
},
"9.9.15-26702": {
"appid": 537237765,
"qua": "V1_WIN_NQ_9.9.15_26702_GW_B"
}
}

View File

@@ -3,6 +3,8 @@ import { BuddyCategoryType, FriendRequestNotify } from '@/core/entities';
export type OnBuddyChangeParams = BuddyCategoryType[] export type OnBuddyChangeParams = BuddyCategoryType[]
interface IBuddyListener { interface IBuddyListener {
onBuddyListChangedV2(arg: unknown): void,//V2版本 还没兼容
onBuddyListChange(arg: OnBuddyChangeParams): void, onBuddyListChange(arg: OnBuddyChangeParams): void,
onBuddyInfoChange(arg: unknown): void, onBuddyInfoChange(arg: unknown): void,
@@ -44,6 +46,9 @@ export interface NodeIKernelBuddyListener extends IBuddyListener {
} }
export class BuddyListener implements IBuddyListener { export class BuddyListener implements IBuddyListener {
onBuddyListChangedV2(arg: unknown): void {
//throw new Error('Method not implemented.');
}
onAddBuddyNeedVerify(arg: unknown) { onAddBuddyNeedVerify(arg: unknown) {
} }

View File

@@ -1,7 +1,7 @@
import { Group, GroupMember, GroupNotify } from '@/core/entities'; import { Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/core/entities';
interface IGroupListener { interface IGroupListener {
onGroupListUpdate(updateType: number, groupList: Group[]): void; onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): void;
onGroupExtListUpdate(...args: unknown[]): void; onGroupExtListUpdate(...args: unknown[]): void;
@@ -96,7 +96,7 @@ export class GroupListener implements IGroupListener {
onGroupFirstBulletinNotify(...args: unknown[]) { onGroupFirstBulletinNotify(...args: unknown[]) {
} }
onGroupListUpdate(updateType: number, groupList: Group[]) { onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
} }
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) { onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
@@ -202,7 +202,7 @@ export class DebugGroupListener implements IGroupListener {
console.log('onGroupNotifiesUnreadCountUpdated:', ...args); console.log('onGroupNotifiesUnreadCountUpdated:', ...args);
} }
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]){ onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
console.log('onGroupSingleScreenNotifies:'); console.log('onGroupSingleScreenNotifies:');
} }

View File

@@ -40,14 +40,14 @@ export interface onGroupFileInfoUpdateParamType {
// fromNick: '拾xxxx, // fromNick: '拾xxxx,
// sig: '0x' // sig: '0x'
// } // }
export interface TempOnRecvParams{ export interface TempOnRecvParams {
sessionType: number,//1 sessionType: number,//1
chatType: ChatType,//100 chatType: ChatType,//100
peerUid: string,//uid peerUid: string,//uid
groupCode: string,//gc groupCode: string,//gc
fromNick: string,//gc name fromNick: string,//gc name
sig: string, sig: string,
} }
export interface IKernelMsgListener { export interface IKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage): void; onAddSendMsg(msgRecord: RawMessage): void;
@@ -158,7 +158,41 @@ export interface IKernelMsgListener {
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): void; onRichMediaUploadComplete(fileTransNotifyInfo: unknown): void;
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown): void; onSearchGroupFileInfoUpdate(searchGroupFileResult:
{
result: {
retCode: number,
retMsg: string,
clientWording: string
},
syncCookie: string,
totalMatchCount: number,
ownerMatchCount: number,
isEnd: boolean,
reqId: number,
item: Array<{
groupCode: string,
groupName: string,
uploaderUin: string,
uploaderName: string,
matchUin: string,
matchWords: Array<unknown>,
fileNameHits: Array<{
start: number,
end: number
}>,
fileModelId: string,
fileId: string,
fileName: string,
fileSize: string,
busId: number,
uploadTime: number,
modifyTime: number,
deadTime: number,
downloadTimes: number,
localPath: string
}>
}): void;
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): void; onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): void;
@@ -192,7 +226,7 @@ export interface IKernelMsgListener {
} }
export interface NodeIKernelMsgListener { export interface NodeIKernelMsgListener extends IKernelMsgListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new // eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IKernelMsgListener): NodeIKernelMsgListener; new(listener: IKernelMsgListener): NodeIKernelMsgListener;
} }

View File

@@ -1,8 +1,8 @@
import { User } from '@/core/entities'; import { User, UserDetailInfoListenerArg } from '@/core/entities';
interface IProfileListener { interface IProfileListener {
onProfileSimpleChanged(...args: unknown[]): void; onProfileSimpleChanged(...args: unknown[]): void;
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void;
onProfileDetailInfoChanged(profile: User): void; onProfileDetailInfoChanged(profile: User): void;
onStatusUpdate(...args: unknown[]): void; onStatusUpdate(...args: unknown[]): void;
@@ -18,6 +18,9 @@ export interface NodeIKernelProfileListener extends IProfileListener {
} }
export class ProfileListener implements IProfileListener { export class ProfileListener implements IProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]) { onProfileSimpleChanged(...args: unknown[]) {
} }

View File

@@ -0,0 +1,44 @@
interface IKernelRecentContactListener {
onDeletedContactsNotify(...args: unknown[]): unknown;
onRecentContactNotification(...args: unknown[]): unknown;
onMsgUnreadCountUpdate(...args: unknown[]): unknown;
onGuildDisplayRecentContactListChanged(...args: unknown[]): unknown;
onRecentContactListChanged(...args: unknown[]): unknown;
onRecentContactListChangedVer2(...args: unknown[]): unknown;
}
export interface NodeIKernelRecentContactListener extends IKernelRecentContactListener {
// eslint-disable-next-line @typescript-eslint/no-misused-new
new(listener: IKernelRecentContactListener): NodeIKernelRecentContactListener;
}
export class KernelRecentContactListener implements IKernelRecentContactListener {
onDeletedContactsNotify(...args: unknown[]) {
}
onRecentContactNotification(...args: unknown[]) {
}
onMsgUnreadCountUpdate(...args: unknown[]) {
}
onGuildDisplayRecentContactListChanged(...args: unknown[]) {
}
onRecentContactListChanged(...args: unknown[]) {
}
onRecentContactListChangedVer2(...args: unknown[]) {
}
}

View File

@@ -1,19 +1,44 @@
import { Friend } from '@/core/entities'; import { Friend } from '@/core/entities';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
import { NodeIKernelBuddyListener } from '@/core/listeners'; import { NodeIKernelBuddyListener } from '@/core/listeners';
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
export interface NodeIKernelBuddyService { export interface NodeIKernelBuddyService {
// 以下为自行添加的wrapper.node中并没有这些方法,目的是简化调用 // 26702 以上
friends: Friend[]; getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
data: Array<{
getFriend(uidOrUin: string): Promise<Friend>; categoryId: number,
categorySortId: number,
categroyName: string,
categroyMbCount: number,
onlineCount: number,
buddyUids: Array<string>
}>
}>;
//26702 以上
getBuddyListFromCache(callFrom: string): Promise<Array<
{
categoryId: number,//9999应该跳过 那是兜底数据吧
categorySortId: number,//排序方式
categroyName: string,//分类名
categroyMbCount: number,//不懂
onlineCount: number,//在线数目
buddyUids: Array<string>//Uids
}>>;
// 以下为原生方法 // 以下为原生方法
addKernelBuddyListener(listener: NodeIKernelBuddyListener): number; addKernelBuddyListener(listener: NodeIKernelBuddyListener): number;
getAllBuddyCount(): number;
removeKernelBuddyListener(listener: unknown): void; removeKernelBuddyListener(listener: unknown): void;
getBuddyList(bool: boolean): Promise<GeneralCallResult>; /**
* @deprecated
* @param nocache 使用缓存
*/
getBuddyList(nocache: boolean): Promise<GeneralCallResult>;
getBuddyNick(uid: number): string; getBuddyNick(uid: number): string;

View File

@@ -0,0 +1,3 @@
export interface NodeIKernelECDHService{
}

View File

@@ -8,7 +8,90 @@ import {
} from '@/core/entities'; } from '@/core/entities';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
//高版本的接口不应该随意使用 使用应该严格进行pr审核 同时部分ipc中未出现的接口不要过于依赖 应该做好数据兜底
export interface NodeIKernelGroupService { export interface NodeIKernelGroupService {
getMemberCommonInfo(Req: {
groupCode: string,
startUin: string,
identifyFlag: string,
uinList: string[],
memberCommonFilter: {
memberUin: number,
uinFlag: number,
uinFlagExt: number,
uinMobileFlag: number,
shutUpTime: number,
privilege: number,
},
memberNum: number,
filterMethod: string,
onlineFlag: string,
realSpecialTitleFlag: number
}): Promise<unknown>;
//26702
getGroupMemberLevelInfo(groupCode: string): Promise<unknown>;
//26702
getGroupHonorList(groupCodes: Array<string>): unknown;
getUinByUids(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uins: Map<string, string>
}>;
getUidByUins(uins: string[]): Promise<{
errCode: number,
errMsg: string,
uids: Map<string, string>
}>;
//26702(其实更早 但是我不知道)
checkGroupMemberCache(arrayList: Array<string>): Promise<unknown>;
//26702(其实更早 但是我不知道)
getGroupLatestEssenceList(groupCode: string): Promise<unknown>;
//26702(其实更早 但是我不知道)
shareDigest(Req: {
appId: string,
appType: number,
msgStyle: number,
recvUin: string,
sendType: number,
clientInfo: {
platform: number
},
richMsg: {
usingArk: boolean,
title: string,
summary: string,
url: string,
pictureUrl: string,
brief: string
}
}): Promise<unknown>;
//26702(其实更早 但是我不知道)
isEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>;
//26702(其实更早 但是我不知道)
queryCachedEssenceMsg(Req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<unknown>;
//26702(其实更早 但是我不知道)
fetchGroupEssenceList(Req: { groupCode: string, pageStart: number, pageLimit: number }, Arg: unknown): Promise<unknown>;
//26702
getAllMemberList(groupCode: string, forceFetch: boolean): Promise<{
errCode: number,
errMsg: string,
result: {
ids: Array<{
uid: string,
index: number//0
}>,
infos: {},
finish: true,
hasRobot: false
}
}>;
setHeader(uid: string, path: string): unknown;
addKernelGroupListener(listener: NodeIKernelGroupListener): number; addKernelGroupListener(listener: NodeIKernelGroupListener): number;
@@ -16,7 +99,7 @@ export interface NodeIKernelGroupService {
createMemberListScene(groupCode: string, scene: string): string; createMemberListScene(groupCode: string, scene: string): string;
destroyMemberListScene(): void; destroyMemberListScene(SceneId:string): void;
//About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string} //About Arg (a) name: lastId 根据手Q来看为object {index:?(number),uid:string}
getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{ getNextMemberList(sceneId: string, a: undefined, num: number): Promise<{
errCode: number, errMsg: string, errCode: number, errMsg: string,
@@ -27,9 +110,9 @@ export interface NodeIKernelGroupService {
monitorMemberList(): unknown; monitorMemberList(): unknown;
searchMember(uid: string): unknown; searchMember(sceneId: string, keywords: string[]): unknown;
getMemberInfo(uid: string): unknown; getMemberInfo(group_id: string, uids: string[], forceFetch: boolean): Promise<GeneralCallResult>;
//getMemberInfo [ '56729xxxx', [ 'u_4Nj08cwW5Hxxxxx' ], true ] //getMemberInfo [ '56729xxxx', [ 'u_4Nj08cwW5Hxxxxx' ], true ]
kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>; kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>;
@@ -38,7 +121,7 @@ export interface NodeIKernelGroupService {
modifyMemberCardName(groupCode: string, uid: string, cardName: string): void; modifyMemberCardName(groupCode: string, uid: string, cardName: string): void;
getTransferableMemberInfo(uid: string): unknown; getTransferableMemberInfo(groupCode: string): unknown;//获取整个群的
transferGroup(uid: string): void; transferGroup(uid: string): void;
@@ -149,5 +232,18 @@ export interface NodeIKernelGroupService {
modifyGroupExtInfo(groupCode: string, arg: unknown): void; modifyGroupExtInfo(groupCode: string, arg: unknown): void;
//需要提前判断是否存在 高版本新增
addGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>;
//需要提前判断是否存在 高版本新增
removeGroupEssence(param: {
groupCode: string
msgRandom: number,
msgSeq: number
}): Promise<unknown>;
isNull(): boolean; isNull(): boolean;
} }

View File

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

View File

@@ -1,11 +1,24 @@
import { ChatType, ElementType, Peer, RawMessage, SendMessageElement } from '@/core/entities'; import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/core/entities';
import { NodeIKernelMsgListener } from '@/core/listeners/NodeIKernelMsgListener'; import { NodeIKernelMsgListener } from '@/core/listeners/NodeIKernelMsgListener';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
export interface QueryMsgsParams {
chatInfo: Peer,
filterMsgType: [],
filterSendersUid: string[],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}
export interface NodeIKernelMsgService { export interface NodeIKernelMsgService {
generateMsgUniqueId(chatType: number, time: string): string;
addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number; addKernelMsgListener(nodeIKernelMsgListener: NodeIKernelMsgListener): number;
sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>): Promise<unknown>; sendMsg(msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>): Promise<GeneralCallResult>;
recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>; recallMsg(peer: Peer, msgIds: string[]): Promise<GeneralCallResult>;
@@ -21,7 +34,7 @@ export interface NodeIKernelMsgService {
getAutoReplyTextList(...args: unknown[]): unknown; getAutoReplyTextList(...args: unknown[]): unknown;
getOnLineDev(): Promise<any>; getOnLineDev(): void;
kickOffLine(DevInfo: Object): unknown; kickOffLine(DevInfo: Object): unknown;
@@ -73,7 +86,7 @@ export interface NodeIKernelMsgService {
cancelSendMsg(...args: unknown[]): unknown; cancelSendMsg(...args: unknown[]): unknown;
switchToOfflineSendMsg(...args: unknown[]): unknown; switchToOfflineSendMsg(peer: Peer, MsgId: string): unknown;
reqToOfflineSendMsg(...args: unknown[]): unknown; reqToOfflineSendMsg(...args: unknown[]): unknown;
@@ -115,9 +128,9 @@ export interface NodeIKernelMsgService {
addLocalTofuRecordMsg(...args: unknown[]): unknown; addLocalTofuRecordMsg(...args: unknown[]): unknown;
addLocalRecordMsg(...args: unknown[]): unknown; addLocalRecordMsg(Peer: Peer, msgId: string, ele: MessageElement, attr: Array<any> | number, front: boolean): Promise<unknown>;
deleteMsg(...args: unknown[]): unknown; deleteMsg(Peer: Peer, msgIds: Array<string>): Promise<any>;
updateElementExtBufForUI(...args: unknown[]): unknown; updateElementExtBufForUI(...args: unknown[]): unknown;
@@ -141,7 +154,7 @@ export interface NodeIKernelMsgService {
getLastMessageList(peer: Peer[]): Promise<unknown>; getLastMessageList(peer: Peer[]): Promise<unknown>;
getAioFirstViewLatestMsgs(...args: unknown[]): unknown; getAioFirstViewLatestMsgs(peer: Peer, num: number): unknown;
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>; getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>;
@@ -154,7 +167,7 @@ export interface NodeIKernelMsgService {
// this.$clientSeq = j3; // this.$clientSeq = j3;
// this.$cnt = i2; // this.$cnt = i2;
getMsgsWithMsgTimeAndClientSeqForC2C(...args: unknown[]): Promise<unknown>; getMsgsWithMsgTimeAndClientSeqForC2C(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getMsgsWithStatus(params: { getMsgsWithStatus(params: {
peer: Peer peer: Peer
@@ -164,9 +177,9 @@ export interface NodeIKernelMsgService {
queryOrder: boolean queryOrder: boolean
isIncludeSelf: boolean isIncludeSelf: boolean
appid: unknown appid: unknown
}): Promise<unknown>; }): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getMsgsBySeqRange(peer: Peer, startSeq: string, endSeq: string): Promise<unknown>; getMsgsBySeqRange(peer: Peer, startSeq: string, endSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>; getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, unknownArg: boolean): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
@@ -174,39 +187,48 @@ export interface NodeIKernelMsgService {
getRecallMsgsByMsgId(peer: Peer, MsgId: string[]): Promise<unknown>; getRecallMsgsByMsgId(peer: Peer, MsgId: string[]): Promise<unknown>;
getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<unknown>; getMsgsBySeqList(peer: Peer, seqList: string[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getSingleMsg(Peer: Peer, msgSeq: string): unknown; getSingleMsg(Peer: Peer, msgSeq: string): Promise<GeneralCallResult & { msgList: RawMessage[] }>;
getSourceOfReplyMsg(...args: unknown[]): unknown; getSourceOfReplyMsg(peer: Peer, MsgId: string, SourceSeq: string): unknown;
getSourceOfReplyMsgV2(...args: unknown[]): unknown; getSourceOfReplyMsgV2(peer: Peer, RootMsgId: string, ReplyMsgId: string): unknown;
getMsgByClientSeqAndTime(...args: unknown[]): unknown; getMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown;
getSourceOfReplyMsgByClientSeqAndTime(...args: unknown[]): unknown; getSourceOfReplyMsgByClientSeqAndTime(peer: Peer, clientSeq: string, time: string): unknown;
//cnt clientSeq?并不是吧
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown;
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: unknown): unknown; getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown;
getMsgsByTypeFilters(...args: unknown[]): unknown;
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown; getMsgWithAbstractByFilterParam(...args: unknown[]): unknown;
queryMsgsWithFilter(...args: unknown[]): unknown; queryMsgsWithFilter(...args: unknown[]): unknown;
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: { /**
chatInfo: { * @deprecated 该函数已被标记为废弃,请使用新的替代方法。
chatType: number, * 使用过滤条件查询消息列表的版本2接口。
peerUid: string *
}, * 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
filterMsgType: [], * 函数返回一个Promise解析为查询结果的未知类型对象。
filterSendersUid: [], *
filterMsgFromTime: string, * @param MsgId 消息ID用于特定消息的查询。
filterMsgToTime: string, * @param MsgTime 消息时间,用于指定消息的时间范围。
pageLimit: number, * @param param 查询参数对象,包含详细的过滤条件和分页信息。
isReverseOrder: boolean, * @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
isIncludeCurrent: boolean * @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
}): Promise<unknown>; * @param param.filterSendersUid 需要过滤的发送者用户ID数组。
* @param param.filterMsgFromTime 查询消息的起始时间。
* @param param.filterMsgToTime 查询消息的结束时间。
* @param param.pageLimit 每页的消息数量限制。
* @param param.isReverseOrder 是否按时间顺序倒序返回消息。
* @param param.isIncludeCurrent 是否包含当前页码。
* @returns 返回一个Promise解析为查询结果的未知类型对象。
*/
queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: QueryMsgsParams): Promise<unknown>;
// this.chatType = i2; // this.chatType = i2;
// this.peerUid = str; // this.peerUid = str;
@@ -222,46 +244,23 @@ export interface NodeIKernelMsgService {
// this.isReverseOrder = z; // this.isReverseOrder = z;
// this.isIncludeCurrent = z2; // this.isIncludeCurrent = z2;
//queryMsgsWithFilterEx(0L, 0L, 0L, new QueryMsgsParams(new ChatInfo(2, str), new ArrayList(), new ArrayList(), 0L, 0L, 250, false, true)) //queryMsgsWithFilterEx(0L, 0L, 0L, new QueryMsgsParams(new ChatInfo(2, str), new ArrayList(), new ArrayList(), 0L, 0L, 250, false, true))
queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: { queryMsgsWithFilterEx(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<GeneralCallResult & {
chatInfo: { msgList: RawMessage[]
chatType: number, }>;
peerUid: string
},
filterMsgType: [],
filterSendersUid: [],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}): Promise<unknown>;
//queryMsgsWithFilterEx(this.$msgId, this.$msgTime, this.$msgSeq, this.$param) //queryMsgsWithFilterEx(this.$msgId, this.$msgTime, this.$msgSeq, this.$param)
queryFileMsgsDesktop(...args: unknown[]): unknown; queryFileMsgsDesktop(...args: unknown[]): unknown;
setMsgRichInfoFlag(...args: unknown[]): unknown; setMsgRichInfoFlag(...args: unknown[]): unknown;
queryPicOrVideoMsgs(msgId: string, msgTime: string, megSeq: string, param: { queryPicOrVideoMsgs(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): Promise<unknown>;
chatInfo: {
chatType: number,
peerUid: string
},
filterMsgType: [],
filterSendersUid: [],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}):Promise<unknown>;
queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown; queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown;
queryEmoticonMsgs(...args: unknown[]): unknown; queryEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>;
queryTroopEmoticonMsgs(...args: unknown[]): unknown; queryTroopEmoticonMsgs(msgId: string, msgTime: string, msgSeq: string, Params: QueryMsgsParams): Promise<unknown>;
queryMsgsAndAbstractsWithFilter(...args: unknown[]): unknown; queryMsgsAndAbstractsWithFilter(msgId: string, msgTime: string, megSeq: string, param: QueryMsgsParams): unknown;
setFocusOnGuild(...args: unknown[]): unknown; setFocusOnGuild(...args: unknown[]): unknown;
@@ -299,7 +298,7 @@ export interface NodeIKernelMsgService {
setMsgRead(peer: Peer): Promise<GeneralCallResult>; setMsgRead(peer: Peer): Promise<GeneralCallResult>;
setAllC2CAndGroupMsgRead(...args: unknown[]): unknown; setAllC2CAndGroupMsgRead(): Promise<unknown>;
setGuildMsgRead(...args: unknown[]): unknown; setGuildMsgRead(...args: unknown[]): unknown;
@@ -327,7 +326,7 @@ export interface NodeIKernelMsgService {
setGuildTabUserFlag(...args: unknown[]): unknown; setGuildTabUserFlag(...args: unknown[]): unknown;
setBuildMode(...args: unknown[]): unknown; setBuildMode(flag: number/*0 1 3*/): unknown;
setConfigurationServiceData(...args: unknown[]): unknown; setConfigurationServiceData(...args: unknown[]): unknown;
@@ -355,12 +354,39 @@ export interface NodeIKernelMsgService {
getFileThumbSavePathForSend(...args: unknown[]): unknown; getFileThumbSavePathForSend(...args: unknown[]): unknown;
getFileThumbSavePath(...args: unknown[]): unknown; getFileThumbSavePath(...args: unknown[]): unknown;
//猜测居多
translatePtt2Text(...args: unknown[]): unknown; translatePtt2Text(MsgId: string, Peer: {}, MsgElement: {}): unknown;
setPttPlayedState(...args: unknown[]): unknown; setPttPlayedState(...args: unknown[]): unknown;
// NodeIQQNTWrapperSession fetchFavEmojiList [
fetchFavEmojiList(...args: unknown[]): unknown; // "",
// 48,
// true,
// true
// ]
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: Array<{
uin: string,
emoId: number,
emoPath: string,
isExist: boolean,
resId: string,
url: string,
md5: string,
emoOriginalPath: string,
thumbPath: string,
RomaingType: string,
isAPNG: false,
isMarkFace: false,
eId: string,
epId: string,
ocrWord: string,
modifyWord: string,
exposeNum: number,
clickNum: number,
desc: string
}>
}>;
addFavEmoji(...args: unknown[]): unknown; addFavEmoji(...args: unknown[]): unknown;
@@ -406,7 +432,7 @@ export interface NodeIKernelMsgService {
getEmojiResourcePath(...args: unknown[]): unknown; getEmojiResourcePath(...args: unknown[]): unknown;
JoinDragonGroupEmoji(...args: unknown[]): unknown; JoinDragonGroupEmoji(JoinDragonGroupEmojiReq: any/*joinDragonGroupEmojiReq*/): unknown;
getMsgAbstracts(...args: unknown[]): unknown; getMsgAbstracts(...args: unknown[]): unknown;
@@ -430,7 +456,10 @@ export interface NodeIKernelMsgService {
downloadRichMedia(...args: unknown[]): unknown; downloadRichMedia(...args: unknown[]): unknown;
getFirstUnreadMsgSeq(...args: unknown[]): unknown; getFirstUnreadMsgSeq(args: {
peerUid: string
guildId: string
}): unknown;
getFirstUnreadCommonMsg(...args: unknown[]): unknown; getFirstUnreadCommonMsg(...args: unknown[]): unknown;
@@ -448,7 +477,19 @@ export interface NodeIKernelMsgService {
setMsgEmojiLikes(...args: unknown[]): unknown; setMsgEmojiLikes(...args: unknown[]): unknown;
getMsgEmojiLikesList(...args: unknown[]): unknown; getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, cookie: string, bForward: boolean, number: number): Promise<{
result: number,
errMsg: string,
emojiLikesList:
Array<{
tinyId: string,
nickName: string,
headUrl: string
}>,
cookie: string,
isLastPage: boolean,
isFirstPage: boolean
}>;
setMsgEmojiLikesForRole(...args: unknown[]): unknown; setMsgEmojiLikesForRole(...args: unknown[]): unknown;
@@ -470,13 +511,13 @@ export interface NodeIKernelMsgService {
queryCalendar(...args: unknown[]): unknown; queryCalendar(...args: unknown[]): unknown;
queryFirstMsgSeq(...args: unknown[]): unknown; queryFirstMsgSeq(peer: Peer, ...args: unknown[]): unknown;
queryRoamCalendar(...args: unknown[]): unknown; queryRoamCalendar(...args: unknown[]): unknown;
queryFirstRoamMsg(...args: unknown[]): unknown; queryFirstRoamMsg(...args: unknown[]): unknown;
fetchLongMsg(...args: unknown[]): unknown; fetchLongMsg(peer: Peer, msgId: string): unknown;
fetchLongMsgWithCb(...args: unknown[]): unknown; fetchLongMsgWithCb(...args: unknown[]): unknown;
@@ -510,9 +551,9 @@ export interface NodeIKernelMsgService {
deleteReplyDraft(...args: unknown[]): unknown; deleteReplyDraft(...args: unknown[]): unknown;
getFirstUnreadAtMsg(...args: unknown[]): unknown; getFirstUnreadAtMsg(peer: Peer): unknown;
clearMsgRecords(...args: unknown[]): unknown;//设置已读后调用我觉得比较好 清理记录 clearMsgRecords(...args: unknown[]): unknown;//设置已读后调用我觉得比较好 清理记录 现在别了
IsExistOldDb(...args: unknown[]): unknown; IsExistOldDb(...args: unknown[]): unknown;
@@ -532,7 +573,7 @@ export interface NodeIKernelMsgService {
getCurChatImportStatusByUin(...args: unknown[]): unknown; getCurChatImportStatusByUin(...args: unknown[]): unknown;
getDataImportUserLevel(...args: unknown[]): unknown; getDataImportUserLevel(): unknown;
getMsgQRCode(...args: unknown[]): unknown; getMsgQRCode(...args: unknown[]): unknown;
@@ -564,6 +605,8 @@ export interface NodeIKernelMsgService {
// this.gameSession = tempChatGameSession; // this.gameSession = tempChatGameSession;
prepareTempChat(args: unknown): unknown;//主动临时消息 不做 prepareTempChat(args: unknown): unknown;//主动临时消息 不做
sendSsoCmdReqByContend(cmd: string, param: string): Promise<unknown>;
//chattype,uid->Promise<any> //chattype,uid->Promise<any>
getTempChatInfo(ChatType: number, Uid: string): unknown; getTempChatInfo(ChatType: number, Uid: string): unknown;
@@ -587,7 +630,7 @@ export interface NodeIKernelMsgService {
getABatchOfContactMsgBoxInfo(...args: unknown[]): unknown; getABatchOfContactMsgBoxInfo(...args: unknown[]): unknown;
insertMsgToMsgBox(...args: unknown[]): unknown; insertMsgToMsgBox(peer: Peer, msgId: string, arg: 2006): unknown;
isHitEmojiKeyword(...args: unknown[]): unknown; isHitEmojiKeyword(...args: unknown[]): unknown;
@@ -627,7 +670,25 @@ export interface NodeIKernelMsgService {
dataMigrationStopOperation(...args: unknown[]): unknown; dataMigrationStopOperation(...args: unknown[]): unknown;
dataMigrationImportMsgPbRecord(...args: unknown[]): unknown; //新的希望
dataMigrationImportMsgPbRecord(DataMigrationMsgInfo: Array<{
extensionData: string//"Hex"
extraData: string //""
chatType: number
chatUin: string
msgType: number
msgTime: string
msgSeq: string
msgRandom: string
}>, DataMigrationResourceInfo: {
extraData: string
filePath: string
fileSize: string
msgRandom: string
msgSeq: string
msgSubType: number
msgType: number
}): unknown;
dataMigrationGetResourceLocalDestinyPath(...args: unknown[]): unknown; dataMigrationGetResourceLocalDestinyPath(...args: unknown[]): unknown;

View File

@@ -1,4 +1,5 @@
import { BuddyProfileLikeReq } from "../entities/user"; import { BuddyProfileLikeReq } from "../entities/user";
import { GeneralCallResult } from "./common";
export interface NodeIKernelProfileLikeService { export interface NodeIKernelProfileLikeService {
addKernelProfileLikeListener(listener: NodeIKernelProfileLikeService): void; addKernelProfileLikeListener(listener: NodeIKernelProfileLikeService): void;
@@ -7,7 +8,13 @@ export interface NodeIKernelProfileLikeService {
setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number }; setBuddyProfileLike(...args: unknown[]): { result: number, errMsg: string, succCounts: number };
getBuddyProfileLike(req: BuddyProfileLikeReq): void; getBuddyProfileLike(req: BuddyProfileLikeReq): Promise<GeneralCallResult & {
"info": {
"userLikeInfos": Array<any>,
"friendMaxVotes": number,
"start": number
}
}>;
getProfileLikeScidResourceInfo(...args: unknown[]): void; getProfileLikeScidResourceInfo(...args: unknown[]): void;

View File

@@ -1,10 +1,36 @@
import { AnyCnameRecord } from 'node:dns'; import { AnyCnameRecord } from 'node:dns';
import { BizKey, ModifyProfileParams, UserDetailInfoByUin } from '../entities'; import { BaseInfo, BizKey, CoreInfo, ModifyProfileParams, SimpleInfo, UserDetailInfoByUin } from '../entities';
import { NodeIKernelProfileListener } from '../listeners'; import { NodeIKernelProfileListener } from '../listeners';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
export enum UserDetailSource {
KDB,
KSERVER
}
export enum ProfileBizType {
KALL,
KBASEEXTEND,
KVAS,
KQZONE,
KOTHER
}
export interface NodeIKernelProfileService { export interface NodeIKernelProfileService {
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string,string>>;//uin->uid
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string,string>>;
// {
// coreInfo: CoreInfo,
// baseInfo: BaseInfo,
// status: null,
// vasInfo: null,
// relationFlags: null,
// otherFlags: null,
// intimate: null
// }
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>;
fetchUserDetailInfo(trace: string, uids: string[], arg2: number, arg3: number[]): Promise<unknown>;
addKernelProfileListener(listener: NodeIKernelProfileListener): number; addKernelProfileListener(listener: NodeIKernelProfileListener): number;
removeKernelProfileListener(listenerId: number): void; removeKernelProfileListener(listenerId: number): void;

View File

@@ -1,5 +1,28 @@
import { Peer } from "../entities"; import { ChatType, Peer } from "../entities";
import { NodeIKernelRecentContactListener } from "../listeners/NodeIKernelRecentContactListener";
import { GeneralCallResult } from "./common";
export interface FSABRecentContactParams {
anchorPointContact: {
contactId: string;
sortField: string;
pos: number;
},
relativeMoveCount: number;
listType: number;
count: number;
fetchOld: boolean;
}
// {
// "anchorPointContact": {
// "contactId": "",
// "sortField": "",
// "pos": 0
// },
// "relativeMoveCount": 0,
// "listType": 1,
// "count": 200,
// "fetchOld": true
// }
export interface NodeIKernelRecentContactService { export interface NodeIKernelRecentContactService {
setGuildDisplayStatus(...args: unknown[]): unknown; // 2 arguments setGuildDisplayStatus(...args: unknown[]): unknown; // 2 arguments
@@ -11,7 +34,20 @@ export interface NodeIKernelRecentContactService {
enterOrExitMsgList(...args: unknown[]): unknown; // 1 arguments enterOrExitMsgList(...args: unknown[]): unknown; // 1 arguments
getRecentContactListSnapShot(...args: unknown[]): unknown; // 1 arguments /*!---!*/getRecentContactListSnapShot(count: number): Promise<GeneralCallResult & {
info: {
errCode: number,
errMsg: string,
sortedContactList: Array<number>,
changedList: Array<{
remark: any;
peerName: any;
sendMemberName: any;
sendNickName: any;
peerUid: string; peerUin: string, msgTime: string, chatType: ChatType, msgId: string
}>
}
}>; // 1 arguments
clearMsgUnreadCount(...args: unknown[]): unknown; // 1 arguments clearMsgUnreadCount(...args: unknown[]): unknown; // 1 arguments
@@ -19,7 +55,7 @@ export interface NodeIKernelRecentContactService {
jumpToSpecifyRecentContact(...args: unknown[]): unknown; // 1 arguments jumpToSpecifyRecentContact(...args: unknown[]): unknown; // 1 arguments
fetchAndSubscribeABatchOfRecentContact(...args: unknown[]): unknown; // 1 arguments /*!---!*/fetchAndSubscribeABatchOfRecentContact(params: FSABRecentContactParams): unknown; // 1 arguments
addRecentContact(peer: Peer): unknown; addRecentContact(peer: Peer): unknown;
@@ -31,9 +67,9 @@ export interface NodeIKernelRecentContactService {
updateGameMsgConfigs(...args: unknown[]): unknown; // 1 arguments updateGameMsgConfigs(...args: unknown[]): unknown; // 1 arguments
removeKernelRecentContactListener(...args: unknown[]): unknown; // 1 arguments removeKernelRecentContactListener(listenerid: number): unknown; // 1 arguments
addKernelRecentContactListener(...args: unknown[]): unknown; // 1 arguments addKernelRecentContactListener(listener: NodeIKernelRecentContactListener): void;
clearRecentContactsByChatType(...args: unknown[]): unknown; // 1 arguments clearRecentContactsByChatType(...args: unknown[]): unknown; // 1 arguments
@@ -43,7 +79,7 @@ export interface NodeIKernelRecentContactService {
deleteRecentContactsVer2(...args: unknown[]): unknown; // 1 arguments deleteRecentContactsVer2(...args: unknown[]): unknown; // 1 arguments
getRecentContactList(): unknown; getRecentContactList(): Promise<any>;
getMsgUnreadCount(): unknown; getMsgUnreadCount(): unknown;

View File

@@ -1,6 +1,50 @@
import { GetFileListParam, Peer } from "../entities"; import { GetFileListParam, MessageElement, Peer, SendMessageElement } from "../entities";
import { GeneralCallResult } from "./common"; import { GeneralCallResult } from "./common";
export enum UrlFileDownloadType {
KUNKNOWN,
KURLFILEDOWNLOADPRIVILEGEICON,
KURLFILEDOWNLOADPHOTOWALL,
KURLFILEDOWNLOADQZONE,
KURLFILEDOWNLOADCOMMON,
KURLFILEDOWNLOADINSTALLAPP
}
export enum RMBizTypeEnum {
KUNKNOWN,
KC2CFILE,
KGROUPFILE,
KC2CPIC,
KGROUPPIC,
KDISCPIC,
KC2CVIDEO,
KGROUPVIDEO,
KC2CPTT,
KGROUPPTT,
KFEEDCOMMENTPIC,
KGUILDFILE,
KGUILDPIC,
KGUILDPTT,
KGUILDVIDEO
}
export interface CommonFileInfo {
bizType: number;
chatType: number;
elemId: string;
favId: string;
fileModelId: string;
fileName: string;
fileSize: string;
md5: string;
md510m: string;
msgId: string;
msgTime: string;
parent: string;
peerUid: string;
picThumbPath: Array<string>
sha: string;
sha3: string;
subId: string;
uuid: string;
}
export interface NodeIKernelRichMediaService { export interface NodeIKernelRichMediaService {
//getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb); //getVideoPlayUrl(peer, msgId, elemId, videoCodecFormat, VideoRequestWay.KHAND, cb);
// public enum VideoCodecFormatType { // public enum VideoCodecFormatType {
@@ -46,7 +90,7 @@ export interface NodeIKernelRichMediaService {
} }
}>; }>;
getRichMediaFileDir(arg1: unknown, arg2: unknown, arg3: unknown): unknown; getRichMediaFileDir(elementType: number, downType: number, isTemp: boolean): unknown;
// this.senderUid = ""; // this.senderUid = "";
// this.peerUid = ""; // this.peerUid = "";
@@ -65,16 +109,43 @@ export interface NodeIKernelRichMediaService {
// this.elem = msgElement; // this.elem = msgElement;
// this.useHttps = num; // this.useHttps = num;
getVideoPlayUrlInVisit(arg: unknown): unknown; getVideoPlayUrlInVisit(arg: {
downloadType: number,
thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): Promise<unknown>;
isFileExpired(arg: unknown): unknown; //arg双端number
isFileExpired(arg: number): unknown;
deleteGroupFolder(GroupCode: string, FolderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>; deleteGroupFolder(GroupCode: string, FolderId: string): Promise<GeneralCallResult & { groupFileCommonResult: { retCode: number, retMsg: string, clientWording: string } }>;
//参数与getVideoPlayUrlInVisit一样 //参数与getVideoPlayUrlInVisit一样
downloadRichMediaInVisit(arg: unknown): unknown; downloadRichMediaInVisit(arg: {
downloadType: number,
downloadFileForModelId(peer: Peer, arg: unknown[], arg3: string): unknown; thumbSize: number,
msgId: string,
msgRandom: string,
msgSeq: string,
msgTime: string,
chatType: number,
senderUid: string,
peerUid: string,
guildId: string,
ele: MessageElement,
useHttps: boolean
}): unknown;
//arg3为“”
downloadFileForModelId(peer: Peer, ModelId: string[], arg3: string): unknown;
//第三个参数 Array<Type> //第三个参数 Array<Type>
// this.fileId = ""; // this.fileId = "";
// this.fileName = ""; // this.fileName = "";
@@ -83,25 +154,30 @@ export interface NodeIKernelRichMediaService {
// this.fileSize = j2; // this.fileSize = j2;
// this.fileModelId = j3; // this.fileModelId = j3;
downloadFileForFileUuid(peer: Peer, arg1: string, arg3: unknown[]): unknown; downloadFileForFileUuid(peer: Peer, uuid: string, arg3: {
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}[]): Promise<unknown>;
downloadFileByUrlListtransgroupfile(arg1: unknown, arg2: unknown): unknown; downloadFileByUrlList(fileDownloadTyp: UrlFileDownloadType, urlList: Array<string>): unknown;
downloadFileForFileInfotransgroupfile(arg1: unknown, arg2: unknown): unknown; downloadFileForFileInfo(fileInfo: CommonFileInfo[], savePath: string): unknown;
createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }> createGroupFolder(GroupCode: string, FolderName: string): Promise<GeneralCallResult & { resultWithGroupItem: { result: any, groupItem: Array<any> } }>
downloadFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown): unknown; downloadFile(commonFile: CommonFileInfo, arg2: unknown, arg3: unknown, savePath: string): unknown;
createGroupFoldertransgroupfile(arg1: unknown, arg2: unknown): unknown; createGroupFolder(arg1: unknown, arg2: unknown): unknown;
downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown; downloadGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown; renameGroupFolder(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
deleteGroupFoldertransgroupfile(arg1: unknown, arg2: unknown): unknown; deleteGroupFolder(arg1: unknown, arg2: unknown): unknown;
deleteTransferInfotransgroupfile(arg1: unknown, arg2: unknown): unknown; deleteTransferInfo(arg1: unknown, arg2: unknown): unknown;
cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown; cancelTransferTask(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
@@ -122,11 +198,9 @@ export interface NodeIKernelRichMediaService {
} }
}>; }>;
getGroupFileInfotransgroupfile(arg1: unknown, arg2: unknown): unknown; getGroupFileInfo(arg1: unknown, arg2: unknown): unknown;
getGroupFileListtransgroupfile(arg1: unknown, arg2: unknown): unknown; getGroupTransferList(arg1: unknown, arg2: unknown): unknown;
getGroupTransferListtransgroupfile(arg1: unknown, arg2: unknown): unknown;
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown; renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
@@ -134,6 +208,16 @@ export interface NodeIKernelRichMediaService {
transGroupFile(arg1: unknown, arg2: unknown): unknown; transGroupFile(arg1: unknown, arg2: unknown): unknown;
searchGroupFile(
keywords: Array<string>,
param: {
groupIds: Array<string>,
fileType: number,
context: string,
count: number,
sortType: number,
groupNames: Array<string>
}): Promise<unknown>;
searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown; searchGroupFileByWord(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & { deleteGroupFile(GroupCode: string, params: Array<number>, Files: Array<string>): Promise<GeneralCallResult & {
@@ -152,19 +236,31 @@ export interface NodeIKernelRichMediaService {
queryPicDownloadSize(arg: unknown): unknown; queryPicDownloadSize(arg: unknown): unknown;
searchGroupFiletransgroupfile(arg1: unknown, arg2: unknown): unknown; searchGroupFile(arg1: unknown, arg2: unknown): unknown;
searchMoreGroupFile(arg: unknown): unknown; searchMoreGroupFile(arg: unknown): unknown;
cancelSearcheGroupFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown; cancelSearcheGroupFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
onlyDownloadFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown; onlyDownloadFile(peer: Peer, arg2: unknown, arg3: Array<{
fileId: string,
fileName: string,
fileSize: string,
fileModelId: string
}
>): unknown;
onlyUploadFiletransgroupfile(arg1: unknown, arg2: unknown): unknown; onlyUploadFile(arg1: unknown, arg2: unknown): unknown;
isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown; isExtraLargePic(arg1: unknown, arg2: unknown, arg3: unknown): unknown;
uploadRMFileWithoutMsg(arg: unknown): unknown; uploadRMFileWithoutMsg(arg: {
bizType: RMBizTypeEnum,
filePath: string,
peerUid: string,
transferId: string
useNTV2: string
}): Promise<unknown>;
isNull(): boolean; isNull(): boolean;
} }

View File

@@ -21,7 +21,7 @@ export interface NodeIKernelRobotService {
removeKernelRobotListener(ListenerId: number): unknown; removeKernelRobotListener(ListenerId: number): unknown;
getAllRobotFriendsFromCache(): unknown; getAllRobotFriendsFromCache(): Promise<unknown>;
fetchAllRobots(arg1: unknown, arg2: unknown): unknown; fetchAllRobots(arg1: unknown, arg2: unknown): unknown;
@@ -30,5 +30,6 @@ export interface NodeIKernelRobotService {
setRobotPickTts(arg1: unknown, arg2: unknown): unknown; setRobotPickTts(arg1: unknown, arg2: unknown): unknown;
getRobotUinRange(data: any): Promise<{ response: { robotUinRanges: any } }> getRobotUinRange(data: any): Promise<{ response: { robotUinRanges: any } }>
isNull(): boolean; isNull(): boolean;
} }

View File

@@ -1,79 +1,129 @@
export interface NodeIKernelSearchService{ import { ChatType } from "../entities";
addKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments
removeKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments export interface NodeIKernelSearchService {
addKernelSearchListener(...args: any[]): unknown;// needs 1 arguments
searchStrangerr(...args: any[]): unknown;// needs 3 arguments removeKernelSearchListener(...args: any[]): unknown;// needs 1 arguments
searchGroupr(...args: any[]): unknown;// needs 1 arguments searchStranger(...args: any[]): unknown;// needs 3 arguments
searchLocalInfor(...args: any[]): unknown;// needs 2 arguments searchGroup(...args: any[]): unknown;// needs 1 arguments
cancelSearchLocalInfor(...args: any[]): unknown;// needs 3 arguments searchLocalInfo(keywords: string, unknown: number/*4*/): unknown;
searchBuddyChatInfor(...args: any[]): unknown;// needs 2 arguments cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments
searchMoreBuddyChatInfor(...args: any[]): unknown;// needs 1 arguments searchBuddyChatInfo(...args: any[]): unknown;// needs 2 arguments
cancelSearchBuddyChatInfor(...args: any[]): unknown;// needs 3 arguments searchMoreBuddyChatInfo(...args: any[]): unknown;// needs 1 arguments
searchContactr(...args: any[]): unknown;// needs 2 arguments cancelSearchBuddyChatInfo(...args: any[]): unknown;// needs 3 arguments
searchMoreContactr(...args: any[]): unknown;// needs 1 arguments searchContact(...args: any[]): unknown;// needs 2 arguments
cancelSearchContactr(...args: any[]): unknown;// needs 3 arguments searchMoreContact(...args: any[]): unknown;// needs 1 arguments
searchGroupChatInfor(...args: any[]): unknown;// needs 3 arguments cancelSearchContact(...args: any[]): unknown;// needs 3 arguments
resetSearchGroupChatInfoSortTyper(...args: any[]): unknown;// needs 3 arguments searchGroupChatInfo(...args: any[]): unknown;// needs 3 arguments
resetSearchGroupChatInfoFilterMembersr(...args: any[]): unknown;// needs 3 arguments resetSearchGroupChatInfoSortType(...args: any[]): unknown;// needs 3 arguments
searchMoreGroupChatInfor(...args: any[]): unknown;// needs 1 arguments resetSearchGroupChatInfoFilterMembers(...args: any[]): unknown;// needs 3 arguments
cancelSearchGroupChatInfor(...args: any[]): unknown;// needs 3 arguments searchMoreGroupChatInfo(...args: any[]): unknown;// needs 1 arguments
searchChatsWithKeywordsr(...args: any[]): unknown;// needs 3 arguments cancelSearchGroupChatInfo(...args: any[]): unknown;// needs 3 arguments
searchMoreChatsWithKeywordsr(...args: any[]): unknown;// needs 1 arguments searchChatsWithKeywords(...args: any[]): unknown;// needs 3 arguments
cancelSearchChatsWithKeywordsr(...args: any[]): unknown;// needs 3 arguments searchMoreChatsWithKeywords(...args: any[]): unknown;// needs 1 arguments
searchChatMsgsr(...args: any[]): unknown;// needs 2 arguments cancelSearchChatsWithKeywords(...args: any[]): unknown;// needs 3 arguments
searchMoreChatMsgsr(...args: any[]): unknown;// needs 1 arguments searchChatMsgs(...args: any[]): unknown;// needs 2 arguments
cancelSearchChatMsgsr(...args: any[]): unknown;// needs 3 arguments searchMoreChatMsgs(...args: any[]): unknown;// needs 1 arguments
searchMsgWithKeywordsr(...args: any[]): unknown;// needs 2 arguments cancelSearchChatMsgs(...args: any[]): unknown;// needs 3 arguments
searchMoreMsgWithKeywordsr(...args: any[]): unknown;// needs 1 arguments searchMsgWithKeywords(...args: any[]): unknown;// needs 2 arguments
cancelSearchMsgWithKeywordsr(...args: any[]): unknown;// needs 3 arguments
searchFileWithKeywordsr(...args: any[]): unknown;// needs 2 arguments searchMoreMsgWithKeywords(...args: any[]): unknown;// needs 1 arguments
searchMoreFileWithKeywordsr(...args: any[]): unknown;// needs 1 arguments cancelSearchMsgWithKeywords(...args: any[]): unknown;// needs 3 arguments
cancelSearchFileWithKeywordsr(...args: any[]): unknown;// needs 3 arguments searchFileWithKeywords(keywords: string[], source: number): Promise<string>;// needs 2 arguments
searchAtMeChatsr(...args: any[]): unknown;// needs 3 arguments searchMoreFileWithKeywords(...args: any[]): unknown;// needs 1 arguments
searchMoreAtMeChatsr(...args: any[]): unknown;// needs 1 arguments cancelSearchFileWithKeywords(...args: any[]): unknown;// needs 3 arguments
cancelSearchAtMeChatsr(...args: any[]): unknown;// needs 3 arguments searchAtMeChats(...args: any[]): unknown;// needs 3 arguments
searchChatAtMeMsgsr(...args: any[]): unknown;// needs 1 arguments searchMoreAtMeChats(...args: any[]): unknown;// needs 1 arguments
searchMoreChatAtMeMsgsr(...args: any[]): unknown;// needs 1 arguments cancelSearchAtMeChats(...args: any[]): unknown;// needs 3 arguments
cancelSearchChatAtMeMsgsr(...args: any[]): unknown;// needs 3 arguments searchChatAtMeMsgs(...args: any[]): unknown;// needs 1 arguments
addSearchHistoryr(...args: any[]): unknown;// needs 1 arguments searchMoreChatAtMeMsgs(...args: any[]): unknown;// needs 1 arguments
removeSearchHistoryr(...args: any[]): unknown;// needs 1 arguments cancelSearchChatAtMeMsgs(...args: any[]): unknown;// needs 3 arguments
searchCacher(...args: any[]): unknown;// needs 3 arguments addSearchHistory(param: {
type: number,//4
contactList: [],
id: number,//-1
groupInfos: [],
msgs: [],
fileInfos: [
{
chatType: ChatType,
buddyChatInfo: Array<{ category_name: string, peerUid: string, peerUin: string, remark: string }>,
discussChatInfo: [],
groupChatInfo: Array<
{
groupCode: string,
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
groupName: string,
remark: string
}>
,
dataLineChatInfo: [],
tmpChatInfo: [],
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderNick: string,
senderRemark: string,
senderCard: string,
elemId: string,
elemType: string,//3
fileSize: string,
filePath: string,
fileName: string,
hits: Array<
{
start: 12,
end: 14
}
>
}
]
clearSearchCacher(...args: any[]): unknown;// needs 1 arguments }): Promise<{
result: number,
errMsg: string,
id?: number
}>;
removeSearchHistory(...args: any[]): unknown;// needs 1 arguments
searchCache(...args: any[]): unknown;// needs 3 arguments
clearSearchCache(...args: any[]): unknown;// needs 1 arguments
} }

View File

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

View File

@@ -0,0 +1,14 @@
import { MessageElement, Peer } from "../entities";
export interface NodeIkernelTestPerformanceService {
insertMsg(MsgParam: {
peer: Peer
msgTime: string
msgId: string
msgSeq: string
batchNums: number
timesPerBatch: number
numPerTime: number
}, msg: Array<MessageElement>): Promise<unknown>;
}

View File

@@ -1,4 +1,4 @@
import { appid, qqPkgInfo, qqVersionConfigInfo } from '@/common/utils/QQBasicInfo'; import { getFullQQVesion, QQVersionAppid } from '@/common/utils/QQBasicInfo';
import { hostname, systemName, systemVersion } from '@/common/utils/system'; import { hostname, systemName, systemVersion } from '@/common/utils/system';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
@@ -34,42 +34,42 @@ export interface WrapperSessionInitConfig {
account_path: string // 可以通过NodeQQNTWrapperUtil().getNTUserDataInfoConfig()获取 account_path: string // 可以通过NodeQQNTWrapperUtil().getNTUserDataInfoConfig()获取
} }
clientVer: string // 9.9.8-22355 clientVer: string // 9.9.8-22355
a2: '', a2: string,
d2: '', d2: string,
d2Key: '', d2Key: string,
machineId: '', machineId: string,
platform: 3, // 3是Windows? platform: PlatformType, // 3是Windows?
platVer: string, // 系统版本号, 应该可以固定 platVer: string, // 系统版本号, 应该可以固定
appid: string, appid: string,
rdeliveryConfig: { rdeliveryConfig: {
appKey: '', appKey: string,
systemId: 0, systemId: number,
appId: '', appId: string,
logicEnvironment: '', logicEnvironment: string,
platform: 3, platform: PlatformType,
language: '', language: string,
sdkVersion: '', sdkVersion: string,
userId: '', userId: string,
appVersion: '', appVersion: string,
osVersion: '', osVersion: string,
bundleId: '', bundleId: string,
serverUrl: '', serverUrl: string,
fixedAfterHitKeys: [''] fixedAfterHitKeys: string[]
} }
'defaultFileDownloadPath': string, // 这个可以通过环境变量获取? defaultFileDownloadPath: string, // 这个可以通过环境变量获取?
'deviceInfo': { deviceInfo: {
'guid': string, guid: string,
'buildVer': string, buildVer: string,
'localId': 2052, localId: number,
'devName': string, devName: string,
'devType': string, devType: string,
'vendorName': '', vendorName: string,
'osVer': string, osVer: string,
'vendorOsName': string, vendorOsName: string,
'setMute': false, setMute: boolean,
'vendorType': 0 vendorType: VendorType
}, },
'deviceConfig': '{"appearance":{"isSplitViewMode":true},"msg":{}}' deviceConfig: '{"appearance":{"isSplitViewMode":true},"msg":{}}'
} }
export const sessionConfig: WrapperSessionInitConfig | any = {}; export const sessionConfig: WrapperSessionInitConfig | any = {};
@@ -79,7 +79,7 @@ export async function genSessionConfig(selfUin: string, selfUid: string, account
fs.mkdirSync(downloadPath, { recursive: true }); fs.mkdirSync(downloadPath, { recursive: true });
let guid: string = await getMachineId(); let guid: string = await getMachineId();
//console.log(guid); //console.log(guid);
// guid = '52afb776-82f6-4e59-9d38-44705b112d0a'; // guid = '52afb776-82f6-4e59-9d38-44705b112d0a';
//let guid: string = await getMachineId(); //let guid: string = await getMachineId();
const config: WrapperSessionInitConfig = { const config: WrapperSessionInitConfig = {
selfUin, selfUin,
@@ -87,20 +87,20 @@ export async function genSessionConfig(selfUin: string, selfUid: string, account
desktopPathConfig: { desktopPathConfig: {
account_path // 可以通过NodeQQNTWrapperUtil().getNTUserDataInfoConfig()获取 account_path // 可以通过NodeQQNTWrapperUtil().getNTUserDataInfoConfig()获取
}, },
clientVer: qqVersionConfigInfo.curVersion, // 9.9.8-22355 clientVer: getFullQQVesion(), // 9.9.8-22355
a2: '', a2: '',
d2: '', d2: '',
d2Key: '', d2Key: '',
machineId: '', machineId: '',
platform: 3, // 3是Windows? platform: PlatformType.KWINDOWS, // 3是Windows?
platVer: systemVersion, // 系统版本号, 应该可以固定 platVer: systemVersion, // 系统版本号, 应该可以固定
appid: appid, appid: QQVersionAppid,
rdeliveryConfig: { rdeliveryConfig: {
appKey: '', appKey: '',
systemId: 0, systemId: 0,
appId: '', appId: '',
logicEnvironment: '', logicEnvironment: '',
platform: 3, platform: PlatformType.KWINDOWS,
language: '', language: '',
sdkVersion: '', sdkVersion: '',
userId: '', userId: '',
@@ -110,22 +110,21 @@ export async function genSessionConfig(selfUin: string, selfUid: string, account
serverUrl: '', serverUrl: '',
fixedAfterHitKeys: [''] fixedAfterHitKeys: ['']
}, },
'defaultFileDownloadPath': downloadPath, defaultFileDownloadPath: downloadPath,
'deviceInfo': { deviceInfo: {
guid, guid,
'buildVer': qqPkgInfo.version, buildVer: getFullQQVesion(),
'localId': 2052, localId: 2052,
'devName': hostname, devName: hostname,
'devType': systemName, devType: systemName,
'vendorName': '', vendorName: '',
'osVer': systemVersion, osVer: systemVersion,
'vendorOsName': systemName, vendorOsName: systemName,
'setMute': false, setMute: false,
'vendorType': 0 vendorType: VendorType.KNOSETONIOS
}, },
'deviceConfig': '{"appearance":{"isSplitViewMode":true},"msg":{}}' deviceConfig: '{"appearance":{"isSplitViewMode":true},"msg":{}}'
}; };
Object.assign(sessionConfig, config); Object.assign(sessionConfig, config);
// log(sessionConfig);
return config; return config;
} }

View File

@@ -12,18 +12,16 @@ export interface NapCatConfig {
consoleLogLevel: LogLevel, consoleLogLevel: LogLevel,
} }
class Config extends ConfigBase<NapCatConfig> implements NapCatConfig{ class Config extends ConfigBase<NapCatConfig> implements NapCatConfig {
name: string = 'napcat'
fileLog = true; fileLog = true;
consoleLog = true; consoleLog = true;
fileLogLevel = LogLevel.DEBUG; fileLogLevel = LogLevel.DEBUG;
consoleLogLevel = LogLevel.INFO; consoleLogLevel = LogLevel.INFO;
constructor() { constructor() {
super(); super();
} }
getConfigPath() {
return path.join(this.getConfigDir(), `napcat_${selfInfo.uin}.json`);
}
} }
export const napCatConfig = new Config(); export const napCatConfig = new Config();

View File

@@ -26,7 +26,6 @@ import {
NodeIKernelRichMediaService, NodeIKernelRichMediaService,
NodeIKernelAvatarService, NodeIKernelAvatarService,
} from './services'; } from './services';
import { qqVersionConfigInfo } from '@/common/utils/QQBasicInfo';
import { NodeIKernelStorageCleanService } from './services/NodeIKernelStorageCleanService'; import { NodeIKernelStorageCleanService } from './services/NodeIKernelStorageCleanService';
import { NodeIKernelRobotService } from './services/NodeIKernelRobotService'; import { NodeIKernelRobotService } from './services/NodeIKernelRobotService';
import { dirname } from "node:path" import { dirname } from "node:path"
@@ -39,6 +38,11 @@ import { NodeIKernelTianShuService } from './services/NodeIKernelTianShuService'
import { NodeIKernelUnitedConfigService } from './services/NodeIKernelUnitedConfigService'; import { NodeIKernelUnitedConfigService } from './services/NodeIKernelUnitedConfigService';
import { NodeIKernelSearchService } from './services/NodeIKernelSearchService'; import { NodeIKernelSearchService } from './services/NodeIKernelSearchService';
import { NodeIKernelCollectionService } from './services/NodeIKernelCollectionService'; import { NodeIKernelCollectionService } from './services/NodeIKernelCollectionService';
import { NodeIKernelRecentContactService } from './services/NodeIKernelRecentContactService';
import { NodeIKernelMSFService } from './services/NodeIKernelMSFService';
import { NodeIkernelTestPerformanceService } from './services/NodeIkernelTestPerformanceService';
import { NodeIKernelECDHService } from './services/NodeIKernelECDHService';
import { getFullQQVesion } from '@/common/utils/QQBasicInfo';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -154,6 +158,10 @@ export interface NodeIQQNTWrapperSession {
startNT(): void; startNT(): void;
getBdhUploadService(): unknown;
getECDHService(): NodeIKernelECDHService;
getMsgService(): NodeIKernelMsgService; getMsgService(): NodeIKernelMsgService;
getProfileService(): NodeIKernelProfileService; getProfileService(): NodeIKernelProfileService;
@@ -208,7 +216,7 @@ export interface NodeIQQNTWrapperSession {
getSkinService(): unknown; getSkinService(): unknown;
getTestPerformanceService(): unknown; getTestPerformanceService(): NodeIkernelTestPerformanceService;
getQQPlayService(): unknown; getQQPlayService(): unknown;
@@ -230,13 +238,13 @@ export interface NodeIQQNTWrapperSession {
getLockService(): unknown; getLockService(): unknown;
getMSFService(): unknown getMSFService(): NodeIKernelMSFService;
getGuildHotUpdateService(): unknown; getGuildHotUpdateService(): unknown;
getAVSDKService(): unknown; getAVSDKService(): unknown;
getRecentContactService(): unknown; getRecentContactService(): NodeIKernelRecentContactService;
getConfigMgrService(): unknown; getConfigMgrService(): unknown;
} }
@@ -284,7 +292,7 @@ export interface WrapperNodeApi {
let wrapperNodePath = path.resolve(path.dirname(process.execPath), './resources/app/wrapper.node'); let wrapperNodePath = path.resolve(path.dirname(process.execPath), './resources/app/wrapper.node');
if (!fs.existsSync(wrapperNodePath)) { if (!fs.existsSync(wrapperNodePath)) {
wrapperNodePath = path.join(path.dirname(process.execPath), `resources/app/versions/${qqVersionConfigInfo.curVersion}/wrapper.node`); wrapperNodePath = path.join(path.dirname(process.execPath), `resources/app/versions/${getFullQQVesion()}/wrapper.node`);
} }
const nativemodule: any = { exports: {} }; const nativemodule: any = { exports: {} };
process.dlopen(nativemodule, wrapperNodePath); process.dlopen(nativemodule, wrapperNodePath);

View File

@@ -13,13 +13,15 @@ import { deleteOldFiles, UpdateConfig } from './common/utils/helper';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import chalk from 'chalk'; import chalk from 'chalk';
import { randomInt } from 'crypto';
import { MessageUnique } from './common/utils/MessageUnique';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const tagColor = chalk.cyan; const tagColor = chalk.cyan;
program program
.option('-q, --qq <type>', 'QQ号') .option('-q, --qq [type]', 'QQ号')
.parse(process.argv); .parse(process.argv);
//deleteOldFiles(path.join(__dirname, 'logs'), 3).then().catch(); //deleteOldFiles(path.join(__dirname, 'logs'), 3).then().catch();
@@ -67,7 +69,18 @@ const showQRCode = async (url: string, base64: string, buffer: Buffer) => {
}); });
}); });
}; };
const quickLoginQQ = cmdOptions.qq;
let quickLoginQQ = cmdOptions.qq; // undefine、true、string
const QuickLoginList = await napCatCore.getQuickLoginList();
if (quickLoginQQ == true) {
if (QuickLoginList.LocalLoginInfoList.length > 0) {
quickLoginQQ = QuickLoginList.LocalLoginInfoList[0].uin;
log('-q 指令指定使用最近的QQ进行快速登录');
} else {
quickLoginQQ = '';
}
}
// napCatCore.on('system.login.error', (result) => { // napCatCore.on('system.login.error', (result) => {
// console.error('登录失败', result); // console.error('登录失败', result);
// napCatCore.qrLogin().then().catch(console.error); // napCatCore.qrLogin().then().catch(console.error);
@@ -79,9 +92,9 @@ napCatCore.getQuickLoginList().then((res) => {
WebUiDataRuntime.setQQQuickLoginCall(async (uin: string) => { WebUiDataRuntime.setQQQuickLoginCall(async (uin: string) => {
const QuickLogin: Promise<{ result: boolean, message: string }> = new Promise((resolve, reject) => { const QuickLogin: Promise<{ result: boolean, message: string }> = new Promise((resolve, reject) => {
if (quickLoginQQ) { if (uin) {
log('正在快速登录 ', quickLoginQQ); log('正在快速登录 ', uin);
napCatCore.quickLogin(quickLoginQQ).then(res => { napCatCore.quickLogin(uin).then(res => {
if (res.loginErrorInfo.errMsg) { if (res.loginErrorInfo.errMsg) {
resolve({ result: false, message: res.loginErrorInfo.errMsg }); resolve({ result: false, message: res.loginErrorInfo.errMsg });
} }
@@ -105,11 +118,14 @@ if (quickLoginQQ) {
logError('快速登录错误:', res.loginErrorInfo.errMsg); logError('快速登录错误:', res.loginErrorInfo.errMsg);
} }
}).catch((e) => { }).catch((e) => {
logError(e); logError('快速登录错误:', e);
napCatCore.qrLogin(showQRCode); napCatCore.qrLogin(showQRCode);
}); });
} else { } else {
log('没有 -q 参数指定快速登录的QQ,将使用二维码登录方式'); log('没有 -q 指令指定快速登录,将使用二维码登录方式');
if (QuickLoginList.LocalLoginInfoList.length > 0) {
log(`可用于快速登录的QQ${QuickLoginList.LocalLoginInfoList.map((u, index) => `\n${index}: ${u.uin} ${u.nickName}`)}`);
}
napCatCore.qrLogin(showQRCode); napCatCore.qrLogin(showQRCode);
} }

View File

@@ -15,6 +15,6 @@ export class SetConfigAction extends BaseAction<OB11Config, void> {
actionName = ActionName.SetConfig; actionName = ActionName.SetConfig;
protected async _handle(payload: OB11Config): Promise<void> { protected async _handle(payload: OB11Config): Promise<void> {
ob11Config.save(payload); ob11Config.save(payload, true);
} }
} }

View File

@@ -0,0 +1,21 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQMsgApi } from '@/core/apis';
const SchemaData = {
type: 'object',
properties: {
count: { type: 'number' },
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class FetchCustomFace extends BaseAction<Payload, string[]> {
actionName = ActionName.FetchCustomFace;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
const ret = await NTQQMsgApi.fetchFavEmojiList(payload.count || 48);
return ret.emojiInfoList.map(e => e.url);
}
}

View File

@@ -0,0 +1,32 @@
//getMsgEmojiLikesList
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQMsgApi } from '@/core/apis';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: 'string' },
group_id: { type: 'string' },
emojiId: { type: 'string' },
emojiType: { type: 'string' },
message_id: { type: ['string', 'number'] },
count: { type: 'number' }
},
required: ['emojiId', 'emojiType', 'message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class FetchEmojioLike extends BaseAction<Payload, any> {
actionName = ActionName.FetchEmojioLike;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
let msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if(!msgIdPeer) throw new Error('消息不存在');
let msg = (await NTQQMsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
const ret = await NTQQMsgApi.getMsgEmojiLikesList(msgIdPeer.Peer,msg.msgSeq,payload.emojiId,payload.emojiType,payload.count);
return ret;
}
}

View File

@@ -1,12 +1,19 @@
import { rawFriends } from '@/core/data'; import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { BuddyCategoryType } from '@/core/entities/'; import { BuddyCategoryType } from '@/core/entities/';
import { NTQQFriendApi } from '@/core';
import { OB11Constructor } from '@/onebot11/constructor';
export class GetFriendWithCategory extends BaseAction<void, Array<BuddyCategoryType>> { export class GetFriendWithCategory extends BaseAction<void, any> {
actionName = ActionName.GetFriendsWithCategory; actionName = ActionName.GetFriendsWithCategory;
protected async _handle(payload: void) { protected async _handle(payload: void) {
return rawFriends; if (requireMinNTQQBuild('26702')) {
//全新逻辑
return OB11Constructor.friendsV2(await NTQQFriendApi.getBuddyV2ExWithCate(true));
} else {
throw new Error('this ntqq version not support, must be 26702 or later');
}
} }
} }

View File

@@ -0,0 +1,16 @@
import { selfInfo } from '@/core/data';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis';
export class GetProfileLike extends BaseAction<void, any> {
actionName = ActionName.GetProfileLike;
protected async _handle(payload: void) {
const ret = await NTQQUserApi.getProfileLike(selfInfo.uid);
const listdata: any[] = ret.info.userLikeInfos[0].favoriteInfo.userInfos;
for (let i = 0; i < listdata.length; i++) {
listdata[i].uin = parseInt((await NTQQUserApi.getUinByUid(listdata[i].uid)) || '');
}
return listdata;
}
}

View File

@@ -0,0 +1,57 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import * as fs from 'node:fs';
import { NTQQUserApi } from '@/core/apis/user';
import { checkFileReceived, uri2local } from '@/common/utils/file';
import { NTQQGroupApi } from '@/core';
// import { log } from "../../../common/utils";
interface Payload {
file: string,
groupCode: string
}
export default class SetGroupHeader extends BaseAction<Payload, any> {
actionName = ActionName.SetGroupHeader;
// 用不着复杂检测
protected async check(payload: Payload): Promise<BaseCheckResult> {
if (!payload.file || typeof payload.file != 'string' || !payload.groupCode || typeof payload.groupCode != 'string') {
return {
valid: false,
message: 'file和groupCode字段不能为空或者类型错误',
};
}
return {
valid: true,
};
}
protected async _handle(payload: Payload): Promise<any> {
const { path, isLocal, errMsg } = (await uri2local(payload.file));
if (errMsg) {
throw `头像${payload.file}设置失败,file字段可能格式不正确`;
}
if (path) {
await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃需要提前判断
const ret = await NTQQGroupApi.setGroupAvatar(payload.groupCode,path);
if (!isLocal) {
fs.unlink(path, () => { });
}
if (!ret) {
throw `头像${payload.file}设置失败,api无返回`;
}
// log(`头像设置返回:${JSON.stringify(ret)}`)
// if (ret['result'] == 1004022) {
// throw `头像${payload.file}设置失败,文件可能不是图片格式`;
// } else if (ret['result'] != 0) {
// throw `头像${payload.file}设置失败,未知的错误,${ret['result']}:${ret['errMsg']}`;
// }
return ret;
} else {
if (!isLocal) {
fs.unlink(path, () => { });
}
throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`;
}
return null;
}
}

View File

@@ -0,0 +1,32 @@
import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types';
import * as fs from 'node:fs';
import { NTQQUserApi } from '@/core/apis/user';
import { checkFileReceived, uri2local } from '@/common/utils/file';
import { napCatCore, NTQQGroupApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
// import { log } from "../../../common/utils";
const SchemaData = {
type: 'object',
properties: {
cmd: { type: 'string' },
param: { type: 'string' }
},
required: ['cmd', 'param'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class TestApi01 extends BaseAction<Payload, any> {
actionName = ActionName.TestApi01;
// 用不着复杂检测
protected async check(payload: Payload): Promise<BaseCheckResult> {
return {
valid: true,
};
}
protected async _handle(payload: Payload): Promise<any> {
return await napCatCore.session.getMsgService().sendSsoCmdReqByContend(payload.cmd, payload.param);
}
}

View File

@@ -1,15 +1,12 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { dbUtil } from '@/common/utils/db';
import { ob11Config } from '@/onebot11/config'; import { ob11Config } from '@/onebot11/config';
import { log, logDebug } from '@/common/utils/log'; import { UUIDConverter } from '@/common/utils/helper';
import { sleep } from '@/common/utils/helper';
import { uri2local } from '@/common/utils/file';
import { ActionName, BaseCheckResult } from '../types'; import { ActionName, BaseCheckResult } from '../types';
import { FileElement, RawMessage, VideoElement } from '@/core/entities'; import { ChatType, ElementType, FileElement, Peer, RawMessage, VideoElement } from '@/core/entities';
import { NTQQFileApi } from '@/core/apis'; import { NTQQFileApi, NTQQFriendApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv'; import { getGroup } from '@/core/data';
export interface GetFilePayload { export interface GetFilePayload {
file: string; // 文件名或者fileUuid file: string; // 文件名或者fileUuid
@@ -45,69 +42,163 @@ export class GetFileBase extends BaseAction<GetFilePayload, GetFileResponse> {
return { id: element.elementId, element: element.fileElement }; return { id: element.elementId, element: element.fileElement };
} }
protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> { protected async _handle(payload: GetFilePayload): Promise<GetFileResponse> {
let cache = await dbUtil.getFileCacheByName(payload.file);
if (!cache) {
cache = await dbUtil.getFileCacheByUuid(payload.file);
}
if (!cache) {
throw new Error('file not found');
}
const { enableLocalFile2Url } = ob11Config; const { enableLocalFile2Url } = ob11Config;
let UuidData: {
high: string;
low: string;
} | undefined;
try { try {
await fs.access(cache.path, fs.constants.F_OK); UuidData = UUIDConverter.decode(payload.file);
} catch (e) { if (UuidData) {
logDebug('local file not found, start download...'); const peerUin = UuidData.high;
// if (cache.url) { const msgId = UuidData.low;
// const downloadResult = await uri2local(cache.url); const isGroup = await getGroup(peerUin);
// if (downloadResult.success) { let peer: Peer | undefined;
// cache.path = downloadResult.path; //识别Peer
// dbUtil.updateFileCache(cache).then(); if (isGroup) {
// } else { peer = { chatType: ChatType.group, peerUid: peerUin };
// throw new Error('file download failed. ' + downloadResult.errMsg); }
// } const PeerUid = await NTQQUserApi.getUidByUin(peerUin);
// } else { if (PeerUid) {
// // 没有url的可能是私聊文件或者群文件需要自己下载 const isBuddy = await NTQQFriendApi.isBuddy(PeerUid);
// log('需要调用 NTQQ 下载文件api'); if (isBuddy) {
let msg = await dbUtil.getMsgByLongId(cache.msgId); peer = { chatType: ChatType.friend, peerUid: PeerUid };
// log('文件 msg', msg); } else {
if (msg) { peer = { chatType: ChatType.temp, peerUid: PeerUid };
// 构建下载函数 }
const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, }
cache.elementId, '', ''); if (!peer) {
// await sleep(1000); throw new Error('chattype not support');
}
// log('download result', downloadPath); const msgList = await NTQQMsgApi.getMsgsByMsgId(peer, [msgId]);
msg = await dbUtil.getMsgByLongId(cache.msgId); if (msgList.msgList.length == 0) {
// log('下载完成后的msg', msg); throw new Error('msg not found');
cache.path = downloadPath!; }
dbUtil.updateFileCache(cache).then(); const msg = msgList.msgList[0];
// log('下载完成后的msg', msg); const findEle = msg.elements.find(e => e.elementType == ElementType.VIDEO || e.elementType == ElementType.FILE || e.elementType == ElementType.PTT);
// } if (!findEle) {
throw new Error('element not found');
}
const downloadPath = await NTQQFileApi.downloadMedia(msgId, msg.chatType, msg.peerUid, findEle.elementId, '', '');
const fileSize = findEle?.videoElement?.fileSize || findEle?.fileElement?.fileSize || findEle?.pttElement?.fileSize || '0';
const fileName = findEle?.videoElement?.fileName || findEle?.fileElement?.fileName || findEle?.pttElement?.fileName || '';
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: fileSize,
file_name: fileName
};
if (enableLocalFile2Url) {
try {
res.base64 = await fs.readFile(downloadPath, 'base64');
} catch (e) {
throw new Error('文件下载失败. ' + e);
}
}
//不手动删除?文件持久化了
return res;
} }
} catch {
} }
// log('file found', cache);
const res: GetFileResponse = { const NTSearchNameResult = (await NTQQFileApi.searchfile([payload.file])).resultItems;
file: cache.path, if (NTSearchNameResult.length !== 0) {
url: cache.url, const MsgId = NTSearchNameResult[0].msgId;
file_size: cache.size.toString(), let peer: Peer | undefined = undefined;
file_name: cache.name if (NTSearchNameResult[0].chatType == ChatType.group) {
}; peer = { chatType: ChatType.group, peerUid: NTSearchNameResult[0].groupChatInfo[0].groupCode };
if (enableLocalFile2Url) { }
if (!cache.url) { if (!peer) {
throw new Error('chattype not support');
}
const msgList: RawMessage[] = (await NTQQMsgApi.getMsgsByMsgId(peer, [MsgId]))?.msgList;
if (!msgList || msgList.length == 0) {
throw new Error('msg not found');
}
const msg = msgList[0];
const file = msg.elements.filter(e => e.elementType == NTSearchNameResult[0].elemType);
if (file.length == 0) {
throw new Error('file not found');
}
const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, file[0].elementId, '', '');
const res: GetFileResponse = {
file: downloadPath,
url: downloadPath,
file_size: NTSearchNameResult[0].fileSize.toString(),
file_name: NTSearchNameResult[0].fileName
};
if (enableLocalFile2Url) {
try { try {
res.base64 = await fs.readFile(cache.path, 'base64'); res.base64 = await fs.readFile(downloadPath, 'base64');
} catch (e) { } catch (e) {
throw new Error('文件下载失败. ' + e); throw new Error('文件下载失败. ' + e);
} }
} }
//不手动删除?文件持久化了
return res;
} }
// if (autoDeleteFile) { throw new Error('file not found');
// setTimeout(() => { // let cache = await dbUtil.getFileCacheByName(payload.file);
// fs.unlink(cache.filePath) // if (!cache) {
// }, autoDeleteFileSecond * 1000) // cache = await dbUtil.getFileCacheByUuid(payload.file);
// } // }
return res; // if (!cache) {
// throw new Error('file not found');
// }
// const { enableLocalFile2Url } = ob11Config;
// try {
// await fs.access(cache.path, fs.constants.F_OK);
// } catch (e) {
// logDebug('local file not found, start download...');
// // if (cache.url) {
// // const downloadResult = await uri2local(cache.url);
// // if (downloadResult.success) {
// // cache.path = downloadResult.path;
// // dbUtil.updateFileCache(cache).then();
// // } else {
// // throw new Error('file download failed. ' + downloadResult.errMsg);
// // }
// // } else {
// // // 没有url的可能是私聊文件或者群文件需要自己下载
// // log('需要调用 NTQQ 下载文件api');
// let peer = MessageUnique.getPeerByMsgId(cache.msgId);
// let msg = await NTQQMsgApi.getMsgsByMsgId(peer?.Peer!,cache.msgId);
// // log('文件 msg', msg);
// if (msg) {
// // 构建下载函数
// const downloadPath = await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid,
// cache.elementId, '', '');
// // await sleep(1000);
// // log('download result', downloadPath);
// let peer = MessageUnique.getPeerByMsgId(cache.msgId);
// msg = await NTQQMsgApi.getMsgsByMsgId(peer?.Peer!,cache.msgId);
// // log('下载完成后的msg', msg);
// cache.path = downloadPath!;
// dbUtil.updateFileCache(cache).then();
// // log('下载完成后的msg', msg);
// // }
// }
// }
// // log('file found', cache);
// const res: GetFileResponse = {
// file: cache.path,
// url: cache.url,
// file_size: cache.size.toString(),
// file_name: cache.name
// };
// if (enableLocalFile2Url) {
// if (!cache.url) {
// try {
// res.base64 = await fs.readFile(cache.path, 'base64');
// } catch (e) {
// throw new Error('文件下载失败. ' + e);
// }
// }
// }
//return res;
} }
} }

View File

@@ -1,11 +1,10 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName, BaseCheckResult } from '../types'; import { ActionName } from '../types';
import fs from 'fs'; import fs from 'fs';
import { join as joinPath } from 'node:path'; import { join as joinPath } from 'node:path';
import { calculateFileMD5, getTempDir, httpDownload } from '@/common/utils/file'; import { calculateFileMD5, getTempDir, httpDownload } from '@/common/utils/file';
import { v4 as uuid4 } from 'uuid'; import { randomUUID } from 'crypto';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv';
interface FileResponse { interface FileResponse {
file: string; file: string;
} }
@@ -32,7 +31,7 @@ export default class GoCQHTTPDownloadFile extends BaseAction<Payload, FileRespon
PayloadSchema = SchemaData; PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<FileResponse> { protected async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name; const isRandomName = !payload.name;
const name = payload.name || uuid4(); const name = payload.name || randomUUID();
const filePath = joinPath(getTempDir(), name); const filePath = joinPath(getTempDir(), name);
if (payload.base64) { if (payload.base64) {

View File

@@ -1,11 +1,10 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types'; import { OB11ForwardMessage, OB11Message, OB11MessageData } from '../../types';
import { NTQQMsgApi } from '@/core/apis'; import { NTQQMsgApi } from '@/core/apis';
import { dbUtil } from '@/common/utils/db';
import { OB11Constructor } from '../../constructor'; import { OB11Constructor } from '../../constructor';
import { ActionName, BaseCheckResult } from '../types'; import { ActionName, BaseCheckResult } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv'; import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -29,24 +28,19 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction<Payload, any> {
if (!msgId) { if (!msgId) {
throw Error('message_id is required'); throw Error('message_id is required');
} }
let rootMsg = await dbUtil.getMsgByLongId(msgId); const rootMsgId = MessageUnique.getShortIdByMsgId(msgId);
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || parseInt(msgId));
if (!rootMsg) { if (!rootMsg) {
rootMsg = await dbUtil.getMsgByShortId(parseInt(msgId)); throw Error('msg not found');
if (!rootMsg) {
throw Error('msg not found');
}
} }
const data = await NTQQMsgApi.getMultiMsg({ const data = await NTQQMsgApi.getMultiMsg(rootMsg.Peer, rootMsg.MsgId, rootMsg.MsgId);
chatType: rootMsg.chatType,
peerUid: rootMsg.peerUid
}, rootMsg.msgId, rootMsg.msgId);
if (!data || data.result !== 0) { if (!data || data.result !== 0) {
throw Error('找不到相关的聊天记录' + data?.errMsg); throw Error('找不到相关的聊天记录' + data?.errMsg);
} }
const msgList = data.msgList; const msgList = data.msgList;
const messages = await Promise.all(msgList.map(async msg => { const messages = await Promise.all(msgList.map(async msg => {
const resMsg = await OB11Constructor.message(msg); const resMsg = await OB11Constructor.message(msg);
resMsg.message_id = await dbUtil.addMsg(msg); resMsg.message_id = await MessageUnique.createMsg({ guildId:'',chatType:msg.chatType,peerUid:msg.peerUid },msg.msgId)!;
return resMsg; return resMsg;
})); }));
messages.map(msg => { messages.map(msg => {

View File

@@ -2,11 +2,11 @@ import BaseAction from '../BaseAction';
import { OB11Message, OB11User } from '../../types'; import { OB11Message, OB11User } from '../../types';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { ChatType } from '@/core/entities'; import { ChatType } from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { NTQQMsgApi } from '@/core/apis/msg'; import { NTQQMsgApi } from '@/core/apis/msg';
import { OB11Constructor } from '../../constructor'; import { OB11Constructor } from '../../constructor';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQFriendApi, NTQQUserApi } from '@/core'; import { NTQQFriendApi, NTQQUserApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
interface Response { interface Response {
messages: OB11Message[]; messages: OB11Message[];
@@ -32,7 +32,7 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
if (!uid) { if (!uid) {
throw `记录${payload.user_id}不存在`; throw `记录${payload.user_id}不存在`;
} }
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0'; const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId || '0';
const friend = await NTQQFriendApi.isBuddy(uid); const friend = await NTQQFriendApi.isBuddy(uid);
const historyResult = (await NTQQMsgApi.getMsgHistory({ const historyResult = (await NTQQMsgApi.getMsgHistory({
chatType: friend ? ChatType.friend : ChatType.temp, chatType: friend ? ChatType.friend : ChatType.temp,
@@ -41,7 +41,7 @@ export default class GetFriendMsgHistory extends BaseAction<Payload, Response> {
//logDebug(historyResult); //logDebug(historyResult);
const msgList = historyResult.msgList; const msgList = historyResult.msgList;
await Promise.all(msgList.map(async msg => { await Promise.all(msgList.map(async msg => {
msg.id = await dbUtil.addMsg(msg); msg.id = await MessageUnique.createMsg({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
})); }));
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg))); const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
return { 'messages': ob11MsgList }; return { 'messages': ob11MsgList };

View File

@@ -2,12 +2,11 @@ import BaseAction from '../BaseAction';
import { OB11Message, OB11User } from '../../types'; import { OB11Message, OB11User } from '../../types';
import { getGroup, groups } from '@/core/data'; import { getGroup, groups } from '@/core/data';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { ChatType } from '@/core/entities'; import { ChatType, RawMessage } from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { NTQQMsgApi } from '@/core/apis/msg'; import { NTQQMsgApi } from '@/core/apis/msg';
import { OB11Constructor } from '../../constructor'; import { OB11Constructor } from '../../constructor';
import { logDebug } from '@/common/utils/log';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
interface Response { interface Response {
messages: OB11Message[]; messages: OB11Message[];
} }
@@ -15,11 +14,11 @@ interface Response {
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
properties: { properties: {
group_id: { type: [ 'number' , 'string' ] }, group_id: { type: ['number', 'string'] },
message_seq: { type: 'number' }, message_seq: { type: 'number' },
count: { type: 'number' } count: { type: 'number' }
}, },
required: ['group_id', 'message_seq', 'count'] required: ['group_id']
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = FromSchema<typeof SchemaData>;
@@ -32,16 +31,20 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
if (!group) { if (!group) {
throw `${payload.group_id}不存在`; throw `${payload.group_id}不存在`;
} }
const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || '0'; let targetMsgShortId, count = parseInt(payload.count?.toString() ?? '20');
// log("startMsgId", startMsgId) const peer = {
const historyResult = (await NTQQMsgApi.getMsgHistory({
chatType: ChatType.group, chatType: ChatType.group,
peerUid: group.groupCode peerUid: group.groupCode
}, startMsgId, parseInt(payload.count?.toString()) || 20)); };
//logDebug(historyResult); let msgList: RawMessage[];
const msgList = historyResult.msgList; if (!payload.message_seq) {
msgList = (await NTQQMsgApi.getLastestMsgByUids(peer, count)).msgList;
} else {
const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(targetMsgShortId ?? (payload.message_seq ?? 0)))?.MsgId || '0';
msgList = (await NTQQMsgApi.getMsgHistory(peer, startMsgId, count)).msgList;
}
await Promise.all(msgList.map(async msg => { await Promise.all(msgList.map(async msg => {
msg.id = await dbUtil.addMsg(msg); msg.id = await MessageUnique.createMsg({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
})); }));
const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg))); const ob11MsgList = await Promise.all(msgList.map(msg => OB11Constructor.message(msg)));
return { 'messages': ob11MsgList }; return { 'messages': ob11MsgList };

View File

@@ -3,6 +3,7 @@ import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { JSONSchema } from 'json-schema-to-ts'; import { JSONSchema } from 'json-schema-to-ts';
import { NTQQSystemApi } from '@/core'; import { NTQQSystemApi } from '@/core';
import { sleep } from '@/common/utils/helper';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -15,7 +16,8 @@ export class GetOnlineClient extends BaseAction<void, Array<any>> {
actionName = ActionName.GetOnlineClient; actionName = ActionName.GetOnlineClient;
protected async _handle(payload: void) { protected async _handle(payload: void) {
//console.log(await NTQQSystemApi.getOnlineDev()); NTQQSystemApi.getOnlineDev();
await sleep(500);
return DeviceList; return DeviceList;
} }
} }

View File

@@ -5,6 +5,7 @@ import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis/user'; import { NTQQUserApi } from '@/core/apis/user';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { calcQQLevel } from '@/common/utils/qqlevel'; import { calcQQLevel } from '@/common/utils/qqlevel';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -20,24 +21,46 @@ export default class GoCQHTTPGetStrangerInfo extends BaseAction<Payload, OB11Use
actionName = ActionName.GoCQHTTP_GetStrangerInfo; actionName = ActionName.GoCQHTTP_GetStrangerInfo;
protected async _handle(payload: Payload): Promise<OB11User> { protected async _handle(payload: Payload): Promise<OB11User> {
const user_id = payload.user_id.toString(); if (!requireMinNTQQBuild('26702')) {
const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id); const user_id = payload.user_id.toString();
const uid = (await NTQQUserApi.getUidByUin(user_id))!; const extendData = await NTQQUserApi.getUserDetailInfoByUin(user_id);
if (!uid || uid.indexOf('*') != -1) { const uid = (await NTQQUserApi.getUidByUin(user_id))!;
const ret = { if (!uid || uid.indexOf('*') != -1) {
...extendData, const ret = {
user_id: parseInt(extendData.info.uin) || 0, ...extendData,
nickname: extendData.info.nick, user_id: parseInt(extendData.info.uin) || 0,
sex: OB11UserSex.unknown, nickname: extendData.info.nick,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year, sex: OB11UserSex.unknown,
qid: extendData.info.qid, age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0, qid: extendData.info.qid,
login_days: 0, level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
uid: '' login_days: 0,
}; uid: ''
return ret; };
return ret;
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) };
return OB11Constructor.stranger(data);
} else {
const user_id = payload.user_id.toString();
const extendData = await NTQQUserApi.getUserDetailInfoByUinV2(user_id);
//console.log(extendData);
const uid = (await NTQQUserApi.getUidByUin(user_id))!;
if (!uid || uid.indexOf('*') != -1) {
const ret = {
...extendData,
user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown,
age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0,
uid: ''
};
return ret;
}
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) };
return OB11Constructor.stranger(data);
} }
const data = { ...extendData, ...(await NTQQUserApi.getUserDetailInfo(uid)) };
return OB11Constructor.stranger(data);
} }
} }

View File

@@ -0,0 +1,51 @@
import BaseAction from '../BaseAction';
import { getGroup } from '@/core/data';
import { ActionName } from '../types';
import { SendMsgElementConstructor } from '@/core/entities/constructor';
import { ChatType, Peer, SendFileElement } from '@/core/entities';
import fs from 'fs';
import { SendMsg, sendMsg } from '@/onebot11/action/msg/SendMsg';
import { uri2local } from '@/common/utils/file';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQFriendApi, NTQQUserApi } from '@/core';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
file: { type: 'string' },
name: { type: 'string' }
},
required: ['user_id', 'file', 'name']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPUploadPrivateFile extends BaseAction<Payload, null> {
actionName = ActionName.GOCQHTTP_UploadPrivateFile;
PayloadSchema = SchemaData;
async getPeer(payload: Payload): Promise<Peer> {
if (payload.user_id) {
const peerUid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
if (!peerUid) {
throw `私聊${payload.user_id}不存在`;
}
const isBuddy = await NTQQFriendApi.isBuddy(peerUid);
return { chatType: isBuddy ? ChatType.friend : ChatType.temp, peerUid };
}
throw '缺少参数 user_id';
}
protected async _handle(payload: Payload): Promise<null> {
const peer = await this.getPeer(payload);
let file = payload.file;
if (fs.existsSync(file)) {
file = `file://${file}`;
}
const downloadResult = await uri2local(file);
if (downloadResult.errMsg) {
throw new Error(downloadResult.errMsg);
}
const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(downloadResult.path, payload.name);
await sendMsg(peer, [sendFileEle], [], true);
return null;
}
}

View File

@@ -0,0 +1,31 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQGroupApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class DelEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.DelEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.removeGroupEssence(
msg.Peer.peerUid,
msg.MsgId
);
}
}

View File

@@ -2,10 +2,8 @@ import { OB11Group } from '../../types';
import { OB11Constructor } from '../../constructor'; import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { groups } from '@/core/data';
import { NTQQGroupApi } from '@/core/apis'; import { NTQQGroupApi } from '@/core/apis';
import { Group } from '@/core/entities'; import { Group } from '@/core/entities';
import { log } from '@/common/utils/log';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
// no_cache get时传字符串 // no_cache get时传字符串
const SchemaData = { const SchemaData = {
@@ -21,11 +19,7 @@ class GetGroupList extends BaseAction<Payload, OB11Group[]> {
actionName = ActionName.GetGroupList; actionName = ActionName.GetGroupList;
PayloadSchema = SchemaData; PayloadSchema = SchemaData;
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
let groupList: Group[] = Array.from(groups.values()); let groupList: Group[] = await NTQQGroupApi.getGroups(payload?.no_cache === true || payload.no_cache === 'true');
if (groupList.length === 0 || payload?.no_cache === true || payload.no_cache === 'true') {
groupList = await NTQQGroupApi.getGroups(true);
// log('get groups', groups);
}
return OB11Constructor.groups(groupList); return OB11Constructor.groups(groupList);
} }
} }

View File

@@ -1,16 +1,14 @@
import { OB11GroupMember } from '../../types'; import { OB11GroupMember } from '../../types';
import { getGroup, getGroupMember, groupMembers } from '@/core/data';
import { OB11Constructor } from '../../constructor'; import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis/user'; import { NTQQUserApi } from '@/core/apis/user';
import { log, logDebug } from '@/common/utils/log'; import { logDebug } from '@/common/utils/log';
import { isNull } from '../../../common/utils/helper';
import { WebApi } from '@/core/apis/webapi'; import { WebApi } from '@/core/apis/webapi';
import { NTQQGroupApi } from '@/core'; import { NTQQGroupApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { getGroupMember, selfInfo } from '@/core/data';
// no_cache get时传字符串 import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
properties: { properties: {
@@ -27,39 +25,55 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo; actionName = ActionName.GetGroupMemberInfo;
PayloadSchema = SchemaData; PayloadSchema = SchemaData;
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()); const isNocache = payload.no_cache == true || payload.no_cache === 'true';
if (!group) { let uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
throw (`群(${payload.group_id})不存在`); if (!uid) {
throw (`Uin2Uid Error ${payload.user_id}不存在`);
} }
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()); let member = await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), uid, isNocache);
if (payload.no_cache == true || payload.no_cache === 'true') { if (!member) {
groupMembers.set(group.groupCode, await NTQQGroupApi.getGroupMembers(payload.group_id.toString()));
}
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString());
// log(member);
if (member) {
logDebug('获取群成员详细信息');
try {
const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
logDebug('群成员详细信息结果', info);
Object.assign(member, info);
} catch (e) {
logDebug('获取群成员详细信息失败, 只能返回基础信息', e);
}
const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member);
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) {
retMember.join_time = webGroupMembers[i]?.join_time;
retMember.last_sent_time = webGroupMembers[i]?.last_speak_time;
retMember.qage = webGroupMembers[i]?.qage;
retMember.level = webGroupMembers[i]?.lv.level.toString();
}
}
return retMember;
} else {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`); throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
} }
try {
const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
logDebug('群成员详细信息结果', info);
Object.assign(member, info);
} catch (e) {
logDebug('获取群成员详细信息失败, 只能返回基础信息', e);
}
const date = Math.round(Date.now() / 1000);
const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member);
if (!requireMinNTQQBuild('26702')) {
let SelfInfoInGroup = await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), selfInfo.uid, isNocache);
let isPrivilege = false;
if (SelfInfoInGroup) {
isPrivilege = SelfInfoInGroup.role === 3 || SelfInfoInGroup.role === 4;
}
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) {
retMember.join_time = webGroupMembers[i]?.join_time;
retMember.last_sent_time = webGroupMembers[i]?.last_speak_time;
retMember.qage = webGroupMembers[i]?.qage;
retMember.level = webGroupMembers[i]?.lv.level.toString();
}
}
} else {
const LastestMsgList = await NTQQGroupApi.getLastestMsg(payload.group_id.toString(), [payload.user_id.toString()]);
if (LastestMsgList?.msgList?.length && LastestMsgList?.msgList?.length > 0) {
const last_send_time = LastestMsgList.msgList[0].msgTime;
if (last_send_time && last_send_time != '0' && last_send_time != '') {
retMember.last_sent_time = parseInt(last_send_time);
retMember.join_time = Math.round(Date.now() / 1000);//兜底数据 防止群管乱杀
}
}
}
} else {
retMember.last_sent_time = parseInt((await getGroupMember(payload.group_id.toString(), retMember.user_id))?.lastSpeakTime || date.toString());
retMember.join_time = parseInt((await getGroupMember(payload.group_id.toString(), retMember.user_id))?.joinTime || date.toString());
}
return retMember;
} }
} }
export default GetGroupMemberInfo;
export default GetGroupMemberInfo;

View File

@@ -0,0 +1,84 @@
import { OB11GroupMember } from '../../types';
import { getGroup, getGroupMember, groupMembers, selfInfo } from '@/core/data';
import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core/apis/user';
import { logDebug } from '@/common/utils/log';
import { WebApi } from '@/core/apis/webapi';
import { NTQQGroupApi } from '@/core';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
// no_cache get时传字符串
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] },
no_cache: { type: ['boolean', 'string'] },
},
required: ['group_id', 'user_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
actionName = ActionName.GetGroupMemberInfo;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload) {
if (requireMinNTQQBuild('26702')) {
let V2Data = await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), payload.user_id.toString(), payload.no_cache == true || payload.no_cache === 'true');
if (V2Data) {
return OB11Constructor.groupMember(payload.group_id.toString(), V2Data);
} else {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
}
}
const group = await getGroup(payload.group_id.toString());
const role = (await getGroupMember(payload.group_id, selfInfo.uin))?.role;
const isPrivilege = role === 3 || role === 4;
if (!group) {
throw (`群(${payload.group_id})不存在`);
}
if (payload.no_cache == true || payload.no_cache === 'true') {
groupMembers.set(group.groupCode, await NTQQGroupApi.getGroupMembers(payload.group_id.toString()));
}
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString());
//早返回
if (!member) {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
}
//console.log('GetGroupMemberInfo', JSON.stringify(await NTQQGroupApi.getGroupMemberV2(payload.group_id.toString(), member.uid, true), null, 4));
try {
const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
logDebug('群成员详细信息结果', info);
Object.assign(member, info);
} catch (e) {
logDebug('获取群成员详细信息失败, 只能返回基础信息', e);
}
const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member);
if (isPrivilege) {
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) {
retMember.join_time = webGroupMembers[i]?.join_time;
retMember.last_sent_time = webGroupMembers[i]?.last_speak_time;
retMember.qage = webGroupMembers[i]?.qage;
retMember.level = webGroupMembers[i]?.lv.level.toString();
}
}
} else {
const LastestMsgList = await NTQQGroupApi.getLastestMsg(payload.group_id.toString(), [payload.user_id.toString()]);
if (LastestMsgList?.msgList?.length && LastestMsgList?.msgList?.length > 0) {
const last_send_time = LastestMsgList.msgList[0].msgTime;
if (last_send_time && last_send_time != '0' && last_send_time != '') {
retMember.last_sent_time = parseInt(last_send_time);
retMember.join_time = Math.round(Date.now() / 1000);//兜底数据 防止群管乱杀
}
}
}
return retMember;
}
}
export default GetGroupMemberInfo;

View File

@@ -1,15 +1,12 @@
import { getGroup, getGroupMember, groupMembers, selfInfo } from '@/core/data'; import { getGroup, getGroupMember, selfInfo } from '@/core/data';
import { OB11GroupMember } from '../../types'; import { OB11GroupMember } from '../../types';
import { OB11Constructor } from '../../constructor'; import { OB11Constructor } from '../../constructor';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { napCatCore, NTQQGroupApi, NTQQUserApi } from '@/core'; import { NTQQGroupApi } from '@/core';
import { WebApi } from '@/core/apis/webapi'; import { WebApi } from '@/core/apis/webapi';
import { logDebug } from '@/common/utils/log';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { ob11Config } from '@/onebot11/config'; import { requireMinNTQQBuild } from '@/common/utils/QQBasicInfo';
import { dbUtil } from '@/common/utils/db';
import { TypeConvert } from '@/common/utils/type';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -26,51 +23,67 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
actionName = ActionName.GetGroupMemberList; actionName = ActionName.GetGroupMemberList;
PayloadSchema = SchemaData; PayloadSchema = SchemaData;
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const role = (await getGroupMember(payload.group_id, selfInfo.uin))?.role; const isNocache = payload.no_cache == true || payload.no_cache === 'true';
const group = await getGroup(payload.group_id.toString()); const GroupList = await NTQQGroupApi.getGroups(isNocache);
const group = GroupList.find(item => item.groupCode == payload.group_id);
if (!group) { if (!group) {
throw (`${payload.group_id}不存在`); throw (`${payload.group_id}不存在`);
} }
let groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString());
// 从Data里面获取 let _groupMembers = Array.from(groupMembers.values())
let _groupMembers: OB11GroupMember[] = OB11Constructor.groupMembers(group); .map(item => { return OB11Constructor.groupMember(group.groupCode, item); });
if (payload.no_cache == true || payload.no_cache === 'true') {
// webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());'
const _groupMembers = await NTQQGroupApi.getGroupMembers(payload.group_id.toString());
groupMembers.set(group.groupCode, _groupMembers);
}
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>(); const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>();
// 转为Map 方便索引 // 转为Map 方便索引
const date = Math.round(Date.now() / 1000);
for (let i = 0, len = _groupMembers.length; i < len; i++) { for (let i = 0, len = _groupMembers.length; i < len; i++) {
// 保证基础数据有这个 同时避免群管插件过于依赖这个杀了
_groupMembers[i].join_time = date;
_groupMembers[i].last_sent_time = date;
MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]); MemberMap.set(_groupMembers[i].user_id, _groupMembers[i]);
} }
const isPrivilege = role === 3 || role === 4; if (!requireMinNTQQBuild('26702')) {
if (isPrivilege) { const selfRole = groupMembers.get(selfInfo.uid)?.role;
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString()); const isPrivilege = selfRole === 3 || selfRole === 4;
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) { if (isPrivilege) {
continue; const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (!webGroupMembers[i]?.uin) {
continue;
}
const MemberData = MemberMap.get(webGroupMembers[i]?.uin);
if (MemberData) {
MemberData.join_time = webGroupMembers[i]?.join_time;
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time;
MemberData.qage = webGroupMembers[i]?.qage;
MemberData.level = webGroupMembers[i]?.lv.level.toString();
MemberMap.set(webGroupMembers[i]?.uin, MemberData);
}
} }
const MemberData = MemberMap.get(webGroupMembers[i]?.uin); } else {
if (MemberData) { if (isNocache) {
MemberData.join_time = webGroupMembers[i]?.join_time; const DateMap = await NTQQGroupApi.getGroupMemberLastestSendTimeCache(payload.group_id.toString());//开始从本地拉取
MemberData.last_sent_time = webGroupMembers[i]?.last_speak_time; for (const DateUin of DateMap.keys()) {
MemberData.qage = webGroupMembers[i]?.qage; const MemberData = MemberMap.get(parseInt(DateUin));
MemberData.level = webGroupMembers[i]?.lv.level.toString(); if (MemberData) {
MemberMap.set(webGroupMembers[i]?.uin, MemberData); MemberData.last_sent_time = parseInt(DateMap.get(DateUin)!);
//join_time 有基础数据兜底
}
}
} else {
_groupMembers.forEach(item => {
item.last_sent_time = date;
item.join_time = date;
});
} }
} }
} else if (ob11Config.GroupLocalTime.Record && ob11Config.GroupLocalTime.RecordList[0] === '-1' || ob11Config.GroupLocalTime.RecordList.includes(payload.group_id.toString())) { } else {
const _sendAndJoinRember = await dbUtil.getLastSentTimeAndJoinTime(TypeConvert.toNumber(payload.group_id)); _groupMembers.forEach(async item => {
_sendAndJoinRember.forEach((element) => { item.last_sent_time = parseInt((await getGroupMember(payload.group_id.toString(), item.user_id))?.lastSpeakTime || date.toString());
const MemberData = MemberMap.get(element.user_id); item.join_time = parseInt((await getGroupMember(payload.group_id.toString(), item.user_id))?.joinTime || date.toString());
if (MemberData) {
MemberData.join_time = element.join_time;
MemberData.last_sent_time = element.last_sent_time;
}
}); });
} }
// 还原索引到Array 一同返回 // 还原索引到Array 一同返回
@@ -82,7 +95,7 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
// } // }
// _groupMembers = Array.from(retData); // _groupMembers = Array.from(retData);
_groupMembers = Array.from(MemberMap.values()); _groupMembers = Array.from(MemberMap.values());
return _groupMembers; return _groupMembers;
} }

View File

@@ -0,0 +1,30 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { NTQQGroupApi, NTQQMsgApi } from '@/core';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = {
type: 'object',
properties: {
message_id: { type: ['number', 'string'] }
},
required: ['message_id']
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class SetEssenceMsg extends BaseAction<Payload, any> {
actionName = ActionName.SetEssenceMsg;
PayloadSchema = SchemaData;
protected async _handle(payload: Payload): Promise<any> {
const msg = await MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) {
throw new Error('msg not found');
}
return await NTQQGroupApi.addGroupEssence(
msg.Peer.peerUid,
msg.MsgId
);
}
}

View File

@@ -2,7 +2,6 @@ import BaseAction from '../BaseAction';
import { GroupRequestOperateTypes } from '@/core/entities'; import { GroupRequestOperateTypes } from '@/core/entities';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { NTQQGroupApi } from '@/core/apis/group'; import { NTQQGroupApi } from '@/core/apis/group';
import { groupNotifies } from '@/core/data';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = { const SchemaData = {
@@ -10,7 +9,7 @@ const SchemaData = {
properties: { properties: {
flag: { type: 'string' }, flag: { type: 'string' },
approve: { type: ['string', 'boolean'] }, approve: { type: ['string', 'boolean'] },
reason: { type: 'string' } reason: { type: 'string', nullable: true, }
}, },
required: ['flag'], required: ['flag'],
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
@@ -23,11 +22,7 @@ export default class SetGroupAddRequest extends BaseAction<Payload, null> {
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const flag = payload.flag.toString(); const flag = payload.flag.toString();
const approve = payload.approve?.toString() !== 'false'; const approve = payload.approve?.toString() !== 'false';
const notify = groupNotifies[flag]; await NTQQGroupApi.handleGroupRequest(flag,
if (!notify) {
throw `${flag}对应的加群通知不存在`;
}
await NTQQGroupApi.handleGroupRequest(notify,
approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject,
payload.reason payload.reason
); );

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