Compare commits

...

78 Commits

Author SHA1 Message Date
pk5ls20
92d720cd57 Merge pull request #512 from NapNeko/refactor/packet-2
refactor: packet
2024-11-13 17:51:00 +08:00
pk5ls20
2ea025047f feat: comment out logic that is not currently needed 2024-11-13 17:48:42 +08:00
pk5ls20
f7f7e09cab feat: sysMessage oldProto adapter 2024-11-13 17:44:12 +08:00
pk5ls20
75866b435e feat: errorStack 2024-11-13 16:52:03 +08:00
手瓜一十雪
f07941685b Merge branch 'main' into refactor/packet-2 2024-11-13 16:00:45 +08:00
Mlikiowa
60a0539216 release: v3.6.17 2024-11-13 07:57:01 +00:00
手瓜一十雪
3dd4b6549f Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-13 15:56:31 +08:00
手瓜一十雪
0802c35dc1 feat: mface emoji_id 2024-11-13 15:56:17 +08:00
Mlikiowa
7d9d7226ec release: v3.6.16 2024-11-13 07:55:21 +00:00
手瓜一十雪
b5ef6ce6b0 feat: mface key 扩展 2024-11-13 15:54:31 +08:00
pk5ls20
49ec6181b0 fix: macos 2024-11-13 15:35:07 +08:00
pk5ls20
783a534768 fix: NativePacketClient 2024-11-13 14:16:42 +08:00
Mlikiowa
704ac11cbb release: v3.6.15 2024-11-13 05:16:36 +00:00
手瓜一十雪
aa9663d85e fix: 移除chalk 2024-11-13 13:16:09 +08:00
Mlikiowa
05291f34fb release: v3.6.14 2024-11-13 04:31:12 +00:00
手瓜一十雪
2260fe32a1 Merge pull request #514 from NapNeko/refactor-log4js--winston
refactor: log4js to winston
2024-11-13 12:29:35 +08:00
手瓜一十雪
2c398a6832 fix: error 2024-11-13 12:29:17 +08:00
手瓜一十雪
3e1f566699 feat: 异常处理与log4js替换 2024-11-13 12:26:23 +08:00
手瓜一十雪
4f89f184b8 refactor: package json 2024-11-13 12:19:18 +08:00
Mlikiowa
787685c937 release: v3.6.13 2024-11-12 10:45:11 +00:00
手瓜一十雪
ed9cd2fe38 fix: #513 2024-11-12 18:37:44 +08:00
pk5ls20
740d80e851 chore: minor adjust packet module 2024-11-12 04:14:08 +08:00
pk5ls20
4520a20bd4 chore: remove debug output 2024-11-12 04:08:51 +08:00
pk5ls20
98c65c4923 refactor: packet x1 2024-11-12 04:02:19 +08:00
Mlikiowa
e287906a9d release: v3.6.12 2024-11-11 13:03:29 +00:00
手瓜一十雪
8bae789020 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-11 21:00:27 +08:00
手瓜一十雪
ce57b7b725 fix: #509 2024-11-11 21:00:12 +08:00
pk5ls20
1d9872195d fix: offset name 2024-11-11 20:57:30 +08:00
pk5ls20
98d1f8e29f feat: add some macOS offset 2024-11-11 20:54:57 +08:00
手瓜一十雪
221b3fb730 fix 2024-11-11 20:35:13 +08:00
Mlikiowa
90a834495a release: v3.6.11 2024-11-11 12:17:25 +00:00
手瓜一十雪
8bfd102232 fix: macos arm64 28971 2024-11-11 20:16:44 +08:00
Mlikiowa
65e784f169 release: v3.6.10 2024-11-11 02:45:39 +00:00
手瓜一十雪
0fc81c672f fix: once升级 2024-11-11 10:45:15 +08:00
手瓜一十雪
62ae0f4321 feat: 文档镜像 2024-11-10 12:40:18 +08:00
Mlikiowa
a01a0a1a18 release: v3.6.9 2024-11-10 04:11:16 +00:00
手瓜一十雪
4c30cc69ad fix: #73 2024-11-10 12:10:43 +08:00
Mlikiowa
1d43b75df4 release: v3.6.8 2024-11-09 10:22:17 +00:00
手瓜一十雪
d02afdfc3e fix: 缓存 2024-11-09 18:21:50 +08:00
Mlikiowa
5d6dee9fd0 release: v3.6.7 2024-11-09 04:36:58 +00:00
手瓜一十雪
60c67ef41c Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-09 12:36:21 +08:00
手瓜一十雪
917d7c1f19 feat: user boot script 2024-11-09 12:35:50 +08:00
Mlikiowa
ad19f2c99e release: v3.6.6 2024-11-09 04:22:55 +00:00
手瓜一十雪
8a61f5a03f fix: #503 2024-11-09 12:22:28 +08:00
手瓜一十雪
8c164910f6 docs: 因为优点太多所以只好去掉几条了 2024-11-08 16:23:20 +08:00
pk5ls20
a560d3d266 chore: eslint migrate 2024-11-08 15:43:27 +08:00
手瓜一十雪
532f739272 Merge pull request #501 from NapNeko/eslint9
feat: eslint9
2024-11-08 12:38:12 +08:00
手瓜一十雪
a120727f2d feat: eslint9 2024-11-08 12:36:25 +08:00
手瓜一十雪
a9bcb830a8 feat: 迁移29456为标准版本 2024-11-08 12:14:15 +08:00
Mlikiowa
56e5f0033f release: v3.6.5 2024-11-08 01:20:08 +00:00
手瓜一十雪
101106996a feat: 29456 2024-11-08 09:18:40 +08:00
pk5ls20
41a81534dc feat: support 29456 2024-11-08 00:26:09 +08:00
Mlikiowa
1425e8f229 release: v3.6.4 2024-11-07 08:14:56 +00:00
手瓜一十雪
75bb1d2193 Merge pull request #499 from kanocence/main
修复 config 页面样式
2024-11-07 16:14:01 +08:00
手瓜一十雪
2a23820f9b Merge pull request #500 from Stapxs/patch-1
fix: 在处理 file uri 时可能会意外忽略 fragment 段
2024-11-07 16:13:06 +08:00
林小槐
2ee0fed047 fix: 在处理 file uri 时可能会意外忽略 fragment 段
在处理类似 C:\\test\\test#1.txt 时 #1.txt 由于为 url fragment 而被意外截断
2024-11-07 16:10:59 +08:00
kanocence
40be6b9c43 Merge branch 'NapNeko:main' into main 2024-11-07 15:34:19 +08:00
kanocence
a06b3f0246 feat: 💄 修改页面样式 2024-11-07 15:33:57 +08:00
Mlikiowa
4787fa53b4 release: v3.6.3 2024-11-07 04:16:13 +00:00
手瓜一十雪
a06158bf01 fix: 标准化接口 2024-11-07 12:15:49 +08:00
pk5ls20
314e7485b8 chore: format 2024-11-07 10:33:01 +08:00
pk5ls20
aed5d2d9f0 chore: log 2024-11-07 10:31:50 +08:00
pk5ls20
f44e48a28b fix: remove useless import 2024-11-06 16:58:12 +08:00
手瓜一十雪
38be90450c feat: 兼容gocq标准 2024-11-06 16:48:18 +08:00
手瓜一十雪
2dd57d7676 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-06 16:45:04 +08:00
手瓜一十雪
6b3b163fa8 fix: 错误代码 2024-11-06 16:45:01 +08:00
Mlikiowa
9792ebafdc release: v3.6.2 2024-11-06 08:00:05 +00:00
手瓜一十雪
d10e7c37cb fix: link 2024-11-06 15:59:39 +08:00
Mlikiowa
d38f1853a4 release: v3.6.1 2024-11-06 06:42:33 +00:00
手瓜一十雪
bdec16266e fix: #498 2024-11-06 14:42:05 +08:00
Mlikiowa
49ca698ab9 release: v3.6.0 2024-11-06 03:30:17 +00:00
pk5ls20
3efd8163c9 fix: MoeHoo-Linux Amd64 2024-11-06 11:28:57 +08:00
pk5ls20
cc2d11449c release: v3.5.2 2024-11-06 09:28:14 +08:00
pk5ls20
7e9c19ca5b fix: MoeHoo-Linux Arm64 2024-11-06 09:26:15 +08:00
Mlikiowa
3b01b6827f release: v3.5.1 2024-11-05 14:45:36 +00:00
手瓜一十雪
8d9ef851ba fix: linux arm64 2024-11-05 22:45:00 +08:00
手瓜一十雪
b070bc59bc fix: MoeHoo-Linux Amd64 2024-11-05 22:36:47 +08:00
Mlikiowa
8d663946e1 release: v3.5.0 2024-11-05 14:13:33 +00:00
142 changed files with 2815 additions and 2268 deletions

View File

@@ -1,64 +0,0 @@
module.exports = {
'env': {
'browser': true,
'es2021': true,
'node': true
},
'ignorePatterns': ['src/core/proto/'],
'extends': [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
'overrides': [
{
'env': {
'node': true
},
'files': [
'.eslintrc.{js,cjs}'
],
'parserOptions': {
'sourceType': 'script'
}
}
],
'parser': '@typescript-eslint/parser',
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module'
},
'plugins': [
'@typescript-eslint',
'import'
],
'settings': {
'import/parsers': {
'@typescript-eslint/parser': ['.ts']
},
'import/resolver': {
'typescript': {
'alwaysTryTypes': true
}
}
},
'rules': {
'indent': [
'error',
4
],
'linebreak-style': [
'error',
'unix'
],
'semi': [
'error',
'always'
],
'no-unused-vars': 'off',
'no-async-promise-executor': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off',
'object-curly-spacing': ['error', 'always'],
}
};

View File

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

View File

