Compare commits

...

146 Commits

Author SHA1 Message Date
手瓜一十雪
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
手瓜一十雪
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
手瓜一十雪
914136b750 refactor: 移除异常代码 2024-07-10 21:39:03 +08:00
手瓜一十雪
f9a60795f5 feat: uid转换优化 2024-07-10 21:33:31 +08:00
手瓜一十雪
19640927c7 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-07-10 21:11:43 +08:00
手瓜一十雪
22faac7e36 fix: friend uid 异常 2024-07-10 21:11:28 +08:00
手瓜一十雪
30d260ab32 Merge pull request #111 from idranme/main
fix: error catch
2024-07-10 11:55:53 +08:00
idranme
115120d066 Update file.ts 2024-07-10 11:35:55 +08:00
idranme
1327844736 fix: error catch 2024-07-10 03:25:25 +00:00
手瓜一十雪
29904f3cb7 feat: 164 way03启动脚本补充 2024-07-06 13:23:31 +08:00
手瓜一十雪
50395594b7 Merge pull request #106 from jetjinser/fix-editorconfig
fix: `.editorconfig` wrong pair `end_of_line`
2024-07-05 22:43:23 +08:00
Jinser Kafka
9360af88b3 fix: .editorconfig 2024-07-05 19:36:32 +08:00
手瓜一十雪
376370336c release: 1.6.5 2024-07-05 16:50:57 +08:00
手瓜一十雪
70df6e3302 Merge pull request #105 from po-lan/main
对缓存小优化
2024-07-05 16:49:30 +08:00
手瓜一十雪
0a1fc2dc12 feat: 1.6.5 2024-07-05 16:49:16 +08:00
手瓜一十雪
9857f6e437 feat: 优化载入流程 2024-07-05 16:47:08 +08:00
手瓜一十雪
56d6ebe916 refactor: 迁移到新库 2024-07-05 15:48:03 +08:00
po-lan
81134ea2d4 Update LRUCache.ts 2024-07-05 12:13:24 +08:00
po-lan
a9f3e7fc54 Update db.ts
通过读取缓存修复刚说话缺无法获取发言时间的问题
2024-07-05 12:12:40 +08:00
po-lan
eb84e2f8c9 Update LRUCache.ts
Add a get function to the cache
2024-07-05 12:09:59 +08:00
121 changed files with 2459 additions and 1869 deletions

View File

@@ -1,21 +1,21 @@
# EditorConfig is awesome: https://EditorConfig.org # EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file # top-most EditorConfig file
root = true root = true
# Unix-style newlines with a newline ending every file # Unix-style newlines with a newline ending every file
[*] [*]
end_of_line = lf|crlf end_of_line = lf
insert_final_newline = true insert_final_newline = true
# Matches multiple files with brace expansion notation # Matches multiple files with brace expansion notation
# Set default charset # Set default charset
charset = utf-8 charset = utf-8
# 2 space indentation # 2 space indentation
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}] [*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly. # Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
# You'll need to rely on your linter/formatter like ESLint or Prettier for that. # You'll need to rely on your linter/formatter like ESLint or Prettier for that.

3
.gitignore vendored
View File

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

View File

@@ -0,0 +1,20 @@
# v1.6.8
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. 移除数据库文件读写 ~ 优化性能
2. 重构消息发送 极限速度优化 ~ 优化性能
3. WebUi配置热重载优化 ~ 修复问题
4. 修复偶现崩溃问题 ~ 修复问题
## 新增与调整
1. 最后发言时间重构 入群时间失效 ~ 替换功能
新增的 API 详细见[API文档](https://napneko.github.io/zh-CN/develop/extends_api)

View File

@@ -0,0 +1,18 @@
# v1.6.5
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. 优化了WrapperNative载入代码
2. 优化缓存
## 新增与调整
没有哦
新增的 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.4", "version": "1.6.8",
"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/jest": "^29.5.12",
"@types/node": "^20.11.30", "@types/node": "^20.11.30",
"@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"
} }
} }

28
script/NapCat.164.bat Normal file
View File

@@ -0,0 +1,28 @@
@echo off
chcp 65001
:: 检查是否有管理员权限
net session >nul 2>&1
if %errorlevel% neq 0 (
echo 请求管理员权限...
powershell -Command "Start-Process '%~f0' -Verb runAs"
exit /b
)
:: 如果有管理员权限,继续执行
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!QQ.exe"
echo !QQPath!
"!QQPath!" --enable-logging %*
pause

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

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

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();

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

@@ -1,4 +1,3 @@
import { NodeIKernelMsgListener } from '@/core';
import { NodeIQQNTWrapperSession } from '@/core/wrapper'; import { NodeIQQNTWrapperSession } from '@/core/wrapper';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
@@ -6,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 {
@@ -84,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(ListenerMainName, ListenerSubName, ...args,this.EventTask.get(ListenerMainName)?.get(ListenerSubName));
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>>(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;
@@ -108,14 +110,15 @@ 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 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('NTEvent EventName:' + EventName + ' ListenerName:' + ListenerName + ' timeout'));
} else { } else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]); resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
@@ -129,6 +132,7 @@ export class NTEventWrapper {
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);

View File

@@ -1,145 +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 = 2e4, 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);
}
}
}
}
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> { 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,7 +46,15 @@ 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);
@@ -36,18 +64,53 @@ class LimitedHashTable<K, V> {
} }
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) {
return this.msgIdMap.getValue(ShortId); createMsg(peer: Peer, msgId: string): number | undefined {
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
const hash = crypto.createHash('sha1').update(key);
const shortId = parseInt(hash.digest('hex').slice(0, 8), 16);
const isExist = this.msgIdMap.getKey(shortId);
//console.log(`${peer.peerUid} ${msgId} ------- ${shortId}`);
if (isExist && isExist === msgId) {
return shortId;
}
this.msgIdMap.set(msgId, shortId);
this.msgDataMap.set(key, shortId);
return shortId;
} }
getShortIdByMsgId(MsgId: string) {
return this.msgIdMap.getKey(MsgId); 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

@@ -38,11 +38,11 @@ type QQVersionConfigInfo = {
} }
let _qqVersionConfigInfo: QQVersionConfigInfo = { let _qqVersionConfigInfo: QQVersionConfigInfo = {
'baseVersion': '9.9.11-24568', 'baseVersion': '9.9.12-25765',
'curVersion': '9.9.11-24568', 'curVersion': '9.9.12-25765',
'prevVersion': '', 'prevVersion': '',
'onErrorVersions': [], 'onErrorVersions': [],
'buildId': '24568' 'buildId': '25765'
}; };
if (fs.existsSync(configVersionInfoPath)) { if (fs.existsSync(configVersionInfoPath)) {
@@ -55,23 +55,23 @@ if (fs.existsSync(configVersionInfoPath)) {
} }
export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo; export const qqVersionConfigInfo: QQVersionConfigInfo = _qqVersionConfigInfo;
//V1_WIN_NQ_9.9.11_24568_GW_B //V1_WIN_NQ_9.9.12_25765_GW_B
export const qqPkgInfo: QQPkgInfo = JSON.parse(fs.readFileSync(pkgInfoPath).toString()); 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 let _appid: string = '537234702'; // 默认为 Windows 平台的 appid
if (systemPlatform === 'linux') { if (systemPlatform === 'linux') {
_appid = '537226441'; _appid = '537234773';
} }
// todo: mac 平台的 appid // todo: mac 平台的 appid
export const appid = _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,449 +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[]) => {
if (err) {
logError('查询发言时间失败', groupId);
return resolve([]);
}
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 = () => {
@@ -160,7 +159,12 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
} }
} }
} }
const fetchRes = await fetch(url, { headers }); const fetchRes = await fetch(url, { headers }).catch((err) => {
if (err.cause) {
throw err.cause;
}
throw err;
});
if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`); if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`);
const blob = await fetchRes.blob(); const blob = await fetchRes.blob();
@@ -187,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;
@@ -230,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();
@@ -246,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

