Compare commits

..

118 Commits

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

View File

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

View File

@@ -1,5 +1,7 @@
<div align="center"> <div align="center">
<img src="https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto" alt="NapCatQQ" width="640" height="320" />
![Logo](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto)
</div> </div>
--- ---

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 335 KiB

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg } from "@/core/packet/message/message";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
interface ForwardMsgJson { interface ForwardMsgJson {
@@ -50,9 +50,29 @@ interface ForwardAdaptMsgElement {
} }
export class ForwardMsgBuilder { export class ForwardMsgBuilder {
private static build(resId: string, msg: ForwardAdaptMsg[]): ForwardMsgJson { private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg); const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) {
source = isGroupMsg ? "群聊的聊天记录" :
msg.length
? Array.from(new Set(msg.slice(0, 4).map(m => m.senderName)))
.join('和') + '的聊天记录'
: '聊天记录';
}
if (!news) {
news = msg.length === 0 ? [{
text: "Nya~ This message is send from NapCat.Packet!",
}] : msg.map(m => ({
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
}));
}
if (!summary) {
summary = `查看${msg.length}条转发消息`;
}
if (!prompt) {
prompt = "[聊天记录]";
}
return { return {
app: "com.tencent.multimsg", app: "com.tencent.multimsg",
config: { config: {
@@ -62,29 +82,21 @@ export class ForwardMsgBuilder {
type: "normal", type: "normal",
width: 300 width: 300
}, },
desc: "[聊天记录]", desc: prompt,
extra: { extra: {
filename: id, filename: id,
tsum: msg.length, tsum: msg.length,
}, },
meta: { meta: {
detail: { detail: {
news: msg.length === 0 ? [{ news,
text: "Nya~ This message is send from NapCat.Packet!",
}] : msg.map(m => ({
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
})),
resid: resId, resid: resId,
source: isGroupMsg ? "群聊的聊天记录" : source,
msg.length summary,
? Array.from(new Set(msg.map(m => m.senderName)))
.join('和') + '的聊天记录'
: '聊天记录',
summary: `查看${msg.length}条转发消息`,
uniseq: id, uniseq: id,
} }
}, },
prompt: "[聊天记录]", prompt,
ver: "0.0.0.5", ver: "0.0.0.5",
view: "contact", view: "contact",
}; };
@@ -94,13 +106,13 @@ export class ForwardMsgBuilder {
return this.build(resId, []); return this.build(resId, []);
} }
static fromPacketMsg(resId: string, packetMsg: PacketMsg[]): ForwardMsgJson { static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({ return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName, senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined, isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({ msg: msg.msg.map(m => ({
preview: m.toPreview(), preview: m.valid? m.toPreview() : "[该消息类型暂不支持查看]",
})) }))
}))); })), source, news, summary, prompt);
} }
} }

View File

@@ -239,3 +239,9 @@ export function calcQQLevel(level?: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level; const { crownNum, sunNum, moonNum, starNum } = level;
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum; return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
} }
export function stringifyWithBigInt(obj: any) {
return JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
}

View File

@@ -1 +1 @@
export const napCatVersion = '3.1.1'; export const napCatVersion = '3.3.20';

View File

