Compare commits

...

170 Commits

Author SHA1 Message Date
手瓜一十雪
607dd68620 refactor: 标准化与提高缓存策略 2025-01-13 20:35:52 +08:00
手瓜一十雪
7c8cbc0799 style: lint 2025-01-13 20:30:57 +08:00
手瓜一十雪
ec0c2e8c33 refactor: 大部分文件处理部分 2025-01-13 20:30:08 +08:00
手瓜一十雪
7f3dbe0552 fix: groupfile file_size 2025-01-13 19:38:29 +08:00
手瓜一十雪
0e9044e0c8 drop: umami 2025-01-13 19:32:34 +08:00
手瓜一十雪
3171640193 Merge pull request #701 from Stapxs/main
HTTP SSE 消息上报模式
2025-01-13 19:26:56 +08:00
手瓜一十雪
a56cee3485 fix: #682 2025-01-13 19:25:08 +08:00
pk5ls20
c8ee371982 feat: packet ocr 2025-01-12 10:13:12 +08:00
pk5ls20
5778daeb60 refactor: packet 2025-01-11 16:23:02 +08:00
手瓜一十雪
f51f3b9861 fix 2025-01-11 14:29:44 +08:00
Mlikiowa
44dd1a0b02 release: v4.3.4 2025-01-11 04:16:54 +00:00
pk5ls20
61a00ffcbf feat: 31245 2025-01-11 12:10:46 +08:00
pk5ls20
4b0a0f0a32 feat: #702 2025-01-10 15:23:40 +08:00
stapxs
a3088fb8bc 质量保障 2025-01-09 13:42:55 +08:00
stapxs
88fd1f9eb1 http sse 消息上报模式 2025-01-09 13:22:46 +08:00
手瓜一十雪
15156bac1e fix: log 2025-01-07 20:49:49 +08:00
Mlikiowa
a898d2e7be release: v4.3.3 2025-01-07 11:31:56 +00:00
手瓜一十雪
95b003802c fix: win 31245 2025-01-07 19:31:35 +08:00
Mlikiowa
95c9eae4ed release: v4.3.2 2025-01-07 11:30:35 +00:00
手瓜一十雪
e3814403e4 fix: #682 2025-01-07 19:28:46 +08:00
手瓜一十雪
3d16d52dd8 fix: #696 fallback 2025-01-07 18:51:04 +08:00
Mlikiowa
1ae47fffb4 release: v4.2.68 2025-01-06 13:08:43 +00:00
手瓜一十雪
4e7096b9e2 feat: offset win 31219 2025-01-06 21:08:18 +08:00
Mlikiowa
8cc9b7f6a7 release: v4.2.67 2025-01-05 02:50:38 +00:00
手瓜一十雪
fb45c1020e revert: 再也不用umami了 2025-01-05 10:49:49 +08:00
Mlikiowa
e9db4ae8f4 release: v4.2.66 2025-01-04 11:52:38 +00:00
手瓜一十雪
c46ec32bd6 fix: 简化代码 2025-01-04 13:12:04 +08:00
手瓜一十雪
c58a26ed99 fix 2025-01-04 13:06:37 +08:00
手瓜一十雪
a66f5e4971 fix 2025-01-04 13:03:29 +08:00
pk5ls20
574c8c6089 fix: SendGroupAiRecord
- smtx
2025-01-04 12:48:53 +08:00
手瓜一十雪
67afd95910 feat: getUserDetailInfoV2 2025-01-03 21:38:57 +08:00
手瓜一十雪
f7d0cb0be7 fix 2025-01-03 21:07:26 +08:00
手瓜一十雪
be9b68a0b1 feat: CancelableTask&Fallback 2025-01-03 20:46:51 +08:00
Mlikiowa
4637414af2 release: v4.2.65 2025-01-03 05:14:05 +00:00
手瓜一十雪
4bd92a72bd fix: ua agent 2025-01-03 13:09:43 +08:00
手瓜一十雪
a3be26f3e4 fix: error 2025-01-03 12:44:55 +08:00
Mlikiowa
675c906cbf release: v4.2.64 2024-12-31 14:42:17 +00:00
手瓜一十雪
6be6023236 Merge pull request #679 from wu-yafeng/main
fix #678
2024-12-31 22:41:34 +08:00
WuYafeng
42cee0d018 在没有写入权限时记录warn日志 2024-12-31 22:38:01 +08:00
WuYafeng
041f725748 Update config.ts 2024-12-31 22:25:05 +08:00
WuYafeng
0594d61631 Update config.ts 2024-12-31 22:20:34 +08:00
Mlikiowa
15cae6b765 release: v4.2.63 2024-12-31 13:52:37 +00:00
pk5ls20
b984116c35 fix: #677 2024-12-31 21:34:45 +08:00
Mlikiowa
13bda6e3f4 release: v4.2.62 2024-12-31 08:35:55 +00:00
手瓜一十雪
c0d18549d1 fix 2024-12-31 15:09:08 +08:00
手瓜一十雪
3caff72fce fix: cache trace 2024-12-31 13:10:58 +08:00
Mlikiowa
1313e9c3f4 release: v4.2.61 2024-12-30 15:59:33 +00:00
手瓜一十雪
0848d5a39e feat: clouflare 2024-12-30 23:59:09 +08:00
手瓜一十雪
7660646059 fix 2024-12-30 22:16:09 +08:00
Mlikiowa
bcd90fc744 release: v4.2.60 2024-12-30 12:48:11 +00:00
手瓜一十雪
638fc22d62 fix:error 2024-12-30 20:47:50 +08:00
手瓜一十雪
c87d365b88 fix 2024-12-30 20:42:23 +08:00
Mlikiowa
aee9602f25 release: v4.2.59 2024-12-30 12:29:33 +00:00
手瓜一十雪
976fbd0220 fix: 修复精确统计 2024-12-30 20:27:28 +08:00
手瓜一十雪
afd955d06f fix: error 2024-12-30 20:14:51 +08:00
手瓜一十雪
4d548da66b fix: console-log 2024-12-30 19:56:06 +08:00
手瓜一十雪
41b70f53d1 fix: LoginErrorCode 2024-12-30 19:29:14 +08:00
Wesley F. Young
a47a618bcd make author NapNeko in manifest.json
毕竟我已经脱离一线开发挺久了(
2024-12-30 18:40:57 +08:00
Mlikiowa
62170a30af release: v4.2.58 2024-12-30 09:56:02 +00:00
手瓜一十雪
780c5ac23c fix 2024-12-30 17:55:40 +08:00
手瓜一十雪
9fba519a5a fix 2024-12-30 17:55:13 +08:00
Mlikiowa
3cd0e7d26b release: v4.2.57 2024-12-30 09:54:40 +00:00
手瓜一十雪
a8fd6af994 Merge pull request #676 from NapNeko/dependabot/npm_and_yarn/commander-13.0.0
chore(deps-dev): bump commander from 12.1.0 to 13.0.0
2024-12-30 17:54:03 +08:00
手瓜一十雪
4000b89644 fix 2024-12-30 17:53:05 +08:00
Mlikiowa
9c00bbc0b7 release: v4.2.56 2024-12-30 08:55:36 +00:00
手瓜一十雪
a2989d3b38 fix:workname 2024-12-30 16:54:40 +08:00
Mlikiowa
fc731b60d5 release: v4.2.54 2024-12-30 08:47:57 +00:00
手瓜一十雪
193980dd4a fix: trace 2024-12-30 16:47:33 +08:00
dependabot[bot]
35427b0768 chore(deps-dev): bump commander from 12.1.0 to 13.0.0
Bumps [commander](https://github.com/tj/commander.js) from 12.1.0 to 13.0.0.
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tj/commander.js/compare/v12.1.0...v13.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 08:36:27 +00:00
Mlikiowa
73ea130e40 release: v4.2.53 2024-12-30 08:34:23 +00:00
手瓜一十雪
5667e6aaee fix 2024-12-30 16:34:01 +08:00
Mlikiowa
fbd626131d release: v4.2.52 2024-12-30 08:17:53 +00:00
手瓜一十雪
7b82444338 fix 2024-12-30 16:17:29 +08:00
Mlikiowa
8108b9f565 release: v4.2.51 2024-12-30 08:15:48 +00:00
手瓜一十雪
c6ddd00cd9 Merge pull request #675 from NapNeko/test
fix: 重写远程遥测
2024-12-30 16:14:48 +08:00
手瓜一十雪
20c0c00fa0 fixfix 2024-12-30 16:14:32 +08:00
手瓜一十雪
1f90364ba6 fix: 重写远程遥测 2024-12-30 16:10:38 +08:00
手瓜一十雪
49ea4d31a5 fix 2024-12-30 13:23:33 +08:00
Mlikiowa
dc35f1456a release: v4.2.50 2024-12-30 04:33:08 +00:00
手瓜一十雪
0ebeb90804 fix 2024-12-30 12:31:43 +08:00
手瓜一十雪
3ef5436c98 Merge pull request #673 from NapNeko/fixseq
fix: #670
2024-12-30 12:28:29 +08:00
手瓜一十雪
de7996d789 refactor: 进群入群 2024-12-30 12:27:42 +08:00
手瓜一十雪
ac52d9bae2 fix: 算了能用就行 2024-12-30 11:59:16 +08:00
手瓜一十雪
cb02df3b76 Merge pull request #672 from clansty/feat/download_file-file-proto
feat: download_file 支持 file://
2024-12-29 22:45:27 +08:00
手瓜一十雪
5fc5a6f1a6 fix: #670 2024-12-29 22:44:25 +08:00
手瓜一十雪
726a0d0394 fix 2024-12-29 22:30:33 +08:00
Clansty
6edf5345a3 chore: remove unused import 2024-12-29 22:27:41 +08:00
Clansty
242bbfdb14 chore: uriToLocalFile 2024-12-29 22:03:04 +08:00
Clansty
89e7712676 feat: download_file 支持 file:// 2024-12-29 21:41:44 +08:00
手瓜一十雪
9525786929 fix 2024-12-29 16:20:41 +08:00
Mlikiowa
72088e41a8 release: v4.2.46 2024-12-29 07:47:38 +00:00
手瓜一十雪
a3ed9ff2ef Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-29 15:47:18 +08:00
手瓜一十雪
ff16dc73ec fix: typo 2024-12-29 15:47:16 +08:00
Mlikiowa
2da4ef5f0f release: v4.2.45 2024-12-29 07:46:08 +00:00
手瓜一十雪
eaf481799d feat: 追踪退出 2024-12-29 15:44:37 +08:00
手瓜一十雪
1f72863aba fix: 追踪登录失败 2024-12-29 15:25:51 +08:00
Mlikiowa
6b353fd8d8 release: v4.2.44 2024-12-29 06:45:43 +00:00
手瓜一十雪
56cde4ad79 fix: #669 2024-12-29 14:44:47 +08:00
手瓜一十雪
3b86d3c632 feat: umami 统计追踪 2024-12-29 14:39:17 +08:00
Mlikiowa
4ac7a25afb release: v4.2.43 2024-12-29 04:04:55 +00:00
手瓜一十雪
8248011a12 fix: #667 2024-12-29 11:54:09 +08:00
手瓜一十雪
5f454456d2 fix: #655 #663 2024-12-29 11:47:09 +08:00
手瓜一十雪
e99a619c23 Merge pull request #665 from clansty/feat/group_name
fix: wrong notice_type sub_type
2024-12-28 21:42:14 +08:00
Clansty
1fc791bb68 fix: wrong notice_type sub_type 2024-12-28 21:40:32 +08:00
手瓜一十雪
f1d83f7c16 Merge pull request #664 from clansty/feat/group_name
feat: 群名称变更事件
2024-12-28 21:05:50 +08:00
Clansty
527bb72bcf feat: 群名称变更事件 2024-12-28 20:54:41 +08:00
手瓜一十雪
d78409fd07 Merge pull request #662 from Shua-github/main
新增send_poke
2024-12-27 15:16:11 +08:00
手瓜一十雪
d5e7e8944f feat: send_poke 2024-12-27 15:14:57 +08:00
Shua-github
fb405a5c1c all_poke替换成send_poke 2024-12-27 13:15:10 +08:00
Shua-github
a9e471deca 新增all_poke 2024-12-27 00:24:35 +08:00
Mlikiowa
9cd15ae337 release: v4.2.42 2024-12-26 12:37:35 +00:00
手瓜一十雪
8ed4cc4b0a feat: send_packet 2024-12-26 20:36:53 +08:00
Mlikiowa
a62de441cf release: v4.2.41 2024-12-26 05:31:09 +00:00
手瓜一十雪
02a8999410 Merge pull request #652 from JerryZRF/main
feat: add `get_clientkey`
2024-12-25 12:51:44 +08:00
手瓜一十雪
59c7979d69 readme: new 2024-12-25 12:28:53 +08:00
手瓜一十雪
bb7b28cd8f feat: 调整logo 2024-12-25 12:24:03 +08:00
手瓜一十雪
056497b98a Merge pull request #657 from FfmpegZZZ/main
chore:修改文档链接
2024-12-24 20:22:59 +08:00
手瓜一十雪
ac2fb032c4 Merge branch 'main' into main 2024-12-24 20:22:40 +08:00
Ffmpeg
c933bdd5d9 chore:修改链接
## 我是猪咪

文档链接打错了
2024-12-24 20:07:11 +08:00
Ffmpeg
89c71a58fa 添加文档地址 (#656) 2024-12-24 19:45:34 +08:00
Ffmpeg
27ba85b4ff 添加文档地址 2024-12-24 19:41:35 +08:00
手瓜一十雪
79a75fed8e feat: 30899 2024-12-24 15:38:53 +08:00
Mlikiowa
ee7a76b29f release: v4.2.40 2024-12-24 07:29:39 +00:00
手瓜一十雪
c53bdc3ce0 feat: 30899 2024-12-24 15:19:44 +08:00
Mlikiowa
f36e328751 release: v4.2.39 2024-12-22 13:32:51 +00:00
pk5ls20
871b5688c2 feat: webui api (/QQVersion & /GetSysStatusRealTime) 2024-12-22 21:31:14 +08:00
JerryZRF
b96076b297 fix: incorrect import 2024-12-22 13:28:20 +08:00
pk5ls20
d4488e40cf feat: better system status helper
- cpu usage diff
2024-12-22 03:56:24 +08:00
pk5ls20
7e61497243 chore: remove console logs 2024-12-22 02:57:00 +08:00
pk5ls20
e71ccdd12a feat: system status helper
- remove duplicate os import
2024-12-22 02:55:49 +08:00
pk5ls20
202129d491 feat: system status helper
- remove pidusage
2024-12-22 02:24:10 +08:00
JerryZRF
a1700dd503 fix: incorrect import 2024-12-22 01:33:42 +08:00
JerryZRF
2954776539 feat: add get_clientkey 2024-12-21 20:43:15 +08:00
手瓜一十雪
fb1f122ef7 feat: 9.9.17-30851 2024-12-21 14:29:41 +08:00
手瓜一十雪
96c63e4689 refactor: 移除不再使用的代码 2024-12-21 14:19:03 +08:00
手瓜一十雪
c94936d3dc refactor: #637 2024-12-21 14:09:57 +08:00
pk5ls20
8c22f11087 feat: system status helper 2024-12-21 13:11:10 +08:00
Mlikiowa
8a089c84a9 release: v4.2.38 2024-12-20 11:46:02 +00:00
手瓜一十雪
b631e6f8a2 fix: 更精确的筛选 2024-12-20 19:44:57 +08:00
手瓜一十雪
b3b48b032c fix: 锁住esbuild版本 以缓解问题 2024-12-20 19:32:20 +08:00
手瓜一十雪
f3e8230eca fix: 暂时指定esbuild版本以缓解上游破坏
see https://github.com/evanw/esbuild/issues/4010
2024-12-20 19:30:30 +08:00
手瓜一十雪
cc9adf9d40 feat: 过滤空消息 2024-12-20 18:58:59 +08:00
手瓜一十雪
15a640d1dc fix: #637 2024-12-20 18:55:30 +08:00
凌莞~(=^▽^=)
c25b9f86db 优化私聊戳一戳事件上报 (#643) 2024-12-18 21:27:44 +08:00
Mlikiowa
ecfd033afb release: v4.2.37 2024-12-17 07:56:18 +00:00
手瓜一十雪
f3ed8c7dff fix #633 2024-12-17 15:55:18 +08:00
Mlikiowa
6089046721 release: v4.2.36 2024-12-17 03:37:26 +00:00
手瓜一十雪
44ff92ad4b style: lint 2024-12-17 09:25:10 +08:00
手瓜一十雪
892262eb85 Merge pull request #635 from NapNeko/ref/ob-network
refactor: adjust onebot network
2024-12-17 09:07:39 +08:00
pk5ls20
2d9cc4d198 fix: as design 2024-12-17 07:07:04 +08:00
pk5ls20
a0c479485d refactor: adjust onebot network 2024-12-17 05:26:27 +08:00
手瓜一十雪
5650f18e50 Merge pull request #634 from bietiaop/main
fix: handleQuickOperation error
2024-12-17 00:29:16 +08:00
bietiaop
553885d025 fix: handleQuickOperation 2024-12-17 00:27:56 +08:00
Mlikiowa
35de00c4af release: v4.2.35 2024-12-16 14:10:08 +00:00
手瓜一十雪
09583e5de5 fuck javascript 2024-12-16 22:09:37 +08:00
Mlikiowa
38b0b7cd00 release: v4.2.34 2024-12-16 13:17:43 +00:00
手瓜一十雪
8b9c7b0c27 Merge pull request #632 from q8018414/patch-1
Update AboutUs.vue
2024-12-16 21:16:39 +08:00
手瓜一十雪
1005619bf3 Merge pull request #630 from NapNeko/dependabot/npm_and_yarn/rollup/plugin-typescript-12.1.2
chore(deps-dev): bump @rollup/plugin-typescript from 11.1.6 to 12.1.2
2024-12-16 21:16:01 +08:00
手瓜一十雪
3e09cff9cb Merge branch 'main' into dependabot/npm_and_yarn/rollup/plugin-typescript-12.1.2 2024-12-16 21:15:52 +08:00
手瓜一十雪
c24384e454 Merge pull request #629 from NapNeko/dependabot/npm_and_yarn/rollup/plugin-node-resolve-16.0.0
chore(deps-dev): bump @rollup/plugin-node-resolve from 15.3.1 to 16.0.0
2024-12-16 21:15:23 +08:00
手瓜一十雪
f87a543406 fix: #631 2024-12-16 21:14:14 +08:00
手瓜一十雪
f752136283 fix: #631 2024-12-16 21:06:51 +08:00
my_key
7e71622a44 Update AboutUs.vue
新增用于显示New NapCat的tag,便于区分当前版本和最新版本
2024-12-16 20:09:40 +08:00
dependabot[bot]
da92afb379 chore(deps-dev): bump @rollup/plugin-typescript from 11.1.6 to 12.1.2
Bumps [@rollup/plugin-typescript](https://github.com/rollup/plugins/tree/HEAD/packages/typescript) from 11.1.6 to 12.1.2.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/typescript/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/typescript-v12.1.2/packages/typescript)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 08:44:42 +00:00
dependabot[bot]
d3062de5f9 chore(deps-dev): bump @rollup/plugin-node-resolve from 15.3.1 to 16.0.0
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 15.3.1 to 16.0.0.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/commonjs-v16.0.0/packages/node-resolve)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 08:43:46 +00:00
Mlikiowa
f1440b03a8 release: v4.2.33 2024-12-16 05:03:59 +00:00
手瓜一十雪
9a8b266cef Merge pull request #627 from bietiaop/main
feat: 查看登录QQ信息&获取快速登录列表详细信息&获取nc的包信息&优化了部分写法
2024-12-16 13:03:09 +08:00
手瓜一十雪
2a9bc57120 fix: #628 2024-12-16 13:00:07 +08:00
bietiaop
2ed83a0e30 feat: 查看登录QQ信息&获取快速登录列表详细信息&获取nc的包信息&优化了部分写法 2024-12-16 12:46:27 +08:00
Mlikiowa
116e8fd30a release: v4.2.32 2024-12-14 07:03:02 +00:00
104 changed files with 1833 additions and 713 deletions

View File

@@ -1,6 +1,6 @@
<div align="center">
![Logo](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto)
![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
</div>
@@ -32,10 +32,12 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
[Server.Other](https://docs.napcat.cyou/)
[Qbot.News](https://neko.qbot.news)
## 回家旅途
[QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq)
[QQ Group#2](https://qm.qq.com/q/uqh4I87KoM)
[QQ Group#2](https://qm.qq.com/q/HaRcfrHpUk)
[Telegram](https://t.me/MelodicMoonlight)

BIN
external/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View File

@@ -1,9 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.16-29927",
"verHash": "3e273e30",
"linuxVersion": "3.2.13-29927",
"linuxVerHash": "833d113c",
"version": "9.9.17-30899",
"verHash": "ececf273",
"linuxVersion": "3.2.15-30899",
"linuxVerHash": "63c751e8",
"type": "module",
"private": true,
"description": "QQ",
@@ -18,7 +18,7 @@
"qd": "externals/devtools/cli/index.js"
},
"main": "./loadNapCat.js",
"buildVersion": "29927",
"buildVersion": "30899",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 684 KiB

View File

@@ -4,16 +4,12 @@
"name": "NapCatQQ",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
"version": "4.2.31",
"version": "4.3.4",
"icon": "./logo.png",
"authors": [
{
"name": "MliKiowa",
"link": "https://github.com/MliKiowa"
},
{
"name": "Young",
"link": "https://github.com/Wesley-Young"
"name": "NapNeko",
"link": "https://github.com/NapNeko"
}
],
"repository": {

View File

@@ -89,7 +89,11 @@
<t-tag class="tag-item pgk-color"> WebUi: {{ pkg.version }} </t-tag>
<t-tag class="tag-item nc-color">
NapCat:
{{ githubReleasesData&&githubReleasesData[0] ?.tag_name ? githubReleasesData[0].tag_name : napCatVersion }}
{{ napCatVersion }}
</t-tag>
<t-tag v-if="githubReleasesData&&githubReleasesData[0] ?.tag_name" class="tag-item nc-color">
New NapCat:
{{ githubReleasesData[0].tag_name }}
</t-tag>
<t-tag class="tag-item td-color"> TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </t-tag>
</span>

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.2.31",
"version": "4.3.4",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -17,14 +17,15 @@
"dev:depend": "npm i && cd napcat.webui && npm i"
},
"devDependencies": {
"esbuild": "0.24.0",
"@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.2",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@types/cors": "^2.8.17",
"@sinclair/typebox": "^0.34.9",
"@types/express": "^5.0.0",
@@ -36,7 +37,7 @@
"@typescript-eslint/parser": "^8.3.0",
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^12.1.0",
"commander": "^13.0.0",
"cors": "^2.8.5",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1",
@@ -60,4 +61,4 @@
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}
}
}

107
src/common/cancel-task.ts Normal file
View File

@@ -0,0 +1,107 @@
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void;
export class CancelableTask<T> {
private promise: Promise<T>;
private cancelCallback: (() => void) | null = null;
private isCanceled = false;
private cancelListeners: Array<() => void> = [];
constructor(executor: TaskExecutor<T>) {
this.promise = new Promise<T>((resolve, reject) => {
const onCancel = (callback: () => void) => {
this.cancelCallback = callback;
};
executor(
(value) => {
if (!this.isCanceled) {
resolve(value);
}
},
(reason) => {
if (!this.isCanceled) {
reject(reason);
}
},
onCancel
);
});
}
public cancel() {
if (this.cancelCallback) {
this.cancelCallback();
}
this.isCanceled = true;
this.cancelListeners.forEach(listener => listener());
}
public isTaskCanceled(): boolean {
return this.isCanceled;
}
public onCancel(listener: () => void) {
this.cancelListeners.push(listener);
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected);
}
public catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
): Promise<T | TResult> {
return this.promise.catch(onrejected);
}
public finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.promise.finally(onfinally);
}
[Symbol.asyncIterator]() {
return {
next: () => this.promise.then(value => ({ value, done: true })),
};
}
}
async function demoAwait() {
const executor: TaskExecutor<number> = (resolve, reject, onCancel) => {
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log(`Task is running... Count: ${count}`);
if (count === 5) {
clearInterval(intervalId);
resolve(count);
}
}, 1000);
onCancel(() => {
clearInterval(intervalId);
console.log('Task has been canceled.');
reject(new Error('Task was canceled'));
});
};
const task = new CancelableTask(executor);
task.onCancel(() => {
console.log('Cancel listener triggered.');
});
setTimeout(() => {
task.cancel(); // 取消任务
}, 6000);
try {
const result = await task;
console.log(`Task completed with result: ${result}`);
} catch (error) {
console.error('Task failed:', error);
}
}

22
src/common/decorator.ts Normal file
View File

@@ -0,0 +1,22 @@
// decoratorAsyncMethod(this,function,wrapper)
async function decoratorMethod<T, R>(
target: T,
method: () => Promise<R>,
wrapper: (result: R) => Promise<any>,
executeImmediately: boolean = true
): Promise<any> {
const execute = async () => {
try {
const result = await method.call(target);
return wrapper(result);
} catch (error) {
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
}
};
if (executeImmediately) {
return execute();
} else {
return execute;
}
}

43
src/common/fall-back.ts Normal file
View File

@@ -0,0 +1,43 @@
type Handler<T> = () => T | Promise<T>;
type Checker<T> = (result: T) => T | Promise<T>;
export class Fallback<T> {
private handlers: Handler<T>[] = [];
private checker: Checker<T>;
constructor(checker?: Checker<T>) {
this.checker = checker || (async (result: T) => result);
}
add(handler: Handler<T>): this {
this.handlers.push(handler);
return this;
}
// 执行处理程序链
async run(): Promise<T> {
const errors: Error[] = [];
for (const handler of this.handlers) {
try {
const result = await handler();
const data = await this.checker(result);
if (data) {
return data;
}
} catch (error) {
console.log(error);
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
throw new AggregateError(errors, 'All handlers failed');
}
}
export class FallbackUtil {
static boolchecker<T>(value: T, condition: boolean): T {
if (condition) {
return value;
} else {
throw new Error('Condition is false, throwing error');
}
}
}

93
src/common/file-uuid.ts Normal file
View File

@@ -0,0 +1,93 @@
import { Peer } from '@/core';
import { randomUUID } from 'crypto';
interface FileUUIDData {
peer: Peer;
modelId?: string;
fileId?: string;
msgId?: string;
elementId?: string;
fileUUID?: string;
}
class TimeBasedCache<K, V> {
private cache: Map<K, { value: V, timestamp: number }>;
private ttl: number;
constructor(ttl: number) {
this.cache = new Map();
this.ttl = ttl;
}
public put(key: K, value: V): void {
const timestamp = Date.now();
this.cache.set(key, { value, timestamp });
this.cleanup();
}
public get(key: K): V | undefined {
const entry = this.cache.get(key);
if (entry) {
const currentTime = Date.now();
if (currentTime - entry.timestamp < this.ttl) {
return entry.value;
} else {
this.cache.delete(key);
}
}
return undefined;
}
private cleanup(): void {
const currentTime = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (currentTime - entry.timestamp >= this.ttl) {
this.cache.delete(key);
}
}
}
}
class FileUUIDManager {
private cache: TimeBasedCache<string, FileUUIDData>;
constructor(ttl: number) {
this.cache = new TimeBasedCache<string, FileUUIDData>(ttl);
}
public encode(data: FileUUIDData, endString: string = "", customUUID?: string): string {
const uuid = customUUID ? customUUID : randomUUID().replace(/-/g, '') + endString;
this.cache.put(uuid, data);
return uuid;
}
public decode(uuid: string): FileUUIDData | undefined {
return this.cache.get(uuid);
}
}
export class FileNapCatOneBotUUIDWrap {
private manager: FileUUIDManager;
constructor(ttl: number = 86400000) {
this.manager = new FileUUIDManager(ttl);
}
public encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = "", customUUID?: string): string {
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
}
public decodeModelId(uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
public encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", customUUID?: string): string {
return this.manager.encode({ peer, msgId, elementId, fileUUID }, "", customUUID);
}
public decode(uuid: string): FileUUIDData | undefined {
return this.manager.decode(uuid);
}
}
export const FileNapCatOneBotUUID = new FileNapCatOneBotUUIDWrap();

View File

@@ -175,10 +175,9 @@ export async function checkUriType(Uri: string) {
return { Uri: Uri, Type: FileUriType.Unknown };
}
export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2LocalRes> {
export async function uriToLocalFile(dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
const filename = randomUUID();
const filePath = path.join(dir, filename);
switch (UriType) {
@@ -191,7 +190,7 @@ export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2Loca
}
case FileUriType.Remote: {
const buffer = await httpDownload(HandledUri);
const buffer = await httpDownload({ url: HandledUri, headers: headers });
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
}
@@ -206,4 +205,4 @@ export async function uriToLocalFile(dir: string, uri: string): Promise<Uri2Loca
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
}
}
}

View File

@@ -1,7 +1,7 @@
import path from 'node:path';
import fs from 'fs';
import os from 'node:os';
import { Peer, QQLevel } from '@/core';
import { QQLevel } from '@/core';
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
return new Promise<ReturnType<T> | undefined>((resolve) => {
@@ -24,81 +24,6 @@ export async function solveAsyncProblem<T extends (...args: any[]) => Promise<an
});
}
export class FileNapCatOneBotUUID {
static encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = ""): string {
const data = `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}|${fileUUID}`;
//前四个字节塞data长度
const length = Buffer.alloc(4 + data.length);
length.writeUInt32BE(data.length * 2, 0);//储存data的hex长度
length.write(data, 4);
return length.toString('hex') + endString;
}
static decodeModelId(uuid: string): undefined | {
peer: Peer,
modelId: string,
fileId: string,
fileUUID?: string
} {
//前四个字节是data长度
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
//根据length计算需要读取的长度
const dataId = uuid.slice(8, 8 + length);
//hex还原为string
const realData = Buffer.from(dataId, 'hex').toString();
if (!realData.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
const data = realData.split('|');
if (data.length < 6) return undefined; // compatibility requirement
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
return {
peer: {
chatType: +chatType,
peerUid: peerUid,
},
modelId,
fileId,
fileUUID
};
}
static encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", endString: string = ""): string {
const data = `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}|${fileUUID}`;
//前四个字节塞data长度
//一个字节8位 一个ascii字符1字节 一个hex字符4位 表示一个ascii字符需要两个hex字符
const length = Buffer.alloc(4 + data.length);
length.writeUInt32BE(data.length * 2, 0);
length.write(data, 4);
return length.toString('hex') + endString;
}
static decode(uuid: string): undefined | {
peer: Peer,
msgId: string,
elementId: string,
fileUUID?: string
} {
//前四个字节是data长度
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
//根据length计算需要读取的长度
const dataId = uuid.slice(8, 8 + length);
//hex还原为string
const realData = Buffer.from(dataId, 'hex').toString();
if (!realData.startsWith('NapCatOneBot|MsgFile|')) return undefined;
const data = realData.split('|');
if (data.length < 6) return undefined; // compatibility requirement
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
return {
peer: {
chatType: +chatType,
peerUid: peerUid,
},
msgId,
elementId,
fileUUID
};
}
}
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -78,7 +78,7 @@ class MessageUniqueWrapper {
private readonly msgDataMap: LimitedHashTable<string, number>;
private readonly msgIdMap: LimitedHashTable<string, number>;
constructor(maxMap: number = 1000) {
constructor(maxMap: number = 5000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
}

View File

@@ -1 +1 @@
export const napCatVersion = '4.2.31';
export const napCatVersion = '4.3.4';

View File

@@ -26,7 +26,7 @@ export class NTQQGroupApi {
}
async fetchGroupDetail(groupCode: string) {
let [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupDetailInfo',
'NodeIKernelGroupListener/onGroupDetailInfoChange',
[groupCode, GroupInfoSource.KDATACARD],
@@ -44,7 +44,7 @@ export class NTQQGroupApi {
async initCache() {
for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode).then().catch();
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
}
}
@@ -126,14 +126,23 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
}
async refreshGroupMemberCache(groupCode: string) {
try {
const members = await this.getGroupMemberAll(groupCode, true);
this.groupMemberCache.set(groupCode, members.result.infos);
} catch (e) {
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
async refreshGroupMemberCache(groupCode: string, isWait = true) {
const updateCache = async () => {
try {
const members = await this.getGroupMemberAll(groupCode, true);
this.groupMemberCache.set(groupCode, members.result.infos);
} catch (e) {
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
}
};
if (isWait) {
await updateCache();
} else {
updateCache();
}
return this.groupMemberCache;
return this.groupMemberCache.get(groupCode);
}
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
@@ -143,7 +152,7 @@ export class NTQQGroupApi {
// 获取群成员缓存
let members = this.groupMemberCache.get(groupCodeStr);
if (!members) {
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
}
const getMember = () => {
@@ -157,7 +166,7 @@ export class NTQQGroupApi {
let member = getMember();
// 如果缓存中不存在该成员,尝试刷新缓存
if (!member) {
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
member = getMember();
}
return member;

View File

@@ -2,8 +2,7 @@ import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
import { RequestUtil } from '@/common/request';
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
import { solveAsyncProblem } from '@/common/helper';
import { promisify } from 'node:util';
import { LRUCache } from '@/common/lru-cache';
import { Fallback, FallbackUtil } from '@/common/fall-back';
export class NTQQUserApi {
context: InstanceContext;
@@ -108,6 +107,19 @@ export class NTQQUserApi {
return retUser;
}
async getUserDetailInfoV2(uid: string): Promise<User> {
const fallback = new Fallback<User>((user) => FallbackUtil.boolchecker(user, user !== undefined && user.uin !== '0'))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KDB))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER));
const retUser = await fallback.run().then(async (user) => {
if (user && user.uin === '0') {
user.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
}
return user;
});
return retUser;
}
async modifySelfProfile(param: ModifyProfileParams) {
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
}
@@ -169,46 +181,34 @@ export class NTQQUserApi {
return skey;
}
async getUidByUinV2(Uin: string) {
if (!Uin) {
async getUidByUinV2(uin: string) {
if (!uin) {
return '';
}
const services = [
() => this.context.session.getUixConvertService().getUid([Uin]).then((data) => data.uidInfo.get(Uin)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined),
() => this.context.session.getGroupService().getUidByUins([Uin]).then((data) => data.uids.get(Uin)).catch(() => undefined),
() => this.getUserDetailInfoByUin(Uin).then((data) => data.detail.uid).catch(() => undefined),
];
let uid: string | undefined = undefined;
for (const service of services) {
uid = await service();
if (uid && uid.indexOf('*') == -1 && uid !== '') {
break;
}
}
const fallback =
new Fallback<string | undefined>((uid) => FallbackUtil.boolchecker(uid, uid !== undefined && uid.indexOf('*') === -1 && uid !== ''))
.add(() => this.context.session.getUixConvertService().getUid([uin]).then((data) => data.uidInfo.get(uin)))
.add(() => this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [uin]).get(uin))
.add(() => this.context.session.getGroupService().getUidByUins([uin]).then((data) => data.uids.get(uin)))
.add(() => this.getUserDetailInfoByUin(uin).then((data) => data.detail.uid));
const uid = await fallback.run().catch(() => '');
return uid ?? '';
}
async getUinByUidV2(Uid: string) {
if (!Uid) {
async getUinByUidV2(uid: string) {
if (!uid) {
return '0';
}
const services = [
() => this.context.session.getUixConvertService().getUin([Uid]).then((data) => data.uinInfo.get(Uid)).catch(() => undefined),
() => this.context.session.getGroupService().getUinByUids([Uid]).then((data) => data.uins.get(Uid)).catch(() => undefined),
() => promisify<string, string[], Map<string, string>>
(this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined),
() => this.core.apis.FriendApi.getBuddyIdMap(true).then((data) => data.getKey(Uid)).catch(() => undefined),
() => this.getUserDetailInfo(Uid).then((data) => data.uin).catch(() => undefined),
];
let uin: string | undefined = undefined;
for (const service of services) {
uin = await service();
if (uin && uin !== '0' && uin !== '') {
break;
}
}
const fallback = new Fallback<string | undefined>((uin) => FallbackUtil.boolchecker(uin, uin !== undefined && uin !== '0' && uin !== ''))
.add(() => this.context.session.getUixConvertService().getUin([uid]).then((data) => data.uinInfo.get(uid)))
.add(() => this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [uid]).get(uid))
.add(() => this.context.session.getGroupService().getUinByUids([uid]).then((data) => data.uins.get(uid)))
.add(() => this.getUserDetailInfo(uid).then((data) => data.uin));
const uin = await fallback.run().catch(() => '0');
return uin ?? '0';
}

View File

@@ -8,7 +8,7 @@ import {
WebHonorType,
} from '@/core';
import { NapCatCore } from '..';
import { createReadStream, readFileSync, statSync } from 'node:fs';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { basename } from 'node:path';

View File

@@ -134,5 +134,49 @@
"3.2.15-30594": {
"appid": 537258474,
"qua": "V1_LNX_NQ_3.2.15_30594_GW_B"
},
"9.9.17-30851": {
"appid": 537263796,
"qua": "V1_WIN_NQ_9.9.17_30851_GW_B"
},
"3.2.15-30851": {
"appid": 537263831,
"qua": "V1_LNX_NQ_3.2.15_30851_GW_B"
},
"6.9.63-30851": {
"appid": 537263820,
"qua": "V1_MAC_NQ_6.9.63_30851_GW_B"
},
"9.9.17-30899": {
"appid": 537263796,
"qua": "V1_WIN_NQ_9.9.17_30899_GW_B"
},
"3.2.15-30899": {
"appid": 537263831,
"qua": "V1_LNX_NQ_3.2.15_30899_GW_B"
},
"6.9.63-30899": {
"appid": 537263820,
"qua": "V1_MAC_NQ_6.9.63_30899_GW_B"
},
"9.9.17-31219": {
"appid": 537266450,
"qua": "V1_WIN_NQ_9.9.17_31219_GW_B"
},
"9.9.17-31245": {
"appid": 537266450,
"qua": "V1_WIN_NQ_9.9.17_31245_GW_B"
},
"3.2.15-31245": {
"appid": 537266485,
"qua": "V1_LNX_NQ_3.2.15_31245_GW_B"
},
"6.9.63-31245": {
"appid": 537266474,
"qua": "V1_MAC_NQ_6.9.63_31245_GW_B"
},
"9.9.17-31363": {
"appid": 537266500,
"qua": "V1_WIN_NQ_9.9.17_31363_GW_B"
}
}

View File

@@ -162,5 +162,73 @@
"3.2.15-30594-arm64": {
"send": "70C40E8",
"recv": "70C7920"
},
"9.9.17-30851-x64": {
"send": "395C150",
"recv": "3960584"
},
"3.2.15-30851-x64": {
"send": "A4A03E0",
"recv": "A4A3CE0"
},
"3.2.15-30851-arm64": {
"send": "713A318",
"recv": "713DB50"
},
"6.9.63.30851-x64": {
"send": "46C8040",
"recv": "46CA8AC"
},
"6.9.63-30851-arm64": {
"send": "41DCBD8",
"recv": "41DF3F0"
},
"9.9.17-30899-x64": {
"send": "395C150",
"recv": "3960584"
},
"3.2.15-30899-x64": {
"send": "A4A03E0",
"recv": "A4A3CE0"
},
"3.2.15-30899-arm64": {
"send": "713A318",
"recv": "713DB50"
},
"6.9.63.30899-x64": {
"send": "46C8040",
"recv": "46CA8AC"
},
"6.9.63-30899-arm64": {
"send": "41DCBD8",
"recv": "41DF3F0"
},
"9.9.17-31219-x64": {
"send": "39C1350",
"recv": "39C5784"
},
"9.9.17-31245-x64": {
"send": "39C1350",
"recv": "39C5784"
},
"6.9.63.31245-x64": {
"send": "4720A40",
"recv": "47232AC"
},
"6.9.63-31245-arm64": {
"send": "41DCBD8",
"recv": "422D4E8"
},
"3.2.15-31245-x64": {
"send": "A550F80",
"recv": "A554880"
},
"3.2.15-31245-arm64": {
"send": "71BEBB8",
"recv": "71C23F0"
},
"9.9.17-31363-x64": {
"send": "39C1910",
"recv": "39C5d44"
}
}

View File

@@ -1,49 +0,0 @@
// TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const BodyInner = {
msgType: ProtoField(1, ScalarType.UINT32, true),
subType: ProtoField(2, ScalarType.UINT32, true)
};
const NoifyData = {
skip: ProtoField(1, ScalarType.BYTES, true),
innerData: ProtoField(2, ScalarType.BYTES, true)
};
const MsgHead = {
bodyInner: ProtoField(2, () => BodyInner, true),
noifyData: ProtoField(3, () => NoifyData, true)
};
const Message = {
msgHead: ProtoField(1, () => MsgHead)
};
const SubDetail = {
msgSeq: ProtoField(1, ScalarType.UINT32),
msgTime: ProtoField(2, ScalarType.UINT32),
senderUid: ProtoField(6, ScalarType.STRING)
};
const RecallDetails = {
operatorUid: ProtoField(1, ScalarType.STRING),
subDetail: ProtoField(3, () => SubDetail)
};
const RecallGroup = {
type: ProtoField(1, ScalarType.INT32),
peerUid: ProtoField(4, ScalarType.UINT32),
recallDetails: ProtoField(11, () => RecallDetails),
grayTipsSeq: ProtoField(37, ScalarType.UINT32)
};
export function decodeMessage(buffer: Uint8Array) {
const msg = new NapProtoMsg(Message);
return msg.decode(buffer);
}
export function decodeRecallGroup(buffer: Uint8Array){
const msg = new NapProtoMsg(RecallGroup);
return msg.decode(buffer);
}

138
src/core/helper/status.ts Normal file
View File

@@ -0,0 +1,138 @@
import os from "node:os";
import EventEmitter from "node:events";
export interface SystemStatus {
cpu: {
model: string,
speed: string
usage: {
system: string
qq: string
},
core: number
},
memory: {
total: string
usage: {
system: string
qq: string
}
},
arch: string
}
export class StatusHelper {
private psCpuUsage = process.cpuUsage();
private psCurrentTime = process.hrtime();
private cpuTimes = os.cpus().map(cpu => cpu.times);
private replaceNaN(value: number) {
return isNaN(value) ? 0 : value;
}
private sysCpuInfo() {
const currentTimes = os.cpus().map(cpu => cpu.times);
const { total, active } = currentTimes.map((times, index) => {
const prevTimes = this.cpuTimes[index];
const totalCurrent = times.user + times.nice + times.sys + times.idle + times.irq;
const totalPrev = prevTimes.user + prevTimes.nice + prevTimes.sys + prevTimes.idle + prevTimes.irq;
const activeCurrent = totalCurrent - times.idle;
const activePrev = totalPrev - prevTimes.idle;
return {
total: totalCurrent - totalPrev,
active: activeCurrent - activePrev
};
}).reduce((acc, cur) => ({
total: acc.total + cur.total,
active: acc.active + cur.active
}), { total: 0, active: 0 });
this.cpuTimes = currentTimes;
return {
usage: this.replaceNaN(((active / total) * 100)).toFixed(2),
model: os.cpus()[0].model,
speed: os.cpus()[0].speed,
core: os.cpus().length
};
}
private sysMemoryUsage() {
const { total, free } = { total: os.totalmem(), free: os.freemem() };
return ((total - free) / 1024 / 1024).toFixed(2);
}
private qqUsage() {
const mem = process.memoryUsage();
const numCpus = os.cpus().length;
const usageDiff = process.cpuUsage(this.psCpuUsage);
const endTime = process.hrtime(this.psCurrentTime);
this.psCpuUsage = process.cpuUsage();
this.psCurrentTime = process.hrtime();
const usageMS = (usageDiff.user + usageDiff.system) / 1e3;
const totalMS = endTime[0] * 1e3 + endTime[1] / 1e6;
const normPercent = (usageMS / totalMS / numCpus) * 100;
return {
cpu: this.replaceNaN(normPercent).toFixed(2),
memory: ((mem.heapTotal + mem.external + mem.arrayBuffers) / 1024 / 1024).toFixed(2)
};
}
systemStatus(): SystemStatus {
const qqUsage = this.qqUsage();
const sysCpuInfo = this.sysCpuInfo();
return {
cpu: {
core: sysCpuInfo.core,
model: sysCpuInfo.model,
speed: (sysCpuInfo.speed / 1000).toFixed(2),
usage: {
system: sysCpuInfo.usage,
qq: qqUsage.cpu
},
},
memory: {
total: (os.totalmem() / 1024 / 1024).toFixed(2),
usage: {
system: this.sysMemoryUsage(),
qq: qqUsage.memory
}
},
arch: `${os.platform()} ${os.arch()} ${os.release()}`
};
}
}
class StatusHelperSubscription extends EventEmitter {
private statusHelper: StatusHelper;
private interval: NodeJS.Timeout | null = null;
constructor(time: number = 3000) {
super();
this.statusHelper = new StatusHelper();
this.on('newListener', (event: string) => {
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
this.startInterval(time);
}
});
this.on('removeListener', (event: string) => {
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
this.stopInterval();
}
});
}
private startInterval(time: number) {
this.interval ??= setInterval(() => {
const status = this.statusHelper.systemStatus();
this.emit('statusUpdate', status);
}, time);
}
private stopInterval() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
}
export const statusHelperSubscription = new StatusHelperSubscription();

View File

@@ -152,6 +152,7 @@ export class NapCatCore {
// Renamed from 'InitDataListener'
async initNapCatCoreListeners() {
const msgListener = new NodeIKernelMsgListener();
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);

View File

@@ -1,8 +1,9 @@
import { LRUCache } from "@/common/lru-cache";
import crypto, { createHash } from "crypto";
import { PacketContext } from "@/core/packet/context/packetContext";
import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base";
import { LogStack } from "@/core/packet/context/clientContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
export interface RecvPacket {
type: string, // 仅recv
@@ -27,13 +28,15 @@ function randText(len: number): string {
export abstract class IPacketClient {
protected readonly context: PacketContext;
protected readonly napcore: NapCoreContext;
protected readonly logger: PacketLogger;
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
logStack: LogStack;
available: boolean = false;
protected constructor(context: PacketContext, logStack: LogStack) {
this.context = context;
protected constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
this.napcore = napCore;
this.logger = logger;
this.logStack = logStack;
}
@@ -81,7 +84,7 @@ export abstract class IPacketClient {
const md5 = crypto.createHash('md5').update(data).digest('hex');
const trace_id = (randText(4) + md5 + data).slice(0, data.length / 2);
return this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
await this.context.napcore.sendSsoCmdReqByContend(cmd, trace_id);
await this.napcore.sendSsoCmdReqByContend(cmd, trace_id);
});
}

View File

@@ -5,8 +5,9 @@ import fs from "fs";
import { IPacketClient } from "@/core/packet/client/baseClient";
import { constants } from "node:os";
import { LRUCache } from "@/common/lru-cache";
import { PacketContext } from "@/core/packet/context/packetContext";
import { LogStack } from "@/core/packet/context/clientContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
// 0 send 1 recv
export interface NativePacketExportType {
@@ -19,8 +20,8 @@ export class NativePacketClient extends IPacketClient {
private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
private readonly sendEvent = new LRUCache<number, string>(500); // seq->trace_id
constructor(context: PacketContext, logStack: LogStack) {
super(context, logStack);
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
super(napCore, logger, logStack);
}
check(): boolean {

View File

@@ -1,7 +1,8 @@
import { Data, WebSocket, ErrorEvent } from "ws";
import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient";
import { PacketContext } from "@/core/packet/context/packetContext";
import { LogStack } from "@/core/packet/context/clientContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
export class WsPacketClient extends IPacketClient {
private websocket: WebSocket | null = null;
@@ -13,15 +14,15 @@ export class WsPacketClient extends IPacketClient {
private isInitialized: boolean = false;
private initPayload: { pid: number, recv: string, send: string } | null = null;
constructor(context: PacketContext, logStack: LogStack) {
super(context, logStack);
this.clientUrl = this.context.napcore.config.packetServer
? this.clientUrlWrap(this.context.napcore.config.packetServer)
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
super(napCore, logger, logStack);
this.clientUrl = this.napcore.config.packetServer
? this.clientUrlWrap(this.napcore.config.packetServer)
: this.clientUrlWrap('127.0.0.1:8083');
}
check(): boolean {
if (!this.context.napcore.config.packetServer) {
if (!this.napcore.config.packetServer) {
this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`);
return false;
}
@@ -67,7 +68,7 @@ export class WsPacketClient extends IPacketClient {
this.websocket.onopen = () => {
this.available = true;
this.reconnectAttempts = 0;
this.context.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
this.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
if (!this.isInitialized && this.initPayload) {
this.websocket!.send(JSON.stringify({
action: 'init',
@@ -79,15 +80,15 @@ export class WsPacketClient extends IPacketClient {
};
this.websocket.onclose = () => {
this.available = false;
this.context.logger.warn(`WebSocket 连接关闭,尝试重连...`);
this.logger.warn(`WebSocket 连接关闭,尝试重连...`);
reject(new Error('WebSocket 连接关闭'));
};
this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => {
this.context.logger.error(`处理消息时出错: ${err}`);
this.logger.error(`处理消息时出错: ${err}`);
});
this.websocket.onerror = (event: ErrorEvent) => {
this.available = false;
this.context.logger.error(`WebSocket 出错: ${event.message}`);
this.logger.error(`WebSocket 出错: ${event.message}`);
this.websocket?.close();
reject(new Error(`WebSocket 出错: ${event.message}`));
};
@@ -106,7 +107,7 @@ export class WsPacketClient extends IPacketClient {
const event = this.cb.get(`${trace_id_md5}${action}`);
if (event) await event(json.data);
} catch (error) {
this.context.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
this.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
}
}
}

View File

@@ -1,17 +1,17 @@
import { PacketContext } from "@/core/packet/context/packetContext";
import { IPacketClient } from "@/core/packet/client/baseClient";
import { NativePacketClient } from "@/core/packet/client/nativeClient";
import { WsPacketClient } from "@/core/packet/client/wsClient";
import { OidbPacket } from "@/core/packet/transformer/base";
import { PacketLogger } from "@/core/packet/context/loggerContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
type clientPriority = {
[key: number]: (context: PacketContext, logStack: LogStack) => IPacketClient;
[key: number]: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => IPacketClient;
}
const clientPriority: clientPriority = {
10: (context: PacketContext, logStack: LogStack) => new NativePacketClient(context, logStack),
1: (context: PacketContext, logStack: LogStack) => new WsPacketClient(context, logStack),
10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack),
1: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new WsPacketClient(napCore, logger, logStack),
};
export class LogStack {
@@ -51,13 +51,15 @@ export class LogStack {
}
export class PacketClientContext {
private readonly context: PacketContext;
private readonly napCore: NapCoreContext;
private readonly logger: PacketLogger;
private readonly logStack: LogStack;
private readonly _client: IPacketClient;
constructor(context: PacketContext) {
this.context = context;
this.logStack = new LogStack(context.logger);
constructor(napCore: NapCoreContext, logger: PacketLogger) {
this.napCore = napCore;
this.logger = logger;
this.logStack = new LogStack(logger);
this._client = this.newClient();
}
@@ -79,23 +81,23 @@ export class PacketClientContext {
}
private newClient(): IPacketClient {
const prefer = this.context.napcore.config.packetBackend;
const prefer = this.napCore.config.packetBackend;
let client: IPacketClient | null;
switch (prefer) {
case "native":
this.context.logger.info("使用指定的 NativePacketClient 作为后端");
client = new NativePacketClient(this.context, this.logStack);
this.logger.info("使用指定的 NativePacketClient 作为后端");
client = new NativePacketClient(this.napCore, this.logger, this.logStack);
break;
case "frida":
this.context.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
client = new WsPacketClient(this.context, this.logStack);
this.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
client = new WsPacketClient(this.napCore, this.logger, this.logStack);
break;
case "auto":
case undefined:
client = this.judgeClient();
break;
default:
this.context.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
this.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
client = null;
}
if (!client?.check()) {
@@ -110,7 +112,7 @@ export class PacketClientContext {
private judgeClient(): IPacketClient {
const sortedClients = Object.entries(clientPriority)
.map(([priority, clientFactory]) => {
const client = clientFactory(this.context, this.logStack);
const client = clientFactory(this.napCore, this.logger, this.logStack);
const score = +priority * +client.check();
return { client, score };
})
@@ -120,7 +122,7 @@ export class PacketClientContext {
if (!selectedClient) {
throw new Error("[Core] [Packet] 无可用的后端NapCat.Packet将不会加载");
}
this.context.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
this.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
return selectedClient;
}
}

View File

@@ -1,12 +1,12 @@
import { LogLevel, LogWrapper } from "@/common/log";
import { PacketContext } from "@/core/packet/context/packetContext";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
// TODO: check bind?
export class PacketLogger {
private readonly napLogger: LogWrapper;
constructor(context: PacketContext) {
this.napLogger = context.napcore.logger;
constructor(napcore: NapCoreContext) {
this.napLogger = napcore.logger;
}
private _log(level: LogLevel, ...msg: any[]): void {

View File

@@ -13,15 +13,22 @@ import { MiniAppRawData, MiniAppReqParams } from "@/core/packet/entities/miniApp
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from "@napneko/nap-proto-core";
import { IndexNode, MsgInfo } from "@/core/packet/transformer/proto";
import { OidbPacket } from "@/core/packet/transformer/base";
import { ImageOcrResult } from "@/core/packet/entities/ocrResult";
export class PacketOperationContext {
private readonly context: PacketContext;
constructor(context: PacketContext) {
this.context = context;
}
async sendPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
return await this.context.client.sendOidbPacket(pkt, rsp);
}
async GroupPoke(groupUin: number, uin: number) {
const req = trans.SendPoke.build(groupUin, uin);
const req = trans.SendPoke.build(uin, groupUin);
await this.context.client.sendOidbPacket(req);
}
@@ -90,6 +97,46 @@ export class PacketOperationContext {
});
}
async UploadImage(img: PacketMsgPicElement) {
await this.context.highway.uploadImage({
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.context.napcore.basicInfo.uid
}, img);
const index = img.msgInfo?.msgInfoBody?.at(0)?.index;
if (!index) {
throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined');
}
return await this.GetImageUrl(this.context.napcore.basicInfo.uid, index);
}
async GetImageUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadImage.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async ImageOCR(imgUrl: string) {
const req = trans.ImageOCR.build(imgUrl);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.ImageOCR.parse(resp);
return {
texts: res.ocrRspBody.textDetections.map((item) => {
return {
text: item.detectedText,
confidence: item.confidence,
coordinates: item.polygon.coordinates.map((c) => {
return {
x: c.x,
y: c.y
};
}),
};
}),
language: res.ocrRspBody.language
} as ImageOcrResult;
}
async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
await this.UploadResources(msg, groupUin);
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);

View File

@@ -7,19 +7,19 @@ import { PacketOperationContext } from "@/core/packet/context/operationContext";
import { PacketMsgConverter } from "@/core/packet/message/converter";
export class PacketContext {
readonly msgConverter: PacketMsgConverter;
readonly napcore: NapCoreContext;
readonly logger: PacketLogger;
readonly client: PacketClientContext;
readonly highway: PacketHighwayContext;
readonly msgConverter: PacketMsgConverter;
readonly operation: PacketOperationContext;
constructor(core: NapCatCore) {
this.napcore = new NapCoreContext(core);
this.logger = new PacketLogger(this);
this.client = new PacketClientContext(this);
this.highway = new PacketHighwayContext(this);
this.msgConverter = new PacketMsgConverter();
this.napcore = new NapCoreContext(core);
this.logger = new PacketLogger(this.napcore);
this.client = new PacketClientContext(this.napcore, this.logger);
this.highway = new PacketHighwayContext(this.napcore, this.logger, this.client);
this.operation = new PacketOperationContext(this);
}
}

View File

@@ -0,0 +1,15 @@
export interface ImageOcrResult {
texts: TextDetection[];
language: string;
}
export interface TextDetection {
text: string;
confidence: number;
coordinates: Coordinate[];
}
export interface Coordinate {
x: number;
y: number;
}

View File

@@ -1,5 +1,4 @@
import { PacketHighwayClient } from "@/core/packet/highway/client";
import { PacketContext } from "@/core/packet/context/packetContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
import FetchSessionKey from "@/core/packet/transformer/highway/FetchSessionKey";
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
@@ -16,6 +15,8 @@ import { NapProtoMsg } from "@napneko/nap-proto-core";
import * as proto from "@/core/packet/transformer/proto";
import * as trans from "@/core/packet/transformer";
import fs from "fs";
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
import { PacketClientContext } from "@/core/packet/context/clientContext";
export const BlockSize = 1024 * 1024;
@@ -33,23 +34,25 @@ export interface PacketHighwaySig {
}
export class PacketHighwayContext {
private readonly context: PacketContext;
private readonly napcore: NapCoreContext;
private readonly client: PacketClientContext;
protected sig: PacketHighwaySig;
protected logger: PacketLogger;
protected hwClient: PacketHighwayClient;
private cachedPrepareReq: Promise<void> | null = null;
constructor(context: PacketContext) {
this.context = context;
constructor(napcore: NapCoreContext, logger: PacketLogger, client: PacketClientContext) {
this.napcore = napcore;
this.client = client;
this.sig = {
uin: String(context.napcore.basicInfo.uin),
uid: context.napcore.basicInfo.uid,
uin: String(this.napcore.basicInfo.uin),
uid: this.napcore.basicInfo.uid,
sigSession: null,
sessionKey: null,
serverAddr: [],
};
this.logger = context.logger;
this.hwClient = new PacketHighwayClient(this.sig, context.logger);
this.logger = logger;
this.hwClient = new PacketHighwayClient(this.sig, this.logger);
}
private async checkAvailable() {
@@ -66,7 +69,7 @@ export class PacketHighwayContext {
private async prepareUpload(): Promise<void> {
this.logger.debug('[Highway] on prepareUpload!');
const packet = FetchSessionKey.build();
const req = await this.context.client.sendOidbPacket(packet, true);
const req = await this.client.sendOidbPacket(packet, true);
const rsp = FetchSessionKey.parse(req);
this.sig.sigSession = rsp.httpConn.sigSession;
this.sig.sessionKey = rsp.httpConn.sessionKey;
@@ -136,7 +139,7 @@ export class PacketHighwayContext {
private async uploadGroupImage(groupUin: number, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const req = UploadGroupImage.build(groupUin, img);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = UploadGroupImage.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -173,7 +176,7 @@ export class PacketHighwayContext {
private async uploadC2CImage(peerUid: string, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const req = trans.UploadPrivateImage.build(peerUid, img);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateImage.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -211,7 +214,7 @@ export class PacketHighwayContext {
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
const req = trans.UploadGroupVideo.build(groupUin, video);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupVideo.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -276,7 +279,7 @@ export class PacketHighwayContext {
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
const req = trans.UploadPrivateVideo.build(peerUid, video);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateVideo.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -339,7 +342,7 @@ export class PacketHighwayContext {
private async uploadGroupPtt(groupUin: number, ptt: PacketMsgPttElement): Promise<void> {
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
const req = trans.UploadGroupPtt.build(groupUin, ptt);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupPtt.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -375,7 +378,7 @@ export class PacketHighwayContext {
private async uploadC2CPtt(peerUid: string, ptt: PacketMsgPttElement): Promise<void> {
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
const req = trans.UploadPrivatePtt.build(peerUid, ptt);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivatePtt.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
@@ -413,7 +416,7 @@ export class PacketHighwayContext {
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
file.fileSha1 = await calculateSha1(file.filePath);
const req = trans.UploadGroupFile.build(groupUin, file);
const resp = await this.context.client.sendOidbPacket(req, true);
const resp = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupFile.parse(resp);
if (!preRespData?.upload?.boolFileExist) {
this.logger.debug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
@@ -476,7 +479,7 @@ export class PacketHighwayContext {
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
file.fileSha1 = await calculateSha1(file.filePath);
const req = await trans.UploadPrivateFile.build(this.sig.uid, peerUid, file);
const res = await this.context.client.sendOidbPacket(req, true);
const res = await this.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateFile.parse(res);
if (!preRespData.upload?.boolFileExist) {
this.logger.debug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
@@ -531,7 +534,7 @@ export class PacketHighwayContext {
file.fileUuid = preRespData.upload?.uuid;
file.fileHash = preRespData.upload?.fileAddon;
const fileExistReq = trans.DownloadOfflineFile.build(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
const fileExistRes = await this.context.client.sendOidbPacket(fileExistReq, true);
const fileExistRes = await this.client.sendOidbPacket(fileExistReq, true);
file._e37_800_rsp = trans.DownloadOfflineFile.parse(fileExistRes);
file._private_send_uid = this.sig.uid;
file._private_recv_uid = peerUid;

View File

@@ -256,6 +256,8 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
width: number;
height: number;
picType: PicType;
picSubType: number;
summary: string;
sha1: string | null = null;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
@@ -270,6 +272,10 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
this.width = element.picElement.picWidth;
this.height = element.picElement.picHeight;
this.picType = element.picElement.picType;
this.picSubType = element.picElement.picSubType ?? 0;
this.summary = element.picElement.summary === '' ? (
element.picElement.picSubType === 0 ? '[图片]' : '[动画表情]'
) : element.picElement.summary;
}
get valid(): boolean {
@@ -288,7 +294,7 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
}
toPreview(): string {
return "[图片]";
return this.summary;
}
}

View File

@@ -0,0 +1,37 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class ImageOCR extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0xE07_0_Response> {
constructor() {
super();
}
build(url: string): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0).encode(
{
version: 1,
client: 0,
entrance: 1,
ocrReqBody: {
imageUrl: url,
originMd5: "",
afterCompressMd5: "",
afterCompressFileSize: "",
afterCompressWeight: "",
afterCompressHeight: "",
isCut: false,
}
}
);
return OidbBase.build(0XEB7, 1, body, false, false);
}
parse(data: Buffer) {
const base = OidbBase.parse(data);
return new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0_Response).decode(base.body);
}
}
export default new ImageOCR();

View File

@@ -5,3 +5,4 @@ export { default as GroupSign } from './GroupSign';
export { default as GetStrangerInfo } from './GetStrangerInfo';
export { default as SendPoke } from './SendPoke';
export { default as SetSpecialTitle } from './SetSpecialTitle';
export { default as ImageOCR } from './ImageOCR';

View File

@@ -0,0 +1,51 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import { IndexNode } from "@/core/packet/transformer/proto";
class DownloadImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 100
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: selfUid
},
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x11C5, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadImage();

View File

@@ -58,8 +58,11 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
compatQMsgSceneType: 2,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
bizType: img.picSubType,
bytesPbReserveTroop: {
subType: img.picSubType,
},
textSummary: img.summary,
},
video: {
bytesPbReserve: Buffer.alloc(0),

View File

@@ -58,8 +58,11 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
compatQMsgSceneType: 1,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
bizType: img.picSubType,
bytesPbReserveC2C: {
subType: img.picSubType,
},
textSummary: img.summary,
},
video: {
bytesPbReserve: Buffer.alloc(0),

View File

@@ -11,3 +11,4 @@ export { default as UploadPrivateFile } from './UploadPrivateFile';
export { default as UploadPrivateImage } from './UploadPrivateImage';
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
export { default as DownloadImage } from './DownloadImage';

View File

@@ -29,3 +29,4 @@ export * from "./oidb/Oidb.0xEB7";
export * from "./oidb/Oidb.0xED3_1";
export * from "./oidb/Oidb.0XFE1_2";
export * from "./oidb/OidbBase";
export * from "./oidb/Oidb.0xE07";

View File

@@ -72,6 +72,14 @@ export const GroupChange = {
field7: ProtoField(7, ScalarType.BYTES, true),
};
export const GroupInvite = {
groupUin: ProtoField(1, ScalarType.UINT32),
field2: ProtoField(2, ScalarType.UINT32),
field3: ProtoField(2, ScalarType.UINT32),
field4: ProtoField(2, ScalarType.UINT32),
invitorUid: ProtoField(5, ScalarType.STRING),
};
export const PushMsgBody = {
responseHead: ProtoField(1, () => ResponseHead),
contentHead: ProtoField(2, () => ContentHead),

View File

@@ -0,0 +1,59 @@
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0xE07_0 = {
version: ProtoField(1, ScalarType.UINT32),
client: ProtoField(2, ScalarType.UINT32),
entrance: ProtoField(3, ScalarType.UINT32),
ocrReqBody: ProtoField(10, () => OcrReqBody, true),
};
export const OcrReqBody = {
imageUrl: ProtoField(1, ScalarType.STRING),
languageType: ProtoField(2, ScalarType.UINT32),
scene: ProtoField(3, ScalarType.UINT32),
originMd5: ProtoField(10, ScalarType.STRING),
afterCompressMd5: ProtoField(11, ScalarType.STRING),
afterCompressFileSize: ProtoField(12, ScalarType.STRING),
afterCompressWeight: ProtoField(13, ScalarType.STRING),
afterCompressHeight: ProtoField(14, ScalarType.STRING),
isCut: ProtoField(15, ScalarType.BOOL),
};
export const OidbSvcTrpcTcp0xE07_0_Response = {
retCode: ProtoField(1, ScalarType.INT32),
errMsg: ProtoField(2, ScalarType.STRING),
wording: ProtoField(3, ScalarType.STRING),
ocrRspBody: ProtoField(10, () => OcrRspBody),
};
export const OcrRspBody = {
textDetections: ProtoField(1, () => TextDetection, false, true),
language: ProtoField(2, ScalarType.STRING),
requestId: ProtoField(3, ScalarType.STRING),
ocrLanguageList: ProtoField(101, ScalarType.STRING, false, true),
dstTranslateLanguageList: ProtoField(102, ScalarType.STRING, false, true),
languageList: ProtoField(103, () => Language, false, true),
afterCompressWeight: ProtoField(111, ScalarType.UINT32),
afterCompressHeight: ProtoField(112, ScalarType.UINT32),
};
export const TextDetection = {
detectedText: ProtoField(1, ScalarType.STRING),
confidence: ProtoField(2, ScalarType.UINT32),
polygon: ProtoField(3, () => Polygon),
advancedInfo: ProtoField(4, ScalarType.STRING),
};
export const Polygon = {
coordinates: ProtoField(1, () => Coordinate, false, true),
};
export const Coordinate = {
x: ProtoField(1, ScalarType.INT32),
y: ProtoField(2, ScalarType.INT32),
};
export const Language = {
languageCode: ProtoField(1, ScalarType.STRING),
languageDesc: ProtoField(2, ScalarType.STRING),
};

View File

@@ -189,8 +189,8 @@ export const VideoExtBizInfo = {
export const PicExtBizInfo = {
BizType: ProtoField(1, ScalarType.UINT32),
TextSummary: ProtoField(2, ScalarType.STRING),
BytesPbReserveC2c: ProtoField(11, ScalarType.BYTES),
BytesPbReserveTroop: ProtoField(12, ScalarType.BYTES),
BytesPbReserveC2c: ProtoField(11, () => BytesPbReserveC2c),
BytesPbReserveTroop: ProtoField(12, () => BytesPbReserveTroop),
FromScene: ProtoField(1001, ScalarType.UINT32),
ToScene: ProtoField(1002, ScalarType.UINT32),
OldFileId: ProtoField(1003, ScalarType.UINT32),
@@ -211,3 +211,27 @@ export const UploadInfo = {
FileInfo: ProtoField(1, () => FileInfo),
SubFileType: ProtoField(2, ScalarType.UINT32),
};
export const BytesPbReserveC2c = {
subType: ProtoField(1, ScalarType.UINT32),
field3: ProtoField(3, ScalarType.UINT32),
field4: ProtoField(4, ScalarType.UINT32),
field8: ProtoField(8, ScalarType.STRING),
field10: ProtoField(10, ScalarType.UINT32),
field12: ProtoField(12, ScalarType.STRING),
field18: ProtoField(18, ScalarType.STRING),
field19: ProtoField(19, ScalarType.STRING),
field20: ProtoField(20, ScalarType.BYTES),
};
export const BytesPbReserveTroop = {
subType: ProtoField(1, ScalarType.UINT32),
field3: ProtoField(3, ScalarType.UINT32),
field4: ProtoField(4, ScalarType.UINT32),
field9: ProtoField(9, ScalarType.STRING),
field10: ProtoField(10, ScalarType.UINT32),
field12: ProtoField(12, ScalarType.STRING),
field18: ProtoField(18, ScalarType.STRING),
field19: ProtoField(19, ScalarType.STRING),
field21: ProtoField(21, ScalarType.BYTES),
};

View File

@@ -56,6 +56,7 @@ export interface GrayTipElement {
aioOpGrayTipElement: TipAioOpGrayTipElement;
groupElement: TipGroupElement;
xmlElement: {
busiId: string;
content: string;
templId: string;
};

View File

@@ -9,4 +9,5 @@ export * from './sign';
export * from './element';
export * from './constant';
export * from './graytip';
export * from './emoji';
export * from './emoji';
export * from './service';

View File

@@ -508,7 +508,7 @@ export interface RawMessage {
*/
export interface QueryMsgsParams {
chatInfo: Peer;
filterMsgType: [];
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
filterSendersUid: string[];
filterMsgFromTime: string;
filterMsgToTime: string;

35
src/core/types/service.ts Normal file
View File

@@ -0,0 +1,35 @@
export enum LoginErrorCode {
KLOGINERRORACCOUNTNOTUIN = 140022018,
KLOGINERRORACCOUNTORPASSWORDERROR = 140022013,
KLOGINERRORBLACKACCOUNT = 150022021,
KLOGINERRORDEFAULT = 140022000,
KLOGINERROREXPIRETICKET = 140022014,
KLOGINERRORFROZEN = 140022005,
KLOGINERRORILLAGETICKET = 140022016,
KLOGINERRORINVAILDCOOKIE = 140022012,
KLOGINERRORINVALIDPARAMETER = 140022001,
KLOGINERRORKICKEDTICKET = 140022015,
KLOGINERRORMUTIPLEPASSWORDINCORRECT = 150022029,
KLOGINERRORNEEDUPDATE = 140022004,
KLOGINERRORNEEDVERIFYREALNAME = 140022019,
KLOGINERRORNEWDEVICE = 140022010,
KLOGINERRORNICEACCOUNTEXPIRED = 150022020,
KLOGINERRORNICEACCOUNTPARENTCHILDEXPIRED = 150022025,
KLOGINERRORPASSWORD = 2,
KLOGINERRORPROOFWATER = 140022008,
KLOGINERRORPROTECT = 140022006,
KLOGINERRORREFUSEPASSOWRDLOGIN = 140022009,
KLOGINERRORREMINDCANAELLATEDSTATUS = 150022028,
KLOGINERRORSCAN = 1,
KLOGINERRORSCCESS = 0,
KLOGINERRORSECBEAT = 140022017,
KLOGINERRORSMSINVALID = 150022026,
KLOGINERRORSTRICK = 140022007,
KLOGINERRORSYSTEMFAILED = 140022002,
KLOGINERRORTGTGTEXCHAGEA1FORBID = 150022027,
KLOGINERRORTIMEOUTRETRY = 140022003,
KLOGINERRORTOOMANYTIMESTODAY = 150022023,
KLOGINERRORTOOOFTEN = 150022022,
KLOGINERRORUNREGISTERED = 150022024,
KLOGINERRORUNUSUALDEVICE = 140022011,
}

View File

@@ -27,6 +27,7 @@ export async function NCoreInitFramework(
process.on('uncaughtException', (err) => {
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
});
process.on('unhandledRejection', (reason, promise) => {
console.log('[NapCat] [Error] unhandledRejection:', reason);
});

View File

@@ -83,5 +83,5 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
}
}
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
abstract _handle(payload: PayloadType, adaptername: string): Promise<ReturnDataType>;
}

View File

@@ -0,0 +1,14 @@
import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '../OneBotAction';
interface GetClientkeyResponse {
clientkey?: string;
}
export class GetClientkey extends OneBotAction<void, GetClientkeyResponse> {
actionName = ActionName.GetClientkey;
async _handle() {
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };
}
}

View File

@@ -14,22 +14,23 @@ class OCRImageBase extends OneBotAction<Payload, any> {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image));
const { path, success } = await uriToLocalFile(this.core.NapCatTempPath, payload.image);
if (!success) {
throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`);
throw new Error(`OCR ${payload.image}失败, image字段可能格式不正确`);
}
if (path) {
await checkFileExist(path, 5000); // 避免崩溃
const ret = await this.core.apis.SystemApi.ocrImage(path);
fs.unlink(path, () => { });
if (!ret) {
throw new Error(`OCR ${payload.image}失败`);
try {
await checkFileExist(path, 5000); // 避免崩溃
const ret = await this.core.apis.SystemApi.ocrImage(path);
if (!ret) {
throw new Error(`OCR ${payload.image}失败`);
}
return ret.result;
} finally {
fs.unlink(path, () => { });
}
return ret.result;
}
fs.unlink(path, () => { });
throw new Error(`OCR ${payload.image}失败,文件可能不存在`);
throw new Error(`OCR ${payload.image}失败, 文件可能不存在`);
}
}
@@ -39,4 +40,4 @@ export class OCRImage extends OCRImageBase {
export class IOCRImage extends OCRImageBase {
actionName = ActionName.IOCRImage;
}
}

View File

@@ -0,0 +1,21 @@
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
cmd: Type.String(),
data: Type.String(),
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true }),
});
type Payload = Static<typeof SchemaData>;
export class SendPacket extends GetPacketStatusDepends<Payload, any> {
payloadSchema = SchemaData;
actionName = ActionName.SendPacket;
async _handle(payload: Payload) {
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
const data = await this.core.apis.PacketApi.pkt.operation.sendPacket({ cmd: payload.cmd, data: payload.data as any }, rsp);
return typeof data === 'object' ? data.toString('hex') : undefined;
}
}

View File

@@ -17,7 +17,7 @@ class SetGroupSignBase extends GetPacketStatusDepends<Payload, any> {
}
export class SetGroupSign extends SetGroupSignBase {
actionName = ActionName.SendGroupSign;
actionName = ActionName.SetGroupSign;
}
export class SendGroupSign extends SetGroupSignBase {

View File

@@ -1,6 +1,6 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import fs from 'fs/promises';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { ActionName } from '@/onebot/action/router';
import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types';
import { Static, Type } from '@sinclair/typebox';
@@ -28,7 +28,7 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
payload.file ||= payload.file_id || '';
//接收消息标记模式
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file);
if (contextMsgFile) {
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
const { peer, msgId, elementId } = contextMsgFile;
const downloadPath = await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList
@@ -68,7 +68,7 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
//群文件模式
const contextModelIdFile = FileNapCatOneBotUUID.decodeModelId(payload.file);
if (contextModelIdFile) {
if (contextModelIdFile && contextModelIdFile.modelId) {
const { peer, modelId } = contextModelIdFile;
const downloadPath = await this.core.apis.FileApi.downloadFileForModelId(peer, modelId, '');
const res: GetFileResponse = {

View File

@@ -1,5 +1,5 @@
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from "@/common/helper";
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { Static, Type } from '@sinclair/typebox';

View File

@@ -1,7 +1,7 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
@@ -16,7 +16,7 @@ export class DeleteGroupFile extends OneBotAction<Payload, any> {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (!data) throw new Error('Invalid file_id');
if (!data || !data.fileId) throw new Error('Invalid file_id');
return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
}
}

View File

@@ -2,7 +2,7 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import fs from 'fs';
import { join as joinPath } from 'node:path';
import { calculateFileMD5, httpDownload } from '@/common/file';
import { calculateFileMD5, uriToLocalFile } from '@/common/file';
import { randomUUID } from 'crypto';
import { Static, Type } from '@sinclair/typebox';
@@ -26,17 +26,20 @@ export default class GoCQHTTPDownloadFile extends OneBotAction<Payload, FileResp
async _handle(payload: Payload): Promise<FileResponse> {
const isRandomName = !payload.name;
const name = payload.name || randomUUID();
const filePath = joinPath(this.core.NapCatTempPath, name);
let result: Awaited<ReturnType<typeof uriToLocalFile>>;
if (payload.base64) {
fs.writeFileSync(filePath, payload.base64, 'base64');
result = await uriToLocalFile(this.core.NapCatTempPath, `base64://${payload.base64}`, name);
} else if (payload.url) {
const headers = this.getHeaders(payload.headers);
const buffer = await httpDownload({ url: payload.url, headers: headers });
fs.writeFileSync(filePath, Buffer.from(buffer), 'binary');
result = await uriToLocalFile(this.core.NapCatTempPath, payload.url, name, headers);
} else {
throw new Error('不存在任何文件, 无法下载');
}
if (!result.success) {
throw new Error(result.errMsg);
}
const filePath = result.path;
if (fs.existsSync(filePath)) {
if (isRandomName) {

View File

@@ -13,7 +13,7 @@ type Payload = Static<typeof SchemaData>;
export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo;
payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<OB11User> {
const user_id = payload.user_id.toString();
const extendData = await this.core.apis.UserApi.getUserDetailInfoByUin(user_id);

View File

@@ -24,7 +24,7 @@ class GetGroupInfo extends OneBotAction<Payload, OB11Group> {
group_name: data.groupName,
member_count: data.memberNum,
max_member_count: data.maxMemberNum,
}
};
}
return OB11Construct.group(group);
}

View File

@@ -26,20 +26,29 @@ class GetGroupMemberInfo extends OneBotAction<Payload, OB11GroupMember> {
return uid;
}
async _handle(payload: Payload) {
const isNocache = this.parseBoolean(payload.no_cache ?? true);
const uid = await this.getUid(payload.user_id);
const groupMember = this.core.apis.GroupApi.groupMemberCache.get(payload.group_id.toString())?.get(uid);
let [member, info] = await Promise.all([
private async getGroupMemberInfo(payload: Payload, uid: string, isNocache: boolean) {
const groupMemberCache = this.core.apis.GroupApi.groupMemberCache.get(payload.group_id.toString());
const groupMember = groupMemberCache?.get(uid);
const [member, info] = await Promise.all([
this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, isNocache),
this.core.apis.UserApi.getUserDetailInfo(uid),
]);
if (!member || !groupMember) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
if (info) {
member = { ...groupMember, ...member, ...info };
} else {
return info ? { ...groupMember, ...member, ...info } : member;
}
async _handle(payload: Payload) {
const isNocache = this.parseBoolean(payload.no_cache ?? true);
const uid = await this.getUid(payload.user_id);
const member = await this.getGroupMemberInfo(payload, uid, isNocache);
if (!member) {
this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`);
}
return OB11Construct.groupMember(payload.group_id.toString(), member);
}
}

View File

@@ -3,6 +3,7 @@ import { OB11Construct } from '@/onebot/helper/data';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
import { GroupMember } from '@/core';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
@@ -17,25 +18,32 @@ export class GetGroupMemberList extends OneBotAction<Payload, OB11GroupMember[]>
async _handle(payload: Payload) {
const groupIdStr = payload.group_id.toString();
const noCache = payload.no_cache ? this.stringToBoolean(payload.no_cache) : false;
const noCache = this.parseBoolean(payload.no_cache ?? false);
const groupMembers = await this.getGroupMembers(groupIdStr, noCache);
const _groupMembers = await Promise.all(
Array.from(groupMembers.values()).map(item =>
OB11Construct.groupMember(groupIdStr, item)
)
);
return Array.from(new Map(_groupMembers.map(member => [member.user_id, member])).values());
}
private parseBoolean(value: boolean | string): boolean {
return typeof value === 'string' ? value === 'true' : value;
}
private async getGroupMembers(groupIdStr: string, noCache: boolean): Promise<Map<string, GroupMember>> {
const memberCache = this.core.apis.GroupApi.groupMemberCache;
let groupMembers = memberCache.get(groupIdStr);
if (noCache || !groupMembers) {
this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr).then().catch();
//下次刷新
groupMembers = memberCache.get(groupIdStr);
const data = this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr, true).then().catch();
groupMembers = memberCache.get(groupIdStr) || (await data);
if (!groupMembers) {
throw new Error(`Failed to get group member list for group ${groupIdStr}`);
}
}
const memberPromises = Array.from(groupMembers.values()).map(item =>
OB11Construct.groupMember(groupIdStr, item)
);
const _groupMembers = await Promise.all(memberPromises);
const MemberMap = new Map(_groupMembers.map(member => [member.user_id, member]));
return Array.from(MemberMap.values());
return groupMembers;
}
stringToBoolean(str: string | boolean): boolean {
return typeof str === 'boolean' ? str : str.toLowerCase() === "true";
}
}
}

View File

@@ -14,6 +14,6 @@ export class GroupPoke extends GetPacketStatusDepends<Payload, any> {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.user_id, +payload.group_id);
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
}
}

View File

@@ -21,15 +21,9 @@ export class SendGroupAiRecord extends GetPacketStatusDepends<Payload, {
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const rawRsp = await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
const url = await this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+payload.group_id, rawRsp.msgInfoBody[0].index);
const { path, errMsg, success } = (await uriToLocalFile(this.core.NapCatTempPath, url));
if (!success) {
throw new Error(errMsg);
}
const peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() } as Peer;
const element = await this.core.apis.FileApi.createValidSendPttElement(path);
const sendRes = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [element], [path]);
return { message_id: sendRes.id ?? -1 };
await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound);
return {
message_id: 0 // can't get message_id from GetAiVoice
};
}
}

View File

@@ -1,4 +1,4 @@
import {ContextMode, SendMsgBase} from '@/onebot/action/msg/SendMsg';
import { ContextMode, SendMsgBase } from '@/onebot/action/msg/SendMsg';
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
import { OB11PostSendMsg } from '@/onebot/types';

View File

@@ -19,12 +19,11 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
const flag = payload.flag.toString();
const approve = payload.approve?.toString() !== 'false';
const reason = payload.reason ?? ' ';
const notify = await this.findNotify(flag);
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag);
const notify = invite_notify ?? await this.findNotify(flag);
if (!notify) {
throw new Error('No such request');
}
await this.core.apis.GroupApi.handleGroupRequest(
notify,
approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,

View File

@@ -102,6 +102,9 @@ import { SendGroupAiRecord } from "@/onebot/action/group/SendGroupAiRecord";
import { GetAiCharacters } from "@/onebot/action/extends/GetAiCharacters";
import { GetGuildList } from './guild/GetGuildList';
import { GetGuildProfile } from './guild/GetGuildProfile';
import { GetClientkey } from './extends/GetClientkey';
import { SendPacket } from './extends/SendPacket';
import { SendPoke } from "@/onebot/action/packet/SendPoke";
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
@@ -123,6 +126,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetGroupRootFiles(obContext, core),
new SetGroupSign(obContext, core),
new SendGroupSign(obContext, core),
new GetClientkey(obContext, core),
// onebot11
new SendLike(obContext, core),
new GetMsg(obContext, core),
@@ -216,6 +220,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetAiRecord(obContext, core),
new SendGroupAiRecord(obContext, core),
new GetAiCharacters(obContext, core),
new SendPacket(obContext, core),
new SendPoke(obContext, core),
];
type HandlerUnion = typeof actionHandlers[number];

View File

@@ -1,4 +1,4 @@
import {ContextMode, SendMsgBase} from './SendMsg';
import { ContextMode, SendMsgBase } from './SendMsg';
import { ActionName, BaseCheckResult } from '@/onebot/action/router';
import { OB11PostSendMsg } from '@/onebot/types';

View File

@@ -0,0 +1,23 @@
import { ActionName } from '@/onebot/action/router';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
user_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class SendPoke extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SendPoke;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
if (payload.group_id) {
await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.group_id, +payload.user_id);
} else {
await this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id);
}
}
}

View File

@@ -13,134 +13,138 @@ export interface InvalidCheckResult {
[k: string | number]: any;
}
export const ActionName = {
export const ActionName = {
// onebot 11
SendPrivateMsg : 'send_private_msg',
SendGroupMsg : 'send_group_msg',
SendMsg : 'send_msg',
DeleteMsg : 'delete_msg',
GetMsg : 'get_msg',
GoCQHTTP_GetForwardMsg : 'get_forward_msg',
SendLike : 'send_like',
SetGroupKick : 'set_group_kick',
SetGroupBan : 'set_group_ban',
SendPrivateMsg: 'send_private_msg',
SendGroupMsg: 'send_group_msg',
SendMsg: 'send_msg',
DeleteMsg: 'delete_msg',
GetMsg: 'get_msg',
GoCQHTTP_GetForwardMsg: 'get_forward_msg',
SendLike: 'send_like',
SetGroupKick: 'set_group_kick',
SetGroupBan: 'set_group_ban',
// SetGroupAnoymousBan : 'set_group_anonymous_ban',
SetGroupWholeBan : 'set_group_whole_ban',
SetGroupAdmin : 'set_group_admin',
SetGroupWholeBan: 'set_group_whole_ban',
SetGroupAdmin: 'set_group_admin',
// SetGroupAnoymous : 'set_group_anonymous',
SetGroupCard : 'set_group_card',
SetGroupName : 'set_group_name',
SetGroupLeave : 'set_group_leave',
SetSpecialTittle : 'set_group_special_title',
SetFriendAddRequest : 'set_friend_add_request',
SetGroupAddRequest : 'set_group_add_request',
GetLoginInfo : 'get_login_info',
GoCQHTTP_GetStrangerInfo : 'get_stranger_info',
GetFriendList : 'get_friend_list',
GetGroupInfo : 'get_group_info',
GetGroupList : 'get_group_list',
GetGroupMemberInfo : 'get_group_member_info',
GetGroupMemberList : 'get_group_member_list',
GetGroupHonorInfo : 'get_group_honor_info',
GetCookies : 'get_cookies',
GetCSRF : 'get_csrf_token',
GetCredentials : 'get_credentials',
GetRecord : 'get_record',
GetImage : 'get_image',
CanSendImage : 'can_send_image',
CanSendRecord : 'can_send_record',
GetStatus : 'get_status',
GetVersionInfo : 'get_version_info',
SetGroupCard: 'set_group_card',
SetGroupName: 'set_group_name',
SetGroupLeave: 'set_group_leave',
SetSpecialTittle: 'set_group_special_title',
SetFriendAddRequest: 'set_friend_add_request',
SetGroupAddRequest: 'set_group_add_request',
GetLoginInfo: 'get_login_info',
GoCQHTTP_GetStrangerInfo: 'get_stranger_info',
GetFriendList: 'get_friend_list',
GetGroupInfo: 'get_group_info',
GetGroupList: 'get_group_list',
GetGroupMemberInfo: 'get_group_member_info',
GetGroupMemberList: 'get_group_member_list',
GetGroupHonorInfo: 'get_group_honor_info',
GetCookies: 'get_cookies',
GetCSRF: 'get_csrf_token',
GetCredentials: 'get_credentials',
GetRecord: 'get_record',
GetImage: 'get_image',
CanSendImage: 'can_send_image',
CanSendRecord: 'can_send_record',
GetStatus: 'get_status',
GetVersionInfo: 'get_version_info',
// Reboot : 'set_restart',
// CleanCache : 'clean_cache',
// go-cqhttp
SetQQProfile : 'set_qq_profile',
SetQQProfile: 'set_qq_profile',
// QidianGetAccountInfo : 'qidian_get_account_info',
GoCQHTTP_GetModelShow : '_get_model_show',
GoCQHTTP_SetModelShow : '_set_model_show',
GetOnlineClient : 'get_online_clients',
GoCQHTTP_GetModelShow: '_get_model_show',
GoCQHTTP_SetModelShow: '_set_model_show',
GetOnlineClient: 'get_online_clients',
// GetUnidirectionalFriendList : 'get_unidirectional_friend_list',
GoCQHTTP_DeleteFriend : 'delete_friend',
GoCQHTTP_DeleteFriend: 'delete_friend',
// DeleteUnidirectionalFriendList : 'delete_unidirectional_friend',
GoCQHTTP_MarkMsgAsRead : 'mark_msg_as_read',
GoCQHTTP_SendGroupForwardMsg : 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg : 'send_private_forward_msg',
GoCQHTTP_GetGroupMsgHistory : 'get_group_msg_history',
OCRImage : 'ocr_image',
IOCRImage : '.ocr_image',
GetGroupSystemMsg : 'get_group_system_msg',
GoCQHTTP_GetEssenceMsg : 'get_essence_msg_list',
GoCQHTTP_GetGroupAtAllRemain : 'get_group_at_all_remain',
SetGroupPortrait : 'set_group_portrait',
SetEssenceMsg : 'set_essence_msg',
DelEssenceMsg : 'delete_essence_msg',
GoCQHTTP_SendGroupNotice : '_send_group_notice',
GoCQHTTP_GetGroupNotice : '_get_group_notice',
GoCQHTTP_UploadGroupFile : 'upload_group_file',
GOCQHTTP_DeleteGroupFile : 'delete_group_file',
GoCQHTTP_CreateGroupFileFolder : 'create_group_file_folder',
GoCQHTTP_DeleteGroupFileFolder : 'delete_group_folder',
GoCQHTTP_GetGroupFileSystemInfo : 'get_group_file_system_info',
GoCQHTTP_GetGroupRootFiles : 'get_group_root_files',
GoCQHTTP_GetGroupFilesByFolder : 'get_group_files_by_folder',
GOCQHTTP_GetGroupFileUrl : 'get_group_file_url',
GOCQHTTP_UploadPrivateFile : 'upload_private_file',
GoCQHTTP_MarkMsgAsRead: 'mark_msg_as_read',
GoCQHTTP_SendGroupForwardMsg: 'send_group_forward_msg',
GoCQHTTP_SendPrivateForwardMsg: 'send_private_forward_msg',
GoCQHTTP_GetGroupMsgHistory: 'get_group_msg_history',
OCRImage: 'ocr_image',
IOCRImage: '.ocr_image',
GetGroupSystemMsg: 'get_group_system_msg',
GoCQHTTP_GetEssenceMsg: 'get_essence_msg_list',
GoCQHTTP_GetGroupAtAllRemain: 'get_group_at_all_remain',
SetGroupPortrait: 'set_group_portrait',
SetEssenceMsg: 'set_essence_msg',
DelEssenceMsg: 'delete_essence_msg',
GoCQHTTP_SendGroupNotice: '_send_group_notice',
GoCQHTTP_GetGroupNotice: '_get_group_notice',
GoCQHTTP_UploadGroupFile: 'upload_group_file',
GOCQHTTP_DeleteGroupFile: 'delete_group_file',
GoCQHTTP_CreateGroupFileFolder: 'create_group_file_folder',
GoCQHTTP_DeleteGroupFileFolder: 'delete_group_folder',
GoCQHTTP_GetGroupFileSystemInfo: 'get_group_file_system_info',
GoCQHTTP_GetGroupRootFiles: 'get_group_root_files',
GoCQHTTP_GetGroupFilesByFolder: 'get_group_files_by_folder',
GOCQHTTP_GetGroupFileUrl: 'get_group_file_url',
GOCQHTTP_UploadPrivateFile: 'upload_private_file',
// GOCQHTTP_ReloadEventFilter : 'reload_event_filter',
GoCQHTTP_DownloadFile : 'download_file',
GoCQHTTP_CheckUrlSafely : 'check_url_safely',
GoCQHTTP_GetWordSlices : '.get_word_slices',
GoCQHTTP_HandleQuickAction : '.handle_quick_operation',
GoCQHTTP_DownloadFile: 'download_file',
GoCQHTTP_CheckUrlSafely: 'check_url_safely',
GoCQHTTP_GetWordSlices: '.get_word_slices',
GoCQHTTP_HandleQuickAction: '.handle_quick_operation',
// 以下为扩展napcat扩展
Unknown : 'unknown',
SharePeer : 'ArkSharePeer',
ShareGroupEx : 'ArkShareGroup',
Unknown: 'unknown',
SharePeer: 'ArkSharePeer',
ShareGroupEx: 'ArkShareGroup',
// RebootNormal : 'reboot_normal', //无快速登录重新启动
GetRobotUinRange : 'get_robot_uin_range',
SetOnlineStatus : 'set_online_status',
GetFriendsWithCategory : 'get_friends_with_category',
SetQQAvatar : 'set_qq_avatar',
GetFile : 'get_file',
ForwardFriendSingleMsg : 'forward_friend_single_msg',
ForwardGroupSingleMsg : 'forward_group_single_msg',
TranslateEnWordToZn : 'translate_en2zh',
SetMsgEmojiLike : 'set_msg_emoji_like',
GoCQHTTP_SendForwardMsg : 'send_forward_msg',
MarkPrivateMsgAsRead : 'mark_private_msg_as_read',
MarkGroupMsgAsRead : 'mark_group_msg_as_read',
GetFriendMsgHistory : 'get_friend_msg_history',
CreateCollection : 'create_collection',
GetCollectionList : 'get_collection_list',
SetLongNick : 'set_self_longnick',
GetRecentContact : 'get_recent_contact',
_MarkAllMsgAsRead : '_mark_all_as_read',
GetProfileLike : 'get_profile_like',
FetchCustomFace : 'fetch_custom_face',
FetchEmojiLike : 'fetch_emoji_like',
SetInputStatus : 'set_input_status',
GetGroupInfoEx : 'get_group_info_ex',
GetGroupIgnoreAddRequest : 'get_group_ignore_add_request',
DelGroupNotice : '_del_group_notice',
FetchUserProfileLike : 'fetch_user_profile_like',
FriendPoke : 'friend_poke',
GroupPoke : 'group_poke',
GetPacketStatus : 'nc_get_packet_status',
GetUserStatus : 'nc_get_user_status',
GetRkey : 'nc_get_rkey',
GetGroupShutList : 'get_group_shut_list',
GetRobotUinRange: 'get_robot_uin_range',
SetOnlineStatus: 'set_online_status',
GetFriendsWithCategory: 'get_friends_with_category',
SetQQAvatar: 'set_qq_avatar',
GetFile: 'get_file',
ForwardFriendSingleMsg: 'forward_friend_single_msg',
ForwardGroupSingleMsg: 'forward_group_single_msg',
TranslateEnWordToZn: 'translate_en2zh',
SetMsgEmojiLike: 'set_msg_emoji_like',
GoCQHTTP_SendForwardMsg: 'send_forward_msg',
MarkPrivateMsgAsRead: 'mark_private_msg_as_read',
MarkGroupMsgAsRead: 'mark_group_msg_as_read',
GetFriendMsgHistory: 'get_friend_msg_history',
CreateCollection: 'create_collection',
GetCollectionList: 'get_collection_list',
SetLongNick: 'set_self_longnick',
GetRecentContact: 'get_recent_contact',
_MarkAllMsgAsRead: '_mark_all_as_read',
GetProfileLike: 'get_profile_like',
FetchCustomFace: 'fetch_custom_face',
FetchEmojiLike: 'fetch_emoji_like',
SetInputStatus: 'set_input_status',
GetGroupInfoEx: 'get_group_info_ex',
GetGroupIgnoreAddRequest: 'get_group_ignore_add_request',
DelGroupNotice: '_del_group_notice',
FetchUserProfileLike: 'fetch_user_profile_like',
FriendPoke: 'friend_poke',
GroupPoke: 'group_poke',
GetPacketStatus: 'nc_get_packet_status',
GetUserStatus: 'nc_get_user_status',
GetRkey: 'nc_get_rkey',
GetGroupShutList: 'get_group_shut_list',
GetGuildList : 'get_guild_list',
GetGuildProfile : 'get_guild_service_profile',
GetGuildList: 'get_guild_list',
GetGuildProfile: 'get_guild_service_profile',
GetGroupIgnoredNotifies : 'get_group_ignored_notifies',
GetGroupIgnoredNotifies: 'get_group_ignored_notifies',
SetGroupSign : "set_group_sign",
SendGroupSign : "send_group_sign",
GetMiniAppArk : "get_mini_app_ark",
SetGroupSign: "set_group_sign",
SendGroupSign: "send_group_sign",
SendPacket: "send_packet",
GetMiniAppArk: "get_mini_app_ark",
// UploadForwardMsg : "upload_forward_msg",
GetAiRecord : "get_ai_record",
GetAiCharacters : "get_ai_characters",
SendGroupAiRecord : "send_group_ai_record",
GetAiRecord: "get_ai_record",
GetAiCharacters: "get_ai_characters",
SendGroupAiRecord: "send_group_ai_record",
GetClientkey: "get_clientkey",
SendPoke: 'send_poke',
} as const;

View File

@@ -1,5 +1,5 @@
import { ActionName } from '@/onebot/action/router';
import CanSendRecord, {CanSend} from './CanSendRecord';
import CanSendRecord, { CanSend } from './CanSendRecord';
interface ReturnType {
yes: boolean;

View File

@@ -12,16 +12,17 @@ export class OneBotFriendApi {
}
//使用前预先判断 busiId 1061
async parsePrivatePokeEvent(grayTipElement: GrayTipElement) {
async parsePrivatePokeEvent(grayTipElement: GrayTipElement, uin: number) {
const json = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
let pokedetail: Array<{ uid: string }> = json.items;
const pokedetail: Array<{ uid: string }> = json.items;
//筛选item带有uid的元素
pokedetail = pokedetail.filter(item => item.uid);
if (pokedetail.length == 2) {
const poke_uid = pokedetail.filter(item => item.uid);
if (poke_uid.length == 2) {
return new OB11FriendPokeEvent(
this.core,
parseInt((await this.core.apis.UserApi.getUinByUidV2(pokedetail[0].uid))),
parseInt((await this.core.apis.UserApi.getUinByUidV2(pokedetail[1].uid))),
uin,
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid))),
parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid))),
pokedetail,
);
}

View File

@@ -8,6 +8,8 @@ import {
NapCatCore,
NTGrayTipElementSubTypeV2,
RawMessage,
TipGroupElement,
TipGroupElementType,
} from '@/core';
import { NapCatOneBot11Adapter } from '@/onebot';
import { OB11GroupBanEvent } from '@/onebot/event/notice/OB11GroupBanEvent';
@@ -19,8 +21,10 @@ import { OB11GroupPokeEvent } from '@/onebot/event/notice/OB11PokeEvent';
import { OB11GroupEssenceEvent } from '@/onebot/event/notice/OB11GroupEssenceEvent';
import { OB11GroupTitleEvent } from '@/onebot/event/notice/OB11GroupTitleEvent';
import { OB11GroupUploadNoticeEvent } from '../event/notice/OB11GroupUploadNoticeEvent';
import { OB11GroupNameEvent } from '../event/notice/OB11GroupNameEvent';
import { pathToFileURL } from 'node:url';
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
export class OneBotGroupApi {
obContext: NapCatOneBot11Adapter;
@@ -195,7 +199,7 @@ export class OneBotGroupApi {
id: FileNapCatOneBotUUID.encode({
chatType: ChatType.KCHATTYPEGROUP,
peerUid: msg.peerUid,
}, msg.msgId, elementWrapper.elementId, elementWrapper?.fileElement?.fileUuid, "." + element.fileName),
}, msg.msgId, elementWrapper.elementId, elementWrapper?.fileElement?.fileUuid, element.fileName),
url: pathToFileURL(element.filePath).href,
name: element.fileName,
size: parseInt(element.fileSize),
@@ -204,13 +208,63 @@ export class OneBotGroupApi {
);
}
async parseGroupElement(msg: RawMessage, element: TipGroupElement, elementWrapper: GrayTipElement) {
if (element.type === TipGroupElementType.KGROUPNAMEMODIFIED) {
this.core.context.logger.logDebug('收到群名称变更事件', element);
return new OB11GroupNameEvent(
this.core,
parseInt(msg.peerUid),
parseInt(await this.core.apis.UserApi.getUinByUidV2(element.memberUid)),
element.groupName,
);
} else if (element.type === TipGroupElementType.KSHUTUP) {
const event = await this.parseGroupBanEvent(msg.peerUid, elementWrapper);
return event;
} else if (element.type === TipGroupElementType.KMEMBERADD) {
// 自己的通知 协议推送为type->85 在这里实现为了避免邀请出现问题
if (element.memberUid == this.core.selfInfo.uid) {
await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
return new OB11GroupIncreaseEvent(
this.core,
parseInt(msg.peerUid),
+this.core.selfInfo.uin,
element.adminUid ? +await this.core.apis.UserApi.getUinByUidV2(element.adminUid) : 0,
'approve'
);
}
}
}
async parseSelfInviteEvent(msg: RawMessage, inviterUin: string, inviteeUin: string) {
return new OB11GroupIncreaseEvent(
this.core,
parseInt(msg.peerUid),
+inviteeUin,
+inviterUin,
'invite'
);
}
async parseGrayTipElement(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
// 解析群组事件 由sysmsg解析
// return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) {
// 筛选出表情回应 事件
// 筛选自身入群情况
// if (grayTipElement.xmlElement.busiId === '10145') {
// const inviteData = new fastXmlParser.XMLParser({
// ignoreAttributes: false,
// attributeNamePrefix: '',
// }).parse(grayTipElement.xmlElement.content);
// const inviterUin: string = inviteData.gtip.qq[0].jp;
// const inviteeUin: string = inviteData.gtip.qq[1].jp;
// //刷新群缓存
// if (inviteeUin === this.core.selfInfo.uin) {
// this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
// return this.parseSelfInviteEvent(msg, inviterUin, inviteeUin);
// }
// } else
if (grayTipElement.xmlElement?.templId === '10382') {
return await this.obContext.apis.GroupApi.parseGroupEmojiLikeEventByGrayTip(msg.peerUid, grayTipElement);
} else {

View File

@@ -1,19 +1,5 @@
import type { OneBotFriendApi } from '@/onebot/api/friend';
import type { OneBotUserApi } from '@/onebot/api/user';
import type { OneBotGroupApi } from '@/onebot/api/group';
import type { OneBotMsgApi } from '@/onebot/api/msg';
import type { OneBotQuickActionApi } from '@/onebot/api/quick-action';
export * from './friend';
export * from './group';
export * from './user';
export * from './msg';
export * from './quick-action';
export interface StableOneBotApiWrapper {
FriendApi: OneBotFriendApi;
UserApi: OneBotUserApi;
GroupApi: OneBotGroupApi;
MsgApi: OneBotMsgApi;
QuickActionApi: OneBotQuickActionApi,
}
export * from './quick-action';

View File

@@ -1,4 +1,4 @@
import { FileNapCatOneBotUUID } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { MessageUnique } from '@/common/message-unique';
import { pathToFileURL } from 'node:url';
import {
@@ -17,6 +17,7 @@ import {
SendTextElement,
FaceType,
GrayTipElement,
GroupNotify,
} from '@/core';
import faceConfig from '@/core/external/face_config.json';
import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, } from '@/onebot';
@@ -33,7 +34,9 @@ import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent';
import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin';
import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent';
import { GroupChange, GroupChangeInfo, PushMsgBody } from '@/core/packet/transformer/proto';
import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto';
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import { LRUCache } from '@/common/lru-cache';
type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -67,7 +70,8 @@ function keyCanBeParsed(key: string, parser: RawToOb11Converters): key is keyof
export class OneBotMsgApi {
obContext: NapCatOneBot11Adapter;
core: NapCatCore;
notifyGroupInvite: LRUCache<string, GroupNotify> = new LRUCache(50);
// seq -> notify
rawToOb11Converters: RawToOb11Converters = {
textElement: async element => {
if (element.atType === NTMsgAtType.ATTYPEUNKNOWN) {
@@ -112,18 +116,22 @@ export class OneBotMsgApi {
peerUid: msg.peerUid,
guildId: '',
};
const encodedFileId = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, "." + element.fileName);
FileNapCatOneBotUUID.encode(
peer,
msg.msgId,
elementWrapper.elementId,
element.fileUuid,
element.fileName
);
return {
type: OB11MessageDataType.image,
data: {
summary: element.summary,
file: encodedFileId,
file: element.fileName,
sub_type: element.picSubType,
file_id: encodedFileId,
url: await this.core.apis.FileApi.getImageUrl(element),
path: element.filePath,
file_size: element.fileSize,
file_unique: element.md5HexStr ?? element.fileName,
},
};
} catch (e: any) {
@@ -138,15 +146,15 @@ export class OneBotMsgApi {
peerUid: msg.peerUid,
guildId: '',
};
const file = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
return {
type: OB11MessageDataType.file,
data: {
file: element.fileName,
file: file,
path: element.filePath,
url: pathToFileURL(element.filePath).href,
file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, "." + element.fileName),
file_id: file,
file_size: element.fileSize,
file_unique: element.fileMd5 ?? element.fileSha ?? element.fileName,
},
};
},
@@ -197,18 +205,18 @@ export class OneBotMsgApi {
const { emojiId } = _;
const dir = emojiId.substring(0, 2);
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`;
const filename = `${dir}-${emojiId}.gif`;
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", filename);
return {
type: OB11MessageDataType.image,
data: {
summary: _.faceName, // 商城表情名称
file: 'marketface',
file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + _.key + ".jpg"),
file: filename,
path: url,
url: url,
key: _.key,
emoji_id: _.emojiId,
emoji_package_id: _.emojiPackageId,
file_unique: _.key
},
};
},
@@ -323,16 +331,14 @@ export class OneBotMsgApi {
if (!videoDownUrl) {
videoDownUrl = element.filePath;
}
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + element.fileName);
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
return {
type: OB11MessageDataType.video,
data: {
file: fileCode,
path: videoDownUrl,
url: videoDownUrl ?? pathToFileURL(element.filePath).href,
file_id: fileCode,
file_size: element.fileSize,
file_unique: element.videoMd5 ?? element.thumbMd5 ?? element.fileName,
},
};
},
@@ -343,16 +349,14 @@ export class OneBotMsgApi {
peerUid: msg.peerUid,
guildId: '',
};
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + element.fileName);
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", element.fileName);
return {
type: OB11MessageDataType.voice,
data: {
file: fileCode,
path: element.filePath,
url: pathToFileURL(element.filePath).href,
file_id: fileCode,
file_size: element.fileSize,
file_unique: element.fileUuid
},
};
},
@@ -679,7 +683,7 @@ export class OneBotMsgApi {
async parsePrivateMsgEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
if (grayTipElement.jsonGrayTipElement.busiId == 1061) {
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement);
const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
if (PokeEvent) { return PokeEvent; };
} else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') {
return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid)));
@@ -793,6 +797,13 @@ export class OneBotMsgApi {
private async handlePrivateMessage(resMsg: OB11Message, msg: RawMessage) {
resMsg.sub_type = 'friend';
if (await this.core.apis.FriendApi.isBuddy(msg.senderUid)) {
const nickname = (await this.core.apis.UserApi.getCoreAndBaseInfo([msg.senderUid])).get(msg.senderUid)?.coreInfo.nick;
if (nickname) {
resMsg.sender.nickname = nickname;
return;
}
}
resMsg.sender.nickname = (await this.core.apis.UserApi.getUserDetailInfo(msg.senderUid)).nick;
}
@@ -895,16 +906,16 @@ export class OneBotMsgApi {
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => {
const sizePromises = elements.map(async element => {
switch (element.elementType) {
case ElementType.PTT:
return (await fsPromise.stat(element.pttElement.filePath)).size;
case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size;
default:
return 0;
case ElementType.PTT:
return (await fsPromise.stat(element.pttElement.filePath)).size;
case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size;
default:
return 0;
}
});
const sizes = await Promise.all(sizePromises);
@@ -968,25 +979,55 @@ export class OneBotMsgApi {
return { path, fileName: inputdata.name ?? fileName };
}
groupChangDecreseType2String(type: number): GroupDecreaseSubType {
switch (type) {
case 130:
return 'leave';
case 131:
return 'kick';
case 3:
return 'kick_me';
default:
return 'kick';
case 130:
return 'leave';
case 131:
return 'kick';
case 3:
return 'kick_me';
default:
return 'kick';
}
}
async waitGroupNotify(groupUin: string, memberUid?: string, operatorUid?: string) {
const groupRole = this.core.apis.GroupApi.groupMemberCache.get(groupUin)?.get(this.core.selfInfo.uid.toString())?.role;
const isAdminOrOwner = groupRole === 3 || groupRole === 4;
if (isAdminOrOwner && !operatorUid) {
let dataNotify: GroupNotify | undefined;
await this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onGroupNotifiesUpdated',
(doubt, notifies) => {
for (const notify of notifies) {
if (notify.group.groupCode === groupUin && notify.user1.uid === memberUid) {
dataNotify = notify;
return true;
}
}
return false;
}, 1, 1000).catch(undefined);
if (dataNotify) {
return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid;
}
}
return operatorUid;
}
async parseSysMessage(msg: number[]) {
const SysMessage = new NapProtoMsg(PushMsgBody).decode(Uint8Array.from(msg));
// 邀请需要解grayTipElement
if (SysMessage.contentHead.type == 33 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
const operatorUid = groupChange.operatorInfo?.toString();
await this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString(), true);
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.operatorInfo ? Buffer.from(groupChange.operatorInfo).toString() : ''
);
return new OB11GroupIncreaseEvent(
this.core,
groupChange.groupUin,
@@ -994,19 +1035,24 @@ export class OneBotMsgApi {
operatorUid ? +await this.core.apis.UserApi.getUinByUidV2(operatorUid) : 0,
groupChange.decreaseType == 131 ? 'invite' : 'approve',
);
} else if (SysMessage.contentHead.type == 34 && SysMessage.body?.msgContent) {
const groupChange = new NapProtoMsg(GroupChange).decode(SysMessage.body.msgContent);
// 自身被踢出时operatorInfo会是一个protobuf 否则大多数情况为一个string
const operatorUid = groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString();
const operatorUid = await this.waitGroupNotify(
groupChange.groupUin.toString(),
groupChange.memberUid,
groupChange.decreaseType === 3 && groupChange.operatorInfo ?
new NapProtoMsg(GroupChangeInfo).decode(groupChange.operatorInfo).operator?.operatorUid :
groupChange.operatorInfo?.toString()
);
if (groupChange.memberUid === this.core.selfInfo.uid) {
setTimeout(() => {
this.core.apis.GroupApi.groupMemberCache.delete(groupChange.groupUin.toString());
}, 5000);
// 自己被踢了 5S后回收
} else {
this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString()).then().catch();
await this.core.apis.GroupApi.refreshGroupMemberCache(groupChange.groupUin.toString(), true);
}
return new OB11GroupDecreaseEvent(
this.core,
@@ -1017,7 +1063,7 @@ export class OneBotMsgApi {
);
} else if (SysMessage.contentHead.type == 44 && SysMessage.body?.msgContent) {
const groupAmin = new NapProtoMsg(GroupAdmin).decode(SysMessage.body.msgContent);
this.core.apis.GroupApi.refreshGroupMemberCache(groupAmin.groupUin.toString()).then().catch();
await this.core.apis.GroupApi.refreshGroupMemberCache(groupAmin.groupUin.toString(), true);
let enabled = false;
let uid = '';
if (groupAmin.body.extraEnable != null) {
@@ -1033,6 +1079,72 @@ export class OneBotMsgApi {
+await this.core.apis.UserApi.getUinByUidV2(uid),
enabled ? 'set' : 'unset'
);
} else if (SysMessage.contentHead.type == 87 && SysMessage.body?.msgContent) {
const groupInvite = new NapProtoMsg(GroupInvite).decode(SysMessage.body.msgContent);
let request_seq = '';
try {
await this.core.eventWrapper.registerListen('NodeIKernelMsgListener/onRecvMsg', (msgs) => {
for (const msg of msgs) {
if (msg.senderUid === groupInvite.invitorUid && msg.msgType === 11) {
const jumpUrl = JSON.parse(msg.elements.find(e => e.elementType === 10)?.arkElement?.bytesData ?? '').meta?.news?.jumpUrl;
const jumpUrlParams = new URLSearchParams(jumpUrl);
const groupcode = jumpUrlParams.get('groupcode');
const receiveruin = jumpUrlParams.get('receiveruin');
const msgseq = jumpUrlParams.get('msgseq');
request_seq = msgseq ?? '';
if (groupcode === groupInvite.groupUin.toString() && receiveruin === this.core.selfInfo.uin) {
return true;
}
}
}
return false;
}, 1, 1000);
} catch (error) {
request_seq = '';
}
// 未拉取到seq
if (request_seq === '') {
return;
}
// 创建个假的
this.notifyGroupInvite.put(request_seq, {
seq: request_seq,
type: 1,
group: {
groupCode: groupInvite.groupUin.toString(),
groupName: '',
},
user1: {
uid: groupInvite.invitorUid,
nickName: '',
},
user2: {
uid: this.core.selfInfo.uid,
nickName: '',
},
actionUser: {
uid: groupInvite.invitorUid,
nickName: '',
},
actionTime: Date.now().toString(),
postscript: '',
repeatSeqs: [],
warningTips: '',
invitationExt: {
srcType: 1,
groupCode: groupInvite.groupUin.toString(),
waitStatus: 1,
},
status: 1
});
return new OB11GroupRequestEvent(
this.core,
+groupInvite.groupUin,
+await this.core.apis.UserApi.getUinByUidV2(groupInvite.invitorUid),
'invite',
'',
request_seq
);
} else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
}

View File

@@ -18,8 +18,8 @@ import { ContextMode, createContext, normalize } from '@/onebot/action/msg/SendM
import { isNull } from '@/common/helper';
export class OneBotQuickActionApi {
private obContext: NapCatOneBot11Adapter;
private core: NapCatCore;
obContext: NapCatOneBot11Adapter;
core: NapCatCore;
constructor(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
this.obContext = obContext;
this.core = core;
@@ -91,11 +91,13 @@ export class OneBotQuickActionApi {
}
async handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) {
let noify = await this.findNotify(request.flag);
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag);
const notify = invite_notify ?? await this.findNotify(request.flag);
if (!isNull(quickAction.approve) && noify) {
if (!isNull(quickAction.approve) && notify) {
this.core.apis.GroupApi.handleGroupRequest(
noify,
notify,
quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
quickAction.reason,
).catch(e => this.core.context.logger.logError(e));

View File

@@ -38,6 +38,14 @@ export interface AdapterConfig extends AdapterConfigInner {
const createDefaultAdapterConfig = <T extends AdapterConfig>(config: T): T => config;
export interface PluginConfig extends AdapterConfig {
name: string;
enable: boolean;
messagePostFormat: string;
reportSelfMessage: boolean;
debug: boolean;
}
export const httpServerDefaultConfigs = createDefaultAdapterConfig({
name: 'http-server',
enable: false as boolean,
@@ -51,6 +59,13 @@ export const httpServerDefaultConfigs = createDefaultAdapterConfig({
});
export type HttpServerConfig = typeof httpServerDefaultConfigs;
export const httpSseServerDefaultConfigs = createDefaultAdapterConfig({
...httpServerDefaultConfigs,
name: 'http-sse-server',
reportSelfMessage: false,
});
export type HttpSseServerConfig = typeof httpSseServerDefaultConfigs;
export const httpClientDefaultConfigs = createDefaultAdapterConfig({
name: 'http-client',
enable: false as boolean,
@@ -91,6 +106,7 @@ export type WebsocketClientConfig = typeof websocketClientDefaultConfigs;
export interface NetworkConfig {
httpServers: Array<HttpServerConfig>;
httpSseServers: Array<HttpSseServerConfig>;
httpClients: Array<HttpClientConfig>;
websocketServers: Array<WebsocketServerConfig>;
websocketClients: Array<WebsocketClientConfig>;
@@ -112,6 +128,7 @@ const createDefaultConfig = <T>(config: T): T => config;
export const defaultOneBotConfigs = createDefaultConfig<OneBotConfig>({
network: {
httpServers: [],
httpSseServers: [],
httpClients: [],
websocketServers: [],
websocketClients: [],
@@ -128,7 +145,7 @@ export const mergeNetworkDefaultConfig = {
websocketClients: websocketClientDefaultConfigs,
} as const;
export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | AdapterConfig;
export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig | PluginConfig;
type NetworkConfigKeys = keyof typeof mergeNetworkDefaultConfig;
export function mergeOneBotConfigs(
@@ -234,4 +251,4 @@ export function getConfigBoolKey(
}
});
return result;
}
}

View File

@@ -0,0 +1,13 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
import { NapCatCore } from '@/core';
export class OB11GroupNameEvent extends OB11GroupNoticeEvent {
notice_type = 'notify';
sub_type = 'group_name';
name_new: string;
constructor(core: NapCatCore, groupId: number, userId: number, nameNew: string) {
super(core, groupId, userId);
this.name_new = nameNew;
}
}

View File

@@ -10,12 +10,14 @@ class OB11PokeEvent extends OB11BaseNoticeEvent {
export class OB11FriendPokeEvent extends OB11PokeEvent {
raw_info: any;
sender_id: number;
//raw_message nb等框架标准为string
constructor(core: NapCatCore, user_id: number, target_id: number, raw_message: any) {
constructor(core: NapCatCore, user_id: number, sender_id: number, target_id: number, raw_message: any) {
super(core);
this.target_id = target_id;
this.user_id = user_id;
this.sender_id = sender_id;
this.raw_info = raw_message;
}
}

View File

@@ -1,4 +1,5 @@
import { calcQQLevel, FileNapCatOneBotUUID } from '@/common/helper';
import { calcQQLevel } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { FriendV2, Group, GroupFileInfoUpdateParamType, GroupMember, SelfInfo, NTSex } from '@/core';
import {
OB11Group,
@@ -90,6 +91,7 @@ export class OB11Construct {
file_name: file.fileName,
busid: file.busId,
size: +file.fileSize,
file_size: +file.fileSize,
upload_time: file.uploadTime,
dead_time: file.deadTime,
modify_time: file.modifyTime,

View File

@@ -16,7 +16,6 @@ import {
} from '@/core';
import { OB11ConfigLoader } from '@/onebot/config';
import {
IOB11NetworkAdapter,
OB11ActiveHttpAdapter,
OB11ActiveWebSocketAdapter,
OB11NetworkManager,
@@ -31,7 +30,6 @@ import {
OneBotMsgApi,
OneBotQuickActionApi,
OneBotUserApi,
StableOneBotApiWrapper,
} from '@/onebot/api';
import { ActionMap, createActionMap } from '@/onebot/action';
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
@@ -45,9 +43,17 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal
import { LRUCache } from '@/common/lru-cache';
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
import { BotOfflineEvent } from './event/notice/BotOfflineEvent';
import { AdapterConfigWrap, mergeOneBotConfigs, migrateOneBotConfigsV1, NetworkConfigAdapter, OneBotConfig } from './config/config';
import {
AdapterConfigWrap,
mergeOneBotConfigs,
migrateOneBotConfigsV1,
NetworkConfigAdapter,
OneBotConfig,
} from './config/config';
import { OB11Message } from './types';
import { OB11PluginAdapter } from './network/plugin';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
import { OB11ActiveHttpSSEAdapter } from './network/active-http-sse';
//OneBot实现类
export class NapCatOneBot11Adapter {
@@ -55,7 +61,7 @@ export class NapCatOneBot11Adapter {
readonly context: InstanceContext;
configLoader: OB11ConfigLoader;
public readonly apis: StableOneBotApiWrapper;
public readonly apis;
networkManager: OB11NetworkManager;
actions: ActionMap;
private readonly bootTime = Date.now() / 1000;
@@ -72,7 +78,7 @@ export class NapCatOneBot11Adapter {
UserApi: new OneBotUserApi(this, core),
FriendApi: new OneBotFriendApi(this, core),
MsgApi: new OneBotMsgApi(this, core),
QuickActionApi: new OneBotQuickActionApi(this, core),
QuickActionApi: new OneBotQuickActionApi(this, core)
} as const;
this.actions = createActionMap(this, core);
this.networkManager = new OB11NetworkManager();
@@ -82,6 +88,9 @@ export class NapCatOneBot11Adapter {
for (const key of ob11Config.network.httpServers) {
log += `HTTP服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
for (const key of ob11Config.network.httpSseServers) {
log += `HTTP-SSE服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
for (const key of ob11Config.network.httpClients) {
log += `HTTP上报服务: ${key.url}, : ${key.enable ? '已启动' : '未启动'}\n`;
}
@@ -111,12 +120,19 @@ export class NapCatOneBot11Adapter {
// 注册Plugin 如果需要基于NapCat进行快速开发
// this.networkManager.registerAdapter(
// new OB11PluginAdapter('plugin', this.core, this,this.actions)
// new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
// );
for (const key of ob11Config.network.httpServers) {
if (key.enable) {
this.networkManager.registerAdapter(
new OB11PassiveHttpAdapter(key.name, key, this.core, this.actions)
new OB11PassiveHttpAdapter(key.name, key, this.core, this, this.actions)
);
}
}
for(const key of ob11Config.network.httpSseServers){
if(key.enable) {
this.networkManager.registerAdapter(
new OB11ActiveHttpSSEAdapter(key.name, key, this.core, this, this.actions)
);
}
}
@@ -134,6 +150,7 @@ export class NapCatOneBot11Adapter {
key.name,
key,
this.core,
this,
this.actions
)
);
@@ -146,6 +163,7 @@ export class NapCatOneBot11Adapter {
key.name,
key,
this.core,
this,
this.actions
)
);
@@ -157,12 +175,16 @@ export class NapCatOneBot11Adapter {
this.initBuddyListener();
this.initGroupListener();
await WebUiDataRuntime.setQQLoginUin(selfInfo.uin.toString());
await WebUiDataRuntime.setQQLoginStatus(true);
await WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
WebUiDataRuntime.setQQVersion(this.core.context.basicInfoWrapper.getFullQQVesion());
WebUiDataRuntime.setQQLoginInfo(selfInfo);
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
const prev = this.configLoader.configData;
// 保证默认配置
newConfig = mergeOneBotConfigs(newConfig);
this.configLoader.save(newConfig);
this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
//this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`);
await this.reloadNetwork(prev, newConfig);
});
}
@@ -192,10 +214,12 @@ export class NapCatOneBot11Adapter {
await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11ActiveWebSocketAdapter);
}
private async handleConfigChange(
private async handleConfigChange<CT extends NetworkConfigAdapter>(
prevConfig: NetworkConfigAdapter[],
nowConfig: NetworkConfigAdapter[],
adapterClass: new (...args: any[]) => IOB11NetworkAdapter
adapterClass: new (
...args: ConstructorParameters<typeof IOB11NetworkAdapter<CT>>
) => IOB11NetworkAdapter<CT>
): Promise<void> {
// 比较旧的在新的找不到的回收
for (const adapterConfig of prevConfig) {
@@ -207,7 +231,7 @@ export class NapCatOneBot11Adapter {
}
}
}
// 通知新配置重载 删除关闭的 加入新开的
// 通知新配置重载 删除关闭的 加入新开的
for (const adapterConfig of nowConfig) {
const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name);
if (existingAdapter) {
@@ -216,7 +240,7 @@ export class NapCatOneBot11Adapter {
await this.networkManager.closeSomeAdaterWhenOpen([existingAdapter]);
}
} else if (adapterConfig.enable) {
const newAdapter = new adapterClass(adapterConfig.name, adapterConfig, this.core, this.actions);
const newAdapter = new adapterClass(adapterConfig.name, adapterConfig as CT, this.core, this, this.actions);
await this.networkManager.registerAdapterAndOpen(newAdapter);
}
}
@@ -301,7 +325,7 @@ export class NapCatOneBot11Adapter {
guildId: ''
};
const msg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, msgSeq)).msgList.find(e => e.msgType == NTMsgType.KMSGTYPEGRAYTIPS);
const element = msg?.elements[0];
const element = msg?.elements.find(e => !!e.grayTipElement?.revokeElement);
if (msg && element) {
const recallEvent = await this.emitRecallMsg(msg, element);
try {
@@ -379,10 +403,7 @@ export class NapCatOneBot11Adapter {
) {
this.context.logger.logDebug('有加群请求');
try {
let requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
if (isNaN(parseInt(requestUin))) {
requestUin = (await this.core.apis.UserApi.getUserDetailInfo(notify.user1.uid)).uin;
}
const requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid);
const groupRequestEvent = new OB11GroupRequestEvent(
this.core,
parseInt(notify.group.groupCode),
@@ -458,6 +479,10 @@ export class NapCatOneBot11Adapter {
}
private async handleMsg(message: RawMessage, network: Array<AdapterConfigWrap>) {
// 过滤无效消息
if (message.msgType === NTMsgType.KMSGTYPENULL) {
return;
}
try {
const ob11Msg = await this.apis.MsgApi.parseMessageV2(message, this.configLoader.configData.parseMultMsg);
if (ob11Msg) {

View File

@@ -0,0 +1,34 @@
import { OB11EmitEventContent } from './index';
import { Request, Response } from 'express';
import { OB11Response } from '@/onebot/action/OneBotAction';
import { OB11PassiveHttpAdapter } from './passive-http';
export class OB11ActiveHttpSSEAdapter extends OB11PassiveHttpAdapter {
private sseClients: Response[] = [];
async handleRequest(req: Request, res: Response): Promise<any> {
if (req.path === '/_events') {
return this.createSseSupport(req, res);
} else {
super.httpApiRequest(req, res);
}
}
private async createSseSupport(req: Request, res: Response) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
this.sseClients.push(res);
req.on('close', () => {
this.sseClients = this.sseClients.filter((client) => client !== res);
});
}
onEvent<T extends OB11EmitEventContent>(event: T) {
this.sseClients.forEach((res) => {
res.write(`data: ${JSON.stringify(event)}\n\n`);
});
}
}

View File

@@ -1,27 +1,18 @@
import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { createHmac } from 'crypto';
import { LogWrapper } from '@/common/log';
import { QuickAction, QuickActionEvent } from '@/onebot/types';
import { NapCatCore } from '@/core';
import { NapCatOneBot11Adapter } from '..';
import { RequestUtil } from '@/common/request';
import { HttpClientConfig } from '@/onebot/config/config';
import { ActionMap } from '@/onebot/action';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
logger: LogWrapper;
isEnable: boolean = false;
config: HttpClientConfig;
export class OB11ActiveHttpAdapter extends IOB11NetworkAdapter<HttpClientConfig> {
constructor(
public name: string,
config: HttpClientConfig,
public core: NapCatCore,
public obContext: NapCatOneBot11Adapter,
public actions: ActionMap,
name: string, config: HttpClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {
this.logger = core.context.logger;
this.config = structuredClone(config);
super(name, config, core, obContext, actions);
}
onEvent<T extends OB11EmitEventContent>(event: T) {
@@ -30,7 +21,6 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
async emitEventAsync<T extends OB11EmitEventContent>(event: T) {
if (!this.isEnable) return;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-self-id': this.core.selfInfo.uin,
@@ -44,7 +34,8 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
}
const data = await RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers);
const resJson: QuickAction = JSON.parse(data);
const resJson: QuickAction = data ? JSON.parse(data) : {};
await this.obContext.apis.QuickActionApi.handleQuickOperation(event as QuickActionEvent, resJson);
}
@@ -75,4 +66,4 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter {
return OB11NetworkReloadType.Normal;
}
}
}

View File

@@ -1,29 +1,21 @@
import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index';
import { WebSocket } from 'ws';
import { OB11HeartbeatEvent } from '@/onebot/event/meta/OB11HeartbeatEvent';
import { NapCatCore } from '@/core';
import { ActionName } from '@/onebot/action/router';
import { OB11Response } from '@/onebot/action/OneBotAction';
import { LogWrapper } from '@/common/log';
import { ActionMap } from '@/onebot/action';
import { LifeCycleSubType, OB11LifeCycleEvent } from '@/onebot/event/meta/OB11LifeCycleEvent';
import { WebsocketClientConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
isEnable: boolean = false;
logger: LogWrapper;
export class OB11ActiveWebSocketAdapter extends IOB11NetworkAdapter<WebsocketClientConfig> {
private connection: WebSocket | null = null;
private heartbeatRef: NodeJS.Timeout | null = null;
public config: WebsocketClientConfig;
constructor(
public name: string,
confg: WebsocketClientConfig,
public core: NapCatCore,
public actions: ActionMap,
) {
this.logger = core.context.logger;
this.config = structuredClone(confg);
constructor(name: string, config: WebsocketClientConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
super(name, config, core, obContext, actions);
}
onEvent<T extends OB11EmitEventContent>(event: T) {
@@ -133,7 +125,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
}
private async handleMessage(message: any) {
let receiveData: { action: ActionName, params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
let echo = undefined;
try {
@@ -145,7 +137,7 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter {
return;
}
receiveData.params = (receiveData?.params) ? receiveData.params : {};// 兼容类型验证
const action = this.actions.get(receiveData.action);
const action = this.actions.get(receiveData.action as any);
if (!action) {
this.logger.logError('[OneBot] [WebSocket Client] 发生错误', '不支持的Api ' + receiveData.action);
this.checkStateAndReply<any>(OB11Response.error('不支持的Api ' + receiveData.action, 1404, echo));

View File

@@ -0,0 +1,33 @@
import { NetworkConfigAdapter } from "@/onebot/config/config";
import { LogWrapper } from "@/common/log";
import { NapCatCore } from "@/core";
import { NapCatOneBot11Adapter } from "@/onebot";
import { ActionMap } from "@/onebot/action";
import { OB11EmitEventContent, OB11NetworkReloadType } from "@/onebot/network/index";
export abstract class IOB11NetworkAdapter<CT extends NetworkConfigAdapter> {
name: string;
isEnable: boolean = false;
config: CT;
readonly logger: LogWrapper;
readonly core: NapCatCore;
readonly obContext: NapCatOneBot11Adapter;
readonly actions: ActionMap;
constructor(name: string, config: CT, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
this.name = name;
this.config = structuredClone(config);
this.core = core;
this.obContext = obContext;
this.actions = actions;
this.logger = core.context.logger;
}
abstract onEvent<T extends OB11EmitEventContent>(event: T): void;
abstract open(): void | Promise<void>;
abstract close(): void | Promise<void>;
abstract reload(config: any): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
}

View File

@@ -1,7 +1,7 @@
import { OneBotEvent } from '@/onebot/event/OneBotEvent';
import { OB11Message } from '@/onebot';
import { ActionMap } from '@/onebot/action';
import { NetworkConfigAdapter } from '@/onebot/config/config';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export type OB11EmitEventContent = OneBotEvent | OB11Message;
export enum OB11NetworkReloadType {
@@ -11,23 +11,9 @@ export enum OB11NetworkReloadType {
NetWorkClose = 3,
NetWorkOpen = 4
}
export interface IOB11NetworkAdapter {
actions: ActionMap;
name: string;
isEnable: boolean;
config: NetworkConfigAdapter;
onEvent<T extends OB11EmitEventContent>(event: T): void;
open(): void | Promise<void>;
close(): void | Promise<void>;
reload(config: any): OB11NetworkReloadType | Promise<OB11NetworkReloadType>;
}
export class OB11NetworkManager {
adapters: Map<string, IOB11NetworkAdapter> = new Map();
adapters: Map<string, IOB11NetworkAdapter<NetworkConfigAdapter>> = new Map();
async openAllAdapters() {
return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.open()));
@@ -63,22 +49,22 @@ export class OB11NetworkManager {
}));
}
registerAdapter(adapter: IOB11NetworkAdapter) {
registerAdapter<CT extends NetworkConfigAdapter>(adapter: IOB11NetworkAdapter<CT>) {
this.adapters.set(adapter.name, adapter);
}
async registerAdapterAndOpen(adapter: IOB11NetworkAdapter) {
async registerAdapterAndOpen<CT extends NetworkConfigAdapter>(adapter: IOB11NetworkAdapter<CT>) {
this.registerAdapter(adapter);
await adapter.open();
}
async closeSomeAdapters(adaptersToClose: IOB11NetworkAdapter[]) {
async closeSomeAdapters<CT extends NetworkConfigAdapter>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
await adapter.close();
}
}
async closeSomeAdaterWhenOpen(adaptersToClose: IOB11NetworkAdapter[]) {
async closeSomeAdaterWhenOpen<CT extends NetworkConfigAdapter>(adaptersToClose: IOB11NetworkAdapter<CT>[]) {
for (const adapter of adaptersToClose) {
this.adapters.delete(adapter.name);
if (adapter.isEnable) {
@@ -91,7 +77,7 @@ export class OB11NetworkManager {
return this.adapters.get(name);
}
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter) => boolean) {
async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter<NetworkConfigAdapter>) => boolean) {
const adaptersToClose = Array.from(this.adapters.values()).filter(closeFilter);
await this.closeSomeAdapters(adaptersToClose);
}
@@ -118,4 +104,4 @@ export class OB11NetworkManager {
export * from './active-http';
export * from './active-websocket';
export * from './passive-http';
export * from './passive-websocket';
export * from './passive-websocket';

View File

@@ -1,4 +1,4 @@
import { IOB11NetworkAdapter, OB11NetworkReloadType } from './index';
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import express, { Express, Request, Response } from 'express';
import http from 'http';
import { NapCatCore } from '@/core';
@@ -6,23 +6,18 @@ import { OB11Response } from '@/onebot/action/OneBotAction';
import { ActionMap } from '@/onebot/action';
import cors from 'cors';
import { HttpServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
export class OB11PassiveHttpAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
private app: Express | undefined;
private server: http.Server | undefined;
isEnable: boolean = false;
public config: HttpServerConfig;
constructor(
public name: string,
config: HttpServerConfig,
public core: NapCatCore,
public actions: ActionMap,
) {
this.config = structuredClone(config);
constructor(name: string, config: HttpServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap) {
super(name, config, core, obContext, actions);
}
onEvent() {
onEvent<T extends OB11EmitEventContent>(event: T) {
// http server is passive, no need to emit event
}
@@ -87,12 +82,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
}
}
private async handleRequest(req: Request, res: Response) {
if (!this.isEnable) {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Server is closed`);
return res.json(OB11Response.error('Server is closed', 200));
}
async httpApiRequest(req: Request, res: Response) {
let payload = req.body;
if (req.method == 'get') {
payload = req.query;
@@ -118,6 +108,15 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter {
}
}
async handleRequest(req: Request, res: Response) {
if (!this.isEnable) {
this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Server is closed`);
return res.json(OB11Response.error('Server is closed', 200));
}
return this.httpApiRequest(req, res);
}
async reload(newConfig: HttpServerConfig) {
const wasEnabled = this.isEnable;
const oldPort = this.config.port;

View File

@@ -1,36 +1,29 @@
import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from './index';
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import urlParse from 'url';
import { WebSocket, WebSocketServer } from 'ws';
import { Mutex } from 'async-mutex';
import { OB11Response } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { NapCatCore } from '@/core';
import { LogWrapper } from '@/common/log';
import { OB11HeartbeatEvent } from '@/onebot/event/meta/OB11HeartbeatEvent';
import { IncomingMessage } from 'http';
import { ActionMap } from '@/onebot/action';
import { LifeCycleSubType, OB11LifeCycleEvent } from '@/onebot/event/meta/OB11LifeCycleEvent';
import { WebsocketServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from "@/onebot";
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
export class OB11PassiveWebSocketAdapter extends IOB11NetworkAdapter<WebsocketServerConfig> {
wsServer: WebSocketServer;
wsClients: WebSocket[] = [];
wsClientsMutex = new Mutex();
isEnable: boolean = false;
heartbeatInterval: number = 0;
logger: LogWrapper;
public config: WebsocketServerConfig;
private heartbeatIntervalId: NodeJS.Timeout | null = null;
wsClientWithEvent: WebSocket[] = [];
constructor(
public name: string,
config: WebsocketServerConfig,
public core: NapCatCore,
public actions: ActionMap,
name: string, config: WebsocketServerConfig, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {
this.config = structuredClone(config);
this.logger = core.context.logger;
super(name, config, core, obContext, actions);
this.wsServer = new WebSocketServer({
port: this.config.port,
host: this.config.host === '0.0.0.0' ? '' : this.config.host,
@@ -106,7 +99,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port);
this.isEnable = true;
if (this.heartbeatInterval > 0) {
if (this.config.heartInterval > 0) {
this.registerHeartBeat();
}
@@ -140,11 +133,11 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
this.wsClientsMutex.runExclusive(async () => {
this.wsClientWithEvent.forEach((wsClient) => {
if (wsClient.readyState === WebSocket.OPEN) {
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatInterval, this.core.selfInfo.online ?? true, true)));
wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.config.heartInterval, this.core.selfInfo.online ?? true, true)));
}
});
});
}, this.heartbeatInterval);
}, this.config.heartInterval);
}
private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
@@ -166,7 +159,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
}
private async handleMessage(wsClient: WebSocket, message: any) {
let receiveData: { action: ActionName, params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
let receiveData: { action: typeof ActionName[keyof typeof ActionName], params?: any, echo?: any } = { action: ActionName.Unknown, params: {} };
let echo = undefined;
try {
receiveData = JSON.parse(message.toString());
@@ -177,7 +170,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
return;
}
receiveData.params = (receiveData?.params) ? receiveData.params : {};//兼容类型验证 不然类型校验爆炸
const action = this.actions.get(receiveData.action);
const action = this.actions.get(receiveData.action as any);
if (!action) {
this.logger.logError('[OneBot] [WebSocket Client] 发生错误', '不支持的API ' + receiveData.action);
this.checkStateAndReply<any>(OB11Response.error('不支持的API ' + receiveData.action, 1404, echo), wsClient);
@@ -191,7 +184,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
const wasEnabled = this.isEnable;
const oldPort = this.config.port;
const oldHost = this.config.host;
const oldHeartbeatInterval = this.heartbeatInterval;
const oldHeartbeatInterval = this.config.heartInterval;
this.config = newConfig;
if (newConfig.enable && !wasEnabled) {
@@ -220,7 +213,6 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null;
}
this.heartbeatInterval = newConfig.heartInterval;
if (newConfig.heartInterval > 0 && this.isEnable) {
this.registerHeartBeat();
}

View File

@@ -1,42 +1,37 @@
import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from './index';
import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import { NapCatOneBot11Adapter, OB11Message } from '@/onebot';
import { NapCatCore } from '@/core';
import { AdapterConfig } from '../config/config';
import { PluginConfig } from '../config/config';
import { plugin_onmessage } from '@/plugin';
import { ActionMap } from '../action';
import { IOB11NetworkAdapter } from "@/onebot/network/adapter";
export class OB11PluginAdapter implements IOB11NetworkAdapter {
isEnable: boolean = true;
public config: AdapterConfig;
export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
constructor(
public name: string,
public core: NapCatCore,
public obCore: NapCatOneBot11Adapter,
public actions: ActionMap,
name: string, core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap
) {
// 基础配置
this.config = {
const config = {
name: name,
messagePostFormat: 'array',
reportSelfMessage: false,
enable: true,
debug: false,
}
};
super(name, config, core, obContext, actions);
}
onEvent<T extends OB11EmitEventContent>(event: T) {
if (event.post_type === 'message') {
plugin_onmessage(this.config.name, this.core, this.obCore, event as OB11Message,this.actions).then().catch();
plugin_onmessage(this.config.name, this.core, this.obContext, event as OB11Message,this.actions).then().catch();
}
}
open() {
this.isEnable = true;
}
async close() {
this.isEnable = false;
}
async reload() {

View File

@@ -75,6 +75,7 @@ export interface OB11Sender {
}
export interface OB11GroupFile {
file_size: number; // 文件大小 GOCQHTTP 群文件Api扩展
group_id: number; // 群ID
file_id: string; // 文件ID
file_name: string; // 文件名称

View File

@@ -110,7 +110,6 @@ export interface OB11MessageContext {
// 文件消息基础接口定义
export interface OB11MessageFileBase {
data: {
file_unique?: string;
path?: string;
thumb?: string;
name?: string;

View File

@@ -1,10 +1,10 @@
import { NapCatOneBot11Adapter, OB11Message } from "@/onebot";
import { NapCatCore } from "../core";
import { NapCatCore } from "@/core";
import { ActionMap } from "@/onebot/action";
export const plugin_onmessage = async (adapter: string, core: NapCatCore, obCore: NapCatOneBot11Adapter, message: OB11Message, action: ActionMap) => {
export const plugin_onmessage = async (adapter: string, core: NapCatCore, obCtx: NapCatOneBot11Adapter, message: OB11Message, action: ActionMap) => {
if (message.raw_message === 'ping') {
const ret = await action.get('send_group_msg')?.handle({ group_id: String(message.group_id), message: 'pong' }, adapter);
console.log(ret);
}
}
};

View File

@@ -152,7 +152,7 @@ async function handleLogin(
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => {
if (!isLogined) {
logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg);
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
if (errType == 1 && errCode == 3) {
// 二维码过期刷新
}
@@ -160,8 +160,8 @@ async function handleLogin(
}
};
loginListener.onLoginFailed = (args) => {
logger.logError('[Core] [Login] Login Error , ErrInfo: ', args);
loginListener.onLoginFailed = (...args) => {
logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args));
};
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
@@ -175,7 +175,9 @@ async function handleLogin(
loginService.getLoginList().then((res) => {
// 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList
WebUiDataRuntime.setQQQuickLoginList(res.LocalLoginInfoList.filter((item) => item.isQuickLogin).map((item) => item.uin.toString()));
const list = res.LocalLoginInfoList.filter((item) => item.isQuickLogin);
WebUiDataRuntime.setQQQuickLoginList(list.map((item) => item.uin.toString()));
WebUiDataRuntime.setQQNewLoginList(list);
});
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
@@ -264,7 +266,6 @@ export async function NCoreInitShell() {
const pathWrapper = new NapCatPathWrapper();
const logger = new LogWrapper(pathWrapper.logsPath);
handleUncaughtExceptions(logger);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
@@ -285,7 +286,7 @@ export async function NCoreInitShell() {
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
const cmdOptions = program.opts();
const quickLoginUin = cmdOptions.qq;
@@ -293,9 +294,7 @@ export async function NCoreInitShell() {
const dataTimestape = new Date().getTime().toString();
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));

View File

@@ -18,7 +18,7 @@ export const LoginHandler: RequestHandler = async (req, res) => {
return sendError(res, 'token is empty');
}
// 检查登录频率
if (!(await WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate))) {
if (!WebUiDataRuntime.checkLoginRate(WebUiConfigData.loginRate)) {
return sendError(res, 'login rate limit');
}
//验证config.token是否等于token

View File

@@ -1,15 +1,15 @@
import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendSuccess } from '@webapi/utils/response';
// TODO: Implement LogFileListHandler
export const LogFileListHandler: RequestHandler = async (_, res) => {
const fakeData = {
uin: 0,
nick: 'NapCat',
avatar: 'https://q1.qlogo.cn/g?b=qq&nk=0&s=640',
status: 'online',
boottime: Date.now(),
};
sendSuccess(res, fakeData);
export const PackageInfoHandler: RequestHandler = (_, res) => {
const data = WebUiDataRuntime.getPackageJson();
sendSuccess(res, data);
};
export const QQVersionHandler: RequestHandler = (_, res) => {
const data = WebUiDataRuntime.getQQVersion();
sendSuccess(res, data);
};

View File

@@ -10,15 +10,15 @@ import { sendError, sendSuccess } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check';
// 获取OneBot11配置
export const OB11GetConfigHandler: RequestHandler = async (_, res) => {
export const OB11GetConfigHandler: RequestHandler = (_, res) => {
// 获取QQ登录状态
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
const isLogin = WebUiDataRuntime.getQQLoginStatus();
// 如果未登录,返回错误
if (!isLogin) {
return sendError(res, 'Not Login');
}
// 获取登录的QQ号
const uin = await WebUiDataRuntime.getQQLoginUin();
const uin = WebUiDataRuntime.getQQLoginUin();
// 读取配置文件
const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`);
// 尝试解析配置文件
@@ -39,7 +39,7 @@ export const OB11GetConfigHandler: RequestHandler = async (_, res) => {
// 写入OneBot11配置
export const OB11SetConfigHandler: RequestHandler = async (req, res) => {
// 获取QQ登录状态
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
const isLogin = WebUiDataRuntime.getQQLoginStatus();
// 如果未登录,返回错误
if (!isLogin) {
return sendError(res, 'Not Login');

View File

@@ -7,12 +7,12 @@ import { sendError, sendSuccess } from '@webapi/utils/response';
// 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (req, res) => {
// 判断是否已经登录
if (await WebUiDataRuntime.getQQLoginStatus()) {
if (WebUiDataRuntime.getQQLoginStatus()) {
// 已经登录
return sendError(res, 'QQ Is Logined');
}
// 获取二维码
const qrcodeUrl = await WebUiDataRuntime.getQQLoginQrcodeURL();
const qrcodeUrl = WebUiDataRuntime.getQQLoginQrcodeURL();
// 判断二维码是否为空
if (isEmpty(qrcodeUrl)) {
return sendError(res, 'QRCode Get Error');
@@ -27,8 +27,8 @@ export const QQGetQRcodeHandler: RequestHandler = async (req, res) => {
// 获取QQ登录状态
export const QQCheckLoginStatusHandler: RequestHandler = async (req, res) => {
const data = {
isLogin: await WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: await WebUiDataRuntime.getQQLoginQrcodeURL(),
isLogin: WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
};
return sendSuccess(res, data);
};
@@ -38,7 +38,7 @@ export const QQSetQuickLoginHandler: RequestHandler = async (req, res) => {
// 获取QQ号
const { uin } = req.body;
// 判断是否已经登录
const isLogin = await WebUiDataRuntime.getQQLoginStatus();
const isLogin = WebUiDataRuntime.getQQLoginStatus();
if (isLogin) {
return sendError(res, 'QQ Is Logined');
}
@@ -53,12 +53,24 @@ export const QQSetQuickLoginHandler: RequestHandler = async (req, res) => {
return sendError(res, message);
}
//本来应该验证 但是http不宜这么搞 建议前端验证
//isLogin = await WebUiDataRuntime.getQQLoginStatus();
//isLogin = WebUiDataRuntime.getQQLoginStatus();
return sendSuccess(res, null);
};
// 获取快速登录列表
export const QQGetQuickLoginListHandler: RequestHandler = async (_, res) => {
const quickLoginList = await WebUiDataRuntime.getQQQuickLoginList();
const quickLoginList = WebUiDataRuntime.getQQQuickLoginList();
return sendSuccess(res, quickLoginList);
};
// 获取快速登录列表(新)
export const QQGetLoginListNewHandler: RequestHandler = async (_, res) => {
const newLoginList = WebUiDataRuntime.getQQNewLoginList();
return sendSuccess(res, newLoginList);
};
// 获取登录的QQ的信息
export const getQQLoginInfoHandler: RequestHandler = async (_, res) => {
const data = WebUiDataRuntime.getQQLoginInfo();
return sendSuccess(res, data);
};

View File

@@ -0,0 +1,19 @@
import { RequestHandler } from 'express';
import { SystemStatus, statusHelperSubscription } from "@/core/helper/status";
export const StatusRealTimeHandler: RequestHandler = async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Connection', 'keep-alive');
const sendStatus = (status: SystemStatus) => {
try{
res.write(`data: ${JSON.stringify(status)}\n\n`);
} catch (e) {
console.error(`An error occurred when writing sendStatus data to client: ${e}`);
}
};
statusHelperSubscription.on('statusUpdate', sendStatus);
req.on('close', () => {
statusHelperSubscription.off('statusUpdate', sendStatus);
res.end();
});
};

View File

@@ -1,11 +1,17 @@
import { OneBotConfig } from '@/onebot/config/config';
import type { LoginRuntimeType } from '../types/data';
import packageJson from '../../../../package.json';
const LoginRuntime: LoginRuntimeType = {
LoginCurrentTime: Date.now(),
LoginCurrentRate: 0,
QQLoginStatus: false, //已实现 但太傻了 得去那边注册个回调刷新
QQQRCodeURL: '',
QQLoginUin: '',
QQLoginInfo: {
uid: '',
uin: '',
nick: '',
},
QQVersion: 'unknown',
NapCatHelper: {
onOB11ConfigChanged: async () => {
return;
@@ -14,11 +20,13 @@ const LoginRuntime: LoginRuntimeType = {
return { result: false, message: '' };
},
QQLoginList: [],
NewQQLoginList: [],
},
packageJson: packageJson,
};
export const WebUiDataRuntime = {
checkLoginRate: async function (RateLimit: number): Promise<boolean> {
checkLoginRate(RateLimit: number): boolean {
LoginRuntime.LoginCurrentRate++;
//console.log(RateLimit, LoginRuntime.LoginCurrentRate, Date.now() - LoginRuntime.LoginCurrentTime);
if (Date.now() - LoginRuntime.LoginCurrentTime > 1000 * 60) {
@@ -29,51 +37,76 @@ export const WebUiDataRuntime = {
return LoginRuntime.LoginCurrentRate <= RateLimit;
},
getQQLoginStatus: async function (): Promise<boolean> {
getQQLoginStatus(): LoginRuntimeType['QQLoginStatus'] {
return LoginRuntime.QQLoginStatus;
},
setQQLoginStatus: async function (status: boolean): Promise<void> {
setQQLoginStatus(status: LoginRuntimeType['QQLoginStatus']): void {
LoginRuntime.QQLoginStatus = status;
},
setQQLoginQrcodeURL: async function (url: string): Promise<void> {
setQQLoginQrcodeURL(url: LoginRuntimeType['QQQRCodeURL']): void {
LoginRuntime.QQQRCodeURL = url;
},
getQQLoginQrcodeURL: async function (): Promise<string> {
getQQLoginQrcodeURL(): LoginRuntimeType['QQQRCodeURL'] {
return LoginRuntime.QQQRCodeURL;
},
setQQLoginUin: async function (uin: string): Promise<void> {
LoginRuntime.QQLoginUin = uin;
setQQLoginInfo(info: LoginRuntimeType['QQLoginInfo']): void {
LoginRuntime.QQLoginInfo = info;
LoginRuntime.QQLoginUin = info.uin.toString();
},
getQQLoginUin: async function (): Promise<string> {
getQQLoginInfo(): LoginRuntimeType['QQLoginInfo'] {
return LoginRuntime.QQLoginInfo;
},
getQQLoginUin(): LoginRuntimeType['QQLoginUin'] {
return LoginRuntime.QQLoginUin;
},
getQQQuickLoginList: async function (): Promise<any[]> {
getQQQuickLoginList(): LoginRuntimeType['NapCatHelper']['QQLoginList'] {
return LoginRuntime.NapCatHelper.QQLoginList;
},
setQQQuickLoginList: async function (list: string[]): Promise<void> {
setQQQuickLoginList(list: LoginRuntimeType['NapCatHelper']['QQLoginList']): void {
LoginRuntime.NapCatHelper.QQLoginList = list;
},
setQuickLoginCall(func: (uin: string) => Promise<{ result: boolean; message: string }>): void {
getQQNewLoginList(): LoginRuntimeType['NapCatHelper']['NewQQLoginList'] {
return LoginRuntime.NapCatHelper.NewQQLoginList;
},
setQQNewLoginList(list: LoginRuntimeType['NapCatHelper']['NewQQLoginList']): void {
LoginRuntime.NapCatHelper.NewQQLoginList = list;
},
setQuickLoginCall(func: LoginRuntimeType['NapCatHelper']['onQuickLoginRequested']): void {
LoginRuntime.NapCatHelper.onQuickLoginRequested = func;
},
requestQuickLogin: async function (uin: string): Promise<{ result: boolean; message: string }> {
return await LoginRuntime.NapCatHelper.onQuickLoginRequested(uin);
},
requestQuickLogin: function (uin) {
return LoginRuntime.NapCatHelper.onQuickLoginRequested(uin);
} as LoginRuntimeType['NapCatHelper']['onQuickLoginRequested'],
setOnOB11ConfigChanged: async function (func: (ob11: OneBotConfig) => Promise<void>): Promise<void> {
setOnOB11ConfigChanged(func: LoginRuntimeType['NapCatHelper']['onOB11ConfigChanged']): void {
LoginRuntime.NapCatHelper.onOB11ConfigChanged = func;
},
setOB11Config: async function (ob11: OneBotConfig): Promise<void> {
await LoginRuntime.NapCatHelper.onOB11ConfigChanged(ob11);
setOB11Config: function (ob11) {
return LoginRuntime.NapCatHelper.onOB11ConfigChanged(ob11);
} as LoginRuntimeType['NapCatHelper']['onOB11ConfigChanged'],
getPackageJson() {
return LoginRuntime.packageJson;
},
setQQVersion(version: string) {
LoginRuntime.QQVersion = version;
},
getQQVersion() {
return LoginRuntime.QQVersion;
}
};

View File

@@ -90,7 +90,7 @@ export class WebUiConfigWrapper {
try {
const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
if (!await fs.access(configPath, constants.R_OK | constants.W_OK).then(() => true).catch(() => false)) {
if (!await fs.access(configPath, constants.F_OK).then(() => true).catch(() => false)) {
await fs.writeFile(configPath, JSON.stringify(defaultconfig, null, 4));
}
@@ -101,7 +101,12 @@ export class WebUiConfigWrapper {
if (!parsedConfig.prefix.startsWith('/')) parsedConfig.prefix = '/' + parsedConfig.prefix;
if (parsedConfig.prefix.endsWith('/')) parsedConfig.prefix = parsedConfig.prefix.slice(0, -1);
// 配置已经被操作过了,还是回写一下吧,不然新配置不会出现在配置文件里
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4));
if (await fs.access(configPath, constants.W_OK).then(() => true).catch(() => false)) {
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4));
}
else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
}
// 不希望回写的配置放后面
// 查询主机地址是否可用

View File

@@ -0,0 +1,11 @@
import { Router } from 'express';
import { PackageInfoHandler, QQVersionHandler } from '../api/BaseInfo';
import { StatusRealTimeHandler } from "@webapi/api/Status";
const router = Router();
// router: 获取nc的package.json信息
router.get('/QQVersion', QQVersionHandler);
router.get('/PackageInfo', PackageInfoHandler);
router.get('/GetSysStatusRealTime', StatusRealTimeHandler);
export { router as BaseRouter };

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