@@ -9,14 +9,12 @@
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
## 猫猫技能
- [x] **超高性能**:轻松数千群聊 独创消息队列
- [x] **启动方式**支持以无头、LiteLoader 插件、仅 QQ GUI 三种方式启动
- [x] **覆盖平台**: 覆盖 Windows / Linux (可选 Docker) / Android Termux / MacOS
- [x] **安装简单**: 支持一键脚本/程序自动部署/镜像部署等多种覆盖范围
- [x] **超低占用**:无头模式占用资源极低,适合在服务器上运行
- [x] **超多接口**:实现大部分 OneBot 和 go-cqhttp 接口,超多扩展 API
- [x] **远程管理**:自带 WebUI 支持,远程管理更加便捷
- [x] **扩展支持**:基于 MoeHoo 的Native 可实现发包与收包
## 使用猫猫
@@ -32,6 +30,10 @@ NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
[Cloudflare.Pages](https://napneko.pages.dev/)
[Server.China](https://napneko.com/)
[Server.Other](https://napcat.cyou/)
[Github.IO](https://napneko.github.io/)
## 回家旅途
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS)

70
eslint.config.mjs Normal file
View File

@@ -0,0 +1,70 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import _import from "eslint-plugin-import";
import { fixupPluginRules } from "@eslint/compat";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
const compat = new FlatCompat({
baseDirectory: dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [{
ignores: ["src/core/proto/"],
}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), {
plugins: {
"@typescript-eslint": typescriptEslint,
import: fixupPluginRules(_import),
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
},
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
rules: {
indent: ["error", 4],
semi: ["error", "always"],
"no-unused-vars": "off",
"no-async-promise-executor": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
"object-curly-spacing": ["error", "always"],
},
}, {
files: ["**/.eslintrc.{js,cjs}"],
languageOptions: {
globals: {
...globals.node,
},
ecmaVersion: 5,
sourceType: "commonjs",
},
}];

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
@echo off
chcp 65001
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
pause

View File

@@ -0,0 +1,33 @@
@echo off
chcp 65001
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
pause

View File

@@ -1,9 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.16-28788",
"verHash": "73b0c8f6",
"linuxVersion": "3.2.13-28788",
"linuxVerHash": "55fb6434",
"version": "9.9.16-29456",
"verHash": "dd395162",
"linuxVersion": "3.2.13-29456",
"linuxVerHash": "e379390a",
"type": "module",
"private": true,
"description": "QQ",
@@ -18,7 +18,7 @@
"qd": "externals/devtools/cli/index.js"
},
"main": "./loadNapCat.js",
"buildVersion": "28788",
"buildVersion": "29456",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",

View File

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

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "3.4.11",
"version": "3.6.17",
"scripts": {
"build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell",
@@ -12,9 +12,11 @@
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.2",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.2",
"@protobuf-ts/runtime": "^2.9.4",
"@napneko/nap-proto-core": "^0.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17",
@@ -27,25 +29,26 @@
"@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": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"fast-xml-parser": "^4.3.6",
"file-type": "^19.0.0",
"globals": "^15.12.0",
"image-size": "^1.1.1",
"json-schema-to-ts": "^3.1.1",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^5.2.6",
"vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0"
"vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0"
},
"dependencies": {
"express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2",
"log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"

View File

@@ -215,7 +215,7 @@ export async function checkUriType(Uri: string) {
}
if (uri.startsWith('file://')) {
let filePath: string;
const pathname = decodeURIComponent(new URL(uri).pathname);
const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash);
if (process.platform === 'win32') {
filePath = pathname.slice(1);
} else {

View File

@@ -1,5 +1,5 @@
import { PacketMsg } from "@/core/packet/message/message";
import * as crypto from "node:crypto";
import { PacketMsg } from "@/core/packet/message/message";
interface ForwardMsgJson {
app: string

View File

@@ -1,7 +1,7 @@
import log4js, { Configuration } from 'log4js';
import winston, { format, transports } from 'winston';
import { truncateString } from '@/common/helper';
import path from 'node:path';
import chalk from 'chalk';
import fs from 'node:fs';
import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
export enum LogLevel {
@@ -27,97 +27,133 @@ function getFormattedTimestamp() {
export class LogWrapper {
fileLogEnabled = true;
consoleLogEnabled = true;
logConfig: Configuration;
loggerConsole: log4js.Logger;
loggerFile: log4js.Logger;
loggerDefault: log4js.Logger;
// eslint-disable-next-line no-control-regex
colorEscape = /\x1B[@-_][0-?]*[ -/]*[@-~]/g;
logger: winston.Logger;
constructor(logDir: string) {
const filename = `${getFormattedTimestamp()}.log`;
const logPath = path.join(logDir, filename);
this.logConfig = {
appenders: {
FileAppender: { // 输出到文件的appender
type: 'file',
filename: logPath, // 指定日志文件的位置和文件名
maxLogSize: 10485760, // 日志文件的最大大小单位字节这里设置为10MB
layout: {
type: 'pattern',
pattern: '%d{yyyy-MM-dd hh:mm:ss} [%p] %X{userInfo} | %m',
},
},
ConsoleAppender: { // 输出到控制台的appender
type: 'console',
layout: {
type: 'pattern',
pattern: `%d{yyyy-MM-dd hh:mm:ss} [%[%p%]] ${chalk.magenta('%X{userInfo}')} | %m`,
},
},
},
categories: {
default: { appenders: ['FileAppender', 'ConsoleAppender'], level: 'debug' }, // 默认情况下同时输出到文件和控制台
file: { appenders: ['FileAppender'], level: 'debug' },
console: { appenders: ['ConsoleAppender'], level: 'debug' },
},
};
log4js.configure(this.logConfig);
this.loggerConsole = log4js.getLogger('console');
this.loggerFile = log4js.getLogger('file');
this.loggerDefault = log4js.getLogger('default');
this.logger = winston.createLogger({
level: 'debug',
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
),
transports: [
new transports.File({
filename: logPath,
level: 'debug',
maxsize: 5 * 1024 * 1024, // 5MB
maxFiles: 5
}),
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
)
})
]
});
this.setLogSelfInfo({ nick: '', uin: '', uid: '' });
this.cleanOldLogs(logDir);
}
cleanOldLogs(logDir: string) {
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
fs.readdir(logDir, (err, files) => {
if (err) {
this.logger.error('Failed to read log directory', err);
return;
}
files.forEach(file => {
const filePath = path.join(logDir, file);
fs.stat(filePath, (err, stats) => {
if (err) {
this.logger.error('Failed to get file stats', err);
return;
}
if (stats.mtime.getTime() < oneWeekAgo) {
fs.unlink(filePath, err => {
if (err) {
if (err.code === 'ENOENT') {
this.logger.warn(`File already deleted: ${file}`);
} else {
this.logger.error('Failed to delete old log file', err);
}
} else {
this.logger.info(`Deleted old log file: ${file}`);
}
});
}
});
});
});
}
setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
this.logConfig.categories.file.level = fileLogLevel;
this.logConfig.categories.console.level = consoleLogLevel;
log4js.configure(this.logConfig);
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.level = fileLogLevel;
} else if (transport instanceof transports.Console) {
transport.level = consoleLogLevel;
}
});
}
setLogSelfInfo(selfInfo: { nick: string, uin: string, uid: string }) {
const userInfo = `${selfInfo.nick}(${selfInfo.uin})`;
this.loggerConsole.addContext('userInfo', userInfo);
this.loggerFile.addContext('userInfo', userInfo);
this.loggerDefault.addContext('userInfo', userInfo);
this.logger.defaultMeta = { userInfo };
}
setFileLogEnabled(isEnabled: boolean) {
this.fileLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.silent = !isEnabled;
}
});
}
setConsoleLogEnabled(isEnabled: boolean) {
this.consoleLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.Console) {
transport.silent = !isEnabled;
}
});
}
formatMsg(msg: any[]) {
let logMsg = '';
for (const msgItem of msg) {
if (msgItem instanceof Error) { // 判断是否是错误
logMsg += msgItem.stack + ' ';
continue;
} else if (typeof msgItem === 'object') { // 判断是否是对象
const obj = JSON.parse(JSON.stringify(msgItem, null, 2));
logMsg += JSON.stringify(truncateString(obj)) + ' ';
continue;
return msg.map(msgItem => {
if (msgItem instanceof Error) {
return msgItem.stack;
} else if (typeof msgItem === 'object') {
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
}
logMsg += msgItem + ' ';
}
return logMsg;
return msgItem;
}).join(' ');
}
_log(level: LogLevel, ...args: any[]) {
if (this.consoleLogEnabled) {
this.loggerConsole[level](this.formatMsg(args));
}
if (this.fileLogEnabled) {
this.loggerFile[level](this.formatMsg(args).replace(this.colorEscape, ''));
const message = this.formatMsg(args);
if (this.consoleLogEnabled && this.fileLogEnabled) {
this.logger.log(level, message);
} else if (this.consoleLogEnabled) {
this.logger.log(level, message);
} else if (this.fileLogEnabled) {
// eslint-disable-next-line no-control-regex
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
}
}
log(...args: any[]) {
// info 等级
this._log(LogLevel.INFO, ...args);
}
@@ -140,12 +176,11 @@ export class LogWrapper {
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
const isSelfSent = msg.senderUin === selfInfo.uin;
// Intercept grey tip
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
return;
}
this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`);
this.log(`${isSelfSent ? '发送 ->' : '接收 <-'} ${rawMessageToText(msg)}`);
}
}
@@ -167,12 +202,10 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
}
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备');
} else /* temp */ {
} else {
tokens.push(`临时消息 (${msg.peerUin})`);
}
// message content
function msgElementToText(element: MessageElement) {
if (element.textElement) {
if (element.textElement.atType === AtType.notAt) {
@@ -190,7 +223,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
record => element.replyElement!.sourceMsgIdInRecords === record.msgId,
);
return `[回复消息 ${recordMsgOrNull &&
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
?
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`

View File

@@ -1 +1 @@
export const napCatVersion = '3.4.11';
export const napCatVersion = '3.6.17';

View File

@@ -175,14 +175,18 @@ export class NTQQFileApi {
const thumbPath = pathLib.join(thumb, thumbFileName);
ffmpeg(filePath)
.on('error', (err) => {
logger.logDebug('获取视频封面失败,使用默认封面', err);
if (diyThumbPath) {
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
try {
logger.logDebug('获取视频封面失败,使用默认封面', err);
if (diyThumbPath) {
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
}).catch(reject);
} else {
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
resolve(thumbPath);
}).catch(reject);
} else {
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
resolve(thumbPath);
}
} catch (error) {
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
}
})
.screenshots({
@@ -429,7 +433,7 @@ export class NTQQFileApi {
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.pkt.operation.FetchRkey();
}
if (this.packetRkey && this.packetRkey.length > 0) {
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);

View File

@@ -375,8 +375,10 @@ export class NTQQGroupApi {
};
}
async getGroupMembersV2(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
//console.log('getGroupMembers -->', groupQQ);
async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise<Map<string, GroupMember>> {
if (no_cache) {
return (await this.getGroupMemberAll(groupQQ, true)).result.infos;
}
let res = await this.GetGroupMembersV3(groupQQ, num);
let ret = res.infos;
if (res.infos.size === 0 && !res.listenerMode) {
@@ -386,14 +388,13 @@ export class NTQQGroupApi {
if (res.infos.size === 0) {
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
}
//console.log("<---------------")
return ret;
}
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const groupService = this.context.session.getGroupService();
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
const result = await groupService.getNextMemberList(sceneId!, undefined, num);
const result = await groupService.getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
@@ -401,8 +402,8 @@ export class NTQQGroupApi {
return result.result.infos;
}
async getGroupFileCount(Gids: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(Gids);
async getGroupFileCount(group_ids: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids);
}
async getArkJsonGroupShare(GroupCode: string) {

View File

@@ -1,31 +1,9 @@
import * as crypto from 'crypto';
import * as os from 'os';
import { ChatType, InstanceContext, NapCatCore } from '..';
import offset from '@/core/external/offset.json';
import { PacketSession } from "@/core/packet/session";
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { NapProtoMsg, NapProtoEncodeStructType, NapProtoDecodeStructType } from "@napneko/nap-proto-core";
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
import { InstanceContext, NapCatCore } from "@/core";
import { LogWrapper } from "@/common/log";
import { SendLongMsgResp } from "@/core/packet/proto/message/action";
import { PacketMsg } from "@/core/packet/message/message";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp";
import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
import { AIVoiceChatType, AIVoiceItemList } from "@/core/packet/entities/aiChat";
import { OidbSvcTrpcTcp0X929B_0Resp, OidbSvcTrpcTcp0X929D_0Resp } from "@/core/packet/proto/oidb/Oidb.0x929";
import { IndexNode, MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
import { RecvPacketData } from "@/core/packet/client/client";
import { PacketClientSession } from "@/core/packet/clientSession";
import { napCatVersion } from "@/common/version";
interface OffsetType {
[key: string]: {
@@ -41,200 +19,47 @@ export class NTQQPacketApi {
core: NapCatCore;
logger: LogWrapper;
qqVersion: string | undefined;
packetSession: PacketSession | undefined;
pkt!: PacketClientSession;
errStack: string[] = [];
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.logger = core.context.logger;
this.packetSession = undefined;
this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
.then()
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
.catch((err) => {
this.logger.logError.bind(this.core.context.logger);
this.errStack.push(err);
});
}
get available(): boolean {
return this.packetSession?.client.available ?? false;
return this.pkt?.available ?? false;
}
async InitSendPacket(qqversion: string) {
this.qqVersion = qqversion;
const table = typedOffset[qqversion + '-' + os.arch()];
get clientLogStack() {
return this.pkt?.clientLogStack + '\n' + this.errStack.join('\n');
}
async InitSendPacket(qqVer: string) {
this.qqVersion = qqVer;
const table = typedOffset[qqVer + '-' + os.arch()];
if (!table) {
this.logger.logError('[Core] [Packet] PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch());
const err = `[Core] [Packet] PacketBackend 不支持当前QQ版本架构${qqVer}-${os.arch()}
请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本`;
this.logger.logError(err);
this.errStack.push(err);
return false;
}
if (this.core.configLoader.configData.packetBackend === 'disable') {
this.logger.logWarn('[Core] [Packet] 已禁用Packet后端NapCat.Packet将不会加载');
const err = '[Core] [Packet] 已禁用PacketBackendNapCat.Packet将不会加载';
this.logger.logError(err);
this.errStack.push(err);
return false;
}
this.packetSession = new PacketSession(this.core);
const cb = () => {
if (this.packetSession && this.packetSession.client) {
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
}
};
await this.packetSession.client.connect(cb);
this.pkt = new PacketClientSession(this.core);
await this.pkt.init(process.pid, table.recv, table.send);
return true;
}
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
return this.packetSession!.client.sendPacket(cmd, data, rsp);
}
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
return this.sendPacket(pkt.cmd, pkt.data, rsp);
}
async sendPokePacket(peer: number, group?: number) {
const data = this.packetSession?.packer.packPokePacket(peer, group);
await this.sendOidbPacket(data!, false);
}
async sendRkeyPacket() {
const packet = this.packetSession?.packer.packRkeyPacket();
const ret = await this.sendOidbPacket(packet!, true);
if (!ret?.hex_data) return [];
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
return retData.data.rkeyList;
}
async sendGroupSignPacket(groupCode: string) {
const packet = this.packetSession?.packer.packGroupSignReq(this.core.selfInfo.uin, groupCode);
await this.sendOidbPacket(packet!, true);
}
async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
let status = 0;
try {
const packet = this.packetSession?.packer.packStatusPacket(uin);
const ret = await this.sendOidbPacket(packet!, true);
const data = Buffer.from(ret.hex_data, 'hex');
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
// ext & 0xff00 + ext >> 16 & 0xff
const extBigInt = BigInt(ext); // 转换为 BigInt
if (extBigInt <= 10n) {
return { status: Number(extBigInt) * 10, ext_status: 0 };
}
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); // 使用 BigInt 操作符
return { status: 10, ext_status: status };
} catch (error) {
return undefined;
}
}
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
await this.sendOidbPacket(data!, true);
}
// TODO: can simplify this
async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
const reqList = [];
for (const m of msg) {
for (const e of m.msg) {
if (e instanceof PacketMsgPicElement) {
reqList.push(this.packetSession?.highwaySession.uploadImage({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgVideoElement) {
reqList.push(this.packetSession?.highwaySession.uploadVideo({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgPttElement) {
reqList.push(this.packetSession?.highwaySession.uploadPtt({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
if (e instanceof PacketMsgFileElement) {
reqList.push(this.packetSession?.highwaySession.uploadFile({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
}
}
const res = await Promise.allSettled(reqList);
this.logger.log(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.logger.logError(`上传第${index + 1}个资源失败:${result.reason}`);
}
});
}
async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
await this.uploadResources(msg, groupUin);
const data = await this.packetSession?.packer.packUploadForwardMsg(this.core.selfInfo.uid, msg, groupUin);
const ret = await this.sendPacket('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', data!, true);
this.logger.logDebug('sendUploadForwardMsg', ret);
const resp = new NapProtoMsg(SendLongMsgResp).decode(Buffer.from(ret.hex_data, 'hex'));
return resp.result.resId;
}
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
if (resp.download.retCode !== 0) {
throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`);
}
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`;
}
async sendGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const data = this.packetSession?.packer.packGroupPttFileDownloadReq(groupUin, node);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(NTV2RichMediaResp).decode(body);
const info = resp.download.info;
return `https://${info.domain}${info.urlPath}${resp.download.rKeyParam}`;
}
async sendMiniAppShareInfoReq(param: MiniAppReqParams) {
const data = this.packetSession?.packer.packMiniAppAdaptShareInfo(param);
const ret = await this.sendPacket("LightAppSvc.mini_app_share.AdaptShareInfo", data!, true);
const body = new NapProtoMsg(MiniAppAdaptShareInfoResp).decode(Buffer.from(ret.hex_data, 'hex'));
return JSON.parse(body.content.jsonContent) as MiniAppRawData;
}
async sendFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType) : Promise<AIVoiceItemList[] | null> {
const data = this.packetSession?.packer.packFetchAiVoiceListReq(groupUin, chatType);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929D_0Resp).decode(body);
if (!resp.content) return null;
return resp.content.map((item) => {
return {
category: item.category,
voices: item.voices
};
});
}
async sendAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
let reqTime = 0;
const reqMaxTime = 30;
const sessionId = crypto.randomBytes(4).readUInt32BE(0);
while (true) {
if (reqTime >= reqMaxTime) {
throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`);
}
reqTime++;
const data = this.packetSession?.packer.packAiVoiceChatReq(groupUin, voiceId, text, chatType, sessionId);
const ret = await this.sendOidbPacket(data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBase).decode(Buffer.from(ret.hex_data, 'hex'));
if (body.errorCode) {
throw new Error(`sendAiVoiceChatReq retCode: ${body.errorCode} error: ${body.errorMsg}`);
}
const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929B_0Resp).decode(body.body);
if (!resp.msgInfo) continue;
return resp.msgInfo;
}
}
}

View File

@@ -283,7 +283,7 @@ export class NTQQWebApi {
this.context.logger.logError.bind(this.context.logger)('获取群聊之火失败');
}
}
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) {
const RetInternal = await getDataInternal(groupCode, 3);
if (RetInternal) {
HonorInfo.legend_list = [];

View File

@@ -74,5 +74,17 @@
"6.9.59-29271": {
"appid": 537249863,
"qua": "V1_MAC_NQ_6.9.59_29271_GW_B"
},
"9.9.16-29456": {
"appid": 537249875,
"qua": "V1_WIN_NQ_9.9.16_29456_GW_B"
},
"3.2.13-29456": {
"appid": 537249996,
"qua": "V1_LNX_NQ_3.2.13_29456_GW_B"
},
"6.9.59-29456": {
"appid": 537249961,
"qua": "V1_MAC_NQ_6.9.59_29456_GW_B"
}
}
}

View File

@@ -1,8 +1,4 @@
{
"6.9.56-28418-arm64": {
"send": "4471360",
"recv": "4473BCC"
},
"3.2.12-28418-x64": {
"recv": "A0723E0",
"send": "A06EAE0"
@@ -11,6 +7,14 @@
"recv": "37A9004",
"send": "37A4BD0"
},
"6.9.56-28418-x64": {
"send": "4471360",
"recv": "4473BCC"
},
"6.9.56-28418-arm64": {
"send": "3FBDBF8",
"recv": "3FC0410"
},
"9.9.15-28498-x64": {
"recv": "37A9004",
"send": "37A4BD0"
@@ -39,10 +43,14 @@
"send": "6E91318",
"recv": "6E94B50"
},
"6.9.58-28971-arm64": {
"6.9.58-28971-x64": {
"send": "449ACA0",
"recv": "449D50C"
},
"6.9.58-28971-arm64": {
"send": "3FE0DB0",
"recv": "3FE35C8"
},
"9.9.16-29271-x64": {
"send": "3833510",
"recv": "3837944"
@@ -54,5 +62,25 @@
"3.2.13-29271-arm64": {
"send": "6ECA098",
"recv": "6ECD8D0"
},
"9.9.16-29456-x64": {
"send": "3835CD0",
"recv": "383A104"
},
"3.2.13-29456-x64": {
"send": "A11E820",
"recv": "A122120"
},
"3.2.13-29456-arm64": {
"send": "6ECA130",
"recv": "6ECD968"
},
"6.9.59-29456-x64": {
"send": "44C57A0",
"recv": "44C800C"
},
"6.9.59-29456-arm64": {
"send": "4005FE8",
"recv": "4008800"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
// TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const LikeDetail = {
txt: ProtoField(1, ScalarType.STRING),
uin: ProtoField(3, ScalarType.INT64),
nickname: ProtoField(5, ScalarType.STRING)
};
const LikeMsg = {
times: ProtoField(1, ScalarType.INT32),
time: ProtoField(2, ScalarType.INT32),
detail: ProtoField(3, () => LikeDetail)
};
const ProfileLikeSubTip = {
msg: ProtoField(14, () => LikeMsg)
};
const ProfileLikeTip = {
msgType: ProtoField(1, ScalarType.INT32),
subType: ProtoField(2, ScalarType.INT32),
content: ProtoField(203, () => ProfileLikeSubTip)
};
const SysMessageHeader = {
PeerNumber: ProtoField(1, ScalarType.UINT32),
PeerString: ProtoField(2, ScalarType.STRING),
Uin: ProtoField(5, ScalarType.UINT32),
Uid: ProtoField(6, ScalarType.STRING, true)
};
const SysMessageMsgSpec = {
msgType: ProtoField(1, ScalarType.UINT32),
subType: ProtoField(2, ScalarType.UINT32),
subSubType: ProtoField(3, ScalarType.UINT32),
msgSeq: ProtoField(5, ScalarType.UINT32),
time: ProtoField(6, ScalarType.UINT32),
msgId: ProtoField(12, ScalarType.UINT64),
other: ProtoField(13, ScalarType.UINT32)
};
const SysMessageBodyWrapper = {
wrappedBody: ProtoField(2, ScalarType.BYTES)
};
const SysMessage = {
header: ProtoField(1, () => SysMessageHeader, false, true),
msgSpec: ProtoField(2, () => SysMessageMsgSpec, false, true),
bodyWrapper: ProtoField(3, () => SysMessageBodyWrapper)
};
export function decodeProfileLikeTip(buffer: Uint8Array) {
const msg = new NapProtoMsg(ProfileLikeTip);
return msg.decode(buffer);
}
export function decodeSysMessage(buffer: Uint8Array) {
const msg = new NapProtoMsg(SysMessage);
return msg.decode(buffer);
}

View File

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

View File

@@ -1,4 +1,4 @@
import { ChatType, RawMessage } from '@/core/entities';
import { ChatType, KickedOffLineInfo, RawMessage } from '@/core/entities';
import { CommonFileInfo } from '@/core';
export interface OnRichMediaDownloadCompleteParams {
@@ -212,7 +212,7 @@ export class NodeIKernelMsgListener {
}
onKickedOffLine(kickedInfo: unknown) {
onKickedOffLine(kickedInfo: KickedOffLineInfo) {
}

View File

@@ -1,9 +1,8 @@
import { LRUCache } from "@/common/lru-cache";
import { NapCatCore } from "@/core";
import { LogWrapper } from "@/common/log";
import crypto, { createHash } from "crypto";
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
import { NapCatConfig } from "@/core/helper/config";
import { PacketContext } from "@/core/packet/context/packetContext";
import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base";
import { LogStack } from "@/core/packet/context/clientContext";
export interface RecvPacket {
type: string, // 仅recv
@@ -17,38 +16,31 @@ export interface RecvPacketData {
hex_data: string
}
export abstract class PacketClient {
readonly napCatCore: NapCatCore;
protected readonly logger: LogWrapper;
function randText(len: number): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
export abstract class IPacketClient {
protected readonly context: PacketContext;
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
protected isAvailable: boolean = false;
protected config: NapCatConfig;
logStack: LogStack;
available: boolean = false;
protected constructor(core: NapCatCore) {
this.napCatCore = core;
this.logger = core.context.logger;
this.config = core.configLoader.configData;
protected constructor(context: PacketContext, logStack: LogStack) {
this.context = context;
this.logStack = logStack;
}
private randText(len: number): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
get available(): boolean {
return this.isAvailable;
}
abstract check(core: NapCatCore): boolean;
abstract check(): boolean;
abstract init(pid: number, recv: string, send: string): Promise<void>;
abstract connect(cb: () => void): Promise<void>;
abstract sendCommandImpl(cmd: string, data: string, trace_id: string): void;
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
@@ -58,9 +50,14 @@ export abstract class PacketClient {
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
}): Promise<RecvPacketData> {
return new Promise<RecvPacketData>((resolve, reject) => {
if (!this.available) {
reject(new Error('packetBackend 当前不可用!'));
}
const timeoutHandle = setTimeout(() => {
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
}, timeout);
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
sendcb(json);
if (!rsp) {
@@ -68,30 +65,23 @@ export abstract class PacketClient {
resolve(json);
}
});
if (rsp) {
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
clearTimeout(timeoutHandle);
resolve(json);
});
}
this.sendCommandImpl(cmd, data, trace_id);
});
}
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
return new Promise((resolve, reject) => {
if (!this.available) {
this.logger.logError('NapCat.Packet 未初始化!');
return undefined;
}
const md5 = crypto.createHash('md5').update(data).digest('hex');
const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2);
this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
//console.log('sendPacket:', cmd, data, trace_id);
await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
}).then((res) => resolve(res)).catch((e: Error) => reject(e));
const md5 = crypto.createHash('md5').update(data).digest('hex');
const trace_id = (randText(4) + md5 + data).slice(0, data.length / 2);
return this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
await this.context.napcore.sendSsoCmdReqByContend(cmd, trace_id);
});
}

View File

@@ -1,37 +1,37 @@
import crypto, { createHash } from "crypto";
import { NapCatCore } from "@/core";
import { createHash } from "crypto";
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import { PacketClient } from "@/core/packet/client/client";
import { IPacketClient } from "@/core/packet/client/baseClient";
import { constants } from "node:os";
import { LRUCache } from "@/common/lru-cache";
//0 send 1recv
import { PacketContext } from "@/core/packet/context/packetContext";
import { LogStack } from "@/core/packet/context/clientContext";
// 0 send 1 recv
export interface NativePacketExportType {
InitHook?: (recv: string, send: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean;
InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean;
SendPacket?: (cmd: string, data: string, trace_id: string) => void;
}
export class NativePacketClient extends PacketClient {
private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64'];
private MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
private sendEvent = new LRUCache<number, string>(500);//seq->trace_id
constructor(core: NapCatCore) {
super(core);
}
get available(): boolean {
return this.isAvailable;
export class NativePacketClient extends IPacketClient {
private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64'];
private MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
private sendEvent = new LRUCache<number, string>(500); // seq->trace_id
constructor(context: PacketContext, logStack: LogStack) {
super(context, logStack);
}
check(): boolean {
const platform = process.platform + '.' + process.arch;
if (!this.supportedPlatforms.includes(platform)) {
this.logger.logWarn(`[Core] [Packet:Native] 不支持的平台: ${platform}`);
this.logStack.pushLogWarn(`NativePacketClient: 不支持的平台: ${platform}`);
return false;
}
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node');
if (!fs.existsSync(moehoo_path)) {
this.logger.logWarn(`[Core] [Packet:Native] 缺失运行时文件: ${moehoo_path}`);
this.logStack.pushLogWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`);
return false;
}
return true;
@@ -54,27 +54,13 @@ export class NativePacketClient extends PacketClient {
// console.log('callback:', callback, trace_id);
callback?.({ seq, cmd, hex_data });
}
// const callback = this.cb.get(createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex') + (type === 0 ? 'send' : 'recv'));
// if (callback) {
// callback({ seq, cmd, hex_data });
// } else {
// this.logger.logError(`Callback not found for hex_data: ${hex_data}`);
// }
//console.log('type:', type, 'cmd:', cmd, 'trace_id:', trace_id);
});
this.isAvailable = true;
this.available = true;
}
sendCommandImpl(cmd: string, data: string, trace_id: string): void {
const trace_id_md5 = createHash('md5').update(trace_id).digest('hex');
//console.log('sendCommandImpl:', cmd, data, trace_id_md5);
this.MoeHooExport.exports.SendPacket?.(cmd, data, trace_id_md5);
this.cb.get(trace_id_md5 + 'send')?.({ seq: 0, cmd, hex_data: '' });
}
connect(cb: () => void): Promise<void> {
cb();
return Promise.resolve();
}
}