@@ -12,6 +12,25 @@ 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 +108,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 }>();

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

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,6 @@
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, RawMessage
@@ -15,31 +12,13 @@ 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';
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);
@@ -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,79 @@ export class NTQQFileApi {
}); });
}); });
} }
static async searchfile(keys: string[]) {
static async getImageUrl(element: { originImageUrl: any; md5HexStr?: any; fileUuid: any; }, isPrivateImage: boolean) { 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 [id, data] = await NTEventDispatch.CallNormalEvent<EventType, (params: OnListener) => void>(
'NodeIKernelSearchService/searchFileWithKeywords',
'NodeIKernelSearchListener/onSearchFileKeywordsResult',
1,
10000,
(arg): boolean => { return id == data.searchId },
keys,
12
);
return data.resultItems[0];
}
static async getImageUrl(element: { originImageUrl: any; md5HexStr?: any; fileUuid: any; }) {
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

@@ -14,6 +14,7 @@ export class NTQQFriendApi {
'NodeIKernelBuddyListener/onBuddyListChange', 'NodeIKernelBuddyListener/onBuddyListChange',
1, 1,
5000, 5000,
() => true,
forced forced
); );
const friends: User[] = []; const friends: User[] = [];

View File

@@ -1,12 +1,13 @@
import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType } from '../entities'; import { GroupMember, GroupRequestOperateTypes, GroupMemberRole, GroupNotify, Group, MemberExtSourceType, GroupNotifyTypes, ChatType } from '../entities';
import { GeneralCallResult, NTQQUserApi, napCatCore } from '@/core'; import { GeneralCallResult, NTQQUserApi, 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) {
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>, (updateType: number, groupList: Group[]) => void>
@@ -15,10 +16,99 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onGroupListUpdate', 'NodeIKernelGroupListener/onGroupListUpdate',
1, 1,
5000, 5000,
() => true,
forced forced
); );
return groupList; return groupList;
} }
@CacheClassFuncAsyncExtend(600, "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 getLastestMsg(GroupCode: string, uins: string[]) {
let uids: Array<string> = [];
for (let uin of uins) {
try {
let uid = await NTQQUserApi.getUidByUin(uin)
if (uid) {
uids.push(uid);
}
} catch (error) {
log("getLastestMsg--->", error);
return undefined;
}
}
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 +121,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,6 +153,7 @@ export class NTQQGroupApi {
'NodeIKernelGroupListener/onGroupSingleScreenNotifies', 'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
1, 1,
5000, 5000,
() => true,
false, false,
'', '',
num num
@@ -87,16 +202,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,56 @@
import { GetFileListParam, Peer, RawMessage, SendMessageElement } from '@/core/entities'; import { GetFileListParam, Peer, RawMessage, SendMessageElement } 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';
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) / 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[]; }>> = [];
MessageUnique.resize(predict * 50);
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 * 50} 条历史消息记录完成`);
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);
loadMessageUnique().then().catch();
}); });
}, 100); }, 100);
export class NTQQMsgApi { export class NTQQMsgApi {
// 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
@@ -97,135 +70,121 @@ export class NTQQMsgApi {
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/sendMsg',
} '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 sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
static async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000): Promise<RawMessage> { let msgId = await NTQQMsgApi.getMsgUnique(await NTQQMsgApi.getServerTime());
const peerUid = peer.peerUid; let data = await NTEventDispatch.CallNormalEvent<
// 等待上一个相同的peer发送完 (msgId: string, peer: Peer, msgElements: SendMessageElement[], map: Map<any, any>) => Promise<unknown>,
let checkLastSendUsingTime = 0; (msgList: RawMessage[]) => void
const waitLastSend: () => Promise<void> = async () => { >(
if (checkLastSendUsingTime > timeout) { 'NodeIKernelMsgService/sendMsg',
throw ('发送超时'); 'NodeIKernelMsgListener/onMsgInfoListUpdate',
} 1,
const lastSending = sendMessagePool[peer.peerUid]; timeout,
if (lastSending) { (msgRecords: RawMessage[]) => {
// log("有正在发送的消息,等待中...") for (let msgRecord of msgRecords) {
await sleep(500); if (msgRecord.msgId === msgId && msgRecord.sendStatus === 2) {
checkLastSendUsingTime += 500;
return await waitLastSend();
} else {
return;
}
};
await waitLastSend();
return new Promise((resolve, reject) => {
let completed = false;
let sentMessage: RawMessage | null = null;
const sendSuccessCBId = randomUUID() as string;
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 async getMsgUniqueEx(){
let msgId = await NTQQMsgApi.getMsgUnique(await NTQQMsgApi.getServerTime());
return msgId;
}
static async getMsgUnique(time: string) {
return napCatCore.session.getMsgService().getMsgUniqueId(time);
}
static async getServerTime() {
return napCatCore.session.getMSFService().getServerTime();
} }
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

@@ -16,7 +16,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 +33,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,28 @@
import { ModifyProfileParams, SelfInfo, User, UserDetailInfoByUin } from '@/core/entities'; import { ModifyProfileParams, SelfInfo, User, UserDetailInfoByUin } from '@/core/entities';
import { selfInfo } from '@/core/data'; import { friends, selfInfo } from '@/core/data';
import { CacheClassFuncAsync } from '@/common/utils/helper'; import { CacheClassFuncAsync, CacheClassFuncAsyncExtend } from '@/common/utils/helper';
import { GeneralCallResult, napCatCore } from '@/core'; import { napCatCore } 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 } from '@/core/services';
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,6 +46,9 @@ 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) {
return napCatCore.session.getGroupService().setHeader(gc, filePath);
}
static async getSelfInfo() { static async getSelfInfo() {
@@ -73,45 +64,26 @@ export class NTQQUserApi {
// KQZONE, // KQZONE,
// KOTHER // KOTHER
// } // }
static async getUserDetailInfo(uid: string): Promise<User> { static async getUserDetailInfo(uid: string) {
// const existUser = userInfoCache[uid]; type EventService = NodeIKernelProfileService['getUserDetailInfoWithBizInfo'];
// if (existUser) { type EventListener = NodeIKernelProfileListener['onProfileDetailInfoChanged'];
// return existUser; let [_retData, profile] = await NTEventDispatch.CallNormalEvent
// } <EventService, EventListener>
const profileService = napCatCore.session.getProfileService(); (
// console.log('getUserDetailInfo', result); 'NodeIKernelProfileService/getUserDetailInfoWithBizInfo',
return new Promise((resolve, reject) => { 'NodeIKernelProfileListener/onProfileDetailInfoChanged',
const uuid = randomUUID(); 2,
let completed = false; 5000,
let retData: User | undefined = undefined; (profile: User) => {
let isFirst = true; if (profile.uid === uid) {
// 不管返回几次 超时有数据就该返回 兼容就好了 return true;
setTimeout(() => {
if (!completed) {
if (retData) {
resolve(retData);
} else {
reject('getUserDetailInfo timeout');
} }
} return false;
userDetailHandlers.delete(uuid); },
}, 5000); uid,
userDetailHandlers.set(uuid, (profile) => { [0]
if (profile.uid === uid) { );
if (isFirst) { return profile;
retData = profile;
isFirst = false;
// console.log('getUserDetailInfo', profile);
} else {
completed = true;
resolve(profile);
}
}
});
profileService.getUserDetailInfoWithBizInfo(uid, [0]).then(result => {
// console.log('getUserDetailInfo', result);
});
});
} }
static async modifySelfProfile(param: ModifyProfileParams) { static async modifySelfProfile(param: ModifyProfileParams) {
return napCatCore.session.getProfileService().modifyDesktopMiniProfile(param); return napCatCore.session.getProfileService().modifyDesktopMiniProfile(param);
@@ -163,6 +135,13 @@ export class NTQQUserApi {
} }
return skey; return skey;
} }
@CacheClassFuncAsyncExtend(3600, 'Uin2Uid', (Uin: string, Uid: string | undefined) => {
if (Uid && Uid.indexOf('u_') != -1) {
return true
}
logWarn("uin转换到uid时异常", Uin);
return false;
})
static async getUidByUin(Uin: string) { static async getUidByUin(Uin: string) {
let ret = await NTEventDispatch.CallNoListenerEvent let ret = await NTEventDispatch.CallNoListenerEvent
<(Uin: string[]) => Promise<{ uidInfo: Map<string, string> }>>( <(Uin: string[]) => Promise<{ uidInfo: Map<string, string> }>>(
@@ -170,8 +149,39 @@ export class NTQQUserApi {
5000, 5000,
[Uin] [Uin]
); );
return ret.uidInfo.get(Uin); let uid = ret.uidInfo.get(Uin); //通过QQ默认方式转换
if (!uid) {
Array.from(friends.values()).forEach((t) => {
if (t.uin == Uin) {
//logDebug('getUidByUin', t.uid, t.uin, Uin);
uid = t.uid;
}
//console.log(t.uid, t.uin, Uin);
});
//uid = Array.from(friends.values()).find((t) => { t.uin == Uin })?.uid; // 从NC维护的QQ Buddy缓存 转换
}
// if (!uid) {
// uid = (await NTQQFriendApi.getFriends(false)).find((t) => { t.uin == Uin })?.uid; //从QQ Native 缓存转换 方法一
// }
// if (!uid) {
// uid = (await NTQQFriendApi.getFriends(true)).find((t) => { t.uin == Uin })?.uid; //从QQ Native 非缓存转换 方法二
// }
if (!uid) {
let unveifyUid = (await NTQQUserApi.getUserDetailInfoByUin(Uin)).info.uid;//从QQ Native 特殊转换 方法三
if (unveifyUid.indexOf("*") == -1) {
uid = unveifyUid;
}
}
return uid;
} }
@CacheClassFuncAsyncExtend(3600, 'Uid2Uin', (Uid: string | undefined, Uin: number | undefined) => {
if (Uin && Uin != 0 && !isNaN(Uin)) {
return true
}
logWarn("uid转换到uin时异常", Uid);
return false;
})
static async getUinByUid(Uid: string | undefined) { static async getUinByUid(Uid: string | undefined) {
if (!Uid) { if (!Uid) {
return ''; return '';
@@ -182,7 +192,38 @@ export class NTQQUserApi {
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 getUserDetailInfoByUin(Uin: string) { static async getUserDetailInfoByUin(Uin: string) {
return NTEventDispatch.CallNoListenerEvent return NTEventDispatch.CallNoListenerEvent

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,
@@ -17,11 +18,10 @@ import fs from 'node:fs';
import { appid, qqVersionConfigInfo } from '@/common/utils/QQBasicInfo'; import { appid, qqVersionConfigInfo } 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 { rawFriends, friends, 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,
@@ -84,20 +84,21 @@ 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(() => { // 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);
}); });
// }).catch((e) => {
// logError('数据库初始化失败', e);
// });
// this.initDataListener(); // 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 +109,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);
@@ -189,7 +190,6 @@ export class NapCatCore {
} }
}); });
} }
private initDataListener() { private initDataListener() {
// 消息相关 // 消息相关
interface LineDevice { interface LineDevice {
@@ -208,10 +208,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 +249,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);
@@ -346,7 +354,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 +367,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 +482,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,9 +1,9 @@
import { import {
type Friend, type Friend,
type Group, type Group,
type GroupMember, GroupNotify, type GroupMember,
type SelfInfo, type SelfInfo,
BuddyCategoryType type BuddyCategoryType
} from './entities'; } from './entities';
import { isNumeric } from '@/common/utils/helper'; import { isNumeric } from '@/common/utils/helper';
import { NTQQGroupApi } from '@/core/apis'; import { NTQQGroupApi } from '@/core/apis';
@@ -30,7 +30,6 @@ export const groupMembers: Map<string, Map<string, GroupMember>> = new Map<strin
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 rawFriends: Array<BuddyCategoryType> = []; // 带分组的好友列表
export const groupNotifies: Record<string, GroupNotify> = {}; // flag->GroupNotify
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

@@ -52,4 +52,5 @@ export interface GroupMember {
isRobot: boolean; isRobot: boolean;
sex?: Sex sex?: Sex
qqLevel?: QQLevel qqLevel?: QQLevel
isChangeRole: boolean;
} }

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,182 @@ 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 SendActionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: InlineKeyboardRow[];
botAppid: string;
}
}
export interface SendRecommendedMsgElement {
elementType: ElementType.RECOMMENDEDMSG;
elementId: string;
recommendedMsgElement: {
rows: InlineKeyboardRow[];
botAppid: string;
}
}
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 SendTaskTopMsgElement {
elementType: ElementType.TASKTOPMSG;
elementId: string;
taskTopMsgElement: {
msgTitle: string;
msgSummary: string;
iconUrl: string;
topMsgType: number;
}
}
export interface SendTofuRecordElement {
elementType: ElementType.TOFURECORD;
elementId: string;
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 SendFaceBubbleElement {
elementType: ElementType.FACEBUBBLE;
elementId: string;
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 SendavRecordElement {
elementType: ElementType.AVRECORD;
elementId: string;
avRecordElement: {
type: number;
time: string;
text: string;
mainType: number;
hasRead: boolean;
extraType: number;
}
}
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 SendYoloGameResultElement {
elementType: ElementType.YOLOGAMERESULT;
yoloGameResultElement: {
UserInfo: YoloUserInfo[];
}
}
export interface SendGiphyElement {
elementType: ElementType.GIPHY;
elementId: string;
giphyElement: {
id: string;
isClip: boolean;
width: number;
height: number;
}
}
export interface SendWalletElement {
elementType: ElementType.UNKNOWN;//不做 设置位置
elementId: string;
walletElement: {}
}
export interface SendCalendarElement {
elementType: ElementType.CALENDAR;
elementId: string;
calendarElement: {
summary: string;
msg: string;
expireTimeMs: string;
schemaType: number;
schema: string
}
}
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;
@@ -118,6 +290,30 @@ export interface SendMarketFaceElement {
elementType: ElementType.MFACE; elementType: ElementType.MFACE;
marketFaceElement: MarketFaceElement; marketFaceElement: MarketFaceElement;
} }
export interface SendstructLongMsgElement {
elementType: ElementType.STRUCTLONGMSG;
elementId: string;
structLongMsgElement: {
xmlContent: string;
resId: string;
}
}
export interface SendactionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: number;
botAppid: string;
}
}
export interface sendShareLocationElement {
elementType: ElementType.SHARELOCATION;
elementId: string;
shareLocationElement: {
text: string;
ext: string;
}
}
export interface FileElement { export interface FileElement {
fileMd5?: string; fileMd5?: string;
@@ -162,7 +358,7 @@ export interface SendMarkdownElement {
} }
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 enum AtType { export enum AtType {
notAt = 0, notAt = 0,
@@ -289,6 +485,7 @@ export interface GrayTipElement {
templId: string; templId: string;
}; };
jsonGrayTipElement: { jsonGrayTipElement: {
busiId?: number;
jsonStr: string; jsonStr: string;
}; };
} }
@@ -329,7 +526,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 +687,7 @@ export interface MultiForwardMsgElement {
} }
export interface RawMessage { export interface RawMessage {
msgRandom: string;
// int32, 自己维护的消息id // int32, 自己维护的消息id
id?: number; id?: number;

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;

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

@@ -9,6 +9,7 @@ import {
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService { export interface NodeIKernelGroupService {
setHeader(uid:string,path:string): unknown;
addKernelGroupListener(listener: NodeIKernelGroupListener): number; addKernelGroupListener(listener: NodeIKernelGroupListener): number;
@@ -149,5 +150,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,4 +1,4 @@
import { ChatType, ElementType, Peer, RawMessage, SendMessageElement } from '@/core/entities'; import { ElementType, 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';
@@ -21,7 +21,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;
@@ -115,7 +115,7 @@ export interface NodeIKernelMsgService {
addLocalTofuRecordMsg(...args: unknown[]): unknown; addLocalTofuRecordMsg(...args: unknown[]): unknown;
addLocalRecordMsg(...args: unknown[]): unknown; addLocalRecordMsg(Peer: Peer, msgId: string, rawMessage: RawMessage, attr: Array<any> | number, front: boolean): Promise<unknown>;
deleteMsg(...args: unknown[]): unknown; deleteMsg(...args: unknown[]): unknown;
@@ -141,7 +141,7 @@ export interface NodeIKernelMsgService {
getLastMessageList(peer: Peer[]): Promise<unknown>; getLastMessageList(peer: Peer[]): Promise<unknown>;
getAioFirstViewLatestMsgs(...args: unknown[]): unknown; getAioFirstViewLatestMsgs(peer: Peer, unknown: number): unknown;
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>; getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>;
@@ -186,27 +186,48 @@ export interface NodeIKernelMsgService {
getSourceOfReplyMsgByClientSeqAndTime(...args: unknown[]): unknown; getSourceOfReplyMsgByClientSeqAndTime(...args: unknown[]): unknown;
getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: unknown): unknown; getMsgsByTypeFilter(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilter: { type: number, subtype: Array<number> }): unknown;
getMsgsByTypeFilters(...args: unknown[]): unknown; getMsgsByTypeFilters(peer: Peer, msgId: string, cnt: unknown, queryOrder: boolean, typeFilters: Array<{ type: number, subtype: Array<number> }>): unknown;
getMsgWithAbstractByFilterParam(...args: unknown[]): unknown; getMsgWithAbstractByFilterParam(...args: unknown[]): unknown;
queryMsgsWithFilter(...args: unknown[]): unknown; queryMsgsWithFilter(...args: unknown[]): unknown;
/**
* @deprecated 该函数已被标记为废弃,请使用新的替代方法。
* 使用过滤条件查询消息列表的版本2接口。
*
* 该函数通过一系列过滤条件来查询特定聊天中的消息列表。这些条件包括消息类型、发送者、时间范围等。
* 函数返回一个Promise解析为查询结果的未知类型对象。
*
* @param MsgId 消息ID用于特定消息的查询。
* @param MsgTime 消息时间,用于指定消息的时间范围。
* @param param 查询参数对象,包含详细的过滤条件和分页信息。
* @param param.chatInfo 聊天信息包括聊天类型和对方用户ID。
* @param param.filterMsgType 需要过滤的消息类型数组,留空表示不过滤。
* @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: { queryMsgsWithFilterVer2(MsgId: string, MsgTime: string, param: {
chatInfo: { chatInfo: {
chatType: number, chatType: number,
peerUid: string peerUid: string
}, },
filterMsgType: [], filterMsgType: [],
filterSendersUid: [], filterSendersUid: Array<string>,
filterMsgFromTime: string, filterMsgFromTime: string,
filterMsgToTime: string, filterMsgToTime: string,
pageLimit: number, pageLimit: number,
isReverseOrder: boolean, isReverseOrder: boolean,
isIncludeCurrent: boolean isIncludeCurrent: boolean
}): Promise<unknown>; }): Promise<unknown>;
// this.chatType = i2; // this.chatType = i2;
// this.peerUid = str; // this.peerUid = str;
@@ -228,14 +249,15 @@ export interface NodeIKernelMsgService {
peerUid: string peerUid: string
}, },
filterMsgType: [], filterMsgType: [],
filterSendersUid: [], filterSendersUid: string[],
filterMsgFromTime: string, filterMsgFromTime: string,
filterMsgToTime: string, filterMsgToTime: string,
pageLimit: number, pageLimit: number,
isReverseOrder: boolean, isReverseOrder: boolean,
isIncludeCurrent: boolean isIncludeCurrent: boolean
}): Promise<GeneralCallResult & {
}): Promise<unknown>; msgList: RawMessage[]
}>;
//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;
@@ -253,7 +275,7 @@ export interface NodeIKernelMsgService {
pageLimit: number, pageLimit: number,
isReverseOrder: boolean, isReverseOrder: boolean,
isIncludeCurrent: boolean isIncludeCurrent: boolean
}):Promise<unknown>; }): Promise<unknown>;
queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown; queryPicOrVideoMsgsDesktop(...args: unknown[]): unknown;
@@ -261,7 +283,19 @@ export interface NodeIKernelMsgService {
queryTroopEmoticonMsgs(...args: unknown[]): unknown; queryTroopEmoticonMsgs(...args: unknown[]): unknown;
queryMsgsAndAbstractsWithFilter(...args: unknown[]): unknown; queryMsgsAndAbstractsWithFilter(msgId: string, msgTime: string, megSeq: string, param: {
chatInfo: {
chatType: number,
peerUid: string
},
filterMsgType: [],
filterSendersUid: [],
filterMsgFromTime: string,
filterMsgToTime: string,
pageLimit: number,
isReverseOrder: boolean,
isIncludeCurrent: boolean
}): unknown;
setFocusOnGuild(...args: unknown[]): unknown; setFocusOnGuild(...args: unknown[]): unknown;
@@ -299,7 +333,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;
@@ -355,8 +389,8 @@ 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;
@@ -406,7 +440,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 +464,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;

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,5 +1,28 @@
import { Peer } from "../entities"; import { 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,14 @@ 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<any>
}
}>; // 1 arguments
clearMsgUnreadCount(...args: unknown[]): unknown; // 1 arguments clearMsgUnreadCount(...args: unknown[]): unknown; // 1 arguments
@@ -19,7 +49,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 +61,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 +73,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

@@ -85,23 +85,23 @@ export interface NodeIKernelRichMediaService {
downloadFileForFileUuid(peer: Peer, arg1: string, arg3: unknown[]): unknown; downloadFileForFileUuid(peer: Peer, arg1: string, arg3: unknown[]): unknown;
downloadFileByUrlListtransgroupfile(arg1: unknown, arg2: unknown): unknown; downloadFileByUrlList(arg1: unknown, arg2: unknown): unknown;
downloadFileForFileInfotransgroupfile(arg1: unknown, arg2: unknown): unknown; downloadFileForFileInfo(arg1: unknown, arg2: unknown): 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(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown): 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,18 +122,28 @@ export interface NodeIKernelRichMediaService {
} }
}>; }>;
getGroupFileInfotransgroupfile(arg1: unknown, arg2: unknown): unknown; getGroupFileInfo(arg1: unknown, arg2: unknown): unknown;
getGroupFileListtransgroupfile(arg1: unknown, arg2: unknown): unknown; getGroupFileList(arg1: unknown, arg2: unknown): unknown;
getGroupTransferListtransgroupfile(arg1: unknown, arg2: unknown): unknown; getGroupTransferList(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;
moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown; moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
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,7 +162,7 @@ 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;
@@ -160,7 +170,7 @@ export interface NodeIKernelRichMediaService {
onlyDownloadFile(arg1: unknown, arg2: unknown, arg3: unknown): unknown; onlyDownloadFile(arg1: unknown, arg2: unknown, arg3: unknown): 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;

View File

@@ -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,4 +1,4 @@
export interface NodeIKernelSearchService{ export interface NodeIKernelSearchService {
addKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments addKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments
removeKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments removeKernelSearchListenerr(...args: any[]): unknown;// needs 1 arguments
@@ -48,10 +48,10 @@ export interface NodeIKernelSearchService{
searchMsgWithKeywordsr(...args: any[]): unknown;// needs 2 arguments searchMsgWithKeywordsr(...args: any[]): unknown;// needs 2 arguments
searchMoreMsgWithKeywordsr(...args: any[]): unknown;// needs 1 arguments searchMoreMsgWithKeywordsr(...args: any[]): unknown;// needs 1 arguments
cancelSearchMsgWithKeywordsr(...args: any[]): unknown;// needs 3 arguments
searchFileWithKeywordsr(...args: any[]): unknown;// needs 2 arguments cancelSearchMsgWithKeywords(...args: any[]): unknown;// needs 3 arguments
searchFileWithKeywords(keywords: string[], source: number): Promise<unknown>;// needs 2 arguments
searchMoreFileWithKeywordsr(...args: any[]): unknown;// needs 1 arguments searchMoreFileWithKeywordsr(...args: any[]): unknown;// needs 1 arguments

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

@@ -39,6 +39,8 @@ 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';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -171,7 +173,7 @@ export interface NodeIQQNTWrapperSession {
getTicketService(): NodeIKernelTicketService; getTicketService(): NodeIKernelTicketService;
getTipOffService(): NodeIKernelTipOffService; getTipOffService(): NodeIKernelTipOffService;
getNodeMiscService(): NodeIKernelNodeMiscService; getNodeMiscService(): NodeIKernelNodeMiscService;
getRichMediaService(): NodeIKernelRichMediaService; getRichMediaService(): NodeIKernelRichMediaService;
@@ -230,13 +232,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;
} }
@@ -286,11 +288,7 @@ let wrapperNodePath = path.resolve(path.dirname(process.execPath), './resources/
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/${qqVersionConfigInfo.curVersion}/wrapper.node`);
} }
let WrapperLoader = path.join(__dirname, "WrapperLoader.cjs"); const nativemodule: any = { exports: {} };
//此处待优化 process.dlopen(nativemodule, wrapperNodePath);
fs.writeFileSync(WrapperLoader, ` const QQWrapper: WrapperNodeApi = nativemodule.exports;
module.exports = require("${wrapperNodePath.replace(/\\/g, "\\\\")}");
exports = module.exports;
`)
const QQWrapper: WrapperNodeApi = (await import("file://" + WrapperLoader)).default;
export default QQWrapper; export default QQWrapper;

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);
@@ -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