@@ -6,6 +6,7 @@ import {
Peer, Peer,
PicElement, PicElement,
PicType, PicType,
RawMessage,
SendFileElement, SendFileElement,
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
@@ -30,7 +31,7 @@ export class NTQQFileApi {
context: InstanceContext; context: InstanceContext;
core: NapCatCore; core: NapCatCore;
rkeyManager: RkeyManager; rkeyManager: RkeyManager;
packetRkey: Array<{ rkey: string; time: number; type: number; }> | undefined; packetRkey: Array<{ rkey: string; time: number; type: number; ttl: bigint }> | undefined;
constructor(context: InstanceContext, core: NapCatCore) { constructor(context: InstanceContext, core: NapCatCore) {
this.context = context; this.context = context;
@@ -238,7 +239,7 @@ export class NTQQFileApi {
fileName: fileName, fileName: fileName,
filePath: path, filePath: path,
md5HexStr: md5, md5HexStr: md5,
fileSize: fileSize, fileSize: fileSize.toString(),
duration: duration ?? 1, duration: duration ?? 1,
formatType: 1, formatType: 1,
voiceType: 1, voiceType: 1,
@@ -267,6 +268,53 @@ export class NTQQFileApi {
return fileTransNotifyInfo.filePath; return fileTransNotifyInfo.filePath;
} }
async downloadRawMsgMedia(msg: RawMessage[]) {
const res = await Promise.all(
msg.map(m =>
Promise.all(
m.elements
.filter(element =>
element.elementType === ElementType.PIC ||
element.elementType === ElementType.VIDEO ||
element.elementType === ElementType.PTT ||
element.elementType === ElementType.FILE
)
.map(element =>
this.downloadMedia(m.msgId, m.chatType, m.peerUid, element.elementId, '', '', 1000 * 60 * 2, true)
)
)
)
);
msg.forEach((m, msgIndex) => {
const elementResults = res[msgIndex];
let elementIndex = 0;
m.elements.forEach(element => {
if (
element.elementType === ElementType.PIC ||
element.elementType === ElementType.VIDEO ||
element.elementType === ElementType.PTT ||
element.elementType === ElementType.FILE
) {
switch (element.elementType) {
case ElementType.PIC:
element.picElement!.sourcePath = elementResults[elementIndex];
break;
case ElementType.VIDEO:
element.videoElement!.filePath = elementResults[elementIndex];
break;
case ElementType.PTT:
element.pttElement!.filePath = elementResults[elementIndex];
break;
case ElementType.FILE:
element.fileElement!.filePath = elementResults[elementIndex];
break;
}
elementIndex++;
}
});
});
}
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) { async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
// 用于下载收到的消息中的图片等 // 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) { if (sourcePath && fs.existsSync(sourcePath)) {
@@ -296,7 +344,7 @@ export class NTQQFileApi {
filePath: thumbPath, filePath: thumbPath,
}], }],
() => true, () => true,
(arg) => arg.msgId === msgId, (arg) => arg.msgElementId === elementId && arg.msgId === msgId,
1, 1,
timeout, timeout,
); );
@@ -378,10 +426,12 @@ export class NTQQFileApi {
}; };
try { try {
if (this.core.apis.PacketApi.available) { if (this.core.apis.PacketApi.available) {
if ((!this.packetRkey || this.packetRkey[0].time > Date.now() / 1000)) { const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
if (rkey_expired_private || rkey_expired_group) {
this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket(); this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket();
} }
if (this.packetRkey.length > 0) { if (this.packetRkey && this.packetRkey.length > 0) {
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6); rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6); rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
rkeyData.online_rkey = true; rkeyData.online_rkey = true;

View File

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

View File

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

View File

@@ -82,6 +82,18 @@ export class NTQQMsgApi {
pageLimit: 1, pageLimit: 1,
}); });
} }
async queryMsgsWithFilterExWithSeqV3(peer: Peer, msgSeq: string, SendersUid: string[]) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
}
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) { async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, { return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer, chatInfo: peer,

View File

@@ -3,16 +3,23 @@ import { ChatType, InstanceContext, NapCatCore } from '..';
import offset from '@/core/external/offset.json'; import offset from '@/core/external/offset.json';
import { PacketClient, RecvPacketData } from '@/core/packet/client'; import { PacketClient, RecvPacketData } from '@/core/packet/client';
import { PacketSession } from "@/core/packet/session"; import { PacketSession } from "@/core/packet/session";
import { PacketHexStr } from "@/core/packet/packer"; import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { NapProtoMsg } from '@/core/packet/proto/NapProto'; import { NapProtoMsg } from '@/core/packet/proto/NapProto';
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202'; import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase'; import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2'; import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
import { LogWrapper } from "@/common/log"; import { LogWrapper } from "@/common/log";
import { SendLongMsgResp } from "@/core/packet/proto/message/action"; import { SendLongMsgResp } from "@/core/packet/proto/message/action";
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg } from "@/core/packet/message/message";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { PacketMsgPicElement } from "@/core/packet/msg/element"; import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp";
import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
interface OffsetType { interface OffsetType {
@@ -44,7 +51,7 @@ export class NTQQPacketApi {
.then() .then()
.catch(this.core.context.logger.logError.bind(this.core.context.logger)); .catch(this.core.context.logger.logError.bind(this.core.context.logger));
} else { } else {
this.core.context.logger.logWarn('PacketServer is not set, will not init NapCat.Packet!'); this.core.context.logger.logWarn('PacketServer未配置,NapCat.Packet将不会加载!');
} }
} }
@@ -57,7 +64,10 @@ export class NTQQPacketApi {
this.qqVersion = qqversion; this.qqVersion = qqversion;
const offsetTable: OffsetType = offset; const offsetTable: OffsetType = offset;
const table = offsetTable[qqversion + '-' + os.arch()]; const table = offsetTable[qqversion + '-' + os.arch()];
if (!table) return false; if (!table) {
this.logger.logError('PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch());
return false;
}
const url = 'ws://' + this.serverUrl + '/ws'; const url = 'ws://' + this.serverUrl + '/ws';
this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core)); this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core));
const cb = () => { const cb = () => {
@@ -73,25 +83,32 @@ export class NTQQPacketApi {
return this.packetSession!.client.sendPacket(cmd, data, rsp); return this.packetSession!.client.sendPacket(cmd, data, rsp);
} }
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp);
}
async sendPokePacket(peer: number, group?: number) { async sendPokePacket(peer: number, group?: number) {
const data = this.packetSession?.packer.packPokePacket(peer, group); const data = this.packetSession?.packer.packPokePacket(peer, group);
await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false); await this.sendOidbPacket(data!, false);
} }
async sendRkeyPacket() { async sendRkeyPacket() {
const packet = this.packetSession?.packer.packRkeyPacket(); const packet = this.packetSession?.packer.packRkeyPacket();
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', packet!, true); const ret = await this.sendOidbPacket(packet!, true);
if (!ret?.hex_data) return []; if (!ret?.hex_data) return [];
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body); const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
return retData.data.rkeyList; return retData.data.rkeyList;
} }
async sendGroupSignPacket(groupCode: string) {
const packet = this.packetSession?.packer.packGroupSignReq(this.core.selfInfo.uin, groupCode);
await this.sendOidbPacket(packet!, true);
}
async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> { async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
let status = 0; let status = 0;
try { try {
const packet = this.packetSession?.packer.packStatusPacket(uin); const packet = this.packetSession?.packer.packStatusPacket(uin);
const ret = await this.sendPacket('OidbSvcTrpcTcp.0xfe1_2', packet!, true); const ret = await this.sendOidbPacket(packet!, true);
const data = Buffer.from(ret.hex_data, 'hex'); const data = Buffer.from(ret.hex_data, 'hex');
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value; const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
// ext & 0xff00 + ext >> 16 & 0xff // ext & 0xff00 + ext >> 16 & 0xff
@@ -108,22 +125,47 @@ export class NTQQPacketApi {
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) { async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle); const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true); await this.sendOidbPacket(data!, true);
} }
private async uploadResources(msg: PacketMsg[], groupUin: number = 0) { // TODO: can simplify this
async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
const reqList = []; const reqList = [];
for (const m of msg) { for (const m of msg) {
for (const e of m.msg) { for (const e of m.msg) {
if (e instanceof PacketMsgPicElement) { if (e instanceof PacketMsgPicElement) {
reqList.push(this.packetSession?.highwaySession.uploadImage({ reqList.push(this.packetSession?.highwaySession.uploadImage({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgVideoElement) {
reqList.push(this.packetSession?.highwaySession.uploadVideo({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgPttElement) {
reqList.push(this.packetSession?.highwaySession.uploadPtt({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgFileElement) {
reqList.push(this.packetSession?.highwaySession.uploadFile({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e)); }, e));
} }
} }
} }
return Promise.all(reqList); const res = await Promise.allSettled(reqList);
this.logger.log(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.logger.logError(`上传第${index + 1}个资源失败:${result.reason}`);
}
});
} }
async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) { async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
@@ -137,7 +179,7 @@ export class NTQQPacketApi {
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) { async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID); const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x6d6_2', data!, true); const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body); const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
if (resp.download.retCode !== 0) { if (resp.download.retCode !== 0) {
@@ -145,4 +187,11 @@ export class NTQQPacketApi {
} }
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`; return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
} }
async sendMiniAppShareInfoReq(param: MiniAppReqParams) {
const data = this.packetSession?.packer.packMiniAppAdaptShareInfo(param);
const ret = await this.sendPacket("LightAppSvc.mini_app_share.AdaptShareInfo", data!, true);
const body = new NapProtoMsg(MiniAppAdaptShareInfoResp).decode(Buffer.from(ret.hex_data, 'hex'))
return JSON.parse(body.content.jsonContent) as MiniAppRawData;
}
} }

View File

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

View File

@@ -50,5 +50,13 @@
"9.9.16-28788": { "9.9.16-28788": {
"appid": 537249739, "appid": 537249739,
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B" "qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
},
"9.9.16-28971":{
"appid": 537249775,
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
},
"3.2.13-28971": {
"appid": 537249848,
"qua": "V1_LNX_NQ_3.2.13_28971_GW_B"
} }
} }

View File

@@ -19,8 +19,24 @@
"send": "A0CEC20", "send": "A0CEC20",
"recv": "A0D2520" "recv": "A0D2520"
}, },
"3.2.13-28788-arm64":{ "3.2.13-28788-arm64": {
"send": "6E91018", "send": "6E91018",
"recv": "6E94850" "recv": "6E94850"
},
"9.9.16-28971-x64": {
"send": "38079F0",
"recv": "380BE24"
},
"3.2.13-28971-x64": {
"send": "A0CEF60",
"recv": "A0D2860"
},
"3.2.12-28971-arm64": {
"send": "6E91318",
"recv": "6E94B50"
},
"6.9.56-28418-arm64": {
"send": "4471360",
"recv": "4473BCC"
} }
} }

View File

@@ -71,7 +71,8 @@ export class NodeIKernelGroupListener {
sceneId: string, sceneId: string,
ids: string[], ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean, hasPrev: boolean,
hasNext: boolean,
hasRobot: boolean hasRobot: boolean
}) { }) {
} }

View File

@@ -3,8 +3,7 @@ import { LRUCache } from "@/common/lru-cache";
import WebSocket, { Data } from "ws"; import WebSocket, { Data } from "ws";
import crypto, { createHash } from "crypto"; import crypto, { createHash } from "crypto";
import { NapCatCore } from "@/core"; import { NapCatCore } from "@/core";
import { PacketHexStr } from "@/core/packet/packer"; import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { sleep } from "@/common/helper";
export interface RecvPacket { export interface RecvPacket {
type: string, // 仅recv type: string, // 仅recv
@@ -56,7 +55,7 @@ export class PacketClient {
this.websocket.onopen = () => { this.websocket.onopen = () => {
this.isConnected = true; this.isConnected = true;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.logger.log.bind(this.logger)(`[Core] [Packet Server] Connected to ${this.clientUrl}`); this.logger.log.bind(this.logger)(`[Core] [Packet Server] 已连接到 ${this.clientUrl}`);
cb(); cb();
resolve(); resolve();
}; };
@@ -86,14 +85,14 @@ export class PacketClient {
this.reconnectAttempts++; this.reconnectAttempts++;
setTimeout(() => { setTimeout(() => {
this.connect(cb).catch((error) => { this.connect(cb).catch((error) => {
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] Reconnecting attempt failed,${error.message}`); this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 尝试重连失败:${error.message}`);
}); });
}, 5000 * this.reconnectAttempts); }, 5000 * this.reconnectAttempts);
} else { } else {
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] Max reconnect attempts reached. ${this.clientUrl}`); this.logger.logError.bind(this.logger)(`[Core] [Packet Server] ${this.clientUrl} 已达到最大重连次数!`);
} }
} catch (error: any) { } catch (error: any) {
this.logger.logError.bind(this.logger)(`Error attempting to reconnect: ${error.message}`); this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 重连时出错: ${error.message}`);
} }
} }
@@ -166,7 +165,7 @@ export class PacketClient {
// 校验失败和异常 可能返回undefined // 校验失败和异常 可能返回undefined
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.available) { if (!this.available) {
this.logger.logError('NapCat.Packet is not init'); this.logger.logError('NapCat.Packet 未初始化!');
return undefined; return undefined;
} }
const md5 = crypto.createHash('md5').update(data).digest('hex'); const md5 = crypto.createHash('md5').update(data).digest('hex');
@@ -177,4 +176,8 @@ export class PacketClient {
}).then((res) => resolve(res)).catch((e: Error) => reject(e)); }).then((res) => resolve(res)).catch((e: Error) => reject(e));
}); });
} }
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp);
}
} }

View File

@@ -0,0 +1,79 @@
export interface MiniAppReqCustomParams {
title: string;
desc: string;
picUrl: string;
jumpUrl: string;
}
export interface MiniAppReqTemplateParams {
sdkId: string;
appId: string;
scene: number;
iconUrl: string;
templateType: number;
businessType: number;
verType: number;
shareType: number;
versionId: string;
withShareTicket: number;
}
export interface MiniAppReqParams extends MiniAppReqCustomParams, MiniAppReqTemplateParams {}
export interface MiniAppData {
ver: string;
prompt: string;
config: Config;
app: string;
view: string;
meta: MetaData;
miniappShareOrigin: number;
miniappOpenRefer: string;
}
export interface MiniAppRawData {
appName: string;
appView: string;
ver: string;
desc: string;
prompt: string;
metaData: MetaData;
config: Config;
}
interface Config {
type: string;
width: number;
height: number;
forward: number;
autoSize: number;
ctime: number;
token: string;
}
interface Host {
uin: number;
nick: string;
}
interface Detail {
appid: string;
appType: number;
title: string;
desc: string;
icon: string;
preview: string;
url: string;
scene: number;
host: Host;
shareTemplateId: string;
shareTemplateData: Record<string, unknown>;
showLittleTail: string;
gamePoints: string;
gamePointsUrl: string;
shareOrigin: number;
}
interface MetaData {
detail_1: Detail;
}

View File

@@ -0,0 +1,94 @@
import {
MiniAppData,
MiniAppReqParams,
MiniAppRawData,
MiniAppReqCustomParams,
MiniAppReqTemplateParams
} from "@/core/packet/entities/miniApp";
type MiniAppTemplateNameList = "bili" | "weibo";
export abstract class MiniAppInfo {
static sdkId: string = "V1_PC_MINISDK_99.99.99_1_APP_A";
template: MiniAppReqTemplateParams;
private static appMap = new Map<MiniAppTemplateNameList, MiniAppInfo>();
protected constructor(template: MiniAppReqTemplateParams) {
this.template = template;
}
static get(name: MiniAppTemplateNameList): MiniAppInfo | undefined {
return this.appMap.get(name);
}
static Bili = new class extends MiniAppInfo {
constructor() {
super({
sdkId: MiniAppInfo.sdkId,
appId: "1109937557",
scene: 1,
templateType: 1,
businessType: 0,
verType: 3,
shareType: 0,
versionId: "cfc5f7b05b44b5956502edaecf9d2240",
withShareTicket: 0,
iconUrl: "https://miniapp.gtimg.cn/public/appicon/51f90239b78a2e4994c11215f4c4ba15_200.jpg"
});
MiniAppInfo.appMap.set("bili", this);
}
}
static WeiBo = new class extends MiniAppInfo {
constructor() {
super({
sdkId: MiniAppInfo.sdkId,
appId: "1109224783",
scene: 1,
templateType: 1,
businessType: 0,
verType: 3,
shareType: 0,
versionId: "e482a3cc4e574d9b772e96ba6eec9ba2",
withShareTicket: 0,
iconUrl: "https://miniapp.gtimg.cn/public/appicon/35bbb44dc68e65194cfacfb206b8f1f7_200.jpg"
});
MiniAppInfo.appMap.set("weibo", this);
}
}
}
export class MiniAppInfoHelper {
static generateReq(custom: MiniAppReqCustomParams, template: MiniAppReqTemplateParams): MiniAppReqParams {
return {
...custom,
...template
};
}
static RawToSend(rawData: MiniAppRawData): MiniAppData {
return {
ver: rawData.ver,
prompt: rawData.prompt,
config: rawData.config,
app: rawData.appName,
view: rawData.appView,
meta: rawData.metaData,
miniappShareOrigin: 3,
miniappOpenRefer: "10002",
};
}
static SendToRaw(data: MiniAppData): MiniAppRawData {
return {
appName: data.app,
appView: data.view,
ver: data.ver,
desc: data.meta.detail_1.desc,
prompt: data.prompt,
metaData: data.meta,
config: data.config,
};
}
}

View File

@@ -36,7 +36,7 @@ export class PacketHighwayClient {
this.port = port; this.port = port;
} }
private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 3600): PacketHighwayTrans { private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 1200): PacketHighwayTrans {
return { return {
uin: this.sig.uin, uin: this.sig.uin,
cmd: cmd, cmd: cmd,

View File

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

View File

@@ -19,11 +19,20 @@ abstract class HighwayUploader {
this.logger = logger; this.logger = logger;
} }
encryptTransExt(key: Uint8Array) { private encryptTransExt(key: Uint8Array) {
if (!this.trans.encrypt) return; if (!this.trans.encrypt) return;
this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key)); this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key));
} }
protected timeout(): Promise<void> {
return new Promise<void>((_, reject) => {
setTimeout(() => {
reject(new Error(`[Highway] timeout after ${this.trans.timeout}s`));
}, (this.trans.timeout ?? Infinity) * 1000
);
});
}
buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array { buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array {
return new NapProtoMsg(ReqDataHighwayHead).encode({ return new NapProtoMsg(ReqDataHighwayHead).encode({
msgBaseHead: { msgBaseHead: {
@@ -86,15 +95,18 @@ class HighwayTcpUploaderTransform extends stream.Transform {
export class HighwayTcpUploader extends HighwayUploader { export class HighwayTcpUploader extends HighwayUploader {
async upload(): Promise<void> { async upload(): Promise<void> {
const highwayTransForm = new HighwayTcpUploaderTransform(this); const controller = new AbortController();
const upload = new Promise<void>((resolve, _) => { const { signal } = controller;
const upload = new Promise<void>((resolve, reject) => {
const highwayTransForm = new HighwayTcpUploaderTransform(this);
const socket = net.connect(this.trans.port, this.trans.server, () => { const socket = net.connect(this.trans.port, this.trans.server, () => {
this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false }); this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false });
}); });
const handleRspHeader = (header: Buffer) => { const handleRspHeader = (header: Buffer) => {
const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header); const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header);
if (rsp.errorCode !== 0) { if (rsp.errorCode !== 0) {
this.logger.logWarn(`[Highway] tcpUpload failed (code: ${rsp.errorCode})`); socket.end();
reject(new Error(`[Highway] tcpUpload failed (code=${rsp.errorCode})`));
} }
const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2); const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2);
this.logger.logDebug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`); this.logger.logDebug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`);
@@ -105,49 +117,58 @@ export class HighwayTcpUploader extends HighwayUploader {
} }
}; };
socket.on('data', (chunk: Buffer) => { socket.on('data', (chunk: Buffer) => {
try { if (signal.aborted) {
const [head, _] = Frame.unpack(chunk); socket.end();
handleRspHeader(head); reject(new Error('Upload aborted due to timeout'));
} catch (e) {
this.logger.logError(`[Highway] tcpUpload parse response error: ${e}`);
} }
const [head, _] = Frame.unpack(chunk);
handleRspHeader(head);
}); });
socket.on('close', () => { socket.on('close', () => {
this.logger.logDebug('[Highway] tcpUpload socket closed.'); this.logger.logDebug('[Highway] tcpUpload socket closed.');
resolve(); resolve();
}); });
socket.on('error', (err) => { socket.on('error', (err) => {
this.logger.logError('[Highway] tcpUpload socket.on error:', err); socket.end();
reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`));
}); });
this.trans.data.on('error', (err) => { this.trans.data.on('error', (err) => {
this.logger.logError('[Highway] tcpUpload readable error:', err);
socket.end(); socket.end();
reject(new Error(`[Highway] tcpUpload readable error: ${err}`));
}); });
}); });
const timeout = new Promise<void>((_, reject) => { const timeout = this.timeout().catch((err) => {
setTimeout(() => { controller.abort();
reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`)); throw new Error(err.message);
}, (this.trans.timeout ?? Infinity) * 1000
);
}); });
await Promise.race([upload, timeout]); await Promise.race([upload, timeout]);
} }
} }
// TODO: timeout impl
export class HighwayHttpUploader extends HighwayUploader { export class HighwayHttpUploader extends HighwayUploader {
async upload(): Promise<void> { async upload(): Promise<void> {
let offset = 0; const controller = new AbortController();
for await (const chunk of this.trans.data) { const { signal } = controller;
const block = chunk as Buffer; const upload = (async () => {
try { let offset = 0;
await this.uploadBlock(block, offset); for await (const chunk of this.trans.data) {
} catch (err) { if (signal.aborted) {
this.logger.logError(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`); throw new Error('Upload aborted due to timeout');
throw err; }
const block = chunk as Buffer;
try {
await this.uploadBlock(block, offset);
} catch (err) {
throw new Error(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`);
}
offset += block.length;
} }
offset += block.length; })();
} const timeout = this.timeout().catch((err) => {
controller.abort();
throw new Error(err.message);
});
await Promise.race([upload, timeout]);
} }
private async uploadBlock(block: Buffer, offset: number): Promise<void> { private async uploadBlock(block: Buffer, offset: number): Promise<void> {
@@ -158,9 +179,7 @@ export class HighwayHttpUploader extends HighwayUploader {
const [head, body] = Frame.unpack(resp); const [head, body] = Frame.unpack(resp);
const headData = new NapProtoMsg(RespDataHighwayHead).decode(head); const headData = new NapProtoMsg(RespDataHighwayHead).decode(head);
this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`); this.logger.logDebug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`);
if (headData.errorCode !== 0) { if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
this.logger.logError(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
}
} }
private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> { private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> {
@@ -176,12 +195,12 @@ export class HighwayHttpUploader extends HighwayUploader {
}, },
}; };
const req = http.request(serverURL, options, (res) => { const req = http.request(serverURL, options, (res) => {
let data = Buffer.alloc(0); const data: Buffer[] = [];
res.on('data', (chunk) => { res.on('data', (chunk) => {
data = Buffer.concat([data, chunk]); data.push(chunk);
}); });
res.on('end', () => { res.on('end', () => {
resolve(data); resolve(Buffer.concat(data));
}); });
}); });
req.write(frame); req.write(frame);

View File

@@ -2,7 +2,9 @@ import * as crypto from "crypto";
import { PushMsgBody } from "@/core/packet/proto/message/message"; import { PushMsgBody } from "@/core/packet/proto/message/message";
import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto";
import { LogWrapper } from "@/common/log"; import { LogWrapper } from "@/common/log";
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/element";
import { SendTextElement } from "@/core";
export class PacketMsgBuilder { export class PacketMsgBuilder {
private logger: LogWrapper; private logger: LogWrapper;
@@ -11,10 +13,23 @@ export class PacketMsgBuilder {
this.logger = logger; this.logger = logger;
} }
protected static failBackText = new PacketMsgTextElement(
{
textElement: { content: "[该消息类型暂不支持查看]" }!
} as SendTextElement
);
buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] { buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType<typeof PushMsgBody>[] {
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => { return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`; const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`;
const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => {
return acc !== undefined ? acc : msg.buildContent();
}, undefined);
const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []);
if (!msgContent && !msgElement.length) {
this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement`);
msgElement.push(PacketMsgBuilder.failBackText.buildElement());
}
return { return {
responseHead: { responseHead: {
fromUid: "", fromUid: "",
@@ -35,7 +50,7 @@ export class PacketMsgBuilder {
divSeq: node.groupId ? undefined : 4, divSeq: node.groupId ? undefined : 4,
msgId: crypto.randomBytes(4).readUInt32LE(0), msgId: crypto.randomBytes(4).readUInt32LE(0),
sequence: crypto.randomBytes(4).readUInt32LE(0), sequence: crypto.randomBytes(4).readUInt32LE(0),
timeStamp: Math.floor(Date.now() / 1000), timeStamp: +node.time.toString().substring(0, 10),
field7: BigInt(1), field7: BigInt(1),
field8: 0, field8: 0,
field9: 0, field9: 0,
@@ -50,7 +65,8 @@ export class PacketMsgBuilder {
body: { body: {
richText: { richText: {
elems: msgElement elems: msgElement
} },
msgContent: msgContent,
} }
}; };
}); });

View File

@@ -0,0 +1,163 @@
import {
Peer,
ChatType,
ElementType,
MessageElement,
RawMessage,
SendArkElement,
SendFaceElement,
SendFileElement,
SendMarkdownElement,
SendMarketFaceElement,
SendPicElement,
SendPttElement,
SendReplyElement,
SendStructLongMsgElement,
SendTextElement,
SendVideoElement
} from "@/core";
import {
IPacketMsgElement,
PacketMsgAtElement,
PacketMsgFaceElement,
PacketMsgFileElement,
PacketMsgLightAppElement,
PacketMsgMarkDownElement,
PacketMsgMarkFaceElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgReplyElement,
PacketMsgTextElement,
PacketMsgVideoElement,
PacketMultiMsgElement
} from "@/core/packet/message/element";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
import { LogWrapper } from "@/common/log";
const SupportedElementTypes = [
ElementType.TEXT,
ElementType.PIC,
ElementType.REPLY,
ElementType.FACE,
ElementType.MFACE,
ElementType.VIDEO,
ElementType.FILE,
ElementType.PTT,
ElementType.ARK,
ElementType.MARKDOWN,
ElementType.STRUCTLONGMSG
];
type SendMessageTypeElementMap = {
[ElementType.TEXT]: SendTextElement,
[ElementType.PIC]: SendPicElement,
[ElementType.FILE]: SendFileElement,
[ElementType.PTT]: SendPttElement,
[ElementType.VIDEO]: SendVideoElement,
[ElementType.FACE]: SendFaceElement,
[ElementType.REPLY]: SendReplyElement,
[ElementType.ARK]: SendArkElement,
[ElementType.MFACE]: SendMarketFaceElement,
[ElementType.STRUCTLONGMSG]: SendStructLongMsgElement,
[ElementType.MARKDOWN]: SendMarkdownElement,
};
type ElementToPacketMsgConverters = {
[K in keyof SendMessageTypeElementMap]: (
sendElement: MessageElement
) => IPacketMsgElement<SendMessageTypeElementMap[K]>;
}
export type rawMsgWithSendMsg = {
senderUin: number;
senderUid?: string;
senderName: string;
groupId?: number;
time: number;
msg: PacketSendMsgElement[]
}
export class PacketMsgConverter {
private logger: LogWrapper;
constructor(logger: LogWrapper) {
this.logger = logger;
}
private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters {
return SupportedElementTypes.includes(type);
}
rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg {
return {
senderUid: msg.senderUid ?? '',
senderUin: msg.senderUin,
senderName: msg.senderName,
groupId: msg.groupId,
time: msg.time,
msg: msg.msg.map((element) => {
if (!this.isValidElementType(element.elementType)) return null;
return this.rawToPacketMsgConverters[element.elementType](element as MessageElement);
}).filter((e) => e !== null)
};
}
rawMsgToPacketMsg(msg: RawMessage, ctxPeer: Peer): PacketMsg {
return {
seq: +msg.msgSeq,
groupId: ctxPeer.chatType === ChatType.KCHATTYPEGROUP ? +msg.peerUid : undefined,
senderUid: msg.senderUid,
senderUin: +msg.senderUin,
senderName: msg.sendMemberName && msg.sendMemberName !== ''
? msg.sendMemberName
: msg.sendNickName && msg.sendNickName !== ''
? msg.sendNickName
: "QQ用户",
time: +msg.msgTime,
msg: msg.elements.map((element) => {
if (!this.isValidElementType(element.elementType)) return null;
return this.rawToPacketMsgConverters[element.elementType](element);
}).filter((e) => e !== null)
};
}
private rawToPacketMsgConverters: ElementToPacketMsgConverters = {
[ElementType.TEXT]: (element) => {
if (element.textElement?.atType) {
return new PacketMsgAtElement(element as SendTextElement);
}
return new PacketMsgTextElement(element as SendTextElement);
},
[ElementType.PIC]: (element) => {
return new PacketMsgPicElement(element as SendPicElement);
},
[ElementType.REPLY]: (element) => {
return new PacketMsgReplyElement(element as SendReplyElement);
},
[ElementType.FACE]: (element) => {
return new PacketMsgFaceElement(element as SendFaceElement);
},
[ElementType.MFACE]: (element) => {
return new PacketMsgMarkFaceElement(element as SendMarketFaceElement);
},
[ElementType.VIDEO]: (element) => {
return new PacketMsgVideoElement(element as SendVideoElement);
},
[ElementType.FILE]: (element) => {
return new PacketMsgFileElement(element as SendFileElement);
},
[ElementType.PTT]: (element) => {
return new PacketMsgPttElement(element as SendPttElement);
},
[ElementType.ARK]: (element) => {
return new PacketMsgLightAppElement(element as SendArkElement);
},
[ElementType.MARKDOWN]: (element) => {
return new PacketMsgMarkDownElement(element as SendMarkdownElement);
},
// TODO: check this logic, move it in arkElement?
[ElementType.STRUCTLONGMSG]: (element) => {
return new PacketMultiMsgElement(element as SendStructLongMsgElement);
}
};
}

View File

@@ -1,4 +1,3 @@
import assert from "node:assert";
import * as zlib from "node:zlib"; import * as zlib from "node:zlib";
import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto"; import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto";
import { import {
@@ -26,8 +25,10 @@ import {
SendVideoElement SendVideoElement
} from "@/core"; } from "@/core";
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
import { FileExtra, GroupFileExtra } from "@/core/packet/proto/message/component";
import { OidbSvcTrpcTcp0XE37_800Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
// raw <-> packet // raw <-> packet
// TODO: SendStructLongMsgElement // TODO: SendStructLongMsgElement
@@ -35,16 +36,20 @@ export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
protected constructor(rawElement: T) { protected constructor(rawElement: T) {
} }
get valid(): boolean {
return true;
}
buildContent(): Uint8Array | undefined { buildContent(): Uint8Array | undefined {
return undefined; return undefined;
} }
buildElement(): NapProtoEncodeStructType<typeof Elem>[] | undefined { buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
return undefined; return [];
} }
toPreview(): string { toPreview(): string {
return '[nya~]'; return '[暂不支持该消息类型喵~]';
} }
} }
@@ -93,50 +98,6 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
} }
}]; }];
} }
toPreview(): string {
return `@${this.targetUid} ${this.text}`;
}
}
export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
path: string;
name: string;
size: number;
md5: string;
width: number;
height: number;
picType: PicType;
sha1: string | null = null;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
c2cPicExt: NapProtoEncodeStructType<typeof NotOnlineImage> | null = null;
constructor(element: SendPicElement) {
super(element);
this.path = element.picElement.sourcePath;
this.name = element.picElement.fileName;
this.size = Number(element.picElement.fileSize);
this.md5 = element.picElement.md5HexStr ?? '';
this.width = element.picElement.picWidth;
this.height = element.picElement.picHeight;
this.picType = element.picElement.picType;
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
assert(this.msgInfo !== null, 'msgInfo is null, expected not null');
return [{
commonElem: {
serviceType: 48,
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
businessType: 10,
}
}];
}
toPreview(): string {
return "[图片]";
}
} }
export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> { export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
@@ -151,11 +112,11 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
constructor(element: SendReplyElement) { constructor(element: SendReplyElement) {
super(element); super(element);
this.messageId = BigInt(element.replyElement.replayMsgId ?? 0); this.messageId = BigInt(element.replyElement.replayMsgId ?? 0);
this.messageSeq = Number(element.replyElement.replayMsgSeq ?? 0); this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0);
this.messageClientSeq = Number(element.replyElement.replyMsgClientSeq ?? 0); this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0);
this.targetUin = Number(element.replyElement.senderUin ?? 0); this.targetUin = +(element.replyElement.senderUin ?? 0);
this.targetUid = element.replyElement.senderUidStr ?? ''; this.targetUid = element.replyElement.senderUidStr ?? '';
this.time = Number(element.replyElement.replyMsgTime ?? 0); this.time = +(element.replyElement.replyMsgTime ?? 0);
this.elems = []; // TODO: in replyElement.sourceMsgTextElems this.elems = []; // TODO: in replyElement.sourceMsgTextElems
} }
@@ -189,7 +150,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
} }
toPreview(): string { toPreview(): string {
return "[回复]"; return "[回复消息]";
} }
} }
@@ -284,21 +245,216 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
} }
} }
export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> { export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
constructor(element: SendVideoElement) { path: string;
name: string;
size: number;
md5: string;
width: number;
height: number;
picType: PicType;
sha1: string | null = null;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
c2cPicExt: NapProtoEncodeStructType<typeof NotOnlineImage> | null = null;
constructor(element: SendPicElement) {
super(element); super(element);
this.path = element.picElement.sourcePath;
this.name = element.picElement.fileName;
this.size = +element.picElement.fileSize;
this.md5 = element.picElement.md5HexStr ?? '';
this.width = element.picElement.picWidth;
this.height = element.picElement.picHeight;
this.picType = element.picElement.picType;
}
get valid(): boolean {
return !!this.msgInfo;
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
if (!this.msgInfo) return [];
return [{
commonElem: {
serviceType: 48,
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
businessType: 10,
}
}];
}
toPreview(): string {
return "[图片]";
} }
} }
export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> { export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
constructor(element: SendFileElement) { fileSize?: string;
filePath?: string;
thumbSize?: number;
thumbPath?: string;
fileMd5?: string;
fileSha1?: string;
thumbMd5?: string;
thumbSha1?: string;
thumbWidth?: number;
thumbHeight?: number;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
constructor(element: SendVideoElement) {
super(element); super(element);
this.fileSize = element.videoElement.fileSize;
this.filePath = element.videoElement.filePath;
this.thumbSize = element.videoElement.thumbSize;
this.thumbPath = element.videoElement.thumbPath?.get(0);
this.fileMd5 = element.videoElement.videoMd5;
this.thumbMd5 = element.videoElement.thumbMd5;
this.thumbWidth = element.videoElement.thumbWidth;
this.thumbHeight = element.videoElement.thumbHeight;
}
get valid(): boolean {
return !!this.msgInfo;
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
if (!this.msgInfo) return [];
return [{
commonElem: {
serviceType: 48,
pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
businessType: 21,
}
}];
}
toPreview(): string {
return "[视频]";
} }
} }
export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> { export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
filePath: string;
fileSize: number;
fileMd5: string;
fileSha1?: string;
fileDuration: number;
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
constructor(element: SendPttElement) { constructor(element: SendPttElement) {
super(element); super(element);
this.filePath = element.pttElement.filePath;
this.fileSize = +element.pttElement.fileSize; // TODO: cc
this.fileMd5 = element.pttElement.md5HexStr;
this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc
}
get valid(): boolean {
return false;
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
return [];
// if (!this.msgInfo) return [];
// return [{
// commonElem: {
// serviceType: 48,
// pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo),
// businessType: 22,
// }
// }];
}
toPreview(): string {
return "[语音]";
}
}
export class PacketMsgFileElement extends IPacketMsgElement<SendFileElement> {
fileName: string;
filePath: string;
fileSize: number;
fileSha1?: Uint8Array;
fileMd5?: Uint8Array;
fileUuid?: string;
fileHash?: string;
isGroupFile?: boolean;
_private_send_uid?: string;
_private_recv_uid?: string;
_e37_800_rsp?: NapProtoEncodeStructType<typeof OidbSvcTrpcTcp0XE37_800Response>;
constructor(element: SendFileElement) {
super(element);
this.fileName = element.fileElement.fileName;
this.filePath = element.fileElement.filePath;
this.fileSize = +element.fileElement.fileSize;
}
get valid(): boolean {
return this.isGroupFile || Boolean(this._e37_800_rsp);
}
buildContent(): Uint8Array | undefined {
if (this.isGroupFile || !this._e37_800_rsp) return undefined;
return new NapProtoMsg(FileExtra).encode({
file: {
fileType: 0,
fileUuid: this.fileUuid,
fileMd5: this.fileMd5,
fileName: this.fileName,
fileSize: BigInt(this.fileSize),
subcmd: 1,
dangerEvel: 0,
expireTime: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
fileHash: this.fileHash,
},
field6: {
field2: {
field1: this._e37_800_rsp?.body?.field30?.field110,
fileUuid: this.fileUuid,
fileName: this.fileName,
field6: this._e37_800_rsp?.body?.field30?.field3,
field7: this._e37_800_rsp?.body?.field30?.field101,
field8: this._e37_800_rsp?.body?.field30?.field100,
timestamp1: this._e37_800_rsp?.body?.field30?.timestamp1,
fileHash: this.fileHash,
selfUid: this._private_send_uid,
destUid: this._private_recv_uid,
}
}
});
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
if (!this.isGroupFile) return [];
const lb = Buffer.alloc(2);
const transElemVal = new NapProtoMsg(GroupFileExtra).encode({
field1: 6,
fileName: this.fileName,
inner: {
info: {
busId: 102,
fileId: this.fileUuid,
fileSize: BigInt(this.fileSize),
fileName: this.fileName,
fileSha: this.fileSha1,
extInfoString: "",
fileMd5: this.fileMd5,
}
}
});
lb.writeUInt16BE(transElemVal.length);
return [{
transElem: {
elemType: 24,
elemValue: Buffer.concat([Buffer.from([0x01]), lb, transElemVal]) // TLV
}
}];
}
toPreview(): string {
return `[文件]${this.fileName}`;
} }
} }

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import * as zlib from "node:zlib"; import * as zlib from "node:zlib";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import { calculateSha1 } from "@/core/packet/utils/crypto/hash"; import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
import { NapProtoMsg } from "@/core/packet/proto/NapProto"; import { NapProtoMsg } from "@/core/packet/proto/NapProto";
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase"; import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202"; import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202";
@@ -10,17 +10,32 @@ import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1";
import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action"; import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action"; import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
import { PacketMsgBuilder } from "@/core/packet/msg/builder"; import { PacketMsgBuilder } from "@/core/packet/message/builder";
import { PacketMsgPicElement } from "@/core/packet/msg/element"; import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { LogWrapper } from "@/common/log"; import { LogWrapper } from "@/common/log";
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg } from "@/core/packet/message/message";
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6"; import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
import { PacketMsgConverter } from "@/core/packet/msg/converter"; import { PacketMsgConverter } from "@/core/packet/message/converter";
import { PacketClient } from "@/core/packet/client"; import { PacketClient } from "@/core/packet/client";
import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700";
import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
export type PacketHexStr = string & { readonly hexNya: unique symbol }; export type PacketHexStr = string & { readonly hexNya: unique symbol };
export interface OidbPacket {
cmd: string;
data: PacketHexStr
}
export class PacketPacker { export class PacketPacker {
readonly logger: LogWrapper; readonly logger: LogWrapper;
readonly client: PacketClient; readonly client: PacketClient;
@@ -34,30 +49,34 @@ export class PacketPacker {
this.packetConverter = new PacketMsgConverter(logger); this.packetConverter = new PacketMsgConverter(logger);
} }
private toHexStr(byteArray: Uint8Array): PacketHexStr { private packetPacket(byteArray: Uint8Array): PacketHexStr {
return Buffer.from(byteArray).toString('hex') as PacketHexStr; return Buffer.from(byteArray).toString('hex') as PacketHexStr;
} }
packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): Uint8Array { packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
return new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({
command: cmd, command: cmd,
subCommand: subCmd, subCommand: subCmd,
body: body, body: body,
isReserved: isUid ? 1 : 0 isReserved: isUid ? 1 : 0
}); });
return {
cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`,
data: this.packetPacket(data)
};
} }
packPokePacket(peer: number, group?: number): PacketHexStr { packPokePacket(peer: number, group?: number): OidbPacket {
const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({ const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({
uin: peer, uin: peer,
groupUin: group, groupUin: group,
friendUin: group ?? peer, friendUin: group ?? peer,
ext: 0 ext: 0
}); });
return this.toHexStr(this.packOidbPacket(0xed3, 1, oidb_0xed3)); return this.packOidbPacket(0xed3, 1, oidb_0xed3);
} }
packRkeyPacket(): PacketHexStr { packRkeyPacket(): OidbPacket {
const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({ const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({
reqHead: { reqHead: {
common: { common: {
@@ -77,10 +96,10 @@ export class PacketPacker {
key: [10, 20, 2] key: [10, 20, 2]
}, },
}); });
return this.toHexStr(this.packOidbPacket(0x9067, 202, oidb_0x9067_202)); return this.packOidbPacket(0x9067, 202, oidb_0x9067_202);
} }
packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): PacketHexStr { packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket {
const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({ const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({
targetUid: uid, targetUid: uid,
specialTitle: tittle, specialTitle: tittle,
@@ -91,15 +110,15 @@ export class PacketPacker {
groupUin: +groupCode, groupUin: +groupCode,
body: oidb_0x8FC_2_body body: oidb_0x8FC_2_body
}); });
return this.toHexStr(this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false)); return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false);
} }
packStatusPacket(uin: number): PacketHexStr { packStatusPacket(uin: number): OidbPacket {
const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({ const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({
uin: uin, uin: uin,
key: [{ key: 27372 }] key: [{ key: 27372 }]
}); });
return this.toHexStr(this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2)); return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2);
} }
async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> { async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> {
@@ -131,12 +150,12 @@ export class PacketPacker {
} }
); );
// this.logger.logDebug("packUploadForwardMsg REQ!!!", req); // this.logger.logDebug("packUploadForwardMsg REQ!!!", req);
return this.toHexStr(req); return this.packetPacket(req);
} }
// highway part // highway part
packHttp0x6ff_501(): PacketHexStr { packHttp0x6ff_501(): PacketHexStr {
return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({ return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({
httpConn: { httpConn: {
field1: 0, field1: 0,
field2: 0, field2: 0,
@@ -153,7 +172,7 @@ export class PacketPacker {
})); }));
} }
async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<PacketHexStr> { async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode( const req = new NapProtoMsg(NTV2RichMediaReq).encode(
{ {
reqHead: { reqHead: {
@@ -177,9 +196,9 @@ export class PacketPacker {
uploadInfo: [ uploadInfo: [
{ {
fileInfo: { fileInfo: {
fileSize: Number(img.size), fileSize: +img.size,
fileHash: img.md5, fileHash: img.md5,
fileSha1: this.toHexStr(await calculateSha1(img.path)), fileSha1: img.sha1!,
fileName: img.name, fileName: img.name,
type: { type: {
type: 1, type: 1,
@@ -218,10 +237,10 @@ export class PacketPacker {
} }
} }
); );
return this.toHexStr(this.packOidbPacket(0x11c4, 100, req, true, false)); return this.packOidbPacket(0x11c4, 100, req, true, false);
} }
async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise<PacketHexStr> { async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode({ const req = new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: { reqHead: {
common: { common: {
@@ -245,9 +264,9 @@ export class PacketPacker {
uploadInfo: [ uploadInfo: [
{ {
fileInfo: { fileInfo: {
fileSize: Number(img.size), fileSize: +img.size,
fileHash: img.md5, fileHash: img.md5,
fileSha1: this.toHexStr(await calculateSha1(img.path)), fileSha1: img.sha1!,
fileName: img.name, fileName: img.name,
type: { type: {
type: 1, type: 1,
@@ -286,24 +305,379 @@ export class PacketPacker {
} }
} }
); );
return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false)); return this.packOidbPacket(0x11c5, 100, req, true, false);
} }
packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<OidbPacket> {
return this.toHexStr( if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ const req = new NapProtoMsg(NTV2RichMediaReq).encode({
download: { reqHead: {
groupUin: groupUin, common: {
appId: 7, requestId: 3,
busId: 102, command: 100
fileId: fileUUID },
scene: {
requestType: 2,
businessType: 2,
sceneType: 2,
group: {
groupUin: groupUin
},
},
client: {
agentType: 2
} }
}), true, false) },
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: +video.fileSize,
fileHash: video.fileMd5,
fileSha1: video.fileSha1,
fileName: "nya.mp4",
type: {
type: 2,
picFormat: 0,
videoFormat: 0,
voiceFormat: 0
},
height: 0,
width: 0,
time: 0,
original: 0
},
subFileType: 0
}, {
fileInfo: {
fileSize: +video.thumbSize,
fileHash: video.thumbMd5,
fileSha1: video.thumbSha1,
fileName: "nya.jpg",
type: {
type: 1,
picFormat: 0,
videoFormat: 0,
voiceFormat: 0
},
height: video.thumbHeight,
width: video.thumbWidth,
time: 0,
original: 0
},
subFileType: 100
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 2,
extBizInfo: {
pic: {
bizType: 0,
textSummary: "Nya~",
},
video: {
bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.alloc(0),
bytesGeneralFlags: Buffer.alloc(0),
}
},
clientSeq: 0,
noNeedCompatMsg: false
}
});
return this.packOidbPacket(0x11EA, 100, req, true, false);
}
async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise<OidbPacket> {
if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 3,
command: 100
},
scene: {
requestType: 2,
businessType: 2,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: peerUin
}
},
client: {
agentType: 2
}
},
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: +video.fileSize,
fileHash: video.fileMd5,
fileSha1: video.fileSha1,
fileName: "nya.mp4",
type: {
type: 2,
picFormat: 0,
videoFormat: 0,
voiceFormat: 0
},
height: 0,
width: 0,
time: 0,
original: 0
},
subFileType: 0
}, {
fileInfo: {
fileSize: +video.thumbSize,
fileHash: video.thumbMd5,
fileSha1: video.thumbSha1,
fileName: "nya.jpg",
type: {
type: 1,
picFormat: 0,
videoFormat: 0,
voiceFormat: 0
},
height: video.thumbHeight,
width: video.thumbWidth,
time: 0,
original: 0
},
subFileType: 100
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 2,
extBizInfo: {
pic: {
bizType: 0,
textSummary: "Nya~",
},
video: {
bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.alloc(0),
bytesGeneralFlags: Buffer.alloc(0),
}
},
clientSeq: 0,
noNeedCompatMsg: false
}
});
return this.packOidbPacket(0x11E9, 100, req, true, false);
}
async packUploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 100
},
scene: {
requestType: 2,
businessType: 3,
sceneType: 2,
group: {
groupUin: groupUin
}
},
client: {
agentType: 2
}
},
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: ptt.fileSize,
fileHash: ptt.fileMd5,
fileSha1: ptt.fileSha1,
fileName: `${ptt.fileMd5}.amr`,
type: {
type: 3,
picFormat: 0,
videoFormat: 0,
voiceFormat: 1
},
height: 0,
width: 0,
time: ptt.fileDuration,
original: 0
},
subFileType: 0
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 2,
extBizInfo: {
pic: {
textSummary: "Nya~",
},
video: {
bytesPbReserve: Buffer.alloc(0),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.from([0x08, 0x00, 0x38, 0x00]),
bytesGeneralFlags: Buffer.from([0x9a, 0x01, 0x07, 0xaa, 0x03, 0x04, 0x08, 0x08, 0x12, 0x00]),
}
},
clientSeq: 0,
noNeedCompatMsg: false
}
});
return this.packOidbPacket(0x126E, 100, req, true, false);
}
async packUploadC2CPttReq(peerUin: string, ptt: PacketMsgPttElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 4,
command: 100
},
scene: {
requestType: 2,
businessType: 3,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: peerUin
}
},
client: {
agentType: 2
}
},
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: ptt.fileSize,
fileHash: ptt.fileMd5,
fileSha1: ptt.fileSha1,
fileName: `${ptt.fileMd5}.amr`,
type: {
type: 3,
picFormat: 0,
videoFormat: 0,
voiceFormat: 1
},
height: 0,
width: 0,
time: ptt.fileDuration,
original: 0
},
subFileType: 0
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 1,
extBizInfo: {
pic: {
textSummary: "Nya~",
},
ptt: {
bytesReserve: Buffer.from([0x08, 0x00, 0x38, 0x00]),
bytesGeneralFlags: Buffer.from([0x9a, 0x01, 0x0b, 0xaa, 0x03, 0x08, 0x08, 0x04, 0x12, 0x04, 0x00, 0x00, 0x00, 0x00]),
}
},
clientSeq: 0,
noNeedCompatMsg: false
}
});
return this.packOidbPacket(0x126D, 100, req, true, false);
}
async packUploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise<OidbPacket> {
const body = new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({
file: {
groupUin: groupUin,
appId: 4,
busId: 102,
entrance: 6,
targetDirectory: '/', // TODO:
fileName: file.fileName,
localDirectory: `/${file.fileName}`,
fileSize: BigInt(file.fileSize),
fileMd5: file.fileMd5,
fileSha1: file.fileSha1,
fileSha3: Buffer.alloc(0),
field15: true
}
});
return this.packOidbPacket(0x6D6, 0, body, true, false);
}
async packUploadC2CFileReq(selfUid: string, peerUid: string, file: PacketMsgFileElement): Promise<OidbPacket> {
const body = new NapProtoMsg(OidbSvcTrpcTcp0XE37_1700).encode({
command: 1700,
seq: 0,
upload: {
senderUid: selfUid,
receiverUid: peerUid,
fileSize: file.fileSize,
fileName: file.fileName,
md510MCheckSum: await computeMd5AndLengthWithLimit(file.filePath, 10 * 1024 * 1024),
sha1CheckSum: file.fileSha1,
localPath: "/",
md5CheckSum: file.fileMd5,
sha3CheckSum: Buffer.alloc(0)
},
businessId: 3,
clientType: 1,
flagSupportMediaPlatform: 1
});
return this.packOidbPacket(0xE37, 1700, body, false, false);
}
packOfflineFileDownloadReq(fileUUID: string, fileHash: string, senderUid: string, receiverUid: string): OidbPacket {
return this.packOidbPacket(0xE37, 800, new NapProtoMsg(OidbSvcTrpcTcp0XE37_800).encode({
subCommand: 800,
field2: 0,
body: {
senderUid: senderUid,
receiverUid: receiverUid,
fileUuid: fileUUID,
fileHash: fileHash,
},
field101: 3,
field102: 1,
field200: 1,
}), false, false);
}
packGroupFileDownloadReq(groupUin: number, fileUUID: string): OidbPacket {
return this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({
download: {
groupUin: groupUin,
appId: 7,
busId: 102,
fileId: fileUUID
}
}), true, false
); );
} }
packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr { packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr {
return this.toHexStr( return this.packetPacket(
new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({ new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({
subCommand: 1200, subCommand: 1200,
field2: 1, field2: 1,
@@ -321,4 +695,53 @@ export class PacketPacker {
}) })
); );
} }
packGroupSignReq(uin: string, groupCode: string): OidbPacket {
return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode(
{
body: {
uin: uin,
groupUin: groupCode,
version: "9.0.90"
}
}
), false, false);
}
packMiniAppAdaptShareInfo(req: MiniAppReqParams): PacketHexStr {
return this.packetPacket(
new NapProtoMsg(MiniAppAdaptShareInfoReq).encode(
{
appId: req.sdkId,
body: {
extInfo: {
field2: Buffer.alloc(0)
},
appid: req.appId,
title: req.title,
desc: req.desc,
time: BigInt(Date.now()),
scene: req.scene,
templateType: req.templateType,
businessType: req.businessType,
picUrl: req.picUrl,
vidUrl: "",
jumpUrl: req.jumpUrl,
iconUrl: req.iconUrl,
verType: req.verType,
shareType: req.shareType,
versionId: req.versionId,
withShareTicket: req.withShareTicket,
webURL: "",
appidRich: Buffer.alloc(0),
template: {
templateId: "",
templateData: ""
},
field20: ""
}
}
)
)
}
} }

View File

@@ -0,0 +1,49 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto";
export const MiniAppAdaptShareInfoReq = {
appId: ProtoField(2, ScalarType.STRING),
body: ProtoField(4, () => MiniAppAdaptShareInfoReqBody),
};
export const MiniAppAdaptShareInfoReqBody = {
extInfo: ProtoField(1, () => ExtInfo),
appid: ProtoField(2, ScalarType.STRING),
title: ProtoField(3, ScalarType.STRING),
desc: ProtoField(4, ScalarType.STRING),
time: ProtoField(5, ScalarType.UINT64),
scene: ProtoField(6, ScalarType.UINT32),
templateType: ProtoField(7, ScalarType.UINT32),
businessType: ProtoField(8, ScalarType.UINT32),
picUrl: ProtoField(9, ScalarType.STRING),
vidUrl: ProtoField(10, ScalarType.STRING),
jumpUrl: ProtoField(11, ScalarType.STRING),
iconUrl: ProtoField(12, ScalarType.STRING),
verType: ProtoField(13, ScalarType.UINT32),
shareType: ProtoField(14, ScalarType.UINT32),
versionId: ProtoField(15, ScalarType.STRING),
withShareTicket: ProtoField(16, ScalarType.UINT32),
webURL: ProtoField(17, ScalarType.STRING),
appidRich: ProtoField(18, ScalarType.BYTES),
template: ProtoField(19, () => Template),
field20: ProtoField(20, ScalarType.STRING),
};
export const ExtInfo = {
field2: ProtoField(2, ScalarType.BYTES),
};
export const Template = {
templateId: ProtoField(1, ScalarType.STRING),
templateData: ProtoField(2, ScalarType.STRING),
};
export const MiniAppAdaptShareInfoResp = {
field2: ProtoField(2, ScalarType.UINT32),
field3: ProtoField(3, ScalarType.STRING),
content: ProtoField(4, () => MiniAppAdaptShareInfoRespContent),
};
export const MiniAppAdaptShareInfoRespContent = {
jsonContent: ProtoField(2, ScalarType.STRING),
};

View File

@@ -113,6 +113,24 @@ export const Permission = {
export const FileExtra = { export const FileExtra = {
file: ProtoField(1, () => NotOnlineFile), file: ProtoField(1, () => NotOnlineFile),
field6: ProtoField(6, () => PrivateFileExtra),
};
export const PrivateFileExtra = {
field2: ProtoField(2, () => PrivateFileExtraField2),
};
export const PrivateFileExtraField2 = {
field1: ProtoField(1, ScalarType.UINT32),
fileUuid: ProtoField(4, ScalarType.STRING),
fileName: ProtoField(5, ScalarType.STRING),
field6: ProtoField(6, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.BYTES),
field8: ProtoField(8, ScalarType.BYTES),
timestamp1: ProtoField(9, ScalarType.UINT32),
fileHash: ProtoField(14, ScalarType.STRING),
selfUid: ProtoField(15, ScalarType.STRING),
destUid: ProtoField(16, ScalarType.STRING),
}; };
export const GroupFileExtra = { export const GroupFileExtra = {
@@ -132,8 +150,9 @@ export const GroupFileExtraInfo = {
fileSize: ProtoField(3, ScalarType.UINT64), fileSize: ProtoField(3, ScalarType.UINT64),
fileName: ProtoField(4, ScalarType.STRING), fileName: ProtoField(4, ScalarType.STRING),
field5: ProtoField(5, ScalarType.UINT32), field5: ProtoField(5, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.STRING), fileSha: ProtoField(6, ScalarType.BYTES),
fileMd5: ProtoField(8, ScalarType.STRING), extInfoString: ProtoField(7, ScalarType.STRING),
fileMd5: ProtoField(8, ScalarType.BYTES),
}; };
export const ImageExtraUrl = { export const ImageExtraUrl = {

View File

@@ -0,0 +1,62 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "../NapProto";
import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
export const OidbSvcTrpcTcp0XE37_800 = {
subCommand: ProtoField(1, ScalarType.UINT32),
field2: ProtoField(2, ScalarType.INT32),
body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800Body, true),
field101: ProtoField(101, ScalarType.INT32),
field102: ProtoField(102, ScalarType.INT32),
field200: ProtoField(200, ScalarType.INT32)
};
export const OidbSvcTrpcTcp0XE37_800Body = {
senderUid: ProtoField(10, ScalarType.STRING, true),
receiverUid: ProtoField(20, ScalarType.STRING, true),
fileUuid: ProtoField(30, ScalarType.STRING, true),
fileHash: ProtoField(40, ScalarType.STRING, true)
};
export const OidbSvcTrpcTcp0XE37Response = {
command: ProtoField(1, ScalarType.UINT32),
seq: ProtoField(2, ScalarType.INT32),
upload: ProtoField(19, () => ApplyUploadRespV3, true),
businessId: ProtoField(101, ScalarType.INT32),
clientType: ProtoField(102, ScalarType.INT32),
flagSupportMediaPlatform: ProtoField(200, ScalarType.INT32)
};
export const ApplyUploadRespV3 = {
retCode: ProtoField(10, ScalarType.INT32),
retMsg: ProtoField(20, ScalarType.STRING, true),
totalSpace: ProtoField(30, ScalarType.INT64),
usedSpace: ProtoField(40, ScalarType.INT64),
uploadedSize: ProtoField(50, ScalarType.INT64),
uploadIp: ProtoField(60, ScalarType.STRING, true),
uploadDomain: ProtoField(70, ScalarType.STRING, true),
uploadPort: ProtoField(80, ScalarType.UINT32),
uuid: ProtoField(90, ScalarType.STRING, true),
uploadKey: ProtoField(100, ScalarType.BYTES, true),
boolFileExist: ProtoField(110, ScalarType.BOOL),
packSize: ProtoField(120, ScalarType.INT32),
uploadIpList: ProtoField(130, ScalarType.STRING, false, true), // repeated
uploadHttpsPort: ProtoField(140, ScalarType.INT32),
uploadHttpsDomain: ProtoField(150, ScalarType.STRING, true),
uploadDns: ProtoField(160, ScalarType.STRING, true),
uploadLanip: ProtoField(170, ScalarType.STRING, true),
fileAddon: ProtoField(200, ScalarType.STRING, true),
mediaPlatformUploadKey: ProtoField(220, ScalarType.BYTES, true)
};
export const OidbSvcTrpcTcp0XE37_800Response = {
command: ProtoField(1, ScalarType.UINT32, true),
subCommand: ProtoField(2, ScalarType.UINT32, true),
body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800ResponseBody, true),
field50: ProtoField(50, ScalarType.UINT32, true),
};
export const OidbSvcTrpcTcp0XE37_800ResponseBody = {
field10: ProtoField(10, ScalarType.UINT32, true),
field30: ProtoField(30, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true),
};

View File

@@ -14,6 +14,7 @@ export const OidbSvcTrpcTcp0X9067_202Key = {
//Rsp //Rsp
export const OidbSvcTrpcTcp0X9067_202_RkeyList = { export const OidbSvcTrpcTcp0X9067_202_RkeyList = {
rkey: ProtoField(1, ScalarType.STRING), rkey: ProtoField(1, ScalarType.STRING),
ttl: ProtoField(2, ScalarType.UINT64),
time: ProtoField(4, ScalarType.UINT32), time: ProtoField(4, ScalarType.UINT32),
type: ProtoField(5, ScalarType.UINT32), type: ProtoField(5, ScalarType.UINT32),

View File

@@ -30,7 +30,7 @@ export const OidbSvcTrpcTcp0XE37_1200ResponseBody = {
field10: ProtoField(10, ScalarType.UINT32, true), field10: ProtoField(10, ScalarType.UINT32, true),
state: ProtoField(20, ScalarType.STRING, true), state: ProtoField(20, ScalarType.STRING, true),
result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true), result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true),
metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_1200Metadata, true), metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true),
}; };
export const OidbSvcTrpcTcp0XE37_1200Result = { export const OidbSvcTrpcTcp0XE37_1200Result = {
@@ -43,7 +43,7 @@ export const OidbSvcTrpcTcp0XE37_1200Result = {
extra: ProtoField(120, ScalarType.BYTES, true), extra: ProtoField(120, ScalarType.BYTES, true),
}; };
export const OidbSvcTrpcTcp0XE37_1200Metadata = { export const OidbSvcTrpcTcp0XE37_800_1200Metadata = {
uin: ProtoField(1, ScalarType.UINT32, true), uin: ProtoField(1, ScalarType.UINT32, true),
field2: ProtoField(2, ScalarType.UINT32, true), field2: ProtoField(2, ScalarType.UINT32, true),
field3: ProtoField(3, ScalarType.UINT32, true), field3: ProtoField(3, ScalarType.UINT32, true),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -327,8 +327,7 @@ export interface NodeIKernelMsgService {
setPttPlayedState(...args: unknown[]): unknown; setPttPlayedState(...args: unknown[]): unknown;
//uk1 uk2 true fetchFavEmojiList(str: string, num: number, backward: boolean, forceRefresh: boolean): Promise<GeneralCallResult & {
fetchFavEmojiList(str: string, num: number, uk1: boolean, uk2: boolean): Promise<GeneralCallResult & {
emojiInfoList: Array<{ emojiInfoList: Array<{
uin: string, uin: string,
emoId: number, emoId: number,

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
group_id: { type: ['number', 'string'] }
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPGetGroupAtAllRemain extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const ret = await this.core.apis.GroupApi.getGroupRemainAtTimes(payload.group_id.toString());
const data = {
can_at_all: ret.atInfo.canAtAll,
remain_at_all_count_for_group: ret.atInfo.RemainAtAllCountForGroup,
remain_at_all_count_for_uin: ret.atInfo.RemainAtAllCountForUin
};
return data;
}
}

View File

@@ -0,0 +1,21 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
url: { type: 'string' },
},
required: ['url'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPCheckUrlSafely extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_CheckUrlSafely;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
return { level: 1 };
}
}

View File

@@ -0,0 +1,38 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
friend_id: { type: ['string', 'number'] },
temp_block: { type: 'boolean' },
temp_both_del: { type: 'boolean' },
},
required: ['friend_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPDeleteFriend extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DeleteFriend;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.friend_id.toString());
if (!uid) {
return {
valid: false,
message: '好友不存在',
};
}
const isBuddy = await this.core.apis.FriendApi.isBuddy(uid);
if (!isBuddy) {
return {
valid: false,
message: '不是好友',
};
}
return await this.core.apis.FriendApi.delBuudy(uid, payload.temp_block, payload.temp_both_del);
}
}

View File

@@ -0,0 +1,28 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {
model: { type: 'string' },
}
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPGetModelShow extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetModelShow;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
if (!payload.model) {
payload.model = 'napcat';
}
return [{
variants: {
model_show: "napcat",
need_pay: false
}
}];
}
}

View File

@@ -0,0 +1,19 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
const SchemaData = {
type: 'object',
properties: {},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
//兼容性代码
export class GoCQHTTPSetModelShow extends BaseAction<Payload, any> {
actionName = ActionName.GoCQHTTP_SetModelShow;
payloadSchema = SchemaData;
async _handle(payload: Payload) {
return null;
}
}

View File

@@ -1,10 +0,0 @@
import BaseAction from '../BaseAction';
import { ActionName } from '../types';
export default class SetModelShow extends BaseAction<null, null> {
actionName = ActionName.SetModelShow;
async _handle(payload: null): Promise<null> {
return null;
}
}

View File

@@ -21,7 +21,14 @@ export class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const groupIdStr = payload.group_id.toString(); const groupIdStr = payload.group_id.toString();
const groupMembers = await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr); const noCache = payload.no_cache ? this.stringToBoolean(payload.no_cache) : false;
const memberCache = this.core.apis.GroupApi.groupMemberCache;
let groupMembers;
if (noCache) {
groupMembers = await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr);
} else {
groupMembers = memberCache.get(groupIdStr) ?? await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr);
}
const memberPromises = Array.from(groupMembers.values()).map(item => const memberPromises = Array.from(groupMembers.values()).map(item =>
OB11Entities.groupMember(groupIdStr, item) OB11Entities.groupMember(groupIdStr, item)
@@ -30,4 +37,7 @@ export class GetGroupMemberList extends BaseAction<Payload, OB11GroupMember[]> {
const MemberMap = new Map(_groupMembers.map(member => [member.user_id, member])); const MemberMap = new Map(_groupMembers.map(member => [member.user_id, member]));
return Array.from(MemberMap.values()); return Array.from(MemberMap.values());
} }
} stringToBoolean(str: string | boolean): boolean {
return typeof str === 'boolean' ? str : str.toLowerCase() === "true";
}
}

View File

@@ -71,7 +71,6 @@ import { FetchUserProfileLike } from './extends/FetchUserProfileLike';
import { NapCatCore } from '@/core'; import { NapCatCore } from '@/core';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
import GetGuildProfile from './guild/GetGuildProfile'; import GetGuildProfile from './guild/GetGuildProfile';
import SetModelShow from './go-cqhttp/SetModelShow';
import { SetInputStatus } from './extends/SetInputStatus'; import { SetInputStatus } from './extends/SetInputStatus';
import { GetCSRF } from './system/GetCSRF'; import { GetCSRF } from './system/GetCSRF';
import { DelGroupNotice } from './group/DelGroupNotice'; import { DelGroupNotice } from './group/DelGroupNotice';
@@ -93,6 +92,13 @@ import { GetGroupFileUrl } from "@/onebot/action/file/GetGroupFileUrl";
import { GetPacketStatus } from "@/onebot/action/packet/GetPacketStatus"; import { GetPacketStatus } from "@/onebot/action/packet/GetPacketStatus";
import { FriendPoke } from "@/onebot/action/user/FriendPoke"; import { FriendPoke } from "@/onebot/action/user/FriendPoke";
import { GetCredentials } from './system/GetCredentials'; import { GetCredentials } from './system/GetCredentials';
import { SetGroupSign } from './extends/SetGroupSign';
import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain';
import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely';
import { GoCQHTTPGetModelShow } from './go-cqhttp/GoCQHTTPGetModelShow';
import { GoCQHTTPSetModelShow } from './go-cqhttp/GoCQHTTPSetModelShow';
import { GoCQHTTPDeleteFriend } from './go-cqhttp/GoCQHTTPDeleteFriend';
import { GetMiniAppArk } from "@/onebot/action/extends/GetMiniAppArk";
export type ActionMap = Map<string, BaseAction<any, any>>; export type ActionMap = Map<string, BaseAction<any, any>>;
@@ -115,6 +121,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new SetQQAvatar(obContext, core), new SetQQAvatar(obContext, core),
new TranslateEnWordToZn(obContext, core), new TranslateEnWordToZn(obContext, core),
new GetGroupRootFiles(obContext, core), new GetGroupRootFiles(obContext, core),
new SetGroupSign(obContext, core),
// onebot11 // onebot11
new SendLike(obContext, core), new SendLike(obContext, core),
new GetMsg(obContext, core), new GetMsg(obContext, core),
@@ -149,6 +156,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetRobotUinRange(obContext, core), new GetRobotUinRange(obContext, core),
new GetFriendWithCategory(obContext, core), new GetFriendWithCategory(obContext, core),
//以下为go-cqhttp api //以下为go-cqhttp api
new GoCQHTTPDeleteFriend(obContext, core),
new GoCQHTTPCheckUrlSafely(obContext, core),
new GetOnlineClient(obContext, core), new GetOnlineClient(obContext, core),
new OCRImage(obContext, core), new OCRImage(obContext, core),
new IOCRImage(obContext, core), new IOCRImage(obContext, core),
@@ -156,6 +165,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new SendGroupNotice(obContext, core), new SendGroupNotice(obContext, core),
new GetGroupNotice(obContext, core), new GetGroupNotice(obContext, core),
new GetGroupEssence(obContext, core), new GetGroupEssence(obContext, core),
new GoCQHTTPGetGroupAtAllRemain(obContext, core),
new GoCQHTTPSendForwardMsg(obContext, core), new GoCQHTTPSendForwardMsg(obContext, core),
new GoCQHTTPSendGroupForwardMsg(obContext, core), new GoCQHTTPSendGroupForwardMsg(obContext, core),
new GoCQHTTPSendPrivateForwardMsg(obContext, core), new GoCQHTTPSendPrivateForwardMsg(obContext, core),
@@ -178,7 +188,9 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new FetchCustomFace(obContext, core), new FetchCustomFace(obContext, core),
new GoCQHTTPUploadPrivateFile(obContext, core), new GoCQHTTPUploadPrivateFile(obContext, core),
new GetGuildProfile(obContext, core), new GetGuildProfile(obContext, core),
new SetModelShow(obContext, core), new GoCQHTTPGetModelShow(obContext, core),
new GoCQHTTPSetModelShow(obContext, core),
new GoCQHTTPCheckUrlSafely(obContext, core),
new SetInputStatus(obContext, core), new SetInputStatus(obContext, core),
new GetCSRF(obContext, core), new GetCSRF(obContext, core),
new GetCredentials(obContext, core), new GetCredentials(obContext, core),
@@ -199,6 +211,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
// new UploadForwardMsg(obContext, core), // new UploadForwardMsg(obContext, core),
new GetGroupShutList(obContext, core), new GetGroupShutList(obContext, core),
new GetGroupFileUrl(obContext, core), new GetGroupFileUrl(obContext, core),
new GetMiniAppArk(obContext, core),
]; ];
const actionMap = new Map(); const actionMap = new Map();
for (const action of actionHandlers) { for (const action of actionHandlers) {

View File

@@ -11,9 +11,10 @@ import { decodeCQCode } from '@/onebot/cqcode';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { ChatType, ElementType, NapCatCore, Peer, RawMessage, SendArkElement, SendMessageElement } from '@/core'; import { ChatType, ElementType, NapCatCore, Peer, RawMessage, SendArkElement, SendMessageElement } from '@/core';
import BaseAction from '../BaseAction'; import BaseAction from '../BaseAction';
import { rawMsgWithSendMsg } from "@/core/packet/msg/converter"; import { rawMsgWithSendMsg } from "@/core/packet/message/converter";
import { PacketMsg } from "@/core/packet/msg/message"; import { PacketMsg } from "@/core/packet/message/message";
import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
import { stringifyWithBigInt } from "@/common/helper";
export interface ReturnDataType { export interface ReturnDataType {
message_id: number; message_id: number;
@@ -116,9 +117,17 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (getSpecialMsgNum(payload, OB11MessageDataType.node)) { if (getSpecialMsgNum(payload, OB11MessageDataType.node)) {
const packetMode = this.core.apis.PacketApi.available; const packetMode = this.core.apis.PacketApi.available;
const returnMsgAndResId = packetMode let returnMsgAndResId: { message: RawMessage | null, res_id?: string } | null;
? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[]) try {
: await this.handleForwardedNodes(peer, messages as OB11MessageNode[]); returnMsgAndResId = packetMode
? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[], payload.source, payload.news, payload.summary, payload.prompt)
: await this.handleForwardedNodes(peer, messages as OB11MessageNode[]);
} catch (e) {
throw Error(packetMode ? `发送伪造合并转发消息失败: ${e}` : `发送合并转发消息失败: ${e}`);
}
if (!returnMsgAndResId) {
throw Error('发送合并转发消息失败returnMsgAndResId 为空!');
}
if (returnMsgAndResId.message) { if (returnMsgAndResId.message) {
const msgShortId = MessageUnique.createUniqueMsgId({ const msgShortId = MessageUnique.createUniqueMsgId({
guildId: '', guildId: '',
@@ -129,7 +138,6 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) { } else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
throw Error(`发送转发消息res_id${returnMsgAndResId.res_id} 失败`); throw Error(`发送转发消息res_id${returnMsgAndResId.res_id} 失败`);
} }
throw Error('发送转发消息失败');
} else { } else {
// if (getSpecialMsgNum(payload, OB11MessageDataType.music)) { // if (getSpecialMsgNum(payload, OB11MessageDataType.music)) {
// const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic; // const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic;
@@ -145,48 +153,96 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
return { message_id: returnMsg!.id! }; return { message_id: returnMsg!.id! };
} }
// TODO: recursively handle forwarded nodes private async uploadForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: {
private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{ text: string
message: RawMessage | null, }[], summary?: string, prompt?: string, parentMeta?: {
user_id: string,
nickname: string,
}, dp: number = 0): Promise<{
finallySendElements: SendArkElement,
res_id?: string res_id?: string
}> { } | null> {
const logger = this.core.context.logger; const logger = this.core.context.logger;
const packetMsg: PacketMsg[] = []; const packetMsg: PacketMsg[] = [];
for (const node of messageNodes) { for (const node of messageNodes) {
if ((node.data.id && typeof node.data.content !== "string") || !node.data.id) { if (dp >= 3) {
const OB11Data = normalize(node.data.content); logger.logWarn('转发消息深度超过3层将停止解析');
const { sendElements } = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer); break;
}
if (!node.data.id) {
const OB11Data = normalize(node.type === OB11MessageDataType.node ? node.data.content : node);
let sendElements: SendMessageElement[];
if (getSpecialMsgNum({ message: OB11Data }, OB11MessageDataType.node)) {
const uploadReturnData = await this.uploadForwardedNodesPacket(msgPeer, OB11Data as OB11MessageNode[], node.data.source, node.data.news, node.data.summary, node.data.prompt, {
user_id: (node.data.user_id || node.data.uin)?.toString() ?? parentMeta?.user_id ?? this.core.selfInfo.uin,
nickname: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? "QQ用户",
}, dp + 1);
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
} else {
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
sendElements = sendElementsCreateReturn.sendElements;
}
const packetMsgElements: rawMsgWithSendMsg = { const packetMsgElements: rawMsgWithSendMsg = {
senderUin: node.data.user_id ?? +this.core.selfInfo.uin, senderUin: Number((node.data.user_id || node.data.uin) ?? parentMeta?.user_id) || +this.core.selfInfo.uin,
senderName: node.data.nickname, senderName: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? "QQ用户",
groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined, groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined,
time: Date.now(), time: Number(node.data.time) || Date.now(),
msg: sendElements, msg: sendElements,
}; };
logger.logDebug(`handleForwardedNodesPacket 开始转换 ${JSON.stringify(packetMsgElements)}`); logger.logDebug(`handleForwardedNodesPacket[SendRaw] 开始转换 ${stringifyWithBigInt(packetMsgElements)}`);
const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements); const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements);
logger.logDebug(`handleForwardedNodesPacket 转换为 ${JSON.stringify(transformedMsg)}`); logger.logDebug(`handleForwardedNodesPacket[SendRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg!);
} else if (node.data.id) {
const id = node.data.id;
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(+id) || MessageUnique.getPeerByMsgId(id);
if (!nodeMsg) {
logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', id);
continue;
}
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0];
logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgToPacketMsg(msg, msgPeer);
logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg!); packetMsg.push(transformedMsg!);
} else { } else {
logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${JSON.stringify(node)}`); logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${stringifyWithBigInt(node)}`);
} }
} }
const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); if (packetMsg.length === 0) {
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg); logger.logWarn('handleForwardedNodesPacket 元素为空!');
const finallySendElements = { return null;
elementType: ElementType.ARK,
elementId: "",
arkElement: {
bytesData: JSON.stringify(forwardJson),
},
} as SendArkElement;
let returnMsg: RawMessage | undefined;
try {
returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], [], true).catch(_ => undefined);
} catch (e) {
logger.logWarn("发送伪造合并转发消息失败!", e);
} }
return { message: returnMsg ?? null, res_id: resid }; const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt);
return {
finallySendElements: {
elementType: ElementType.ARK,
elementId: "",
arkElement: {
bytesData: JSON.stringify(forwardJson),
},
} as SendArkElement,
res_id: resid,
};
}
private async handleForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: {
text: string
}[], summary?: string, prompt?: string): Promise<{
message: RawMessage | null,
res_id?: string
}> {
let returnMsg: RawMessage | undefined, res_id: string | undefined;
const uploadReturnData = await this.uploadForwardedNodesPacket(msgPeer, messageNodes, source, news, summary, prompt);
res_id = uploadReturnData?.res_id;
const finallySendElements = uploadReturnData?.finallySendElements;
if (!finallySendElements) throw Error('转发消息失败,生成节点为空');
returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], [], true).catch(_ => undefined);
return { message: returnMsg ?? null, res_id };
} }
private async handleForwardedNodes(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{ private async handleForwardedNodes(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{
@@ -220,6 +276,7 @@ export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
logger.logError.bind(this.core.context.logger)('子消息中包含非node消息 跳过不合法部分'); logger.logError.bind(this.core.context.logger)('子消息中包含非node消息 跳过不合法部分');
continue; continue;
} }
// @ts-ignore
const nodeMsg = await this.handleForwardedNodes(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node)); const nodeMsg = await this.handleForwardedNodes(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node));
if (nodeMsg) { if (nodeMsg) {
nodeMsgIds.push(nodeMsg.message!.msgId); nodeMsgIds.push(nodeMsg.message!.msgId);

View File

@@ -5,16 +5,14 @@ import { ActionName, BaseCheckResult } from '../types';
export abstract class GetPacketStatusDepends<PT, RT> extends BaseAction<PT, RT> { export abstract class GetPacketStatusDepends<PT, RT> extends BaseAction<PT, RT> {
actionName = ActionName.GetPacketStatus; actionName = ActionName.GetPacketStatus;
protected async check(): Promise<BaseCheckResult>{ protected async check(payload: PT): Promise<BaseCheckResult>{
if (!this.core.apis.PacketApi.available) { if (!this.core.apis.PacketApi.available) {
return { return {
valid: false, valid: false,
message: "packetServer不可用请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置", message: "packetServer不可用请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置",
}; };
} }
return { return await super.check(payload);
valid: true,
};
} }
} }

View File

@@ -57,11 +57,11 @@ export enum ActionName {
// go-cqhttp // go-cqhttp
SetQQProfile = 'set_qq_profile', SetQQProfile = 'set_qq_profile',
// QidianGetAccountInfo = 'qidian_get_account_info', // QidianGetAccountInfo = 'qidian_get_account_info',
// GetModelShow = '_get_model_show', GoCQHTTP_GetModelShow = '_get_model_show',
// SetModelShow = '_set_model_show', GoCQHTTP_SetModelShow = '_set_model_show',
// GetOnlineClient = 'get_online_clients', GetOnlineClient = 'get_online_clients',
// GetUnidirectionalFriendList = 'get_unidirectional_friend_list', // GetUnidirectionalFriendList = 'get_unidirectional_friend_list',
// DeleteFriend = 'delete_friend', GoCQHTTP_DeleteFriend = 'delete_friend',
// DeleteUnidirectionalFriendList = 'delete_unidirectional_friend', // DeleteUnidirectionalFriendList = 'delete_unidirectional_friend',
GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read', GoCQHTTP_MarkMsgAsRead = 'mark_msg_as_read',
GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg', GoCQHTTP_SendGroupForwardMsg = 'send_group_forward_msg',
@@ -71,7 +71,7 @@ export enum ActionName {
IOCRImage = '.ocr_image', IOCRImage = '.ocr_image',
GetGroupSystemMsg = 'get_group_system_msg', GetGroupSystemMsg = 'get_group_system_msg',
GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list', GoCQHTTP_GetEssenceMsg = 'get_essence_msg_list',
// GetGroupAtAllRemain = 'get_group_at_all_remain', GoCQHTTP_GetGroupAtAllRemain = 'get_group_at_all_remain',
SetGroupPortrait = 'set_group_portrait', SetGroupPortrait = 'set_group_portrait',
SetEssenceMsg = 'set_essence_msg', SetEssenceMsg = 'set_essence_msg',
DelEssenceMsg = 'delete_essence_msg', DelEssenceMsg = 'delete_essence_msg',
@@ -88,8 +88,8 @@ export enum ActionName {
GOCQHTTP_UploadPrivateFile = 'upload_private_file', GOCQHTTP_UploadPrivateFile = 'upload_private_file',
// GOCQHTTP_ReloadEventFilter = 'reload_event_filter', // GOCQHTTP_ReloadEventFilter = 'reload_event_filter',
GoCQHTTP_DownloadFile = 'download_file', GoCQHTTP_DownloadFile = 'download_file',
// GoCQHTTP_CheckUrlSafely = 'check_url_safely', GoCQHTTP_CheckUrlSafely = 'check_url_safely',
// GoCQHTTP_GetWordSlices = '.get_word_slices', GoCQHTTP_GetWordSlices = '.get_word_slices',
GoCQHTTP_HandleQuickAction = '.handle_quick_operation', GoCQHTTP_HandleQuickAction = '.handle_quick_operation',
// 以下为扩展napcat扩展 // 以下为扩展napcat扩展
@@ -105,11 +105,6 @@ export enum ActionName {
ForwardFriendSingleMsg = 'forward_friend_single_msg', ForwardFriendSingleMsg = 'forward_friend_single_msg',
ForwardGroupSingleMsg = 'forward_group_single_msg', ForwardGroupSingleMsg = 'forward_group_single_msg',
TranslateEnWordToZn = 'translate_en2zh', TranslateEnWordToZn = 'translate_en2zh',
GetGroupFileCount = 'get_group_file_count',
GetGroupFileList = 'get_group_file_list',
SetGroupFileFolder = 'set_group_file_folder',
DelGroupFile = 'del_group_file',
DelGroupFileFolder = 'del_group_file_folder',
SetMsgEmojiLike = 'set_msg_emoji_like', SetMsgEmojiLike = 'set_msg_emoji_like',
GoCQHTTP_SendForwardMsg = 'send_forward_msg', GoCQHTTP_SendForwardMsg = 'send_forward_msg',
MarkPrivateMsgAsRead = 'mark_private_msg_as_read', MarkPrivateMsgAsRead = 'mark_private_msg_as_read',
@@ -125,7 +120,7 @@ export enum ActionName {
FetchEmojiLike = 'fetch_emoji_like', FetchEmojiLike = 'fetch_emoji_like',
SetInputStatus = 'set_input_status', SetInputStatus = 'set_input_status',
GetGroupInfoEx = 'get_group_info_ex', GetGroupInfoEx = 'get_group_info_ex',
GetGroupIgnoredNotifies = 'get_group_ignored_notifies', GetGroupIgnoreAddRequest = 'get_group_ignore_add_request',
DelGroupNotice = '_del_group_notice', DelGroupNotice = '_del_group_notice',
FetchUserProfileLike = 'fetch_user_profile_like', FetchUserProfileLike = 'fetch_user_profile_like',
FriendPoke = 'friend_poke', FriendPoke = 'friend_poke',
@@ -135,12 +130,12 @@ export enum ActionName {
GetRkey = 'nc_get_rkey', GetRkey = 'nc_get_rkey',
GetGroupShutList = 'get_group_shut_list', GetGroupShutList = 'get_group_shut_list',
// GetGroupIgnoreAddRequest = 'get_group_ignore_add_request', GetGuildList = 'get_guild_list',
// GetConfig = 'get_config', GetGuildProfile = 'get_guild_service_profile',
// SetConfig = 'set_config',
// Debug = 'debug', GetGroupIgnoredNotifies = 'get_group_ignored_notifies',
// GetGuildList = 'get_guild_list',
// TestApi01 = 'test_api_01', SetGroupSign = "set_group_sign",
// GetGuildProfile = 'get_guild_service_profile', GetMiniAppArk = "get_mini_app_ark",
// UploadForwardMsg = "upload_forward_msg", // UploadForwardMsg = "upload_forward_msg",
} }

View File

@@ -120,7 +120,7 @@ export class OneBotMsgApi {
url: await this.core.apis.FileApi.getImageUrl(element), url: await this.core.apis.FileApi.getImageUrl(element),
path: element.filePath, path: element.filePath,
file_size: element.fileSize, file_size: element.fileSize,
file_unique: element.fileName file_unique: element.md5HexStr ?? element.fileName,
}, },
}; };
} catch (e: any) { } catch (e: any) {
@@ -141,9 +141,9 @@ export class OneBotMsgApi {
file: element.fileName, file: element.fileName,
path: element.filePath, path: element.filePath,
url: pathToFileURL(element.filePath).href, url: pathToFileURL(element.filePath).href,
file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid,"." + element.fileName), file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, "." + element.fileName),
file_size: element.fileSize, file_size: element.fileSize,
file_unique: element.fileName, file_unique: element.fileMd5 ?? element.fileSha ?? element.fileName,
}, },
}; };
}, },
@@ -204,7 +204,7 @@ export class OneBotMsgApi {
guildId: '', guildId: '',
}; };
if (!records || !element.replyMsgTime || !element.senderUidStr) { if (!records || !element.replyMsgTime || !element.senderUidStr) {
this.core.context.logger.logError.bind(this.core.context.logger)('获取不到引用的消息', element.replayMsgSeq); this.core.context.logger.logError.bind(this.core.context.logger)('似乎是旧版客户端,获取不到引用的消息', element.replayMsgSeq);
return null; return null;
} }
@@ -218,11 +218,27 @@ export class OneBotMsgApi {
if (records.peerUin === '284840486' || records.peerUin === '1094950020') { if (records.peerUin === '284840486' || records.peerUin === '1094950020') {
return createReplyData(records.msgId); return createReplyData(records.msgId);
} }
const replyMsg = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(peer, element.replayMsgSeq, element.replyMsgTime, [element.senderUidStr])) let replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(peer, element.replayMsgSeq, element.replyMsgTime, [element.senderUidStr])).msgList;
.msgList.find(msg => msg.msgRandom === records.msgRandom); let replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logError.bind(this.core.context.logger)('获取不到引用消息', element.replayMsgSeq); // 我猜测可能是时间参数未对上 导致找不到引用消息 或者msgList 存在问题
this.core.context.logger.logWarn.bind(this.core.context.logger)(
'初次筛选消息失败,获取不到引用的消息 Seq:',
element.replayMsgSeq,
',消息长度:',
replyMsgList.length
);
// 再次筛选
replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(peer, element.replayMsgSeq, [element.senderUidStr])).msgList;
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
}
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logError.bind(this.core.context.logger)(
'最终筛选结果,筛选消息失败,获取不到引用的消息 Seq: ',
element.replayMsgSeq,
',消息长度:',
replyMsgList.length
);
return null; return null;
} }
return createReplyData(replyMsg.msgId); return createReplyData(replyMsg.msgId);
@@ -285,7 +301,7 @@ export class OneBotMsgApi {
url: videoDownUrl ?? pathToFileURL(element.filePath).href, url: videoDownUrl ?? pathToFileURL(element.filePath).href,
file_id: fileCode, file_id: fileCode,
file_size: element.fileSize, file_size: element.fileSize,
file_unique: element.fileName, file_unique: element.videoMd5 ?? element.thumbMd5 ?? element.fileName,
}, },
}; };
}, },
@@ -305,7 +321,7 @@ export class OneBotMsgApi {
url: pathToFileURL(element.filePath).href, url: pathToFileURL(element.filePath).href,
file_id: fileCode, file_id: fileCode,
file_size: element.fileSize, file_size: element.fileSize,
file_unique: element.fileName file_unique: element.fileUuid
}, },
}; };
}, },
@@ -466,6 +482,7 @@ export class OneBotMsgApi {
}, },
}) => ({ }) => ({
elementType: ElementType.MFACE, elementType: ElementType.MFACE,
elementId: '',
marketFaceElement: { marketFaceElement: {
emojiPackageId: emoji_package_id, emojiPackageId: emoji_package_id,
emojiId: emoji_id, emojiId: emoji_id,
@@ -624,12 +641,20 @@ export class OneBotMsgApi {
[OB11MessageDataType.miniapp]: async () => undefined, [OB11MessageDataType.miniapp]: async () => undefined,
[OB11MessageDataType.contact]: async ({ data }, context) => { [OB11MessageDataType.contact]: async ({ data: { type = "qq", id } }, context) => {
const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(data.id.toString(), ''); if (type === "qq") {
return this.ob11ToRawConverters.json({ const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), '');
data: { data: arkJson.arkMsg }, return this.ob11ToRawConverters.json({
type: OB11MessageDataType.json data: { data: arkJson.arkMsg },
}, context); type: OB11MessageDataType.json
}, context);
} else if (type === "group") {
const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString());
return this.ob11ToRawConverters.json({
data: { data: arkJson.arkJson },
type: OB11MessageDataType.json
}, context);
}
} }
}; };