View File

@@ -1,98 +1,101 @@
import { Data, WebSocket } from "ws";
import { NapCatCore } from "@/core";
import { PacketClient, RecvPacket } from "@/core/packet/client/client";
import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient";
import { PacketContext } from "@/core/packet/context/packetContext";
import { LogStack } from "@/core/packet/context/clientContext";
export class wsPacketClient extends PacketClient {
private websocket: WebSocket | undefined;
export class wsPacketClient extends IPacketClient {
private websocket: WebSocket | null = null;
private reconnectAttempts: number = 0;
private readonly maxReconnectAttempts: number = 60; // 现在暂时不可配置
private readonly clientUrl: string | null = null;
private clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`;
private readonly clientUrl: string;
private readonly clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`;
constructor(core: NapCatCore) {
super(core);
this.clientUrl = this.config.packetServer ? this.clientUrlWrap( this.config.packetServer) : null;
private isInitialized: boolean = false;
private initPayload: { pid: number, recv: string, send: string } | null = null;
constructor(context: PacketContext, logStack: LogStack) {
super(context, logStack);
this.clientUrl = this.context.napcore.config.packetServer
? this.clientUrlWrap(this.context.napcore.config.packetServer)
: this.clientUrlWrap('127.0.0.1:8083');
}
check(): boolean {
if (!this.clientUrl) {
this.logger.logWarn(`[Core] [Packet:Server] 未配置服务器地址`);
if (!this.context.napcore.config.packetServer) {
this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`);
return false;
}
return true;
}
connect(cb: () => void): Promise<void> {
return new Promise((resolve, reject) => {
//this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`);
this.websocket = new WebSocket(this.clientUrl!);
this.websocket.on('error', (err) => { }/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/);
async init(pid: number, recv: string, send: string): Promise<void> {
this.initPayload = { pid, recv, send };
await this.connectWithRetry();
}
sendCommandImpl(cmd: string, data: string, trace_id: string): void {
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify({
action: 'send',
cmd,
data,
trace_id
}));
} else {
this.logStack.pushLogWarn(`WebSocket 未连接,无法发送命令: ${cmd}`);
}
}
private async connectWithRetry(): Promise<void> {
while (this.reconnectAttempts < this.maxReconnectAttempts) {
try {
await this.connect();
return;
} catch (error) {
this.reconnectAttempts++;
this.logStack.pushLogWarn(`${this.reconnectAttempts}/${this.maxReconnectAttempts} 次尝试重连失败!`);
await this.delay(5000);
}
}
this.logStack.pushLogError(`wsPacketClient 在 ${this.clientUrl} 达到最大重连次数 (${this.maxReconnectAttempts})`);
throw new Error(`无法连接到 WebSocket 服务器:${this.clientUrl}`);
}
private connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.websocket = new WebSocket(this.clientUrl);
this.websocket.onopen = () => {
this.isAvailable = true;
this.available = true;
this.reconnectAttempts = 0;
this.logger.log.bind(this.logger)(`[Core] [Packet:Server] 已连接到 ${this.clientUrl}`);
cb();
this.context.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
if (!this.isInitialized && this.initPayload) {
this.websocket!.send(JSON.stringify({
action: 'init',
...this.initPayload
}));
this.isInitialized = true;
}
resolve();
};
this.websocket.onerror = (error) => {
//this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
reject(new Error(`${error.message}`));
};
this.websocket.onmessage = (event) => {
// const message = JSON.parse(event.data.toString());
// console.log("Received message:", message);
this.handleMessage(event.data).then().catch();
};
this.websocket.onclose = () => {
this.isAvailable = false;
//this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`);
this.attemptReconnect(cb);
this.available = false;
this.context.logger.warn(`WebSocket 连接关闭,尝试重连...`);
reject(new Error('WebSocket 连接关闭'));
};
this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => {
this.context.logger.error(`处理消息时出错: ${err}`);
});
this.websocket.onerror = (error) => {
this.available = false;
this.context.logger.error(`WebSocket 出错: ${error.message}`);
this.websocket?.close();
reject(error);
};
});
}
private attemptReconnect(cb: any): void {
try {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this.connect(cb).catch((error) => {
this.logger.logError.bind(this.logger)(`[Core] [Packet:Server] 尝试重连失败:${error.message}`);
});
}, 5000 * this.reconnectAttempts);
} else {
this.logger.logError.bind(this.logger)(`[Core] [Packet:Server] ${this.clientUrl} 已达到最大重连次数!`);
}
} catch (error: any) {
this.logger.logError.bind(this.logger)(`[Core] [Packet:Server] 重连时出错: ${error.message}`);
}
}
async init(pid: number, recv: string, send: string): Promise<void> {
if (!this.isAvailable || !this.websocket) {
throw new Error("WebSocket is not connected");
}
const initMessage = {
action: 'init',
pid: pid,
recv: recv,
send: send
};
this.websocket.send(JSON.stringify(initMessage));
}
sendCommandImpl(cmd: string, data: string, trace_id: string) : void {
const commandMessage = {
action: 'send',
cmd: cmd,
data: data,
trace_id: trace_id
};
this.websocket!.send(JSON.stringify(commandMessage));
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private async handleMessage(message: Data): Promise<void> {
@@ -100,13 +103,10 @@ export class wsPacketClient extends PacketClient {
const json: RecvPacket = JSON.parse(message.toString());
const trace_id_md5 = json.trace_id_md5;
const action = json?.type ?? 'init';
const event = this.cb.get(trace_id_md5 + action);
if (event) {
await event(json.data);
}
//console.log("Received message:", json);
const event = this.cb.get(`${trace_id_md5}${action}`);
if (event) await event(json.data);
} catch (error) {
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
this.context.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
}
}
}