@@ -1,2 +0,0 @@
//统一到处入口
export * from './main';

View File

@@ -1,29 +0,0 @@
enum NapCatCorePlatform {
Node = 'Node',//命令行模式加载
LiteLoader = 'LiteLoader',//LL插件模式加载
}
class NewNapCatCore {
platform: NapCatCorePlatform; // 平台
constructor(platform: NapCatCorePlatform) {
this.platform = platform;
}
}
export class NapCatCoreManger {
static core: NewNapCatCore | undefined = undefined;
static defaultPlatform: NapCatCorePlatform = NapCatCorePlatform.Node;
static SetDefaultCore(platform: NapCatCorePlatform) {
if (this.core !== undefined) {
return;
}
this.defaultPlatform = platform;
}
static GetPlatform(): NapCatCorePlatform {
return NapCatCoreManger.defaultPlatform;
}
static GetInstance(): NewNapCatCore {
if (this.core === undefined) {
this.core = new NewNapCatCore(NapCatCoreManger.defaultPlatform);
}
return this.core;
}
}

View File

@@ -1 +0,0 @@
//拦截proxy到会话

View File

@@ -1 +0,0 @@
//初始化跟之前一样

View File

@@ -1 +0,0 @@
//proxy封装工具类

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,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

@@ -1,15 +1,15 @@
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 { log, logDebug } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper'; import { sleep } from '@/common/utils/helper';
import { uri2local } from '@/common/utils/file'; import { uri2local } from '@/common/utils/file';
import { ActionName, BaseCheckResult } from '../types'; import { ActionName, BaseCheckResult } from '../types';
import { FileElement, RawMessage, VideoElement } from '@/core/entities'; import { FileElement, RawMessage, VideoElement } from '@/core/entities';
import { NTQQFileApi } from '@/core/apis'; import { NTQQFileApi, NTQQMsgApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import Ajv from 'ajv'; import Ajv from 'ajv';
import { MessageUnique } from '@/common/utils/MessageUnique';
export interface GetFilePayload { export interface GetFilePayload {
file: string; // 文件名或者fileUuid file: string; // 文件名或者fileUuid
@@ -45,69 +45,67 @@ 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); throw new Error('file not found');
if (!cache) { // let cache = await dbUtil.getFileCacheByName(payload.file);
cache = await dbUtil.getFileCacheByUuid(payload.file); // if (!cache) {
} // cache = await dbUtil.getFileCacheByUuid(payload.file);
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 msg = await dbUtil.getMsgByLongId(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);
msg = await dbUtil.getMsgByLongId(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);
}
}
}
// if (autoDeleteFile) {
// setTimeout(() => {
// fs.unlink(cache.filePath)
// }, autoDeleteFileSecond * 1000)
// } // }
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