View File

@@ -119,11 +119,25 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter {
async close() { async close() {
this.isOpen = false; this.isOpen = false;
this.wsServer.close(); this.wsServer.close((err) => {
if (err) {
this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Error closing server:', err.message);
} else {
this.logger.log.bind(this.logger)('[OneBot] [WebSocket Server] Server Closed');
}
});
if (this.heartbeatIntervalId) { if (this.heartbeatIntervalId) {
clearInterval(this.heartbeatIntervalId); clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null; this.heartbeatIntervalId = null;
} }
await this.wsClientsMutex.runExclusive(async () => {
this.wsClients.forEach((wsClient) => {
wsClient.close();
});
this.wsClients = [];
this.wsClientWithEvent = [];
});
} }
private registerHeartBeat() { private registerHeartBeat() {

View File

@@ -16,8 +16,8 @@ export interface OB11Message {
message_id: number, message_id: number,
message_seq: number, // 和message_id一样 message_seq: number, // 和message_id一样
real_id: number, real_id: number,
user_id: number, user_id: number | string, // number
group_id?: number, group_id?: number | string, // number
message_type: 'private' | 'group', message_type: 'private' | 'group',
sub_type?: 'friend' | 'group' | 'normal', sub_type?: 'friend' | 'group' | 'normal',
sender: OB11Sender, sender: OB11Sender,
@@ -85,6 +85,7 @@ export interface OB11MessageText {
export interface OB11MessageContext { export interface OB11MessageContext {
type: OB11MessageDataType.contact, type: OB11MessageDataType.contact,
data: { data: {
type:"qq"|"group",
id: string, id: string,
} }
} }
@@ -148,9 +149,16 @@ export interface OB11MessageNode {
type: OB11MessageDataType.node; type: OB11MessageDataType.node;
data: { data: {
id?: string id?: string
user_id?: number user_id?: number | string // number
uin?: number | string // number, compatible with go-cqhttp
nickname: string nickname: string
name?: string // compatible with go-cqhttp
content: OB11MessageMixType content: OB11MessageMixType
source?: string,
news?: { text: string }[],
summary?: string,
prompt?: string
time?: string
}; };
} }
@@ -220,6 +228,11 @@ export interface OB11PostSendMsg {
message: OB11MessageMixType; message: OB11MessageMixType;
messages?: OB11MessageMixType; // 兼容 go-cqhttp messages?: OB11MessageMixType; // 兼容 go-cqhttp
auto_escape?: boolean | string auto_escape?: boolean | string
source?: string,
news?: { text: string }[],
summary?: string,
prompt?: string
time?: string
} }
export interface OB11PostContext { export interface OB11PostContext {
message_type?: 'private' | 'group' message_type?: 'private' | 'group'

View File

@@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
undefined, undefined,
SettingButton('V3.1.1', 'napcat-update-button', 'secondary'), SettingButton('V3.3.12', 'napcat-update-button', 'secondary'),
), ),
]), ]),
SettingList([ SettingList([

View File

@@ -164,7 +164,7 @@ async function onSettingWindowCreated(view) {
SettingItem( SettingItem(
'<span id="napcat-update-title">Napcat</span>', '<span id="napcat-update-title">Napcat</span>',
void 0, void 0,
SettingButton("V3.1.1", "napcat-update-button", "secondary") SettingButton("V3.3.20", "napcat-update-button", "secondary")
) )
]), ]),
SettingList([ SettingList([