View File

@@ -0,0 +1,31 @@
import { PacketContext } from "@/core/packet/context/packetContext";
import { NapCatCore } from "@/core";
export class PacketClientSession {
private readonly context: PacketContext;
constructor(core: NapCatCore) {
this.context = new PacketContext(core);
}
init(pid: number, recv: string, send: string): Promise<void> {
return this.context.client.init(pid, recv, send);
}
get clientLogStack() {
return this.context.client.clientLogStack;
}
get available() {
return this.context.client.available;
}
get operation() {
return this.context.operation;
}
// TODO: global message element adapter (?
get msgConverter() {
return this.context.msgConverter;
}
}

View File

@@ -0,0 +1,123 @@
import { PacketContext } from "@/core/packet/context/packetContext";
import { IPacketClient } from "@/core/packet/client/baseClient";
import { NativePacketClient } from "@/core/packet/client/nativeClient";
import { wsPacketClient } from "@/core/packet/client/wsClient";
import { OidbPacket } from "@/core/packet/transformer/base";
import { PacketLogger } from "@/core/packet/context/loggerContext";
type clientPriority = {
[key: number]: (context: PacketContext, logStack: LogStack) => IPacketClient;
}
const clientPriority: clientPriority = {
10: (context: PacketContext, logStack: LogStack) => new NativePacketClient(context, logStack),
1: (context: PacketContext, logStack: LogStack) => new wsPacketClient(context, logStack),
};
export class LogStack {
private stack: string[] = [];
private logger: PacketLogger;
constructor(logger: PacketLogger) {
this.logger = logger;
}
push(msg: string) {
this.stack.push(msg);
}
pushLogInfo(msg: string) {
this.logger.info(msg);
this.stack.push(`${new Date().toISOString()} [INFO] ${msg}`);
}
pushLogWarn(msg: string) {
this.logger.warn(msg);
this.stack.push(`${new Date().toISOString()} [WARN] ${msg}`);
}
pushLogError(msg: string) {
this.logger.error(msg);
this.stack.push(`${new Date().toISOString()} [ERROR] ${msg}`);
}
clear() {
this.stack = [];
}
content() {
return this.stack.join('\n');
}
}
export class PacketClientContext {
private readonly context: PacketContext;
private readonly logStack: LogStack;
private readonly _client: IPacketClient;
constructor(context: PacketContext) {
this.context = context;
this.logStack = new LogStack(context.logger);
this._client = this.newClient();
}
get available(): boolean {
return this._client.available;
}
get clientLogStack(): string {
return this._client.logStack.content();
}
async init(pid: number, recv: string, send: string): Promise<void> {
await this._client.init(pid, recv, send);
}
async sendOidbPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
const raw = await this._client.sendOidbPacket(pkt, rsp);
return (rsp ? Buffer.from(raw.hex_data, "hex") : undefined) as T extends true ? Buffer : void;
}
private newClient(): IPacketClient {
const prefer = this.context.napcore.config.packetBackend;
let client: IPacketClient | null;
switch (prefer) {
case "native":
this.context.logger.info("使用指定的 NativePacketClient 作为后端");
client = new NativePacketClient(this.context, this.logStack);
break;
case "frida":
this.context.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
client = new wsPacketClient(this.context, this.logStack);
break;
case "auto":
case undefined:
client = this.judgeClient();
break;
default:
this.context.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
client = null;
}
if (!(client && client.check())) {
throw new Error("[Core] [Packet] 无可用的后端NapCat.Packet将不会加载");
}
return client;
}
private judgeClient(): IPacketClient {
const sortedClients = Object.entries(clientPriority)
.map(([priority, clientFactory]) => {
const client = clientFactory(this.context, this.logStack);
const score = +priority * +client.check();
return { client, score };
})
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score);
const selectedClient = sortedClients[0]?.client;
if (!selectedClient) {
throw new Error("[Core] [Packet] 无可用的后端NapCat.Packet将不会加载");
}
this.context.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
return selectedClient;
}
}

View File

@@ -0,0 +1,35 @@
import { LogLevel, LogWrapper } from "@/common/log";
import { PacketContext } from "@/core/packet/context/packetContext";
// TODO: check bind?
export class PacketLogger {
private readonly napLogger: LogWrapper;
constructor(context: PacketContext) {
this.napLogger = context.napcore.logger;
}
private _log(level: LogLevel, ...msg: any[]): void {
this.napLogger._log(level, "[Core] [Packet] " + msg);
}
debug(...msg: any[]): void {
this._log(LogLevel.DEBUG, msg);
}
info(...msg: any[]): void {
this._log(LogLevel.INFO, msg);
}
warn(...msg: any[]): void {
this._log(LogLevel.WARN, msg);
}
error(...msg: any[]): void {
this._log(LogLevel.ERROR, msg);
}
fatal(...msg: any[]): void {
this._log(LogLevel.FATAL, msg);
}
}

View File

@@ -0,0 +1,36 @@
import { NapCatCore } from "@/core";
export interface NapCoreCompatBasicInfo {
readonly uin: number;
readonly uid: string;
readonly uin2uid: (uin: number) => Promise<string>;
readonly uid2uin: (uid: string) => Promise<number>;
readonly sendSsoCmdReqByContend: (cmd: string, trace_id: string) => Promise<void>;
}
export class NapCoreContext {
private readonly core: NapCatCore;
constructor(core: NapCatCore) {
this.core = core;
}
get logger() {
return this.core.context.logger;
}
get basicInfo() {
return {
uin: +this.core.selfInfo.uin,
uid: this.core.selfInfo.uid,
uin2uid: (uin: number) => this.core.apis.UserApi.getUidByUinV2(String(uin)).then(res => res ?? ''),
uid2uin: (uid: string) => this.core.apis.UserApi.getUinByUidV2(uid).then(res => +res),
} as NapCoreCompatBasicInfo;
}
get config() {
return this.core.configLoader.configData;
}
sendSsoCmdReqByContend = (cmd: string, trace_id: string) => this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
}

View File