@@ -3,11 +3,10 @@ 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 } 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,7 +14,7 @@ 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' }
}, },
@@ -32,7 +31,7 @@ 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'; const startMsgId = (await MessageUnique.getMsgIdAndPeerByShortId(payload.message_seq))?.MsgId || '0';
// log("startMsgId", startMsgId) // log("startMsgId", startMsgId)
const historyResult = (await NTQQMsgApi.getMsgHistory({ const historyResult = (await NTQQMsgApi.getMsgHistory({
chatType: ChatType.group, chatType: ChatType.group,
@@ -41,7 +40,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction<Payload, Resp
//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

@@ -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

@@ -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

@@ -1,11 +1,10 @@
import { OB11GroupMember } from '../../types'; import { OB11GroupMember } from '../../types';
import { getGroup, getGroupMember, groupMembers } from '@/core/data'; import { getGroup, getGroupMember, groupMembers, selfInfo } 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';
@@ -28,25 +27,29 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
PayloadSchema = SchemaData; PayloadSchema = SchemaData;
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const group = await getGroup(payload.group_id.toString()); 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) { if (!group) {
throw (`群(${payload.group_id})不存在`); throw (`群(${payload.group_id})不存在`);
} }
const webGroupMembers = await WebApi.getGroupMembers(payload.group_id.toString());
if (payload.no_cache == true || payload.no_cache === 'true') { if (payload.no_cache == true || payload.no_cache === 'true') {
groupMembers.set(group.groupCode, await NTQQGroupApi.getGroupMembers(payload.group_id.toString())); groupMembers.set(group.groupCode, await NTQQGroupApi.getGroupMembers(payload.group_id.toString()));
} }
const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString()); const member = await getGroupMember(payload.group_id.toString(), payload.user_id.toString());
// log(member); //早返回
if (member) { if (!member) {
logDebug('获取群成员详细信息'); throw (`群(${payload.group_id})成员${payload.user_id}不存在`);
try { }
const info = (await NTQQUserApi.getUserDetailInfo(member.uid)); try {
logDebug('群成员详细信息结果', info); const info = (await NTQQUserApi.getUserDetailInfo(member.uid));
Object.assign(member, info); logDebug('群成员详细信息结果', info);
} catch (e) { Object.assign(member, info);
logDebug('获取群成员详细信息失败, 只能返回基础信息', e); } catch (e) {
} logDebug('获取群成员详细信息失败, 只能返回基础信息', e);
const retMember = OB11Constructor.groupMember(payload.group_id.toString(), member); }
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++) { for (let i = 0, len = webGroupMembers.length; i < len; i++) {
if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) { if (webGroupMembers[i]?.uin && webGroupMembers[i].uin === retMember.user_id) {
retMember.join_time = webGroupMembers[i]?.join_time; retMember.join_time = webGroupMembers[i]?.join_time;
@@ -55,11 +58,17 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
retMember.level = webGroupMembers[i]?.lv.level.toString(); retMember.level = webGroupMembers[i]?.lv.level.toString();
} }
} }
return retMember;
} else { } else {
throw (`群(${payload.group_id})成员${payload.user_id}不存在`); let LastestMsgList = await NTQQGroupApi.getLastestMsg(payload.group_id.toString(), [payload.user_id.toString()]);
if (LastestMsgList?.msgList?.length && LastestMsgList?.msgList?.length > 0) {
let 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;
export default GetGroupMemberInfo;

View File

@@ -3,13 +3,9 @@ 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 { dbUtil } from '@/common/utils/db';
import { TypeConvert } from '@/common/utils/type';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -43,7 +39,11 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>(); const MemberMap: Map<number, OB11GroupMember> = new Map<number, OB11GroupMember>();
// 转为Map 方便索引 // 转为Map 方便索引
let 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]);
} }
@@ -63,15 +63,15 @@ class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
MemberMap.set(webGroupMembers[i]?.uin, MemberData); MemberMap.set(webGroupMembers[i]?.uin, MemberData);
} }
} }
} 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)); const DateMap = await NTQQGroupApi.getGroupMemberLastestSendTimeCache(payload.group_id.toString());//开始从本地拉取
_sendAndJoinRember.forEach((element) => { for (let DateUin of DateMap.keys()) {
const MemberData = MemberMap.get(element.user_id); const MemberData = MemberMap.get(parseInt(DateUin));
if (MemberData) { if (MemberData) {
MemberData.join_time = element.join_time; MemberData.last_sent_time = parseInt(DateMap.get(DateUin)!);
MemberData.last_sent_time = element.last_sent_time; //join_time 有基础数据兜底
} }
}); }
} }
// 还原索引到Array 一同返回 // 还原索引到Array 一同返回
@@ -82,7 +82,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
); );

View File

@@ -34,7 +34,7 @@ import SetGroupAdmin from './group/SetGroupAdmin';
import SetGroupCard from './group/SetGroupCard'; import SetGroupCard from './group/SetGroupCard';
import GetImage from './file/GetImage'; import GetImage from './file/GetImage';
import GetRecord from './file/GetRecord'; import GetRecord from './file/GetRecord';
import { GoCQHTTPMarkMsgAsRead, MarkGroupMsgAsRead, MarkPrivateMsgAsRead } from './msg/MarkMsgAsRead'; import { GoCQHTTPMarkMsgAsRead, MarkAllMsgAsRead, MarkGroupMsgAsRead, MarkPrivateMsgAsRead } from './msg/MarkMsgAsRead';
import CleanCache from './system/CleanCache'; import CleanCache from './system/CleanCache';
import GoCQHTTPUploadGroupFile from './go-cqhttp/UploadGroupFile'; import GoCQHTTPUploadGroupFile from './go-cqhttp/UploadGroupFile';
import { GetConfigAction, SetConfigAction } from '@/onebot11/action/extends/Config'; import { GetConfigAction, SetConfigAction } from '@/onebot11/action/extends/Config';
@@ -70,6 +70,11 @@ import { SetSelfProfile } from './extends/SetSelfProfile';
import { shareGroupEx, sharePeer } from './extends/sharePeer'; import { shareGroupEx, sharePeer } from './extends/sharePeer';
import { CreateCollection } from './extends/CreateCollection'; import { CreateCollection } from './extends/CreateCollection';
import { SetLongNick } from './extends/SetLongNick'; import { SetLongNick } from './extends/SetLongNick';
import DelEssenceMsg from './group/DelEssenceMsg';
import SetEssenceMsg from './group/SetEssenceMsg';
import GetRecentContact from './user/GetRecentContact';
import { GetProfileLike } from './extends/GetProfileLike';
import SetGroupHeader from './extends/SetGroupHeader';
export const actionHandlers = [ export const actionHandlers = [
new RebootNormal(), new RebootNormal(),
@@ -81,10 +86,6 @@ export const actionHandlers = [
new sharePeer(), new sharePeer(),
new CreateCollection(), new CreateCollection(),
new SetLongNick(), new SetLongNick(),
// new GetConfigAction(),
// new SetConfigAction(),
// new GetGroupAddRequest(),
// TranslateEnWordToZn = "translate_en2zh",
new ForwardFriendSingleMsg(), new ForwardFriendSingleMsg(),
new ForwardGroupSingleMsg(), new ForwardGroupSingleMsg(),
new MarkGroupMsgAsRead(), new MarkGroupMsgAsRead(),
@@ -101,9 +102,13 @@ export const actionHandlers = [
new GetMsg(), new GetMsg(),
new GetLoginInfo(), new GetLoginInfo(),
new GetFriendList(), new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupList(),
new GetGroupMemberList(), new GetGroupMemberInfo(), new GetGroupInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), new GetGroupMemberList(),
new GetGroupMemberInfo(),
new SendGroupMsg(),
new SendPrivateMsg(),
new SendMsg(),
new DeleteMsg(), new DeleteMsg(),
new SetGroupAddRequest(), new SetGroupAddRequest(),
new SetFriendAddRequest(), new SetFriendAddRequest(),
@@ -121,9 +126,7 @@ export const actionHandlers = [
new GetImage(), new GetImage(),
new GetRecord(), new GetRecord(),
new SetMsgEmojiLike(), new SetMsgEmojiLike(),
// new CleanCache(),
new GetCookies(), new GetCookies(),
//
new SetOnlineStatus(), new SetOnlineStatus(),
new GetRobotUinRange(), new GetRobotUinRange(),
new GetFriendWithCategory(), new GetFriendWithCategory(),
@@ -147,7 +150,13 @@ export const actionHandlers = [
new GoCQHTTPGetForwardMsgAction(), new GoCQHTTPGetForwardMsgAction(),
new GetFriendMsgHistory(), new GetFriendMsgHistory(),
new GoCQHTTPHandleQuickAction(), new GoCQHTTPHandleQuickAction(),
new GetGroupSystemMsg() new GetGroupSystemMsg(),
new DelEssenceMsg(),
new SetEssenceMsg(),
new GetRecentContact(),
new MarkAllMsgAsRead(),
new GetProfileLike(),
new SetGroupHeader()
]; ];
function initActionMap() { function initActionMap() {

View File

@@ -1,13 +1,18 @@
import { NTQQMsgApi } from '@/core/apis'; import { NTQQMsgApi } from '@/core/apis';
import { ActionName } from '../types'; import { ActionName } from '../types';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { dbUtil } from '@/common/utils/db';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
properties: { properties: {
message_id: { type: 'number' }, message_id: {
oneOf:[
{ type: 'number' },
{ type: 'string' }
]
}
}, },
required: ['message_id'] required: ['message_id']
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
@@ -18,9 +23,9 @@ class DeleteMsg extends BaseAction<Payload, void> {
actionName = ActionName.DeleteMsg; actionName = ActionName.DeleteMsg;
PayloadSchema = SchemaData; PayloadSchema = SchemaData;
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const msg = await dbUtil.getMsgByShortId(payload.message_id); const msg = await MessageUnique.getMsgIdAndPeerByShortId(Number(payload.message_id));
if (msg) { if (msg) {
await NTQQMsgApi.recallMsg({ peerUid: msg.peerUid, chatType: msg.chatType }, [msg.msgId]); await NTQQMsgApi.recallMsg(msg.Peer, [msg.MsgId]);
} }
} }
} }

View File

@@ -1,16 +1,16 @@
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { NTQQMsgApi, NTQQUserApi } from '@/core/apis'; import { NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { ChatType, Peer } from '@/core/entities'; import { ChatType, Peer } from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { ActionName } from '../types'; import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
properties: { properties: {
message_id: { type: 'number' }, message_id: { type: 'number' },
group_id: { type: [ 'number' , 'string' ] }, group_id: { type: ['number', 'string'] },
user_id: { type: [ 'number' , 'string' ] } user_id: { type: ['number', 'string'] }
}, },
required: ['message_id'] required: ['message_id']
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
@@ -30,18 +30,14 @@ class ForwardSingleMsg extends BaseAction<Payload, null> {
} }
protected async _handle(payload: Payload): Promise<null> { protected async _handle(payload: Payload): Promise<null> {
const msg = await dbUtil.getMsgByShortId(payload.message_id); const msg = await MessageUnique.getMsgIdAndPeerByShortId(payload.message_id);
if (!msg) { if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`); throw new Error(`无法找到消息${payload.message_id}`);
} }
const peer = await this.getTargetPeer(payload); const peer = await this.getTargetPeer(payload);
const ret = await NTQQMsgApi.forwardMsg( const ret = await NTQQMsgApi.forwardMsg(msg.Peer,
{
chatType: msg.chatType,
peerUid: msg.peerUid,
},
peer, peer,
[msg.msgId], [msg.MsgId],
); );
if (ret.result !== 0) { if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`); throw new Error(`转发消息失败 ${ret.errMsg}`);

View File

@@ -2,8 +2,9 @@ import { OB11Message } 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 { dbUtil } from '@/common/utils/db';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
import { NTQQMsgApi } from '@/core';
export type ReturnDataType = OB11Message export type ReturnDataType = OB11Message
@@ -11,7 +12,7 @@ export type ReturnDataType = OB11Message
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
properties: { properties: {
message_id: { type: ['number','string'] }, message_id: { type: ['number', 'string'] },
}, },
required: ['message_id'] required: ['message_id']
} as const satisfies JSONSchema; } as const satisfies JSONSchema;
@@ -26,14 +27,15 @@ class GetMsg extends BaseAction<Payload, OB11Message> {
if (!payload.message_id) { if (!payload.message_id) {
throw Error('参数message_id不能为空'); throw Error('参数message_id不能为空');
} }
let msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); const MsgShortId = await MessageUnique.getShortIdByMsgId(payload.message_id.toString());
if (!msg) { const msgIdWithPeer = await MessageUnique.getMsgIdAndPeerByShortId(MsgShortId || parseInt(payload.message_id.toString()));
msg = await dbUtil.getMsgByLongId(payload.message_id.toString()); if (!msgIdWithPeer) {
}
if (!msg) {
throw ('消息不存在'); throw ('消息不存在');
} }
return await OB11Constructor.message(msg); const msg = await NTQQMsgApi.getMsgsByMsgId(
{ guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType },
[msgIdWithPeer?.MsgId || payload.message_id.toString()]);
return await OB11Constructor.message(msg.msgList[0]);
} }
} }