@@ -0,0 +1,164 @@
import * as crypto from 'crypto';
import { PacketContext } from "@/core/packet/context/packetContext";
import * as trans from "@/core/packet/transformer";
import { PacketMsg } from "@/core/packet/message/message";
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { ChatType } from "@/core";
import { MiniAppRawData, MiniAppReqParams } from "@/core/packet/entities/miniApp";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from "@napneko/nap-proto-core";
import { IndexNode, MsgInfo } from "@/core/packet/transformer/proto";
export class PacketOperationContext {
private context: PacketContext;
constructor(context: PacketContext) {
this.context = context;
}
async GroupPoke(groupUin: number, uin: number) {
const req = trans.SendPoke.build(groupUin, uin);
await this.context.client.sendOidbPacket(req);
}
async FriendPoke(uin: number) {
const req = trans.SendPoke.build(uin);
await this.context.client.sendOidbPacket(req);
}
async FetchRkey() {
const req = trans.FetchRkey.build();
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.FetchRkey.parse(resp);
return res.data.rkeyList;
}
async GroupSign(groupUin: number) {
const req = trans.GroupSign.build(this.context.napcore.basicInfo.uin, groupUin);
await this.context.client.sendOidbPacket(req);
}
async GetStrangerStatus(uin: number) {
let status = 0;
try {
const req = trans.GetStrangerInfo.build(uin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.GetStrangerInfo.parse(resp);
const extBigInt = BigInt(res.data.status.value);
if (extBigInt <= 10n) {
return { status: Number(extBigInt) * 10, ext_status: 0 };
}
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
return { status: 10, ext_status: status };
} catch (e) {
return undefined;
}
}
async SetGroupSpecialTitle(groupUin: number, uid: string, tittle: string) {
const req = trans.SetSpecialTitle.build(groupUin, uid, tittle);
await this.context.client.sendOidbPacket(req);
}
async UploadResources(msg: PacketMsg[], groupUin: number = 0) {
const reqList = [];
for (const m of msg) {
for (const e of m.msg) {
if (e instanceof PacketMsgPicElement) {
reqList.push(this.context.highway.uploadImage({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid
}, e));
}
if (e instanceof PacketMsgVideoElement) {
reqList.push(this.context.highway.uploadVideo({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid
}, e));
}
if (e instanceof PacketMsgPttElement) {
reqList.push(this.context.highway.uploadPtt({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid
}, e));
}
if (e instanceof PacketMsgFileElement) {
reqList.push(this.context.highway.uploadFile({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid
}, e));
}
}
}
const res = await Promise.allSettled(reqList);
this.context.logger.info(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}`);
res.forEach((result, index) => {
if (result.status === 'rejected') {
this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`);
}
});
}
async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
await this.UploadResources(msg, groupUin);
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async GetGroupFileUrl(groupUin: number, fileUUID: string) {
const req = trans.DownloadGroupFile.build(groupUin, fileUUID);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadGroupFile.parse(resp);
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadGroupPtt.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetMiniAppAdaptShareInfo(param: MiniAppReqParams) {
const req = trans.GetMiniAppAdaptShareInfo.build(param);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.GetMiniAppAdaptShareInfo.parse(resp);
return JSON.parse(res.content.jsonContent) as MiniAppRawData;
}
async FetchAiVoiceList(groupUin: number, chatType: AIVoiceChatType) {
const req = trans.FetchAiVoiceList.build(groupUin, chatType);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.FetchAiVoiceList.parse(resp);
if (!res.content) return null;
return res.content.map((item) => {
return {
category: item.category,
voices: item.voices
};
});
}
async GetAiVoice(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
let reqTime = 0;
const reqMaxTime = 30;
const sessionId = crypto.randomBytes(4).readUInt32BE(0);
while (true) {
if (reqTime >= reqMaxTime) {
throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`);
}
reqTime++;
const req = trans.GetAiVoice.build(groupUin, voiceId, text, sessionId, chatType);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.GetAiVoice.parse(resp);
if (!res.msgInfo) continue;
return res.msgInfo;
}
}
}

View File

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

View File

@@ -1,8 +1,9 @@
import * as stream from 'node:stream';
import { ReadStream } from "node:fs";
import { PacketHighwaySig } from "@/core/packet/highway/session";
import { HighwayHttpUploader, HighwayTcpUploader } from "@/core/packet/highway/uploader";
import { LogWrapper } from "@/common/log";
import { HighwayTcpUploader } from "@/core/packet/highway/uploader/highwayTcpUploader";
import { HighwayHttpUploader } from "@/core/packet/highway/uploader/highwayHttpUploader";
import { PacketHighwaySig } from "@/core/packet/highway/highwayContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
export interface PacketHighwayTrans {
uin: string;
@@ -24,9 +25,9 @@ export class PacketHighwayClient {
sig: PacketHighwaySig;
server: string = 'htdata3.qq.com';
port: number = 80;
logger: LogWrapper;
logger: PacketLogger;
constructor(sig: PacketHighwaySig, logger: LogWrapper, server: string = 'htdata3.qq.com', port: number = 80) {
constructor(sig: PacketHighwaySig, logger: PacketLogger, server: string = 'htdata3.qq.com', port: number = 80) {
this.sig = sig;
this.logger = logger;
}
@@ -59,12 +60,12 @@ export class PacketHighwayClient {
const tcpUploader = new HighwayTcpUploader(trans, this.logger);
await tcpUploader.upload();
} catch (e) {
this.logger.logError(`[Highway] upload failed: ${e}, fallback to http upload`);
this.logger.error(`[Highway] upload failed: ${e}, fallback to http upload`);
try {
const httpUploader = new HighwayHttpUploader(trans, this.logger);
await httpUploader.upload();
} catch (e) {
this.logger.logError(`[Highway] http upload failed: ${e}`);
this.logger.error(`[Highway] http upload failed: ${e}`);
throw e;
}
}

View File

@@ -1,24 +1,21 @@
import * as fs from "node:fs";
import { ChatType, Peer } from "@/core";
import { LogWrapper } from "@/common/log";
import { PacketPacker } from "@/core/packet/packer";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
import { PacketHighwayClient } from "@/core/packet/highway/client";
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase";
import { PacketContext } from "@/core/packet/context/packetContext";
import { PacketLogger } from "@/core/packet/context/loggerContext";
import FetchSessionKey from "@/core/packet/transformer/highway/FetchSessionKey";
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { FileUploadExt, NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway";
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
import { ChatType, Peer } from "@/core";
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
import { PacketClient } from "@/core/packet/client/client";
import UploadGroupImage from "@/core/packet/transformer/highway/UploadGroupImage";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import * as proto from "@/core/packet/transformer/proto";
import * as trans from "@/core/packet/transformer";
import fs from "fs";
export const BlockSize = 1024 * 1024;
@@ -35,34 +32,28 @@ export interface PacketHighwaySig {
serverAddr: HighwayServerAddr[]
}
export class PacketHighwaySession {
protected packetClient: PacketClient;
protected packetHighwayClient: PacketHighwayClient;
export class PacketHighwayContext {
private context: PacketContext;
protected sig: PacketHighwaySig;
protected logger: LogWrapper;
protected packer: PacketPacker;
protected logger: PacketLogger;
protected hwClient: PacketHighwayClient;
private cachedPrepareReq: Promise<void> | null = null;
constructor(logger: LogWrapper, client: PacketClient, packer: PacketPacker) {
this.packetClient = client;
this.logger = logger;
constructor(context: PacketContext) {
this.context = context;
this.sig = {
uin: this.packetClient.napCatCore.selfInfo.uin,
uid: this.packetClient.napCatCore.selfInfo.uid,
uin: String(context.napcore.basicInfo.uin),
uid: context.napcore.basicInfo.uid,
sigSession: null,
sessionKey: null,
serverAddr: [],
};
this.packer = packer;
this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger);
this.logger = context.logger;
this.hwClient = new PacketHighwayClient(this.sig, context.logger);
}
private async checkAvailable() {
if (!this.packetClient.available) {
throw new Error('packetBackend不可用请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置');
}
if (this.sig.sigSession === null || this.sig.sessionKey === null) {
this.logger.logWarn('[Highway] sigSession or sessionKey not available!');
if (this.cachedPrepareReq === null) {
this.cachedPrepareReq = this.prepareUpload().finally(() => {
this.cachedPrepareReq = null;
@@ -73,17 +64,16 @@ export class PacketHighwaySession {
}
private async prepareUpload(): Promise<void> {
const packet = this.packer.packHttp0x6ff_501();
const req = await this.packetClient.sendPacket('HttpConn.0x6ff_501', packet, true);
const rsp = new NapProtoMsg(HttpConn0x6ff_501Response).decode(
Buffer.from(req.hex_data, 'hex')
);
this.logger.debug('[Highway] on prepareUpload!');
const packet = FetchSessionKey.build();
const req = await this.context.client.sendOidbPacket(packet, true);
const rsp = FetchSessionKey.parse(req);
this.sig.sigSession = rsp.httpConn.sigSession;
this.sig.sessionKey = rsp.httpConn.sessionKey;
for (const info of rsp.httpConn.serverInfos) {
if (info.serviceType !== 1) continue;
for (const addr of info.serverAddrs) {
this.logger.logDebug(`[Highway PrepareUpload] server addr add: ${int32ip2str(addr.ip)}:${addr.port}`);
this.logger.debug(`[Highway PrepareUpload] server addr add: ${int32ip2str(addr.ip)}:${addr.port}`);
this.sig.serverAddr.push({
ip: int32ip2str(addr.ip),
port: addr.port
@@ -95,9 +85,9 @@ export class PacketHighwaySession {
async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> {
await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupImageReq(+peer.peerUid, img);
await this.uploadGroupImage(+peer.peerUid, img);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CImageReq(peer.peerUid, img);
await this.uploadC2CImage(peer.peerUid, img);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
@@ -109,9 +99,9 @@ export class PacketHighwaySession {
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);
await this.uploadGroupVideo(+peer.peerUid, video);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CVideoReq(peer.peerUid, video);
await this.uploadC2CVideo(peer.peerUid, video);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
@@ -120,9 +110,9 @@ export class PacketHighwaySession {
async uploadPtt(peer: Peer, ptt: PacketMsgPttElement): Promise<void> {
await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupPttReq(+peer.peerUid, ptt);
await this.uploadGroupPtt(+peer.peerUid, ptt);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CPttReq(peer.peerUid, ptt);
await this.uploadC2CPtt(peer.peerUid, ptt);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
@@ -131,29 +121,26 @@ export class PacketHighwaySession {
async uploadFile(peer: Peer, file: PacketMsgFileElement): Promise<void> {
await this.checkAvailable();
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
await this.uploadGroupFileReq(+peer.peerUid, file);
await this.uploadGroupFile(+peer.peerUid, file);
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
await this.uploadC2CFileReq(peer.peerUid, file);
await this.uploadC2CFile(peer.peerUid, file);
} else {
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
}
}
private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise<void> {
private async uploadGroupImage(groupUin: number, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const preReq = await this.packer.packUploadGroupImgReq(groupUin, img);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const req = UploadGroupImage.build(groupUin, img);
const resp = await this.context.client.sendOidbPacket(req, true);
const preRespData = UploadGroupImage.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
this.logger.debug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
@@ -165,7 +152,7 @@ export class PacketHighwaySession {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1004,
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
img.size,
@@ -173,27 +160,24 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`);
this.logger.debug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`);
}
img.msgInfo = preRespData.upload.msgInfo;
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
}
private async uploadC2CImageReq(peerUid: string, img: PacketMsgPicElement): Promise<void> {
private async uploadC2CImage(peerUid: string, img: PacketMsgPicElement): Promise<void> {
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
const preReq = await this.packer.packUploadC2CImgReq(peerUid, img);
const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true);
const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(
Buffer.from(preRespRaw.hex_data, 'hex')
);
const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
const req = trans.UploadPrivateImage.build(peerUid, img);
const resp = await this.context.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateImage.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
this.logger.debug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
@@ -205,7 +189,7 @@ export class PacketHighwaySession {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1003,
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
img.size,
@@ -213,27 +197,24 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
this.logger.debug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
}
img.msgInfo = preRespData.upload.msgInfo;
}
private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<void> {
private async uploadGroupVideo(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 req = trans.UploadGroupVideo.build(groupUin, video);
const resp = await this.context.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupVideo.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
this.logger.debug(`[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({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
@@ -245,7 +226,7 @@ export class PacketHighwaySession {
fileSha1: await calculateSha1StreamBytes(video.filePath!)
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1005,
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
+video.fileSize!,
@@ -253,15 +234,15 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
this.logger.debug(`[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!`);
this.logger.debug(`[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({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: subFile.uKey,
network: {
@@ -273,7 +254,7 @@ export class PacketHighwaySession {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1006,
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
+video.thumbSize!,
@@ -281,27 +262,24 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
this.logger.debug(`[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> {
private async uploadC2CVideo(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 req = trans.UploadPrivateVideo.build(peerUid, video);
const resp = await this.context.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateVideo.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
this.logger.debug(`[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({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
@@ -313,7 +291,7 @@ export class PacketHighwaySession {
fileSha1: await calculateSha1StreamBytes(video.filePath!)
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1001,
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
+video.fileSize!,
@@ -321,15 +299,15 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
this.logger.debug(`[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!`);
this.logger.debug(`[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({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: subFile.uKey,
network: {
@@ -341,7 +319,7 @@ export class PacketHighwaySession {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1002,
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
+video.thumbSize!,
@@ -349,26 +327,23 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
this.logger.debug(`[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> {
private async uploadGroupPtt(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 req = trans.UploadGroupPtt.build(groupUin, ptt);
const resp = await this.context.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupPtt.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
this.logger.debug(`[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({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
@@ -380,7 +355,7 @@ export class PacketHighwaySession {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1008,
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
ptt.fileSize,
@@ -388,26 +363,23 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadGroupPttReq get upload invalid ukey ${ukey}, don't need upload!`);
this.logger.debug(`[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> {
private async uploadC2CPtt(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 req = trans.UploadPrivatePtt.build(peerUid, ptt);
const resp = await this.context.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivatePtt.parse(resp);
const ukey = preRespData.upload.uKey;
if (ukey && ukey != "") {
this.logger.logDebug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
this.logger.debug(`[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({
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
uKey: ukey,
network: {
@@ -419,7 +391,7 @@ export class PacketHighwaySession {
fileSha1: [sha1]
}
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
1007,
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
ptt.fileSize,
@@ -427,24 +399,21 @@ export class PacketHighwaySession {
extend
);
} else {
this.logger.logDebug(`[Highway] uploadC2CPttReq get upload invalid ukey ${ukey}, don't need upload!`);
this.logger.debug(`[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> {
private async uploadGroupFile(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);
const req = trans.UploadGroupFile.build(groupUin, file);
const resp = await this.context.client.sendOidbPacket(req, true);
const preRespData = trans.UploadGroupFile.parse(resp);
if (!preRespData?.upload?.boolFileExist) {
this.logger.logDebug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
const ext = new NapProtoMsg(FileUploadExt).encode({
this.logger.debug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
const ext = new NapProtoMsg(proto.FileUploadExt).encode({
unknown1: 100,
unknown2: 1,
entry: {
@@ -485,7 +454,7 @@ export class PacketHighwaySession {
},
unknown200: 0,
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
71,
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
file.fileSize,
@@ -493,24 +462,21 @@ export class PacketHighwaySession {
ext
);
} else {
this.logger.logDebug(`[Highway] uploadGroupFileReq file exist, don't need upload!`);
this.logger.debug(`[Highway] uploadGroupFileReq file exist, don't need upload!`);
}
file.fileUuid = preRespData.upload.fileId;
}
private async uploadC2CFileReq(peerUid: string, file: PacketMsgFileElement): Promise<void> {
private async uploadC2CFile(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);
const req = await trans.UploadPrivateFile.build(this.sig.uid, peerUid, file);
const res = await this.context.client.sendOidbPacket(req, true);
const preRespData = trans.UploadPrivateFile.parse(res);
if (!preRespData.upload?.boolFileExist) {
this.logger.logDebug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
const ext = new NapProtoMsg(FileUploadExt).encode({
this.logger.debug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
const ext = new NapProtoMsg(proto.FileUploadExt).encode({
unknown1: 100,
unknown2: 1,
entry: {
@@ -550,7 +516,7 @@ export class PacketHighwaySession {
unknown200: 1,
unknown3: 0
});
await this.packetHighwayClient.upload(
await this.hwClient.upload(
95,
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
file.fileSize,
@@ -560,10 +526,9 @@ export class PacketHighwaySession {
}
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);
const fileExistReq = trans.DownloadOfflineFile.build(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
const fileExistRes = await this.context.client.sendOidbPacket(fileExistReq, true);
file._e37_800_rsp = trans.DownloadOfflineFile.parse(fileExistRes);
file._private_send_uid = this.sig.uid;
file._private_recv_uid = peerUid;
}

View File

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

View File

@@ -0,0 +1,75 @@
import crypto from "node:crypto";
import http from "node:http";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { IHighwayUploader } from "@/core/packet/highway/uploader/highwayUploader";
import { Frame } from "@/core/packet/highway/frame";
import * as proto from "@/core/packet/transformer/proto";
export class HighwayHttpUploader extends IHighwayUploader {
async upload(): Promise<void> {
const controller = new AbortController();
const { signal } = controller;
const upload = (async () => {
let offset = 0;
for await (const chunk of this.trans.data) {
if (signal.aborted) {
throw new Error('Upload aborted due to timeout');
}
const block = chunk as Buffer;
try {
await this.uploadBlock(block, offset);
} catch (err) {
throw new Error(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`);
}
offset += block.length;
}
})();
const timeout = this.timeout().catch((err) => {
controller.abort();
throw new Error(err.message);
});
await Promise.race([upload, timeout]);
}
private async uploadBlock(block: Buffer, offset: number): Promise<void> {
const chunkMD5 = crypto.createHash('md5').update(block).digest();
const payload = this.buildPicUpHead(offset, block.length, chunkMD5);
const frame = Frame.pack(Buffer.from(payload), block);
const resp = await this.httpPostHighwayContent(frame, `http://${this.trans.server}:${this.trans.port}/cgi-bin/httpconn?htcmd=0x6FF0087&uin=${this.trans.uin}`);
const [head, body] = Frame.unpack(resp);
const headData = new NapProtoMsg(proto.RespDataHighwayHead).decode(head);
this.logger.debug(`[Highway] httpUploadBlock: ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`);
if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`);
}
private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
try {
const options: http.RequestOptions = {
method: 'POST',
headers: {
'Connection': 'keep-alive',
'Accept-Encoding': 'identity',
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)',
'Content-Length': frame.length.toString(),
},
};
const req = http.request(serverURL, options, (res) => {
const data: Buffer[] = [];
res.on('data', (chunk) => {
data.push(chunk);
});
res.on('end', () => {
resolve(Buffer.concat(data));
});
});
req.write(frame);
req.on('error', (error) => {
reject(error);
});
} catch (error) {
reject(error);
}
});
}
}

View File

@@ -0,0 +1,85 @@
import net from "node:net";
import stream from "node:stream";
import crypto from "node:crypto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { BlockSize } from "@/core/packet/highway/highwayContext";
import { Frame } from "@/core/packet/highway/frame";
import { IHighwayUploader } from "@/core/packet/highway/uploader/highwayUploader";
import * as proto from "@/core/packet/transformer/proto";
class HighwayTcpUploaderTransform extends stream.Transform {
uploader: HighwayTcpUploader;
offset: number;
constructor(uploader: HighwayTcpUploader) {
super();
this.uploader = uploader;
this.offset = 0;
}
_transform(data: Buffer, _: BufferEncoding, callback: stream.TransformCallback) {
let chunkOffset = 0;
while (chunkOffset < data.length) {
const chunkSize = Math.min(BlockSize, data.length - chunkOffset);
const chunk = data.subarray(chunkOffset, chunkOffset + chunkSize);
const chunkMd5 = crypto.createHash('md5').update(chunk).digest();
const head = this.uploader.buildPicUpHead(this.offset, chunk.length, chunkMd5);
chunkOffset += chunk.length;
this.offset += chunk.length;
this.push(Frame.pack(Buffer.from(head), chunk));
}
callback(null);
}
}
export class HighwayTcpUploader extends IHighwayUploader {
async upload(): Promise<void> {
const controller = new AbortController();
const { signal } = controller;
const upload = new Promise<void>((resolve, reject) => {
const highwayTransForm = new HighwayTcpUploaderTransform(this);
const socket = net.connect(this.trans.port, this.trans.server, () => {
this.trans.data.pipe(highwayTransForm).pipe(socket, { end: false });
});
const handleRspHeader = (header: Buffer) => {
const rsp = new NapProtoMsg(proto.RespDataHighwayHead).decode(header);
if (rsp.errorCode !== 0) {
socket.end();
reject(new Error(`[Highway] tcpUpload failed (code=${rsp.errorCode})`));
}
const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2);
this.logger.debug(`[Highway] tcpUpload ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`);
if (Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength) >= Number(rsp.msgSegHead?.filesize)) {
this.logger.debug('[Highway] tcpUpload finished.');
socket.end();
resolve();
}
};
socket.on('data', (chunk: Buffer) => {
if (signal.aborted) {
socket.end();
reject(new Error('Upload aborted due to timeout'));
}
const [head, _] = Frame.unpack(chunk);
handleRspHeader(head);
});
socket.on('close', () => {
this.logger.debug('[Highway] tcpUpload socket closed.');
resolve();
});
socket.on('error', (err) => {
socket.end();
reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`));
});
this.trans.data.on('error', (err) => {
socket.end();
reject(new Error(`[Highway] tcpUpload readable error: ${err}`));
});
});
const timeout = this.timeout().catch((err) => {
controller.abort();
throw new Error(err.message);
});
await Promise.race([upload, timeout]);
}
}

View File

@@ -0,0 +1,63 @@
import * as tea from "@/core/packet/utils/crypto/tea";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { PacketHighwayTrans } from "@/core/packet/highway/client";
import { PacketLogger } from "@/core/packet/context/loggerContext";
import * as proto from "@/core/packet/transformer/proto";
export abstract class IHighwayUploader {
readonly trans: PacketHighwayTrans;
readonly logger: PacketLogger;
constructor(trans: PacketHighwayTrans, logger: PacketLogger) {
this.trans = trans;
this.logger = logger;
}
private encryptTransExt(key: Uint8Array) {
if (!this.trans.encrypt) return;
this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key));
}
protected timeout(): Promise<void> {
return new Promise<void>((_, reject) => {
setTimeout(() => {
reject(new Error(`[Highway] timeout after ${this.trans.timeout}s`));
}, (this.trans.timeout ?? Infinity) * 1000
);
});
}
buildPicUpHead(offset: number, bodyLength: number, bodyMd5: Uint8Array): Uint8Array {
return new NapProtoMsg(proto.ReqDataHighwayHead).encode({
msgBaseHead: {
version: 1,
uin: this.trans.uin,
command: "PicUp.DataUp",
seq: 0,
retryTimes: 0,
appId: 1600001604,
dataFlag: 16,
commandId: this.trans.cmd,
},
msgSegHead: {
serviceId: 0,
filesize: BigInt(this.trans.size),
dataOffset: BigInt(offset),
dataLength: bodyLength,
serviceTicket: this.trans.ticket,
md5: bodyMd5,
fileMd5: this.trans.sum,
cacheAddr: 0,
cachePort: 0,
},
bytesReqExtendInfo: this.trans.ext,
timestamp: BigInt(0),
msgLoginSigHead: {
uint32LoginSigType: 8,
appId: 1600001604,
}
});
}
abstract upload(): Promise<void>;
}

View File

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

View File

@@ -1,21 +1,14 @@
import * as crypto from "crypto";
import { PushMsgBody } from "@/core/packet/proto/message/message";
import { PushMsgBody } from "@/core/packet/transformer/proto";
import { NapProtoEncodeStructType } from "@napneko/nap-proto-core";
import { LogWrapper } from "@/common/log";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/element";
import { SendTextElement } from "@/core";
export class PacketMsgBuilder {
private logger: LogWrapper;
constructor(logger: LogWrapper) {
this.logger = logger;
}
protected static failBackText = new PacketMsgTextElement(
{
textElement: { content: "[该消息类型暂不支持查看]" }!
textElement: { content: "[该消息类型暂不支持查看]" }
} as SendTextElement
);
@@ -27,7 +20,6 @@ export class PacketMsgBuilder {
}, undefined);
const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []);
if (!msgContent && !msgElement.length) {
this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement`);
msgElement.push(PacketMsgBuilder.failBackText.buildElement());
}
return {

View File

@@ -32,7 +32,6 @@ import {
PacketMultiMsgElement
} from "@/core/packet/message/element";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
import { LogWrapper } from "@/common/log";
const SupportedElementTypes = [
ElementType.TEXT,
@@ -77,50 +76,12 @@ export type rawMsgWithSendMsg = {
msg: PacketSendMsgElement[]
}
// TODO: make it become adapter?
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) {
@@ -160,4 +121,37 @@ export class PacketMsgConverter {
return new PacketMultiMsgElement(element as SendStructLongMsgElement);
}
};
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)
};
}
}

View File

@@ -7,8 +7,12 @@ import {
MentionExtra,
NotOnlineImage,
QBigFaceExtra,
QSmallFaceExtra
} from "@/core/packet/proto/message/element";
QSmallFaceExtra,
MsgInfo,
OidbSvcTrpcTcp0XE37_800Response,
FileExtra,
GroupFileExtra
} from "@/core/packet/transformer/proto";
import {
AtType,
PicType,
@@ -24,11 +28,8 @@ import {
SendTextElement,
SendVideoElement
} from "@/core";
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
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";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
// raw <-> packet
// TODO: SendStructLongMsgElement

View File

@@ -1,803 +0,0 @@
import * as zlib from "node:zlib";
import * as crypto from "node:crypto";
import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase";
import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202";
import { OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body } from "@/core/packet/proto/oidb/Oidb.0x8FC_2";
import { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.0XFE1_2";
import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1";
import { IndexNode, NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
import { PacketMsgBuilder } from "@/core/packet/message/builder";
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from "@/core/packet/message/element";
import { LogWrapper } from "@/common/log";
import { PacketMsg } from "@/core/packet/message/message";
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
import { PacketMsgConverter } from "@/core/packet/message/converter";
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";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
import { OidbSvcTrpcTcp0X929B_0, OidbSvcTrpcTcp0X929D_0 } from "@/core/packet/proto/oidb/Oidb.0x929";
import { PacketClient } from "@/core/packet/client/client";
export type PacketHexStr = string & { readonly hexNya: unique symbol };
export interface OidbPacket {
cmd: string;
data: PacketHexStr
}
export class PacketPacker {
readonly logger: LogWrapper;
readonly client: PacketClient;
readonly packetBuilder: PacketMsgBuilder;
readonly packetConverter: PacketMsgConverter;
constructor(logger: LogWrapper, client: PacketClient) {
this.logger = logger;
this.client = client;
this.packetBuilder = new PacketMsgBuilder(logger);
this.packetConverter = new PacketMsgConverter(logger);
}
private packetPacket(byteArray: Uint8Array): PacketHexStr {
return Buffer.from(byteArray).toString('hex') as PacketHexStr;
}
packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({
command: cmd,
subCommand: subCmd,
body: body,
isReserved: isUid ? 1 : 0
});
return {
cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`,
data: this.packetPacket(data)
};
}
packPokePacket(peer: number, group?: number): OidbPacket {
const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({
uin: peer,
groupUin: group,
friendUin: group ?? peer,
ext: 0
});
return this.packOidbPacket(0xed3, 1, oidb_0xed3);
}
packRkeyPacket(): OidbPacket {
const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({
reqHead: {
common: {
requestId: 1,
command: 202
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 0
},
client: {
agentType: 2
}
},
downloadRKeyReq: {
key: [10, 20, 2]
},
});
return this.packOidbPacket(0x9067, 202, oidb_0x9067_202);
}
packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket {
const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({
targetUid: uid,
specialTitle: tittle,
expiredTime: -1,
uinName: tittle
});
const oidb_0x8FC_2 = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2).encode({
groupUin: +groupCode,
body: oidb_0x8FC_2_body
});
return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false);
}
packStatusPacket(uin: number): OidbPacket {
const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({
uin: uin,
key: [{ key: 27372 }]
});
return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2);
}
async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise<PacketHexStr> {
const msgBody = this.packetBuilder.buildFakeMsg(selfUid, msg);
const longMsgResultData = new NapProtoMsg(LongMsgResult).encode(
{
action: {
actionCommand: "MultiMsg",
actionData: {
msgBody: msgBody
}
}
}
);
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
const req = new NapProtoMsg(SendLongMsgReq).encode(
{
info: {
type: groupUin === 0 ? 1 : 3,
uid: {
uid: groupUin === 0 ? selfUid : groupUin.toString(),
},
groupUin: groupUin,
payload: payload
},
settings: {
field1: 4, field2: 1, field3: 7, field4: 0
}
}
);
// this.logger.logDebug("packUploadForwardMsg REQ!!!", req);
return this.packetPacket(req);
}
// highway part
packHttp0x6ff_501(): PacketHexStr {
return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({
httpConn: {
field1: 0,
field2: 0,
field3: 16,
field4: 1,
field6: 3,
serviceTypes: [1, 5, 10, 21],
// tgt: "", // TODO: do we really need tgt? seems not
field9: 2,
field10: 9,
field11: 8,
ver: "1.0.1"
}
}));
}
async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode(
{
reqHead: {
common: {
requestId: 1,
command: 100
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 2,
group: {
groupUin: groupUin
},
},
client: {
agentType: 2
}
},
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: +img.size,
fileHash: img.md5,
fileSha1: img.sha1!,
fileName: img.name,
type: {
type: 1,
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0,
voiceFormat: 0,
},
width: img.width,
height: img.height,
time: 0,
original: 1
},
subFileType: 0,
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 2,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
},
video: {
bytesPbReserve: Buffer.alloc(0),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.alloc(0),
bytesGeneralFlags: Buffer.alloc(0),
}
},
clientSeq: 0,
noNeedCompatMsg: false,
}
}
);
return this.packOidbPacket(0x11c4, 100, req, true, false);
}
async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise<OidbPacket> {
const req = new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 100
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: peerUin
},
},
client: {
agentType: 2,
}
},
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: +img.size,
fileHash: img.md5,
fileSha1: img.sha1!,
fileName: img.name,
type: {
type: 1,
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0,
voiceFormat: 0,
},
width: img.width,
height: img.height,
time: 0,
original: 1
},
subFileType: 0,
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 1,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
},
video: {
bytesPbReserve: Buffer.alloc(0),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.alloc(0),
bytesGeneralFlags: Buffer.alloc(0),
}
},
clientSeq: 0,
noNeedCompatMsg: false,
}
}
);
return this.packOidbPacket(0x11c5, 100, req, true, false);
}
async packUploadGroupVideoReq(groupUin: number, 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: 2,
group: {
groupUin: groupUin
},
},
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(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 {
return this.packetPacket(
new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({
subCommand: 1200,
field2: 1,
body: {
receiverUid: selfUid,
fileUuid: fileUUID,
type: 2,
fileHash: fileHash,
t2: 0
},
field101: 3,
field102: 103,
field200: 1,
field99999: Buffer.from([0xc0, 0x85, 0x2c, 0x01])
})
);
}
packGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
return this.packOidbPacket(0x126E, 200, new NapProtoMsg(NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 4,
command: 200
},
scene: {
requestType: 1,
businessType: 3,
sceneType: 2,
group: {
groupUin: groupUin
}
},
client: {
agentType: 2
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0,
}
}
}
}), true, false);
}
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: ""
}
}
)
);
}
packFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType): OidbPacket {
return this.packOidbPacket(0x929D, 0,
new NapProtoMsg(OidbSvcTrpcTcp0X929D_0).encode({
groupUin: groupUin,
chatType: chatType
})
);
}
packAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType, sessionId: number): OidbPacket {
return this.packOidbPacket(0x929B, 0,
new NapProtoMsg(OidbSvcTrpcTcp0X929B_0).encode({
groupUin: groupUin,
voiceId: voiceId,
text: text,
chatType: chatType,
session: {
sessionId: sessionId
}
})
);
}
}

View File

@@ -1,49 +0,0 @@
// TODO: refactor with NapProto
import { MessageType, BinaryReader, ScalarType } from '@protobuf-ts/runtime';
export const BodyInner = new MessageType("BodyInner", [
{ no: 1, name: "msgType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */, opt: true },
{ no: 2, name: "subType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */, opt: true }
]);
export const NoifyData = new MessageType("NoifyData", [
{ no: 1, name: "skip", kind: "scalar", T: ScalarType.BYTES /* bytes */, opt: true },
{ no: 2, name: "innerData", kind: "scalar", T: ScalarType.BYTES /* bytes */, opt: true }
]);
export const MsgHead = new MessageType("MsgHead", [
{ no: 2, name: "bodyInner", kind: "message", T: () => BodyInner, opt: true },
{ no: 3, name: "noifyData", kind: "message", T: () => NoifyData, opt: true }
]);
export const Message = new MessageType("Message", [
{ no: 1, name: "msgHead", kind: "message", T: () => MsgHead }
]);
export const SubDetail = new MessageType("SubDetail", [
{ no: 1, name: "msgSeq", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 2, name: "msgTime", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 6, name: "senderUid", kind: "scalar", T: ScalarType.STRING /* string */ }
]);
export const RecallDetails = new MessageType("RecallDetails", [
{ no: 1, name: "operatorUid", kind: "scalar", T: ScalarType.STRING /* string */ },
{ no: 3, name: "subDetail", kind: "message", T: () => SubDetail }
]);
export const RecallGroup = new MessageType("RecallGroup", [
{ no: 1, name: "type", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
{ no: 4, name: "peerUid", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 11, name: "recallDetails", kind: "message", T: () => RecallDetails },
{ no: 37, name: "grayTipsSeq", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ }
]);
export function decodeMessage(buffer: Uint8Array): any {
const reader = new BinaryReader(buffer);
return Message.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
}
export function decodeRecallGroup(buffer: Uint8Array): any {
const reader = new BinaryReader(buffer);
return RecallGroup.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
}

View File

@@ -1,59 +0,0 @@
// TODO: refactor with NapProto
import { MessageType, BinaryReader, ScalarType, RepeatType } from '@protobuf-ts/runtime';
export const LikeDetail = new MessageType("likeDetail", [
{ no: 1, name: "txt", kind: "scalar", T: ScalarType.STRING /* string */ },
{ no: 3, name: "uin", kind: "scalar", T: ScalarType.INT64 /* int64 */ },
{ no: 5, name: "nickname", kind: "scalar", T: ScalarType.STRING /* string */ }
]);
export const LikeMsg = new MessageType("likeMsg", [
{ no: 1, name: "times", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
{ no: 2, name: "time", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
{ no: 3, name: "detail", kind: "message", T: () => LikeDetail }
]);
export const ProfileLikeSubTip = new MessageType("profileLikeSubTip", [
{ no: 14, name: "msg", kind: "message", T: () => LikeMsg }
]);
export const ProfileLikeTip = new MessageType("profileLikeTip", [
{ no: 1, name: "msgType", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
{ no: 2, name: "subType", kind: "scalar", T: ScalarType.INT32 /* int32 */ },
{ no: 203, name: "content", kind: "message", T: () => ProfileLikeSubTip }
]);
export const SysMessageHeader = new MessageType("SysMessageHeader", [
{ no: 1, name: "PeerNumber", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 2, name: "PeerString", kind: "scalar", T: ScalarType.STRING /* string */ },
{ no: 5, name: "Uin", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 6, name: "Uid", kind: "scalar", T: ScalarType.STRING /* string */, opt: true }
]);
export const SysMessageMsgSpec = new MessageType("SysMessageMsgSpec", [
{ no: 1, name: "msgType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 2, name: "subType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 3, name: "subSubType", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 5, name: "msgSeq", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 6, name: "time", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ },
{ no: 12, name: "msgId", kind: "scalar", T: ScalarType.UINT64 /* uint64 */ },
{ no: 13, name: "other", kind: "scalar", T: ScalarType.UINT32 /* uint32 */ }
]);
export const SysMessageBodyWrapper = new MessageType("SysMessageBodyWrapper", [
{ no: 2, name: "wrappedBody", kind: "scalar", T: ScalarType.BYTES /* bytes */ }
]);
export const SysMessage = new MessageType("SysMessage", [
{ no: 1, name: "header", kind: "message", T: () => SysMessageHeader, repeat: RepeatType.UNPACKED },
{ no: 2, name: "msgSpec", kind: "message", T: () => SysMessageMsgSpec, repeat: RepeatType.UNPACKED },
{ no: 3, name: "bodyWrapper", kind: "message", T: () => SysMessageBodyWrapper }
]);
export function decodeProfileLikeTip(buffer: Uint8Array): any {
const reader = new BinaryReader(buffer);
return ProfileLikeTip.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
}
export function decodeSysMessage(buffer: Uint8Array): any {
const reader = new BinaryReader(buffer);
return SysMessage.internalBinaryRead(reader, reader.len, { readUnknownField: true, readerFactory: () => new BinaryReader(buffer) });
}

View File

@@ -1,73 +0,0 @@
import { PacketHighwaySession } from "@/core/packet/highway/session";
import { LogWrapper } from "@/common/log";
import { PacketPacker } from "@/core/packet/packer";
import { PacketClient } from "@/core/packet/client/client";
import { NativePacketClient } from "@/core/packet/client/nativeClient";
import { wsPacketClient } from "@/core/packet/client/wsClient";
import { NapCatCore } from "@/core";
type clientPriority = {
[key: number]: (core: NapCatCore) => PacketClient;
}
const clientPriority: clientPriority = {
10: (core: NapCatCore) => new NativePacketClient(core),
1: (core: NapCatCore) => new wsPacketClient(core),
};
export class PacketSession {
readonly logger: LogWrapper;
readonly client: PacketClient ;
readonly packer: PacketPacker;
readonly highwaySession: PacketHighwaySession;
constructor(core: NapCatCore) {
this.logger = core.context.logger;
this.client = this.newClient(core);
this.packer = new PacketPacker(this.logger, this.client);
this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer);
}
private newClient(core: NapCatCore): PacketClient {
const prefer = core.configLoader.configData.packetBackend;
let client: PacketClient | null;
switch (prefer) {
case "native":
this.logger.log("[Core] [Packet] 使用指定的 NativePacketClient 作为后端");
client = new NativePacketClient(core);
break;
case "frida":
this.logger.log("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
client = new wsPacketClient(core);
break;
case "auto":
case undefined:
client = this.judgeClient(core);
break;
default:
this.logger.logError(`[Core] [Packet] 未知的Packet后端类型 ${prefer},请检查配置文件!`);
client = null;
}
if (!(client && client.check(core))) {
throw new Error("[Core] [Packet] 无可用的后端NapCat.Packet将不会加载");
}
return client;
}
private judgeClient(core: NapCatCore): PacketClient {
const sortedClients = Object.entries(clientPriority)
.map(([priority, clientFactory]) => {
const client = clientFactory(core);
const score = +priority * +client.check(core);
return { client, score };
})
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score);
const selectedClient = sortedClients[0]?.client;
if (!selectedClient) {
throw new Error("[Core] [Packet] 无可用的后端NapCat.Packet将不会加载");
}
this.logger.log(`[Core] [Packet] 自动选择 ${selectedClient.constructor.name} 作为后端`);
return selectedClient;
}
}

View File

@@ -0,0 +1,26 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
class FetchAiVoiceList extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0X929D_0Resp> {
constructor() {
super();
}
build(groupUin: number, chatType: AIVoiceChatType): OidbPacket {
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0X929D_0).encode({
groupUin: groupUin,
chatType: chatType
});
return OidbBase.build(0x929D, 0, data);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0X929D_0Resp).decode(oidbBody);
}
}
export default new FetchAiVoiceList();

View File

@@ -0,0 +1,31 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
class GetAiVoice extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0X929B_0Resp> {
constructor() {
super();
}
build(groupUin: number, voiceId: string, text: string, sessionId: number, chatType: AIVoiceChatType): OidbPacket {
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0X929B_0).encode({
groupUin: groupUin,
voiceId: voiceId,
text: text,
chatType: chatType,
session: {
sessionId: sessionId
}
});
return OidbBase.build(0x929B, 0, data);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0X929B_0Resp).decode(oidbBody);
}
}
export default new GetAiVoice();

View File

@@ -0,0 +1,53 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from "@/core/packet/transformer/base";
import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAdaptShareInfoResp> {
constructor() {
super();
}
build(req: MiniAppReqParams): OidbPacket {
const data = new NapProtoMsg(proto.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: ""
}
});
return {
cmd: "LightAppSvc.mini_app_share.AdaptShareInfo",
data: PacketHexStrBuilder(data)
};
}
parse(data: Buffer) {
return new NapProtoMsg(proto.MiniAppAdaptShareInfoResp).decode(data);
}
}
export default new GetMiniAppAdaptShareInfo();