View File

@@ -60,3 +60,12 @@ export class GoCQHTTPMarkMsgAsRead extends BaseAction<Payload, null> {
return null; return null;
} }
} }
export class MarkAllMsgAsRead extends BaseAction<Payload, null> {
actionName = ActionName._MarkAllMsgAsRead;
protected async _handle(payload: Payload): Promise<null> {
await NTQQMsgApi.markallMsgAsRead();
return null;
}
}

View File

@@ -5,18 +5,19 @@ import {
Group, Group,
IdMusicSignPostData, IdMusicSignPostData,
NTQQFileApi, NTQQFileApi,
NTQQMsgApi,
SendArkElement, SendArkElement,
SendMessageElement, SendMessageElement,
SendMsgElementConstructor, SendMsgElementConstructor,
SignMusicWrapper SignMusicWrapper
} from '@/core'; } from '@/core';
import { getGroupMember } from '@/core/data'; import { getGroupMember } from '@/core/data';
import { dbUtil } from '@/common/utils/db';
import { logDebug, logError } from '@/common/utils/log'; import { logDebug, logError } from '@/common/utils/log';
import { uri2local } from '@/common/utils/file'; import { uri2local } from '@/common/utils/file';
import { ob11Config } from '@/onebot11/config'; import { ob11Config } from '@/onebot11/config';
import { RequestUtil } from '@/common/utils/request'; import { RequestUtil } from '@/common/utils/request';
import fs from 'node:fs'; import fs from 'node:fs';
import { MessageUnique } from '@/common/utils/MessageUnique';
export type MessageContext = { export type MessageContext = {
group?: Group, group?: Group,
@@ -27,27 +28,27 @@ async function handleOb11FileLikeMessage(
{ data: { file, name: payloadFileName } }: OB11MessageFileBase, { data: { file, name: payloadFileName } }: OB11MessageFileBase,
{ deleteAfterSentFiles }: MessageContext { deleteAfterSentFiles }: MessageContext
) { ) {
let uri = file; const uri = file;
const cache = await dbUtil.getFileCacheByName(file); // const cache = await dbUtil.getFileCacheByName(file);
if (cache) { // if (cache) {
if (fs.existsSync(cache.path)) { // if (fs.existsSync(cache.path)) {
uri = 'file://' + cache.path; // uri = 'file://' + cache.path;
} else if (cache.url) { // } else if (cache.url) {
uri = cache.url; // uri = cache.url;
} else { // } else {
const fileMsg = await dbUtil.getMsgByLongId(cache.msgId); // const fileMsgPeer = MessageUnique.getPeerByMsgId(cache.msgId);
if (fileMsg) { // if (fileMsgPeer) {
cache.path = await NTQQFileApi.downloadMedia( // cache.path = await NTQQFileApi.downloadMedia(
fileMsg.msgId, fileMsg.chatType, fileMsg.peerUid, // fileMsgPeer.MsgId, fileMsgPeer.Peer.chatType, fileMsgPeer.Peer.peerUid,
cache.elementId, '', '' // cache.elementId, '', ''
); // );
uri = 'file://' + cache.path; // uri = 'file://' + cache.path;
dbUtil.updateFileCache(cache); // dbUtil.updateFileCache(cache);
} // }
} // }
logDebug('找到文件缓存', uri); // logDebug('找到文件缓存', uri);
} // }
const { path, isLocal, fileName, errMsg } = (await uri2local(uri)); const { path, isLocal, fileName, errMsg } = (await uri2local(uri));
@@ -69,9 +70,9 @@ const _handlers: {
// This picks the correct message type out // This picks the correct message type out
// How great the type system of TypeScript is! // How great the type system of TypeScript is!
context: MessageContext context: MessageContext
) => SendMessageElement | undefined | Promise<SendMessageElement | undefined> ) => Promise<SendMessageElement | undefined>
} = { } = {
[OB11MessageDataType.text]: ({ data: { text } }) => SendMsgElementConstructor.text(text), [OB11MessageDataType.text]: async ({ data: { text } }) => SendMsgElementConstructor.text(text),
[OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => { [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => {
if (!context.group) return undefined; if (!context.group) return undefined;
@@ -86,15 +87,16 @@ const _handlers: {
}, },
[OB11MessageDataType.reply]: async ({ data: { id } }) => { [OB11MessageDataType.reply]: async ({ data: { id } }) => {
const replyMsg = await dbUtil.getMsgByShortId(parseInt(id)); const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
const replyMsg = (await NTQQMsgApi.getMsgsByMsgId(replyMsgM?.Peer!, [replyMsgM?.MsgId!])).msgList[0];
return replyMsg ? return replyMsg ?
SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) : SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) :
undefined; undefined;
}, },
[OB11MessageDataType.face]: ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)), [OB11MessageDataType.face]: async ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)),
[OB11MessageDataType.mface]: ({ [OB11MessageDataType.mface]: async ({
data: { data: {
emoji_package_id, emoji_package_id,
emoji_id, emoji_id,
@@ -144,13 +146,13 @@ const _handlers: {
[OB11MessageDataType.voice]: async (sendMsg, context) => [OB11MessageDataType.voice]: async (sendMsg, context) =>
SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path), SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: ({ data: { data } }) => SendMsgElementConstructor.ark(data), [OB11MessageDataType.json]: async ({ data: { data } }) => SendMsgElementConstructor.ark(data),
[OB11MessageDataType.dice]: ({ data: { result } }) => SendMsgElementConstructor.dice(result), [OB11MessageDataType.dice]: async ({ data: { result } }) => SendMsgElementConstructor.dice(result),
[OB11MessageDataType.RPS]: ({ data: { result } }) => SendMsgElementConstructor.rps(result), [OB11MessageDataType.RPS]: async ({ data: { result } }) => SendMsgElementConstructor.rps(result),
[OB11MessageDataType.markdown]: ({ data: { content } }) => SendMsgElementConstructor.markdown(content), [OB11MessageDataType.markdown]: async ({ data: { content } }) => SendMsgElementConstructor.markdown(content),
[OB11MessageDataType.music]: async ({ data }) => { [OB11MessageDataType.music]: async ({ data }) => {
// 保留, 直到...找到更好的解决方案 // 保留, 直到...找到更好的解决方案
@@ -202,20 +204,24 @@ const _handlers: {
} }
}, },
[OB11MessageDataType.node]: () => undefined, [OB11MessageDataType.node]: async () => undefined,
[OB11MessageDataType.forward]: () => undefined, [OB11MessageDataType.forward]: async () => undefined,
[OB11MessageDataType.xml]: () => undefined, [OB11MessageDataType.xml]: async () => undefined,
[OB11MessageDataType.poke]: () => undefined, [OB11MessageDataType.poke]: async () => undefined,
[OB11MessageDataType.Location]: async () => {
return SendMsgElementConstructor.location();
}
}; };
const handlers = <{ const handlers = <{
[Key in OB11MessageDataType]: ( [Key in OB11MessageDataType]: (
sendMsg: OB11MessageData, sendMsg: OB11MessageData,
context: MessageContext context: MessageContext
) => SendMessageElement | undefined | Promise<SendMessageElement | undefined> ) => Promise<SendMessageElement | undefined>
}>_handlers; }>_handlers;
export default async function createSendElements( export default async function createSendElements(
@@ -223,18 +229,20 @@ export default async function createSendElements(
group?: Group, group?: Group,
ignoreTypes: OB11MessageDataType[] = [] ignoreTypes: OB11MessageDataType[] = []
) { ) {
const sendElements: SendMessageElement[] = [];
const deleteAfterSentFiles: string[] = []; const deleteAfterSentFiles: string[] = [];
let callResultList: Array<Promise<SendMessageElement | undefined>> = [];
for (const sendMsg of messageData) { for (const sendMsg of messageData) {
if (ignoreTypes.includes(sendMsg.type)) { if (ignoreTypes.includes(sendMsg.type)) {
continue; continue;
} }
const callResult = await handlers[sendMsg.type]( const callResult = handlers[sendMsg.type](
sendMsg, sendMsg,
{ group, deleteAfterSentFiles } { group, deleteAfterSentFiles }
); )?.catch(undefined);
if (callResult) sendElements.push(callResult); callResultList.push(callResult);
} }
let ret = await Promise.all(callResultList);
const sendElements: SendMessageElement[] = ret.filter(ele => ele) as SendMessageElement[];
return { sendElements, deleteAfterSentFiles }; return { sendElements, deleteAfterSentFiles };
} }

View File

@@ -1,12 +1,11 @@
import { ChatType, ElementType, Group, NTQQMsgApi, Peer, RawMessage, SendMessageElement } from '@/core'; import { ChatType, ElementType, Group, NTQQMsgApi, Peer, RawMessage, SendMessageElement } from '@/core';
import { OB11MessageNode } from '@/onebot11/types'; import { OB11MessageNode } from '@/onebot11/types';
import { selfInfo } from '@/core/data'; import { selfInfo } from '@/core/data';
import { dbUtil } from '@/common/utils/db';
import createSendElements from '@/onebot11/action/msg/SendMsg/create-send-elements'; import createSendElements from '@/onebot11/action/msg/SendMsg/create-send-elements';
import { logDebug, logError } from '@/common/utils/log'; import { logDebug, logError } from '@/common/utils/log';
import { sleep } from '@/common/utils/helper'; import { sleep } from '@/common/utils/helper';
import fs from 'node:fs';
import { normalize, sendMsg } from '@/onebot11/action/msg/SendMsg/index'; import { normalize, sendMsg } from '@/onebot11/action/msg/SendMsg/index';
import { MessageUnique } from '@/common/utils/MessageUnique';
async function cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> { async function cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
const selfPeer = { const selfPeer = {
@@ -14,7 +13,7 @@ async function cloneMsg(msg: RawMessage): Promise<RawMessage | undefined> {
peerUid: selfInfo.uid peerUid: selfInfo.uid
}; };
// logDebug('克隆的目标消息', msg); //logDebug('克隆的目标消息', msg);
const sendElements: SendMessageElement[] = []; const sendElements: SendMessageElement[] = [];
@@ -54,13 +53,14 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
const nodeId = messageNode.data.id; const nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片 // 有nodeId表示一个子转发消息卡片
if (nodeId) { if (nodeId) {
const nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId)); const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId));
if (!needClone) { if (!needClone) {
nodeMsgIds.push(nodeMsg!.msgId); nodeMsgIds.push(nodeMsg!.MsgId);
} else { } else {
if (nodeMsg!.peerUid !== selfInfo.uid) { if (nodeMsg!.Peer.peerUid !== selfInfo.uid) {
// need cloning // need cloning
const clonedMsg = await cloneMsg(nodeMsg!); const rawClone = await NTQQMsgApi.getMsgsByMsgId(nodeMsg?.Peer!, [nodeMsg?.MsgId!]);
const clonedMsg = await cloneMsg(rawClone.msgList[0]);
if (clonedMsg) { if (clonedMsg) {
nodeMsgIds.push(clonedMsg.msgId); nodeMsgIds.push(clonedMsg.msgId);
} }
@@ -91,7 +91,7 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
//logDebug(sendElementsSplit); //logDebug(sendElementsSplit);
} }
// log("分割后的转发节点", sendElementsSplit) // log("分割后的转发节点", sendElementsSplit)
const MsgNodeList: Promise<RawMessage>[] = []; const MsgNodeList: Promise<RawMessage | undefined>[] = [];
for (const sendElementsSplitElement of sendElementsSplit) { for (const sendElementsSplitElement of sendElementsSplit) {
MsgNodeList.push(sendMsg(selfPeer, sendElementsSplitElement, [], true)); MsgNodeList.push(sendMsg(selfPeer, sendElementsSplitElement, [], true));
await sleep(Math.trunc(sendElementsSplit.length / 10) * 100); await sleep(Math.trunc(sendElementsSplit.length / 10) * 100);
@@ -99,7 +99,9 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
} }
for (const msgNode of MsgNodeList) { for (const msgNode of MsgNodeList) {
const result = await msgNode; const result = await msgNode;
nodeMsgIds.push(result.msgId); if (result) {
nodeMsgIds.push(result.msgId);
}
//logDebug('转发节点生成成功', result.msgId); //logDebug('转发节点生成成功', result.msgId);
} }
} catch (e) { } catch (e) {
@@ -107,13 +109,14 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
} }
} }
} }
// 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发 // 检查srcPeer是否一致不一致则需要克隆成自己的消息, 让所有srcPeer都变成自己的使其保持一致才能够转发
const nodeMsgArray: Array<RawMessage> = []; const nodeMsgArray: Array<RawMessage> = [];
let srcPeer: Peer | undefined = undefined; let srcPeer: Peer | undefined = undefined;
let needSendSelf = false; let needSendSelf = false;
for (const msgId of nodeMsgIds) { for (const msgId of nodeMsgIds) {
const nodeMsg = await dbUtil.getMsgByLongId(msgId); const nodeMsgPeer = await MessageUnique.getPeerByMsgId(msgId);
const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer?.Peer!, [msgId])).msgList[0];
if (nodeMsg) { if (nodeMsg) {
nodeMsgArray.push(nodeMsg); nodeMsgArray.push(nodeMsg);
if (!srcPeer) { if (!srcPeer) {
@@ -124,7 +127,7 @@ export async function handleForwardNode(destPeer: Peer, messageNodes: OB11Messag
} }
} }
} }
//logDebug('nodeMsgArray', nodeMsgArray); // logDebug('nodeMsgArray', nodeMsgArray);
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId); nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
if (needSendSelf) { if (needSendSelf) {
//logDebug('需要克隆转发消息'); //logDebug('需要克隆转发消息');

View File

@@ -8,13 +8,13 @@ import {
} from '@/onebot11/types'; } from '@/onebot11/types';
import { ActionName, BaseCheckResult } from '@/onebot11/action/types'; import { ActionName, BaseCheckResult } from '@/onebot11/action/types';
import { getGroup } from '@/core/data'; import { getGroup } from '@/core/data';
import { dbUtil } from '@/common/utils/db';
import { ChatType, ElementType, Group, NTQQFileApi, NTQQFriendApi, NTQQMsgApi, NTQQUserApi, Peer, SendMessageElement, } from '@/core'; import { ChatType, ElementType, Group, NTQQFileApi, NTQQFriendApi, NTQQMsgApi, NTQQUserApi, Peer, SendMessageElement, } from '@/core';
import fs from 'node:fs'; import fs from 'node:fs';
import { logDebug, logError } from '@/common/utils/log'; import { logDebug, logError } from '@/common/utils/log';
import { decodeCQCode } from '@/onebot11/cqcode'; import { decodeCQCode } from '@/onebot11/cqcode';
import createSendElements from './create-send-elements'; import createSendElements from './create-send-elements';
import { handleForwardNode } from '@/onebot11/action/msg/SendMsg/handle-forward-node'; import { handleForwardNode } from '@/onebot11/action/msg/SendMsg/handle-forward-node';
import { MessageUnique } from '@/common/utils/MessageUnique';
export interface ReturnDataType { export interface ReturnDataType {
message_id: number; message_id: number;
@@ -66,10 +66,10 @@ export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], de
} }
const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout); const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, timeout);
try { try {
returnMsg.id = await dbUtil.addMsg(returnMsg, false); returnMsg!.id = await MessageUnique.createMsg({ chatType: peer.chatType, guildId: '', peerUid: peer.peerUid }, returnMsg!.msgId);
} catch (e: any) { } catch (e: any) {
logDebug('发送消息id获取失败', e); logDebug('发送消息id获取失败', e);
returnMsg.id = 0; returnMsg!.id = 0;
} }
deleteAfterSentFiles.map((f) => { deleteAfterSentFiles.map((f) => {
@@ -102,6 +102,7 @@ async function createContext(payload: OB11PostSendMsg, contextMode: ContextMode)
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) { if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await NTQQUserApi.getUidByUin(payload.user_id.toString()); const Uid = await NTQQUserApi.getUidByUin(payload.user_id.toString());
const isBuddy = await NTQQFriendApi.isBuddy(Uid!); const isBuddy = await NTQQFriendApi.isBuddy(Uid!);
//console.log("[调试代码] UIN:", payload.user_id, " UID:", Uid, " IsBuddy:", isBuddy);
return { return {
peer: { peer: {
chatType: isBuddy ? ChatType.friend : ChatType.temp, chatType: isBuddy ? ChatType.friend : ChatType.temp,
@@ -144,7 +145,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} }
protected async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> { protected async _handle(payload: OB11PostSendMsg): Promise<{ message_id: number }> {
let { peer, group } = await createContext(payload, this.contextMode); const { peer, group } = await createContext(payload, this.contextMode);
const messages = normalize( const messages = normalize(
payload.message, payload.message,
@@ -154,8 +155,8 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) { if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
const returnMsg = await handleForwardNode(peer, messages as OB11MessageNode[], group); const returnMsg = await handleForwardNode(peer, messages as OB11MessageNode[], group);
if (returnMsg) { if (returnMsg) {
const msgShortId = await dbUtil.addMsg(returnMsg!, false); const msgShortId = await MessageUnique.createMsg({ guildId: '', peerUid: peer.peerUid, chatType: peer.chatType }, returnMsg!.msgId);
return { message_id: msgShortId }; return { message_id: msgShortId! };
} else { } else {
throw Error('发送转发消息失败'); throw Error('发送转发消息失败');
} }
@@ -171,7 +172,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, group); const { sendElements, deleteAfterSentFiles } = await createSendElements(messages, group);
//console.log(peer, JSON.stringify(sendElements,null,2)); //console.log(peer, JSON.stringify(sendElements,null,2));
const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles); const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles);
return { message_id: returnMsg.id! }; return { message_id: returnMsg!.id! };
} }
} }

View File

@@ -1,8 +1,8 @@
import { ActionName } from '../types'; import { ActionName } from '../types';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { dbUtil } from '@/common/utils/db';
import { NTQQMsgApi } from '@/core/apis'; import { NTQQMsgApi } from '@/core/apis';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/utils/MessageUnique';
const SchemaData = { const SchemaData = {
type: 'object', type: 'object',
@@ -19,16 +19,14 @@ export class SetMsgEmojiLike extends BaseAction<Payload, any> {
actionName = ActionName.SetMsgEmojiLike; actionName = ActionName.SetMsgEmojiLike;
PayloadSchema = SchemaData; PayloadSchema = SchemaData;
protected async _handle(payload: Payload) { protected async _handle(payload: Payload) {
const msg = await dbUtil.getMsgByShortId(parseInt(payload.message_id.toString())); const msg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
if (!msg) { if (!msg) {
throw new Error('msg not found'); throw new Error('msg not found');
} }
if (!payload.emoji_id){ if (!payload.emoji_id){
throw new Error('emojiId not found'); throw new Error('emojiId not found');
} }
return await NTQQMsgApi.setEmojiLike({ const msgSeq = (await NTQQMsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList[0].msgSeq;
chatType: msg.chatType, return await NTQQMsgApi.setEmojiLike(msg.Peer, msgSeq, payload.emoji_id.toString(), true);
peerUid: msg.peerUid
}, msg.msgSeq, payload.emoji_id.toString(), true);
} }
} }

View File

@@ -7,7 +7,6 @@ import {
ChatCacheListItemBasic, ChatCacheListItemBasic,
CacheFileType CacheFileType
} from '@/core/entities'; } from '@/core/entities';
import { dbUtil } from '@/common/utils/db';
import { NTQQFileApi, NTQQFileCacheApi } from '@/core/apis/file'; import { NTQQFileApi, NTQQFileCacheApi } from '@/core/apis/file';
import { logError } from '@/common/utils/log'; import { logError } from '@/common/utils/log';

View File

@@ -93,5 +93,11 @@ export enum ActionName {
SetSelfProfile = 'set_self_profile', SetSelfProfile = 'set_self_profile',
CreateCollection = 'create_collection', CreateCollection = 'create_collection',
GetCollectionList = 'get_collection_list', GetCollectionList = 'get_collection_list',
SetLongNick = 'set_self_longnick' SetLongNick = 'set_self_longnick',
SetEssenceMsg = 'set_essence_msg',
DelEssenceMsg = 'delete_essence_msg',
GetRecentContact = 'get_recent_contact',
_MarkAllMsgAsRead = '_mark_all_as_read',
GetProfileLike = 'get_profile_like',
SetGroupHeader = "set_group_head"
} }

View File

@@ -0,0 +1,12 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { NTQQUserApi } from '@/core';
export default class GetRecentContact extends BaseAction<void, any> {
actionName = ActionName.GetRecentContact;
protected async _handle(payload: void) {
//没有效果
return await NTQQUserApi.getRecentContactListSnapShot(10);
}
}

View File

@@ -42,6 +42,7 @@ export interface OB11Config {
} }
class Config extends ConfigBase<OB11Config> implements OB11Config { class Config extends ConfigBase<OB11Config> implements OB11Config {
name: string = 'onebot11'
http = { http = {
enable: false, enable: false,
host: '', host: '',
@@ -72,10 +73,6 @@ class Config extends ConfigBase<OB11Config> implements OB11Config {
RecordList: [] as Array<string> RecordList: [] as Array<string>
}; };
getConfigPath() {
return path.join(this.getConfigDir(), `onebot11_${selfInfo.uin}.json`);
}
protected getKeys(): string[] | null { protected getKeys(): string[] | null {
return null; return null;
} }

View File

@@ -18,6 +18,7 @@ import {
Group, Group,
GroupMember, GroupMember,
IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, mFaceCache, IMAGE_HTTP_HOST, IMAGE_HTTP_HOST_NT, mFaceCache,
Peer,
RawMessage, RawMessage,
SelfInfo, SelfInfo,
Sex, Sex,
@@ -26,7 +27,6 @@ import {
} from '@/core/entities'; } from '@/core/entities';
import { EventType } from './event/OB11BaseEvent'; import { EventType } from './event/OB11BaseEvent';
import { encodeCQCode } from './cqcode'; import { encodeCQCode } from './cqcode';
import { dbUtil } from '@/common/utils/db';
import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent'; import { OB11GroupIncreaseEvent } from './event/notice/OB11GroupIncreaseEvent';
import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent'; import { OB11GroupBanEvent } from './event/notice/OB11GroupBanEvent';
import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent'; import { OB11GroupUploadNoticeEvent } from './event/notice/OB11GroupUploadNoticeEvent';
@@ -42,7 +42,10 @@ import { ob11Config } from '@/onebot11/config';
import { deleteGroup, getGroupMember, groupMembers, selfInfo, tempGroupCodeMap } from '@/core/data'; import { deleteGroup, getGroupMember, groupMembers, selfInfo, tempGroupCodeMap } from '@/core/data';
import { NTQQFileApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis'; import { NTQQFileApi, NTQQGroupApi, NTQQMsgApi, NTQQUserApi } from '@/core/apis';
import { OB11GroupMsgEmojiLikeEvent } from '@/onebot11/event/notice/OB11MsgEmojiLikeEvent'; import { OB11GroupMsgEmojiLikeEvent } from '@/onebot11/event/notice/OB11MsgEmojiLikeEvent';
import { napCatCore } from '@/core'; import { OB11FriendPokeEvent, OB11GroupPokeEvent } from './event/notice/OB11PokeEvent';
import { OB11BaseNoticeEvent } from './event/notice/OB11BaseNoticeEvent';
import { OB11GroupEssenceEvent } from './event/notice/OB11GroupEssenceEvent';
import { MessageUnique } from '@/common/utils/MessageUnique';
export class OB11Constructor { export class OB11Constructor {
@@ -133,26 +136,26 @@ export class OB11Constructor {
message_data['type'] = 'reply'; message_data['type'] = 'reply';
// log("收到回复消息", element.replyElement.replayMsgSeq) // log("收到回复消息", element.replyElement.replayMsgSeq)
try { try {
// let retData = await NTQQMsgApi.getMsgsBySeqAndCount( let replyMsg = await NTQQMsgApi.getMsgsBySeqAndCount(
// { {
// chatType: msg.chatType, chatType: msg.chatType,
// peerUid: msg.peerUid, peerUid: msg.peerUid,
// guildId: '', guildId: '',
// }, },
// element.replyElement.replayMsgSeq, element.replyElement.replayMsgSeq,
// 1, 1,
// false, true,
// true true
// ); );
// console.log(JSON.stringify(retData, null, 2)); // console.log(JSON.stringify(retData, null, 2));
const replyMsg = await dbUtil.getMsgBySeq(msg.peerUid, element.replyElement.replayMsgSeq); // const replyMsg = await NTQQMsgApi.getMsgsBySeqAndCount({ peerUid: msg.peerUid, guildId: '', chatType: msg.chatType }, element.replyElement.replayMsgSeq, 1, true, true);
// log("找到回复消息", replyMsg.msgShortId, replyMsg.msgId) if (replyMsg) {
if (replyMsg && replyMsg.id) { message_data['data']['id'] = MessageUnique.createMsg({ peerUid: msg.peerUid, guildId: '', chatType: msg.chatType }, replyMsg.msgList[0].msgId)?.toString();
message_data['data']['id'] = replyMsg.id!.toString();
} }
else { else {
continue; continue;
} }
//log("找到回复消息", message_data['data']['id'], replyMsg.msgList[0].msgId)
} catch (e: any) { } catch (e: any) {
logError('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq); logError('获取不到引用的消息', e.stack, element.replyElement.replayMsgSeq);
} }
@@ -162,29 +165,29 @@ export class OB11Constructor {
message_data['type'] = 'image'; message_data['type'] = 'image';
// message_data["data"]["file"] = element.picElement.sourcePath // message_data["data"]["file"] = element.picElement.sourcePath
message_data['data']['file'] = element.picElement.fileName; message_data['data']['file'] = element.picElement.fileName;
message_data['subType']= element.picElement.picSubType; message_data['subType'] = element.picElement.picSubType;
// message_data["data"]["path"] = element.picElement.sourcePath // message_data["data"]["path"] = element.picElement.sourcePath
// let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" // let currentRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
try { try {
message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement, msg.chatType !== ChatType.group); message_data['data']['url'] = await NTQQFileApi.getImageUrl(element.picElement);
} catch (e: any) { } catch (e: any) {
logError('获取图片url失败', e.stack); logError('获取图片url失败', e.stack);
} }
//console.log(message_data['data']['url']) //console.log(message_data['data']['url'])
// message_data["data"]["file_id"] = element.picElement.fileUuid // message_data["data"]["file_id"] = element.picElement.fileUuid
message_data['data']['file_size'] = element.picElement.fileSize; message_data['data']['file_size'] = element.picElement.fileSize;
dbUtil.addFileCache({ // dbUtil.addFileCache({
name: element.picElement.fileName, // name: element.picElement.fileName,
path: element.picElement.sourcePath, // path: element.picElement.sourcePath,
size: element.picElement.fileSize, // size: element.picElement.fileSize,
url: message_data['data']['url'], // url: message_data['data']['url'],
uuid: element.picElement.fileUuid || '', // uuid: element.picElement.fileUuid || '',
msgId: msg.msgId, // msgId: msg.msgId,
element: element.picElement, // element: element.picElement,
elementType: ElementType.PIC, // elementType: ElementType.PIC,
elementId: element.elementId // elementId: element.elementId
}).then(); // }).then();
// 不自动下载图片 // 不自动下载图片
} }
@@ -199,17 +202,18 @@ export class OB11Constructor {
message_data['data']['file_id'] = videoOrFileElement.fileUuid; message_data['data']['file_id'] = videoOrFileElement.fileUuid;
message_data['data']['file_size'] = videoOrFileElement.fileSize; message_data['data']['file_size'] = videoOrFileElement.fileSize;
if (!element.videoElement) { if (!element.videoElement) {
dbUtil.addFileCache({ // dbUtil.addFileCache({
msgId: msg.msgId, // msgId: msg.msgId,
name: videoOrFileElement.fileName, // name: videoOrFileElement.fileName,
path: videoOrFileElement.filePath, // path: videoOrFileElement.filePath,
size: parseInt(videoOrFileElement.fileSize || '0'), // size: parseInt(videoOrFileElement.fileSize || '0'),
uuid: videoOrFileElement.fileUuid || '', // uuid: videoOrFileElement.fileUuid || '',
url: '', // url: '',
element: element.videoElement || element.fileElement, // element: element.videoElement || element.fileElement,
elementType: element.videoElement ? ElementType.VIDEO : ElementType.FILE, // elementType: element.videoElement ? ElementType.VIDEO : ElementType.FILE,
elementId: element.elementId // elementId: element.elementId
}).then(); // }).then();
// }
} }
} }
else if (element.pttElement) { else if (element.pttElement) {
@@ -218,17 +222,17 @@ export class OB11Constructor {
message_data['data']['path'] = element.pttElement.filePath; message_data['data']['path'] = element.pttElement.filePath;
// message_data["data"]["file_id"] = element.pttElement.fileUuid // message_data["data"]["file_id"] = element.pttElement.fileUuid
message_data['data']['file_size'] = element.pttElement.fileSize; message_data['data']['file_size'] = element.pttElement.fileSize;
dbUtil.addFileCache({ // dbUtil.addFileCache({
name: element.pttElement.fileName, // name: element.pttElement.fileName,
path: element.pttElement.filePath, // path: element.pttElement.filePath,
size: parseInt(element.pttElement.fileSize) || 0, // size: parseInt(element.pttElement.fileSize) || 0,
url: '', // url: '',
uuid: element.pttElement.fileUuid || '', // uuid: element.pttElement.fileUuid || '',
msgId: msg.msgId, // msgId: msg.msgId,
element: element.pttElement, // element: element.pttElement,
elementType: ElementType.PTT, // elementType: ElementType.PTT,
elementId: element.elementId // elementId: element.elementId
}).then(); // }).then();
} }
else if (element.arkElement) { else if (element.arkElement) {
@@ -281,16 +285,41 @@ export class OB11Constructor {
else (resMsg.message as OB11MessageData[]).push(message_data); else (resMsg.message as OB11MessageData[]).push(message_data);
resMsg.raw_message += cqCode; resMsg.raw_message += cqCode;
} }
} }
resMsg.raw_message = resMsg.raw_message.trim(); resMsg.raw_message = resMsg.raw_message.trim();
return resMsg; return resMsg;
} }
static async PrivateEvent(msg: RawMessage): Promise<OB11BaseNoticeEvent | undefined> {
if (msg.chatType !== ChatType.friend) {
return;
}
for (const element of msg.elements) {
if (element.grayTipElement) {
if (element.grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(element.grayTipElement.jsonGrayTipElement.jsonStr);
if (element.grayTipElement.jsonGrayTipElement.busiId == 1061) {
//判断业务类型
//Poke事件
let pokedetail: any[] = json.items;
//筛选item带有uid的元素
pokedetail = pokedetail.filter(item => item.uid);
//console.log("[NapCat] 群拍一拍 群:", pokedetail, parseInt(msg.peerUid), " ", await NTQQUserApi.getUinByUid(pokedetail[0].uid), "拍了拍", await NTQQUserApi.getUinByUid(pokedetail[1].uid));
if (pokedetail.length == 2) {
return new OB11FriendPokeEvent(parseInt((await NTQQUserApi.getUinByUid(pokedetail[0].uid))!), parseInt((await NTQQUserApi.getUinByUid(pokedetail[1].uid))!));
}
}
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
}
}
}
}
static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | undefined> { static async GroupEvent(msg: RawMessage): Promise<OB11GroupNoticeEvent | undefined> {
if (msg.chatType !== ChatType.group) { if (msg.chatType !== ChatType.group) {
return; return;
} }
if (msg.senderUin) { //log("group msg", msg);
if (msg.senderUin && msg.senderUin !== '0') {
const member = await getGroupMember(msg.peerUid, msg.senderUin); const member = await getGroupMember(msg.peerUid, msg.senderUin);
if (member && member.cardName !== msg.sendMemberName) { if (member && member.cardName !== msg.sendMemberName) {
const newCardName = msg.sendMemberName || ''; const newCardName = msg.sendMemberName || '';
@@ -299,7 +328,7 @@ export class OB11Constructor {
return event; return event;
} }
} }
// log("group msg", msg);
for (const element of msg.elements) { for (const element of msg.elements) {
const grayTipElement = element.grayTipElement; const grayTipElement = element.grayTipElement;
const groupElement = grayTipElement?.groupElement; const groupElement = grayTipElement?.groupElement;
@@ -369,33 +398,23 @@ export class OB11Constructor {
busid: element.fileElement.fileBizId || 0 busid: element.fileElement.fileBizId || 0
}); });
} }
if (grayTipElement) { if (grayTipElement) {
const xmlElement = grayTipElement.xmlElement; if (grayTipElement.xmlElement?.templId === '10382') {
if (xmlElement?.templId === '10382') {
// 表情回应消息
// "content":
// "<gtip align=\"center\">
// <qq uin=\"u_snYxnEfja-Po_\" col=\"3\" jp=\"3794\"/>
// <nor txt=\"回应了你的\"/>
// <url jp= \"\" msgseq=\"74711\" col=\"3\" txt=\"消息:\"/>
// <face type=\"1\" id=\"76\"> </face>
// </gtip>",
const emojiLikeData = new fastXmlParser.XMLParser({ const emojiLikeData = new fastXmlParser.XMLParser({
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: '' attributeNamePrefix: ''
}).parse(xmlElement.content); }).parse(grayTipElement.xmlElement.content);
logDebug('收到表情回应我的消息', emojiLikeData); logDebug('收到表情回应我的消息', emojiLikeData);
try { try {
const senderUin = emojiLikeData.gtip.qq.jp; const senderUin = emojiLikeData.gtip.qq.jp;
const msgSeq = emojiLikeData.gtip.url.msgseq; const msgSeq = emojiLikeData.gtip.url.msgseq;
const emojiId = emojiLikeData.gtip.face.id; const emojiId = emojiLikeData.gtip.face.id;
const replyMsg = await dbUtil.getMsgBySeq(msg.peerUid, msgSeq); const replyMsgList = (await NTQQMsgApi.getMsgsBySeqAndCount({ chatType: ChatType.group, guildId: '', peerUid: msg.peerUid }, msgSeq, 1, true, true)).msgList;
if (!replyMsg) { if (replyMsgList.length < 1) {
return; return;
} }
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), replyMsg.id!, [{ const replyMsg = replyMsgList[0];
return new OB11GroupMsgEmojiLikeEvent(parseInt(msg.peerUid), parseInt(senderUin), MessageUnique.getShortIdByMsgId(replyMsg?.msgId!)!, [{
emoji_id: emojiId, emoji_id: emojiId,
count: 1 count: 1
}]); }]);
@@ -422,36 +441,40 @@ export class OB11Constructor {
} }
} }
} }
//代码歧义 GrayTipElementSubType.MEMBER_NEW_TITLE
else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) { else if (grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr); const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
/* if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
{ //判断业务类型
align: 'center', //Poke事件
items: [ const pokedetail: any[] = json.items;
{ txt: '恭喜', type: 'nor' }, //筛选item带有uid的元素
{ const poke_uid = pokedetail.filter(item => item.uid);
col: '3', if (poke_uid.length == 2) {
jp: '5', return new OB11GroupPokeEvent(parseInt(msg.peerUid), parseInt((await NTQQUserApi.getUinByUid(poke_uid[0].uid))!), parseInt((await NTQQUserApi.getUinByUid(poke_uid[1].uid))!), pokedetail);
param: ["QQ号"],
txt: '林雨辰',
type: 'url'
},
{ txt: '获得群主授予的', type: 'nor' },
{
col: '3',
jp: '',
txt: '好好好',
type: 'url'
},
{ txt: '头衔', type: 'nor' }
]
} }
}
* */ if (grayTipElement.jsonGrayTipElement.busiId == 2401) {
const memberUin = json.items[1].param[0]; const searchParams = new URL(json.items[0].jp).searchParams;
const title = json.items[3].txt; const msgSeq = searchParams.get('msgSeq')!;
logDebug('收到群成员新头衔消息', json); const Group = searchParams.get('groupCode');
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title); const Businessid = searchParams.get('businessid');
const Peer: Peer = {
guildId: '',
chatType: ChatType.group,
peerUid: Group!
};
const msgData = await NTQQMsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true);
return new OB11GroupEssenceEvent(parseInt(msg.peerUid), MessageUnique.getShortIdByMsgId(msgData.msgList[0].msgId)!, parseInt(msgData.msgList[0].senderUin));
// 获取MsgSeq+Peer可获取具体消息
}
if (grayTipElement.jsonGrayTipElement.busiId == 2407) {
//下面得改 上面也是错的grayTipElement.subElementType == GrayTipElementSubType.MEMBER_NEW_TITLE
const memberUin = json.items[1].param[0];
const title = json.items[3].txt;
logDebug('收到群成员新头衔消息', json);
return new OB11GroupTitleEvent(parseInt(msg.peerUid), parseInt(memberUin), title);
}
} }
} }
} }