View File

@@ -0,0 +1,25 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class GetStrangerInfo extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0XFE1_2RSP> {
constructor() {
super();
}
build(uin: number): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0XFE1_2).encode({
uin: uin,
key: [{ key: 27372 }]
});
return OidbBase.build(0XFE1, 2, body);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0XFE1_2RSP).decode(oidbBody);
}
}
export default new GetStrangerInfo();

View File

@@ -0,0 +1,29 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class GroupSign extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
constructor() {
super();
}
build(uin: number, groupCode: number): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0XEB7).encode(
{
body: {
uin: String(uin),
groupUin: String(groupCode),
version: "9.0.90"
}
}
);
return OidbBase.build(0XEB7, 1, body, false, false);
}
parse(data: Buffer) {
return OidbBase.parse(data);
}
}
export default new GroupSign();

View File

@@ -0,0 +1,26 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class SendPoke extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
constructor() {
super();
}
build(peer: number, group?: number): OidbPacket {
const data = new NapProtoMsg(proto.OidbSvcTrpcTcp0XED3_1).encode({
uin: peer,
groupUin: group,
friendUin: group ?? peer,
ext: 0
});
return OidbBase.build(0xED3, 1, data);
}
parse(data: Buffer) {
return OidbBase.parse(data);
}
}
export default new SendPoke();

View File

@@ -0,0 +1,30 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class SetSpecialTitle extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
constructor() {
super();
}
build(groupCode: number, uid: string, tittle: string): OidbPacket {
const oidb_0x8FC_2_body = new NapProtoMsg(proto.OidbSvcTrpcTcp0X8FC_2_Body).encode({
targetUid: uid,
specialTitle: tittle,
expiredTime: -1,
uinName: tittle
});
const oidb_0x8FC_2 = new NapProtoMsg(proto.OidbSvcTrpcTcp0X8FC_2).encode({
groupUin: +groupCode,
body: oidb_0x8FC_2_body
});
return OidbBase.build(0x8FC, 2, oidb_0x8FC_2, false, false);
}
parse(data: Buffer) {
return OidbBase.parse(data);
}
}
export default new SetSpecialTitle();

View File

@@ -0,0 +1,7 @@
export { default as FetchAiVoiceList } from './FetchAiVoiceList';
export { default as GetAiVoice } from './GetAiVoice';
export { default as GetMiniAppAdaptShareInfo } from './GetMiniAppAdaptShareInfo';
export { default as GroupSign } from './GroupSign';
export { default as GetStrangerInfo } from './GetStrangerInfo';
export { default as SendPoke } from './SendPoke';
export { default as SetSpecialTitle } from './SetSpecialTitle';

View File

@@ -0,0 +1,25 @@
import { NapProtoDecodeStructType } from "@napneko/nap-proto-core";
import { PacketMsgBuilder } from "@/core/packet/message/builder";
export type PacketHexStr = string & { readonly hexNya: unique symbol };
export const PacketHexStrBuilder = (str: Uint8Array): PacketHexStr => {
return Buffer.from(str).toString('hex') as PacketHexStr;
};
export interface OidbPacket {
cmd: string;
data: PacketHexStr
}
export abstract class PacketTransformer<T> {
protected msgBuilder: PacketMsgBuilder;
protected constructor() {
this.msgBuilder = new PacketMsgBuilder();
}
abstract build(...args: any[]): OidbPacket | Promise<OidbPacket>;
abstract parse(data: Buffer): NapProtoDecodeStructType<T>;
}

View File

@@ -0,0 +1,33 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class DownloadGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6Response> {
constructor() {
super();
}
build(groupUin: number, fileUUID: string): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({
download: {
groupUin: groupUin,
appId: 7,
busId: 102,
fileId: fileUUID
}
});
return OidbBase.build(0x6D6, 2, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
const res = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
if (res.download.retCode !== 0) {
throw new Error(`sendGroupFileDownloadReq error: ${res.download.clientWording} (code=${res.download.retCode})`);
}
return res;
}
}
export default new DownloadGroupFile();

View File

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

View File