View File

@@ -67,6 +67,14 @@ export function encodeCQCode(data: OB11MessageData) {
let result = '[CQ:' + data.type; let result = '[CQ:' + data.type;
for (const name in data.data) { for (const name in data.data) {
const value = data.data[name]; const value = data.data[name];
try {
// Check if the value can be converted to a string
value.toString();
} catch (error) {
// If it can't be converted, skip this name-value pair
// console.warn(`Skipping problematic name-value pair. Name: ${name}, Value: ${value}`);
continue;
}
result += `,${name}=${CQCodeEscape(value)}`; result += `,${name}=${CQCodeEscape(value)}`;
} }
result += ']'; result += ']';

View File

@@ -0,0 +1,14 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
export class OB11GroupEssenceEvent extends OB11GroupNoticeEvent {
notice_type = 'essence';
message_id: number;
sender_id: number;
sub_type: 'add' | 'delete' = 'add';
constructor(groupId: number, message_id: number, sender_id: number) {
super();
this.group_id = groupId;
this.message_id = message_id;
this.sender_id = sender_id;
}
}

View File

@@ -1,6 +1,4 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent'; import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
import { dbUtil } from '@/common/utils/db';
import { ob11Config } from '@/onebot11/config';
type GroupIncreaseSubType = 'approve' | 'invite'; type GroupIncreaseSubType = 'approve' | 'invite';
export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent { export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
@@ -13,8 +11,5 @@ export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent {
this.operator_id = operatorId; this.operator_id = operatorId;
this.user_id = userId; this.user_id = userId;
this.sub_type = subType; this.sub_type = subType;
if(ob11Config.GroupLocalTime.Record && (ob11Config.GroupLocalTime.RecordList[0] == '-1' || ob11Config.GroupLocalTime.RecordList.includes(groupId.toString())))
dbUtil.insertJoinTime(groupId, userId, Math.floor(Date.now() / 1000));
} }
} }