@@ -0,0 +1,35 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class DownloadOfflineFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0XE37Response> {
constructor() {
super();
}
build(fileUUID: string, fileHash: string, senderUid: string, receiverUid: string): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37_800).encode({
subCommand: 800,
field2: 0,
body: {
senderUid: senderUid,
receiverUid: receiverUid,
fileUuid: fileUUID,
fileHash: fileHash,
},
field101: 3,
field102: 1,
field200: 1,
});
return OidbBase.build(0xE37, 800, body, false, false);
}
// TODO: check
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37Response).decode(oidbBody);
}
}
export default new DownloadOfflineFile();

View File

@@ -0,0 +1,36 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
class DownloadPrivateFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0XE37_1200Response> {
constructor() {
super();
}
build(selfUid: string, fileUUID: string, fileHash: string): OidbPacket {
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37_1200).encode({
subCommand: 1200,
field2: 1,
body: {
receiverUid: selfUid,
fileUuid: fileUUID,
type: 2,
fileHash: fileHash,
t2: 0
},
field101: 3,
field102: 103,
field200: 1,
field99999: Buffer.from([0xc0, 0x85, 0x2c, 0x01])
});
return OidbBase.build(0xE37, 1200, body, false, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37_1200Response).decode(oidbBody);
}
}
export default new DownloadPrivateFile();

View File

@@ -0,0 +1,37 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from "@/core/packet/transformer/base";
class FetchSessionKey extends PacketTransformer<typeof proto.HttpConn0x6ff_501Response> {
constructor() {
super();
}
build(): OidbPacket {
const req = new NapProtoMsg(proto.HttpConn0x6ff_501).encode({
httpConn: {
field1: 0,
field2: 0,
field3: 16,
field4: 1,
field6: 3,
serviceTypes: [1, 5, 10, 21],
// tgt: "", // TODO: do we really need tgt? seems not
field9: 2,
field10: 9,
field11: 8,
ver: "1.0.1"
}
});
return {
cmd: "HttpConn.0x6ff_501",
data: PacketHexStrBuilder(req)
};
}
parse(data: Buffer) {
return new NapProtoMsg(proto.HttpConn0x6ff_501Response).decode(data);
}
}
export default new FetchSessionKey();

View File

@@ -0,0 +1,38 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import { PacketMsgFileElement } from "@/core/packet/message/element";
class UploadGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6Response> {
constructor() {
super();
}
build(groupUin: number, file: PacketMsgFileElement): OidbPacket {
const body = new NapProtoMsg(proto.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 OidbBase.build(0x6D6, 0, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
}
}
export default new UploadGroupFile();

View File

@@ -0,0 +1,87 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import crypto from "node:crypto";
import { PacketMsgPicElement } from "@/core/packet/message/element";
class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(groupUin: number, img: PacketMsgPicElement): OidbPacket {
const data = new NapProtoMsg(proto.NTV2RichMediaReq).encode(
{
reqHead: {
common: {
requestId: 1,
command: 100
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 2,
group: {
groupUin: groupUin
},
},
client: {
agentType: 2
}
},
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: +img.size,
fileHash: img.md5,
fileSha1: img.sha1!,
fileName: img.name,
type: {
type: 1,
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0,
voiceFormat: 0,
},
width: img.width,
height: img.height,
time: 0,
original: 1
},
subFileType: 0,
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 2,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
},
video: {
bytesPbReserve: Buffer.alloc(0),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.alloc(0),
bytesGeneralFlags: Buffer.alloc(0),
}
},
clientSeq: 0,
noNeedCompatMsg: false,
}
}
);
return OidbBase.build(0x11C4, 100, data, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new UploadGroupImage();

View File

@@ -0,0 +1,84 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import crypto from "node:crypto";
import { PacketMsgPttElement } from "@/core/packet/message/element";
class UploadGroupPtt extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(groupUin: number, ptt: PacketMsgPttElement): OidbPacket {
const data = new NapProtoMsg(proto.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 OidbBase.build(0x126E, 100, data, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new UploadGroupPtt();

View File

@@ -0,0 +1,104 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import crypto from "node:crypto";
import { PacketMsgVideoElement } from "@/core/packet/message/element";
class UploadGroupVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(groupUin: number, video: PacketMsgVideoElement): OidbPacket {
if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
const data = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 3,
command: 100
},
scene: {
requestType: 2,
businessType: 2,
sceneType: 2,
group: {
groupUin: groupUin
},
},
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 OidbBase.build(0x11EA, 100, data, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new UploadGroupVideo();

View File

@@ -0,0 +1,41 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import { PacketMsgFileElement } from "@/core/packet/message/element";
import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
class UploadPrivateFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0XE37Response> {
constructor() {
super();
}
async build(selfUid: string, peerUid: string, file: PacketMsgFileElement): Promise<OidbPacket> {
const body = new NapProtoMsg(proto.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 OidbBase.build(0xE37, 1700, body, false, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37Response).decode(oidbBody);
}
}
export default new UploadPrivateFile();

View File

@@ -0,0 +1,87 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import crypto from "node:crypto";
import { PacketMsgPicElement } from "@/core/packet/message/element";
class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(peerUin: string, img: PacketMsgPicElement): OidbPacket {
const data = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 100
},
scene: {
requestType: 2,
businessType: 1,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: peerUin
},
},
client: {
agentType: 2,
}
},
upload: {
uploadInfo: [
{
fileInfo: {
fileSize: +img.size,
fileHash: img.md5,
fileSha1: img.sha1!,
fileName: img.name,
type: {
type: 1,
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0,
voiceFormat: 0,
},
width: img.width,
height: img.height,
time: 0,
original: 1
},
subFileType: 0,
}
],
tryFastUploadCompleted: true,
srvSendMsg: false,
clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'),
compatQMsgSceneType: 1,
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // TODO:
},
video: {
bytesPbReserve: Buffer.alloc(0),
},
ptt: {
bytesPbReserve: Buffer.alloc(0),
bytesReserve: Buffer.alloc(0),
bytesGeneralFlags: Buffer.alloc(0),
}
},
clientSeq: 0,
noNeedCompatMsg: false,
}
}
);
return OidbBase.build(0x11C5, 100, data,true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new UploadPrivateImage();

View File

@@ -0,0 +1,81 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import crypto from "node:crypto";
import { PacketMsgPttElement } from "@/core/packet/message/element";
class UploadPrivatePtt extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(peerUin: string, ptt: PacketMsgPttElement): OidbPacket {
const data = new NapProtoMsg(proto.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 OidbBase.build(0x126D, 100, data, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new UploadPrivatePtt();

View File

@@ -0,0 +1,105 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
import crypto from "node:crypto";
import { PacketMsgVideoElement } from "@/core/packet/message/element";
class UploadPrivateVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(peerUin: string, video: PacketMsgVideoElement): OidbPacket {
if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty");
const data = new NapProtoMsg(proto.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 OidbBase.build(0x11E9, 100, data, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new UploadPrivateVideo();

View File

@@ -0,0 +1,13 @@
export { default as DownloadGroupFile } from './DownloadGroupFile';
export { default as DownloadGroupPtt } from './DownloadGroupPtt';
export { default as DownloadOfflineFile } from './DownloadOfflineFile';
export { default as DownloadPrivateFile } from './DownloadPrivateFile';
export { default as FetchSessionKey } from './FetchSessionKey';
export { default as UploadGroupFile } from './UploadGroupFile';
export { default as UploadGroupImage } from './UploadGroupImage';
export { default as UploadGroupPtt } from './UploadGroupPtt';
export { default as UploadGroupVideo } from './UploadGroupVideo';
export { default as UploadPrivateFile } from './UploadPrivateFile';
export { default as UploadPrivateImage } from './UploadPrivateImage';
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
export { default as UploadPrivateVideo } from './UploadPrivateVideo';

View File

@@ -0,0 +1,4 @@
export * from './action';
export * from './highway';
export * from './message';
export * from './system';

View File

@@ -0,0 +1,51 @@
import zlib from "node:zlib";
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from "@/core/packet/transformer/base";
import { PacketMsg } from "@/core/packet/message/message";
class UploadForwardMsg extends PacketTransformer<typeof proto.SendLongMsgResp> {
constructor() {
super();
}
build(selfUid: string, msg: PacketMsg[], groupUin: number = 0): OidbPacket {
const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg);
const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode(
{
action: {
actionCommand: "MultiMsg",
actionData: {
msgBody: msgBody
}
}
}
);
const payload = zlib.gzipSync(Buffer.from(longMsgResultData));
const req = new NapProtoMsg(proto.SendLongMsgReq).encode(
{
info: {
type: groupUin === 0 ? 1 : 3,
uid: {
uid: groupUin === 0 ? selfUid : groupUin.toString(),
},
groupUin: groupUin,
payload: payload
},
settings: {
field1: 4, field2: 1, field3: 7, field4: 0
}
}
);
return {
cmd: "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg",
data: PacketHexStrBuilder(req)
};
}
parse(data: Buffer) {
return new NapProtoMsg(proto.SendLongMsgResp).decode(data);
}
}
export default new UploadForwardMsg();

View File

@@ -0,0 +1 @@
export { default as UploadForwardMsg } from './UploadForwardMsg';

View File

@@ -0,0 +1,32 @@
import * as proto from "@/core/packet/transformer/proto";
import { NapProtoMsg } from "@napneko/nap-proto-core";
import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from "@/core/packet/transformer/base";
class OidbBase extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase> {
constructor() {
super();
}
build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket {
const data = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).encode({
command: cmd,
subCommand: subCmd,
body: body,
isReserved: isUid ? 1 : 0
});
return {
cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`,
data: PacketHexStrBuilder(data),
};
}
parse(data: Buffer) {
const res = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).decode(data);
if (res.errorCode !== 0) {
throw new Error(`OidbSvcTrpcTcpBase parse error: ${res.errorMsg} (code=${res.errorCode})`);
}
return res;
}
}
export default new OidbBase();

View File

@@ -1,6 +1,6 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/proto/message/message";
import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/transformer/proto";
export const FaceRoamRequest = {
comm: ProtoField(1, () => PlatInfo, true),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const MiniAppAdaptShareInfoReq = {
appId: ProtoField(2, ScalarType.STRING),

View File

@@ -1,6 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { MsgInfo, MsgInfoBody } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
import { MsgInfoBody } from "@/core/packet/transformer/proto";
export const DataHighwayHead = {
version: ProtoField(1, ScalarType.UINT32),

View File

@@ -0,0 +1,31 @@
// action folder
export * from "./action/action";
export * from "./action/miniAppAdaptShareInfo";
// highway folder
export * from "./highway/highway";
// message folder
export * from "./message/action";
export * from "./message/c2c";
export * from "./message/component";
export * from "./message/element";
export * from "./message/group";
export * from "./message/message";
export * from "./message/notify";
export * from "./message/routing";
// oidb folder
export * from "./oidb/common/Ntv2.RichMediaReq";
export * from "./oidb/common/Ntv2.RichMediaResp";
export * from "./oidb/Oidb.0x6D6";
export * from "./oidb/Oidb.0x8FC_2";
export * from "./oidb/Oidb.0x9067_202";
export * from "./oidb/Oidb.0x929";
export * from "./oidb/Oidb.0xE37_1200";
export * from "./oidb/Oidb.0xE37_1700";
export * from "./oidb/Oidb.0XE37_800";
export * from "./oidb/Oidb.0xEB7";
export * from "./oidb/Oidb.0xED3_1";
export * from "./oidb/Oidb.0XFE1_2";
export * from "./oidb/OidbBase";

View File

@@ -1,6 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { PushMsgBody } from "@/core/packet/proto/message/message";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
import { PushMsgBody } from "@/core/packet/transformer/proto";
export const LongMsgResult = {
action: ProtoField(2, () => LongMsgAction)

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const C2C = {
uin: ProtoField(1, ScalarType.UINT32, true),

View File

@@ -1,6 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { Elem } from "@/core/packet/proto/message/element";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
import { Elem } from "@/core/packet/transformer/proto";
export const Attr = {
codePage: ProtoField(1, ScalarType.INT32),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const Elem = {
text: ProtoField(1, () => Text, true),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const GroupRecallMsg = {
type: ProtoField(1, ScalarType.UINT32),

View File

@@ -1,8 +1,14 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ForwardHead, Grp, GrpTmp, ResponseForward, ResponseGrp, Trans0X211, WPATmp } from "@/core/packet/proto/message/routing";
import { RichText } from "@/core/packet/proto/message/component";
import { C2C } from "@/core/packet/proto/message/c2c";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
import {
C2C,
ForwardHead,
Grp,
GrpTmp,
ResponseForward,
ResponseGrp, RichText,
Trans0X211,
WPATmp
} from "@/core/packet/transformer/proto";
export const ContentHead = {
type: ProtoField(1, ScalarType.UINT32),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const FriendRecall = {
info: ProtoField(1, () => FriendRecallInfo),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const ForwardHead = {
field1: ProtoField(1, ScalarType.UINT32, true),

View File

@@ -1,6 +1,5 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/transformer/proto";
export const OidbSvcTrpcTcp0XE37_800 = {
subCommand: ProtoField(1, ScalarType.UINT32),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0XFE1_2 = {
uin: ProtoField(1, ScalarType.UINT32),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0x6D6 = {
file: ProtoField(1, () => OidbSvcTrpcTcp0x6D6Upload, true),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
//设置群头衔 OidbSvcTrpcTcp.0x8fc_2

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
import { MultiMediaReqHead } from "./common/Ntv2.RichMediaReq";
//Req

View File

@@ -1,6 +1,6 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
import { MsgInfo } from "@/core/packet/transformer/proto";
export const OidbSvcTrpcTcp0X929D_0 = {
groupUin: ProtoField(1, ScalarType.UINT32),

View File

@@ -1,5 +1,4 @@
import { ScalarType } from "@protobuf-ts/runtime";
import { ProtoField } from "@napneko/nap-proto-core";
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
export const OidbSvcTrpcTcp0XE37_1200 = {
subCommand: ProtoField(1, ScalarType.UINT32, true),

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