View File

@@ -1,6 +1,6 @@
import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent { export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent {
group_id: number; group_id: number = 0;
user_id: number; user_id: number = 0;
} }

View File

@@ -2,29 +2,30 @@ import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent';
import { selfInfo } from '@/core/data'; import { selfInfo } from '@/core/data';
import { OB11BaseEvent } from '../OB11BaseEvent'; import { OB11BaseEvent } from '../OB11BaseEvent';
class OB11PokeEvent extends OB11BaseNoticeEvent{ class OB11PokeEvent extends OB11BaseNoticeEvent {
notice_type = 'notify'; notice_type = 'notify';
sub_type = 'poke'; sub_type = 'poke';
target_id = parseInt(selfInfo.uin); target_id = 0;
user_id: number; user_id = 0;
} }
export class OB11FriendPokeEvent extends OB11PokeEvent{ export class OB11FriendPokeEvent extends OB11PokeEvent {
sender_id: number; constructor(user_id: number, target_id: number) {
constructor(user_id: number) {
super(); super();
this.target_id = target_id;
this.user_id = user_id; this.user_id = user_id;
this.sender_id = user_id;
} }
} }
export class OB11GroupPokeEvent extends OB11PokeEvent{ export class OB11GroupPokeEvent extends OB11PokeEvent {
group_id: number; group_id: number;
raw_message: any;
constructor(group_id: number, user_id: number=0) { constructor(group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) {
super(); super();
this.group_id = group_id; this.group_id = group_id;
this.target_id = user_id; this.target_id = target_id;
this.user_id = user_id; this.user_id = user_id;
this.raw_message = raw_message;
} }
} }

View File

@@ -63,6 +63,9 @@ export async function logMessage(ob11Message: OB11Message) {
else if (segment.type === 'markdown') { else if (segment.type === 'markdown') {
msgParts.push(spSegColor(`[markdown|${segment.data.content}]`)); msgParts.push(spSegColor(`[markdown|${segment.data.content}]`));
} }
else if (segment.type === 'video') {
msgParts.push(spSegColor(`[视频|${segment.data.url}]`));
}
else { else {
msgParts.push(spSegColor(`[未实现|${JSON.stringify(segment)}]`)); msgParts.push(spSegColor(`[未实现|${JSON.stringify(segment)}]`));
} }

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