mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
108 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
89f7892681 | ||
![]() |
aad5ed55d2 | ||
![]() |
86da417c17 | ||
![]() |
96b1f71437 | ||
![]() |
5e0b3b2f35 | ||
![]() |
6829fad5bd | ||
![]() |
7af0d9e87b | ||
![]() |
c089ebea99 | ||
![]() |
d2a2c1c39c | ||
![]() |
ce9b09e8d1 | ||
![]() |
2f6dfe51f5 | ||
![]() |
bd227cd0b8 | ||
![]() |
96003724ab | ||
![]() |
6a08b15095 | ||
![]() |
dab0f9ab45 | ||
![]() |
e733a6b69a | ||
![]() |
9aca98bf13 | ||
![]() |
b7c95e53dc | ||
![]() |
f762c450ca | ||
![]() |
d58bbe53da | ||
![]() |
f32edd8af7 | ||
![]() |
c747a86e5b | ||
![]() |
abfda0dd58 | ||
![]() |
f66d7b11a8 | ||
![]() |
f425c9478e | ||
![]() |
756dea71fc | ||
![]() |
71a6c4ccc5 | ||
![]() |
ae2f4777ec | ||
![]() |
dcd9b8168a | ||
![]() |
4bb03ae5ba | ||
![]() |
8bd6f8397b | ||
![]() |
096e52d93e | ||
![]() |
037065291d | ||
![]() |
4cf52e1b13 | ||
![]() |
21b228552d | ||
![]() |
76b404cdd8 | ||
![]() |
937c594ff7 | ||
![]() |
b463140de7 | ||
![]() |
f518fb9214 | ||
![]() |
1092831718 | ||
![]() |
6b377416da | ||
![]() |
8f5baa47ec | ||
![]() |
5494ff0553 | ||
![]() |
7a4805b464 | ||
![]() |
8435375810 | ||
![]() |
c893ec6030 | ||
![]() |
e8bf6fa0a6 | ||
![]() |
f228129c19 | ||
![]() |
cbf98ffb89 | ||
![]() |
f6067b002f | ||
![]() |
636d1103e3 | ||
![]() |
bede517f7e | ||
![]() |
16e4891b7d | ||
![]() |
3bcd79fbb7 | ||
![]() |
aacf6c2917 | ||
![]() |
92d720cd57 | ||
![]() |
2ea025047f | ||
![]() |
f7f7e09cab | ||
![]() |
75866b435e | ||
![]() |
f07941685b | ||
![]() |
60a0539216 | ||
![]() |
3dd4b6549f | ||
![]() |
0802c35dc1 | ||
![]() |
7d9d7226ec | ||
![]() |
b5ef6ce6b0 | ||
![]() |
49ec6181b0 | ||
![]() |
783a534768 | ||
![]() |
704ac11cbb | ||
![]() |
aa9663d85e | ||
![]() |
05291f34fb | ||
![]() |
2260fe32a1 | ||
![]() |
2c398a6832 | ||
![]() |
3e1f566699 | ||
![]() |
4f89f184b8 | ||
![]() |
787685c937 | ||
![]() |
ed9cd2fe38 | ||
![]() |
740d80e851 | ||
![]() |
4520a20bd4 | ||
![]() |
98c65c4923 | ||
![]() |
e287906a9d | ||
![]() |
8bae789020 | ||
![]() |
ce57b7b725 | ||
![]() |
1d9872195d | ||
![]() |
98d1f8e29f | ||
![]() |
221b3fb730 | ||
![]() |
90a834495a | ||
![]() |
8bfd102232 | ||
![]() |
65e784f169 | ||
![]() |
0fc81c672f | ||
![]() |
62ae0f4321 | ||
![]() |
a01a0a1a18 | ||
![]() |
4c30cc69ad | ||
![]() |
1d43b75df4 | ||
![]() |
d02afdfc3e | ||
![]() |
5d6dee9fd0 | ||
![]() |
60c67ef41c | ||
![]() |
917d7c1f19 | ||
![]() |
ad19f2c99e | ||
![]() |
8a61f5a03f | ||
![]() |
8c164910f6 | ||
![]() |
a560d3d266 | ||
![]() |
532f739272 | ||
![]() |
a120727f2d | ||
![]() |
a9bcb830a8 | ||
![]() |
56e5f0033f | ||
![]() |
101106996a | ||
![]() |
41a81534dc | ||
![]() |
1425e8f229 |
@@ -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'],
|
||||
}
|
||||
};
|
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -1,5 +1,8 @@
|
||||
name: "Build Action"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: write-all
|
||||
|
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
38
README.md
38
README.md
@@ -5,20 +5,16 @@
|
||||
</div>
|
||||
|
||||
---
|
||||
## 欢迎回来
|
||||
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
## 欢迎回家
|
||||
NapCatQQ 是现代化的基于 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 可实现发包与收包
|
||||
## 碎碎叨叨
|
||||
- [x] **安装简单**:就算是笨蛋也能使用
|
||||
- [x] **性能友好**:就算是低内存也能使用
|
||||
- [x] **接口丰富**:就算是没有也能使用
|
||||
- [x] **稳定好用**:就算是被捉也能使用
|
||||
|
||||
## 使用猫猫
|
||||
## 使用框架
|
||||
|
||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
||||
|
||||
@@ -32,23 +28,23 @@ 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)
|
||||
|
||||
[Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl)
|
||||
## 感谢他们
|
||||
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
|
||||
|
||||
## 猫猫朋友
|
||||
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
|
||||
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||
|
||||
不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||
|
||||
---
|
||||
|
||||
## 约法三章
|
||||
> [!CAUTION]\
|
||||
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本项目存在相关性的信息**
|
||||
## 开源附加
|
||||
|
||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经授权二次分发或基于 NapCat 代码开发。**
|
||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
|
||||
|
70
eslint.config.mjs
Normal file
70
eslint.config.mjs
Normal 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",
|
||||
},
|
||||
}];
|
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
BIN
external/packet/napcat.packet.arm64
vendored
BIN
external/packet/napcat.packet.arm64
vendored
Binary file not shown.
BIN
external/packet/napcat.packet.exe
vendored
BIN
external/packet/napcat.packet.exe
vendored
Binary file not shown.
BIN
external/packet/napcat.packet.linux
vendored
BIN
external/packet/napcat.packet.linux
vendored
Binary file not shown.
102
external/packet/napcat.packet.production.py
vendored
102
external/packet/napcat.packet.production.py
vendored
File diff suppressed because one or more lines are too long
32
launcher/launcher-user.bat
Normal file
32
launcher/launcher-user.bat
Normal 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
|
33
launcher/launcher-win10-user.bat
Normal file
33
launcher/launcher-win10-user.bat
Normal 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
|
@@ -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",
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "3.6.3",
|
||||
"version": "4.0.2",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
25
package.json
25
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "3.6.3",
|
||||
"version": "4.0.2",
|
||||
"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,27 +29,28 @@
|
||||
"@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"
|
||||
"ws": "^8.18.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"fluent-ffmpeg": "^2.1.2"
|
||||
}
|
||||
}
|
||||
}
|
@@ -21,9 +21,9 @@ type FuncKeys<T> = Extract<
|
||||
export type ListenerClassBase = Record<string, string>;
|
||||
|
||||
export class NTEventWrapper {
|
||||
private WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
|
||||
private listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
|
||||
private EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
|
||||
private readonly WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
|
||||
private readonly listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
|
||||
private readonly EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
|
||||
|
||||
constructor(
|
||||
wrapperSession: NodeIQQNTWrapperSession,
|
||||
@@ -120,9 +120,9 @@ export class NTEventWrapper {
|
||||
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
|
||||
>(
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
checker: (...args: Parameters<ListenerType>) => boolean,
|
||||
waitTimes = 1,
|
||||
timeout = 5000,
|
||||
checker: (...args: Parameters<ListenerType>) => boolean,
|
||||
) {
|
||||
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
@@ -181,36 +181,36 @@ export class NTEventWrapper {
|
||||
callbackTimesToWait = 1,
|
||||
timeout = 5000,
|
||||
) {
|
||||
const id = randomUUID();
|
||||
let complete = 0;
|
||||
let retData: Parameters<ListenerType> | undefined = undefined;
|
||||
let retEvent: any = {};
|
||||
|
||||
function sendDataCallback(resolve: any, reject: any) {
|
||||
if (complete == 0) {
|
||||
reject(
|
||||
new Error(
|
||||
'Timeout: NTEvent serviceAndMethod:' +
|
||||
serviceAndMethod +
|
||||
' ListenerName:' +
|
||||
listenerAndMethod +
|
||||
' EventRet:\n' +
|
||||
JSON.stringify(retEvent, null, 4) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
|
||||
}
|
||||
}
|
||||
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
const ListenerSubName = ListenerNameList[1];
|
||||
|
||||
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
|
||||
async (resolve, reject) => {
|
||||
const id = randomUUID();
|
||||
let complete = 0;
|
||||
let retData: Parameters<ListenerType> | undefined = undefined;
|
||||
let retEvent: any = {};
|
||||
|
||||
function sendDataCallback() {
|
||||
if (complete == 0) {
|
||||
reject(
|
||||
new Error(
|
||||
'Timeout: NTEvent serviceAndMethod:' +
|
||||
serviceAndMethod +
|
||||
' ListenerName:' +
|
||||
listenerAndMethod +
|
||||
' EventRet:\n' +
|
||||
JSON.stringify(retEvent, null, 4) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
|
||||
}
|
||||
}
|
||||
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
const ListenerSubName = ListenerNameList[1];
|
||||
|
||||
const timeoutRef = setTimeout(sendDataCallback, timeout);
|
||||
(resolve, reject) => {
|
||||
const timeoutRef = setTimeout(() => sendDataCallback(resolve, reject), timeout);
|
||||
|
||||
const eventCallback = {
|
||||
timeout: timeout,
|
||||
@@ -221,7 +221,7 @@ export class NTEventWrapper {
|
||||
retData = args as Parameters<ListenerType>;
|
||||
if (complete >= callbackTimesToWait) {
|
||||
clearTimeout(timeoutRef);
|
||||
sendDataCallback();
|
||||
sendDataCallback(resolve, reject);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -233,23 +233,26 @@ export class NTEventWrapper {
|
||||
}
|
||||
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
|
||||
this.createListenerFunction(ListenerMainName);
|
||||
const eventFunction = this.createEventFunction(serviceAndMethod);
|
||||
retEvent = await eventFunction!(...(args));
|
||||
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
|
||||
clearTimeout(timeoutRef);
|
||||
reject(
|
||||
new Error(
|
||||
'EventChecker Failed: NTEvent serviceAndMethod:' +
|
||||
serviceAndMethod +
|
||||
' ListenerName:' +
|
||||
listenerAndMethod +
|
||||
' EventRet:\n' +
|
||||
JSON.stringify(retEvent, null, 4) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.createEventFunction(serviceAndMethod)!(...(args))
|
||||
.then((eventResult: any) => {
|
||||
retEvent = eventResult;
|
||||
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
|
||||
clearTimeout(timeoutRef);
|
||||
reject(
|
||||
new Error(
|
||||
'EventChecker Failed: NTEvent serviceAndMethod:' +
|
||||
serviceAndMethod +
|
||||
' ListenerName:' +
|
||||
listenerAndMethod +
|
||||
' EventRet:\n' +
|
||||
JSON.stringify(retEvent, null, 4) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -54,11 +54,7 @@ export class ForwardMsgBuilder {
|
||||
const id = crypto.randomUUID();
|
||||
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
||||
if (!source) {
|
||||
source = isGroupMsg ? "群聊的聊天记录" :
|
||||
msg.length
|
||||
? Array.from(new Set(msg.slice(0, 4).map(m => m.senderName)))
|
||||
.join('和') + '的聊天记录'
|
||||
: '聊天记录';
|
||||
source = isGroupMsg ? "群聊的聊天记录" : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录';
|
||||
}
|
||||
if (!news) {
|
||||
news = msg.length === 0 ? [{
|
||||
@@ -111,7 +107,7 @@ export class ForwardMsgBuilder {
|
||||
senderName: msg.senderName,
|
||||
isGroupMsg: msg.groupId !== undefined,
|
||||
msg: msg.msg.map(m => ({
|
||||
preview: m.valid? m.toPreview() : "[该消息类型暂不支持查看]",
|
||||
preview: m.valid ? m.toPreview() : "[该消息类型暂不支持查看]",
|
||||
}))
|
||||
})), source, news, summary, prompt);
|
||||
}
|
||||
|
@@ -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,137 @@ 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);
|
||||
this.deleteOldLogFile(filePath, oneWeekAgo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
|
||||
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: ${filePath}`);
|
||||
} else {
|
||||
this.logger.error('Failed to delete old log file', err);
|
||||
}
|
||||
} else {
|
||||
this.logger.info(`Deleted old log file: ${filePath}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 +180,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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,86 +202,93 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
|
||||
}
|
||||
if (msg.senderUin !== '0') {
|
||||
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
|
||||
tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`);
|
||||
}
|
||||
} 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) {
|
||||
const originalContentLines = element.textElement.content.split('\n');
|
||||
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
||||
} else if (element.textElement.atType === AtType.atAll) {
|
||||
return `@全体成员`;
|
||||
} else if (element.textElement.atType === AtType.atUser) {
|
||||
return `${element.textElement.content} (${element.textElement.atUid})`;
|
||||
}
|
||||
}
|
||||
|
||||
if (element.replyElement) {
|
||||
const recordMsgOrNull = msg.records.find(
|
||||
record => element.replyElement!.sourceMsgIdInRecords === record.msgId,
|
||||
);
|
||||
return `[回复消息 ${recordMsgOrNull &&
|
||||
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到
|
||||
?
|
||||
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
||||
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`
|
||||
}]`;
|
||||
}
|
||||
|
||||
if (element.picElement) {
|
||||
return '[图片]';
|
||||
}
|
||||
|
||||
if (element.fileElement) {
|
||||
return `[文件 ${element.fileElement.fileName}]`;
|
||||
}
|
||||
|
||||
if (element.videoElement) {
|
||||
return '[视频]';
|
||||
}
|
||||
|
||||
if (element.pttElement) {
|
||||
return `[语音 ${element.pttElement.duration}s]`;
|
||||
}
|
||||
|
||||
if (element.arkElement) {
|
||||
return '[卡片消息]';
|
||||
}
|
||||
|
||||
if (element.faceElement) {
|
||||
return `[表情 ${element.faceElement.faceText ?? ''}]`;
|
||||
}
|
||||
|
||||
if (element.marketFaceElement) {
|
||||
return element.marketFaceElement.faceName;
|
||||
}
|
||||
|
||||
if (element.markdownElement) {
|
||||
return '[Markdown 消息]';
|
||||
}
|
||||
|
||||
if (element.multiForwardMsgElement) {
|
||||
return '[转发消息]';
|
||||
}
|
||||
|
||||
if (element.elementType === ElementType.GreyTip) {
|
||||
return '[灰条消息]';
|
||||
}
|
||||
|
||||
return `[未实现 (ElementType = ${element.elementType})]`;
|
||||
}
|
||||
|
||||
for (const element of msg.elements) {
|
||||
tokens.push(msgElementToText(element));
|
||||
tokens.push(msgElementToText(element, msg, recursiveLevel));
|
||||
}
|
||||
|
||||
return tokens.join(' ');
|
||||
}
|
||||
|
||||
function msgElementToText(element: MessageElement, msg: RawMessage, recursiveLevel: number): string {
|
||||
if (element.textElement) {
|
||||
return textElementToText(element.textElement);
|
||||
}
|
||||
|
||||
if (element.replyElement) {
|
||||
return replyElementToText(element.replyElement, msg, recursiveLevel);
|
||||
}
|
||||
|
||||
if (element.picElement) {
|
||||
return '[图片]';
|
||||
}
|
||||
|
||||
if (element.fileElement) {
|
||||
return `[文件 ${element.fileElement.fileName}]`;
|
||||
}
|
||||
|
||||
if (element.videoElement) {
|
||||
return '[视频]';
|
||||
}
|
||||
|
||||
if (element.pttElement) {
|
||||
return `[语音 ${element.pttElement.duration}s]`;
|
||||
}
|
||||
|
||||
if (element.arkElement) {
|
||||
return '[卡片消息]';
|
||||
}
|
||||
|
||||
if (element.faceElement) {
|
||||
return `[表情 ${element.faceElement.faceText ?? ''}]`;
|
||||
}
|
||||
|
||||
if (element.marketFaceElement) {
|
||||
return element.marketFaceElement.faceName;
|
||||
}
|
||||
|
||||
if (element.markdownElement) {
|
||||
return '[Markdown 消息]';
|
||||
}
|
||||
|
||||
if (element.multiForwardMsgElement) {
|
||||
return '[转发消息]';
|
||||
}
|
||||
|
||||
if (element.elementType === ElementType.GreyTip) {
|
||||
return '[灰条消息]';
|
||||
}
|
||||
|
||||
return `[未实现 (ElementType = ${element.elementType})]`;
|
||||
}
|
||||
|
||||
function textElementToText(textElement: any): string {
|
||||
if (textElement.atType === AtType.notAt) {
|
||||
const originalContentLines = textElement.content.split('\n');
|
||||
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
||||
} else if (textElement.atType === AtType.atAll) {
|
||||
return `@全体成员`;
|
||||
} else if (textElement.atType === AtType.atUser) {
|
||||
return `${textElement.content} (${textElement.atUid})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
|
||||
const recordMsgOrNull = msg.records.find(
|
||||
record => replyElement.sourceMsgIdInRecords === record.msgId,
|
||||
);
|
||||
return `[回复消息 ${recordMsgOrNull &&
|
||||
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
|
||||
?
|
||||
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
||||
`未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
|
||||
}]`;
|
||||
}
|
@@ -30,4 +30,13 @@ export class LRUCache<K, V> {
|
||||
}
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
public resetCapacity(newCapacity: number): void {
|
||||
this.capacity = newCapacity;
|
||||
while (this.cache.size > this.capacity) {
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,8 +2,8 @@ import { Peer } from '@/core';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export class LimitedHashTable<K, V> {
|
||||
private keyToValue: Map<K, V> = new Map();
|
||||
private valueToKey: Map<V, K> = new Map();
|
||||
private readonly keyToValue: Map<K, V> = new Map();
|
||||
private readonly valueToKey: Map<V, K> = new Map();
|
||||
private maxSize: number;
|
||||
|
||||
constructor(maxSize: number) {
|
||||
@@ -75,8 +75,8 @@ export class LimitedHashTable<K, V> {
|
||||
}
|
||||
|
||||
class MessageUniqueWrapper {
|
||||
private msgDataMap: LimitedHashTable<string, number>;
|
||||
private msgIdMap: LimitedHashTable<string, number>;
|
||||
private readonly msgDataMap: LimitedHashTable<string, number>;
|
||||
private readonly msgIdMap: LimitedHashTable<string, number>;
|
||||
|
||||
constructor(maxMap: number = 1000) {
|
||||
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
|
||||
|
@@ -8,49 +8,48 @@ export class RequestUtil {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = client.get(url, (res) => {
|
||||
let cookies: { [key: string]: string } = {};
|
||||
const handleRedirect = (res: http.IncomingMessage) => {
|
||||
//console.log(res.headers.location);
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
if (res.headers.location) {
|
||||
const redirectUrl = new URL(res.headers.location, url);
|
||||
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
|
||||
// 合并重定向过程中的cookies
|
||||
cookies = { ...cookies, ...redirectCookies };
|
||||
resolve(cookies);
|
||||
}).catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
resolve(cookies);
|
||||
}
|
||||
} else {
|
||||
resolve(cookies);
|
||||
}
|
||||
};
|
||||
res.on('data', () => {
|
||||
}); // Necessary to consume the stream
|
||||
const cookies: { [key: string]: string } = {};
|
||||
|
||||
res.on('data', () => { }); // Necessary to consume the stream
|
||||
res.on('end', () => {
|
||||
handleRedirect(res);
|
||||
this.handleRedirect(res, url, cookies)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
if (res.headers['set-cookie']) {
|
||||
//console.log(res.headers['set-cookie']);
|
||||
res.headers['set-cookie'].forEach((cookie) => {
|
||||
const parts = cookie.split(';')[0].split('=');
|
||||
const key = parts[0];
|
||||
const value = parts[1];
|
||||
if (key && value && key.length > 0 && value.length > 0) {
|
||||
cookies[key] = value;
|
||||
}
|
||||
});
|
||||
this.extractCookies(res.headers['set-cookie'], cookies);
|
||||
}
|
||||
});
|
||||
req.on('error', (error: any) => {
|
||||
|
||||
req.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static async handleRedirect(res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
if (res.headers.location) {
|
||||
const redirectUrl = new URL(res.headers.location, url);
|
||||
const redirectCookies = await this.HttpsGetCookies(redirectUrl.href);
|
||||
// 合并重定向过程中的cookies
|
||||
return { ...cookies, ...redirectCookies };
|
||||
}
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) {
|
||||
setCookieHeaders.forEach((cookie) => {
|
||||
const parts = cookie.split(';')[0].split('=');
|
||||
const key = parts[0];
|
||||
const value = parts[1];
|
||||
if (key && value && key.length > 0 && value.length > 0) {
|
||||
cookies[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 请求和回复都是JSON data传原始内容 自动编码json
|
||||
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
|
||||
@@ -88,13 +87,13 @@ export class RequestUtil {
|
||||
} else {
|
||||
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
||||
}
|
||||
} catch (parseError) {
|
||||
reject(parseError);
|
||||
} catch (parseError: unknown) {
|
||||
reject(new Error((parseError as Error).message));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error: any) => {
|
||||
req.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
||||
@@ -133,62 +132,4 @@ export class RequestUtil {
|
||||
Buffer.from(footer, 'utf8'),
|
||||
]);
|
||||
}
|
||||
|
||||
static async uploadImageForOpenPlatform(filePath: string, cookies: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
type retType = { retcode: number, result?: { url: string } };
|
||||
try {
|
||||
const options = {
|
||||
hostname: 'cgi.connect.qq.com',
|
||||
port: 443,
|
||||
path: '/qqconnectopen/upload_share_image',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Referer': 'https://cgi.connect.qq.com',
|
||||
'Cookie': cookies,
|
||||
'Accept': '*/*',
|
||||
'Connection': 'keep-alive',
|
||||
'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
|
||||
},
|
||||
};
|
||||
const req = https.request(options, async (res) => {
|
||||
let responseBody = '';
|
||||
|
||||
res.on('data', (chunk: string | Buffer) => {
|
||||
responseBody += chunk.toString();
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
const responseJson = JSON.parse(responseBody) as retType;
|
||||
resolve(responseJson.result!.url!);
|
||||
} else {
|
||||
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
||||
}
|
||||
} catch (parseError) {
|
||||
reject(parseError);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
console.log('Error during upload:', error);
|
||||
});
|
||||
|
||||
const body = await RequestUtil.createFormData('WebKitFormBoundary7MA4YWxkTrZu0gW', filePath);
|
||||
// req.setHeader('Content-Length', Buffer.byteLength(body));
|
||||
// console.log(`Prepared data size: ${Buffer.byteLength(body)} bytes`);
|
||||
req.write(body);
|
||||
req.end();
|
||||
return;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '3.6.3';
|
||||
export const napCatVersion = '4.0.2';
|
||||
|
@@ -20,7 +20,7 @@ export async function getVideoInfo(filePath: string, logger: LogWrapper) {
|
||||
ffmpeg.setFfmpegPath(ffmpegPath);
|
||||
ffmpeg(filePath).ffprobe((err: any, metadata: ffmpeg.FfprobeData) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
reject(new Error('无法获取视频信息。'));
|
||||
} else {
|
||||
const videoStream = metadata.streams.find((s: FfprobeStream) => s.codec_type === 'video');
|
||||
if (videoStream) {
|
||||
|
@@ -6,8 +6,10 @@ export class NodeIDependsAdapter {
|
||||
}
|
||||
|
||||
onMSFSsoError(args: unknown) {
|
||||
|
||||
}
|
||||
|
||||
getGroupCode(args: unknown) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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({
|
||||
@@ -353,15 +357,13 @@ export class NTQQFileApi {
|
||||
|
||||
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
imageSize(filePath, (err, dimensions) => {
|
||||
imageSize(filePath, (err: Error | null, dimensions) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
reject(new Error(err.message));
|
||||
} else if (!dimensions) {
|
||||
reject(new Error('获取图片尺寸失败'));
|
||||
} else {
|
||||
if (!dimensions) {
|
||||
reject(new Error('获取图片尺寸失败'));
|
||||
} else {
|
||||
resolve(dimensions);
|
||||
}
|
||||
resolve(dimensions);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -404,70 +406,87 @@ export class NTQQFileApi {
|
||||
return fileData.filePath!;
|
||||
}
|
||||
|
||||
async getImageUrl(element: PicElement) {
|
||||
async getImageUrl(element: PicElement): Promise<string> {
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const url: string = element.originImageUrl ?? '';
|
||||
const md5HexStr = element.md5HexStr;
|
||||
const fileMd5 = element.md5HexStr;
|
||||
|
||||
|
||||
if (url) {
|
||||
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
|
||||
const urlRkey = parsedUrl.searchParams.get('rkey');
|
||||
const imageAppid = parsedUrl.searchParams.get('appid');
|
||||
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||
const imageFileId = parsedUrl.searchParams.get('fileid');
|
||||
|
||||
const rkeyData = {
|
||||
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
||||
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
||||
online_rkey: false
|
||||
};
|
||||
try {
|
||||
if (this.core.apis.PacketApi.available) {
|
||||
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();
|
||||
}
|
||||
if (this.packetRkey && this.packetRkey.length > 0) {
|
||||
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
|
||||
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
|
||||
rkeyData.online_rkey = true;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
|
||||
}
|
||||
|
||||
if (!rkeyData.online_rkey) {
|
||||
try {
|
||||
const tempRkeyData = await this.rkeyManager.getRkey();
|
||||
rkeyData.group_rkey = tempRkeyData.group_rkey;
|
||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
||||
}
|
||||
}
|
||||
if (isNTV2 && urlRkey) {
|
||||
return IMAGE_HTTP_HOST_NT + urlRkey;
|
||||
} else if (isNTV2 && rkeyData.online_rkey) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST_NT + url + `&rkey=${rkey}`;
|
||||
} else if (isNTV2 && imageFileId) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
}
|
||||
|
||||
const rkeyData = await this.getRkeyData();
|
||||
return this.getImageUrlFromParsedUrl(parsedUrl, rkeyData);
|
||||
}
|
||||
//到这里说明可能是旧客户端
|
||||
|
||||
return this.getImageUrlFromMd5(fileMd5, md5HexStr);
|
||||
}
|
||||
|
||||
private async getRkeyData() {
|
||||
const rkeyData = {
|
||||
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
||||
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
||||
online_rkey: false
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.core.apis.PacketApi.available) {
|
||||
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.pkt.operation.FetchRkey();
|
||||
}
|
||||
if (this.packetRkey && this.packetRkey.length > 0) {
|
||||
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
|
||||
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
|
||||
rkeyData.online_rkey = true;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
|
||||
}
|
||||
|
||||
if (!rkeyData.online_rkey) {
|
||||
try {
|
||||
const tempRkeyData = await this.rkeyManager.getRkey();
|
||||
rkeyData.group_rkey = tempRkeyData.group_rkey;
|
||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
||||
}
|
||||
}
|
||||
|
||||
return rkeyData;
|
||||
}
|
||||
|
||||
private getImageUrlFromParsedUrl(parsedUrl: URL, rkeyData: any): string {
|
||||
const urlRkey = parsedUrl.searchParams.get('rkey');
|
||||
const imageAppid = parsedUrl.searchParams.get('appid');
|
||||
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||
const imageFileId = parsedUrl.searchParams.get('fileid');
|
||||
|
||||
if (isNTV2 && urlRkey) {
|
||||
return IMAGE_HTTP_HOST_NT + urlRkey;
|
||||
} else if (isNTV2 && rkeyData.online_rkey) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST_NT + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
} else if (isNTV2 && imageFileId) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private getImageUrlFromMd5(fileMd5: string | undefined, md5HexStr: string | undefined): string {
|
||||
if (fileMd5 || md5HexStr) {
|
||||
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
|
||||
}
|
||||
|
||||
this.context.logger.logDebug('图片url获取失败', element);
|
||||
|
||||
this.context.logger.logDebug('图片url获取失败', { fileMd5, md5HexStr });
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ export class NTQQFriendApi {
|
||||
}
|
||||
async getBuddyV2SimpleInfoMap(refresh = false) {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
||||
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
||||
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
@@ -41,14 +41,10 @@ export class NTQQFriendApi {
|
||||
tempBothDel: tempBothDel
|
||||
});
|
||||
}
|
||||
async getBuddyV2ExWithCate(refresh = false) {
|
||||
const categoryMap: Map<string, any> = new Map();
|
||||
async getBuddyV2ExWithCate() {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
const buddyListV2 = refresh ? (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data : (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
|
||||
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
|
||||
const uids = buddyListV2.flatMap(item => {
|
||||
item.buddyUids.forEach(uid => {
|
||||
categoryMap.set(uid, { categoryId: item.categoryId, categoryName: item.categroyName });
|
||||
});
|
||||
return item.buddyUids;
|
||||
});
|
||||
const data = await this.core.eventWrapper.callNoListenerEvent(
|
||||
|
@@ -25,9 +25,10 @@ export class NTQQGroupApi {
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.initCache().then().catch(context.logger.logError.bind(context.logger));
|
||||
}
|
||||
|
||||
async initApi() {
|
||||
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||
}
|
||||
async initCache() {
|
||||
this.groups = await this.getGroups();
|
||||
for (const group of this.groups) {
|
||||
@@ -54,7 +55,7 @@ export class NTQQGroupApi {
|
||||
}, pskey);
|
||||
}
|
||||
async getGroupShutUpMemberList(groupCode: string) {
|
||||
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', 1, 1000, (group_id) => group_id === groupCode);
|
||||
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
|
||||
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||
return (await data)[1];
|
||||
}
|
||||
@@ -258,9 +259,9 @@ export class NTQQGroupApi {
|
||||
async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
|
||||
const Listener = this.core.eventWrapper.registerListen(
|
||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||
(params, _, members) => params === GroupCode && members.size > 0,
|
||||
1,
|
||||
forced ? 5000 : 250,
|
||||
(params, _, members) => params === GroupCode && members.size > 0,
|
||||
);
|
||||
const retData = await (
|
||||
this.core.eventWrapper
|
||||
@@ -318,13 +319,13 @@ export class NTQQGroupApi {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async tryGetGroupMembersV2(modeListener = false, groupQQ: string, num = 30, timeout = 100): Promise<{
|
||||
async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{
|
||||
infos: Map<string, GroupMember>;
|
||||
finish: boolean;
|
||||
hasNext: boolean | undefined;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 0, timeout, (params) => params.sceneId === sceneId)
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
@@ -352,7 +353,7 @@ export class NTQQGroupApi {
|
||||
listenerMode: boolean;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 0, timeout, (params) => params.sceneId === sceneId)
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
@@ -371,12 +372,14 @@ export class NTQQGroupApi {
|
||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||
finish: result.result.finish,
|
||||
hasNext: resMode2?.hasNext,
|
||||
listenerMode: resMode2?.hasNext !== undefined ? true : false
|
||||
listenerMode: resMode2?.hasNext !== undefined
|
||||
};
|
||||
}
|
||||
|
||||
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 +389,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 +403,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) {
|
||||
|
@@ -4,5 +4,4 @@ export * from './group';
|
||||
export * from './msg';
|
||||
export * from './user';
|
||||
export * from './webapi';
|
||||
export * from './sign';
|
||||
export * from './system';
|
@@ -144,7 +144,7 @@ export class NTQQMsgApi {
|
||||
params,
|
||||
],
|
||||
() => true,
|
||||
() => true, // Todo: 应当通过 groupFileListResult 判断
|
||||
() => true, // 应当通过 groupFileListResult 判断
|
||||
1,
|
||||
5000,
|
||||
);
|
||||
@@ -194,7 +194,7 @@ export class NTQQMsgApi {
|
||||
async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
|
||||
//唉?!我有个想法
|
||||
if (peer.chatType === ChatType.KCHATTYPETEMPC2CFROMGROUP && peer.guildId && peer.guildId !== '') {
|
||||
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid!);
|
||||
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid);
|
||||
if (member) {
|
||||
await this.PrepareTempChat(peer.peerUid, peer.guildId, member.nick);
|
||||
}
|
||||
|
@@ -1,33 +1,10 @@
|
||||
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]: {
|
||||
recv: string;
|
||||
@@ -42,201 +19,48 @@ 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())
|
||||
}
|
||||
async initApi() {
|
||||
await 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] PacketBackend 不支持当前QQ版本架构:${qqversion}-${os.arch()},
|
||||
请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本!`);
|
||||
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] 已禁用PacketBackend,NapCat.Packet将不会加载!');
|
||||
const err = '[Core] [Packet] 已禁用PacketBackend,NapCat.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import { MiniAppLuaJsonType } from '@/core';
|
||||
import { InstanceContext, NapCatCore } from '..';
|
||||
|
||||
export class NTQQMusicSignApi {
|
||||
@@ -10,210 +8,6 @@ export class NTQQMusicSignApi {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async signMiniApp(CardData: MiniAppLuaJsonType) {
|
||||
// {
|
||||
// "app": "com.tencent.miniapp.lua",
|
||||
// "bizsrc": "tianxuan.imgJumpArk",
|
||||
// "view": "miniapp",
|
||||
// "prompt": "hi! 这里有我的日常故事,只想讲给你听",
|
||||
// "config": {
|
||||
// "type": "normal",
|
||||
// "forward": 1,
|
||||
// "autosize": 0
|
||||
// },
|
||||
// "meta": {
|
||||
// "miniapp": {
|
||||
// "title": "hi! 这里有我的日常故事,只想讲给你听",
|
||||
// "preview": "https:\/\/tianquan.gtimg.cn\/qqAIAgent\/item\/7\/square.png",
|
||||
// "jumpUrl": "https:\/\/club.vip.qq.com\/transfer?open_kuikly_info=%7B%22version%22%3A%20%221%22%2C%22src_type%22%3A%20%22web%22%2C%22kr_turbo_display%22%3A%20%221%22%2C%22page_name%22%3A%20%22vas_ai_persona_moments%22%2C%22bundle_name%22%3A%20%22vas_ai_persona_moments%22%7D&page_name=vas_ai_persona_moments&enteranceId=share&robot_uin=3889008584",
|
||||
// "tag": "QQ智能体",
|
||||
// "tagIcon": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png",
|
||||
// "source": "QQ智能体",
|
||||
// "sourcelogo": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// token : function(url,skey){
|
||||
// var str = skey || cookie('skey') || cookie('rv2') || '',
|
||||
// hash = 5381;
|
||||
// if(url){
|
||||
// var hostname = uri(url).hostname;
|
||||
// if(hostname.indexOf('qun.qq.com') > -1 || (hostname.indexOf('qzone.qq.com') > -1 && hostname.indexOf('qun.qzone.qq.com') === -1)){
|
||||
// str = cookie('p_skey') || str;
|
||||
// }
|
||||
// }
|
||||
// for(var i = 0, len = str.length; i < len; ++i){
|
||||
// hash += (hash << 5) + str.charAt(i).charCodeAt();
|
||||
// }
|
||||
// return hash & 0x7fffffff;
|
||||
// },
|
||||
//
|
||||
|
||||
// function signToken(skey: string) {
|
||||
// let hash = 5381;
|
||||
// for (let i = 0, len = skey.length; i < len; ++i) {
|
||||
// hash += (hash << 5) + skey.charCodeAt(i);
|
||||
// }
|
||||
// return hash & 0x7fffffff;
|
||||
// }
|
||||
const signCard = {
|
||||
'app': 'com.tencent.miniapp.lua',
|
||||
'bizsrc': 'tianxuan.imgJumpArk',
|
||||
'view': 'miniapp',
|
||||
'prompt': CardData.prompt,
|
||||
'config': {
|
||||
'type': 'normal',
|
||||
'forward': 1,
|
||||
'autosize': 0,
|
||||
},
|
||||
'meta': {
|
||||
'miniapp': {
|
||||
'title': CardData.title,
|
||||
'preview': (CardData.preview as string).replace(/\\/g, '\\/\\/'),
|
||||
'jumpUrl': (CardData.jumpUrl as string).replace(/\\/g, '\\/\\/'),
|
||||
'tag': CardData.tag,
|
||||
'tagIcon': (CardData.tagIcon as string).replace(/\\/g, '\\/\\/'),
|
||||
'source': CardData.source,
|
||||
'sourcelogo': (CardData.sourcelogo as string).replace(/\\/g, '\\/\\/'),
|
||||
},
|
||||
},
|
||||
};
|
||||
// let signCard = {
|
||||
// "app": "com.tencent.eventshare.lua",
|
||||
// "prompt": "Bot Test",
|
||||
// "bizsrc": "tianxuan.business",
|
||||
// "meta": {
|
||||
// "eventshare": {
|
||||
// "button1URL": "https://www.bilibili.com",
|
||||
// "button1disable": false,
|
||||
// "button1title": "点我前往",
|
||||
// "button2URL": "",
|
||||
// "button2disable": false,
|
||||
// "button2title": "",
|
||||
// "buttonNum": 1,
|
||||
// "jumpURL": "https://www.bilibili.com",
|
||||
// "preview": "https://tianquan.gtimg.cn/shoal/card/9930bc4e-4a92-4da3-814f-8094a2421d9c.png",
|
||||
// "tag": "QQ集卡",
|
||||
// "tagIcon": "https://tianquan.gtimg.cn/shoal/card/c034854b-102d-40be-a545-5ca90a7c49c9.png",
|
||||
// "title": "Bot Test"
|
||||
// }
|
||||
// },
|
||||
// "config": {
|
||||
// "autosize": 0,
|
||||
// "collect": 0,
|
||||
// "ctime": 1716568575,
|
||||
// "forward": 1,
|
||||
// "height": 336,
|
||||
// "reply": 0,
|
||||
// "round": 1,
|
||||
// "type": "normal",
|
||||
// "width": 263
|
||||
// },
|
||||
// "view": "eventshare",
|
||||
// "ver": "0.0.0.1"
|
||||
// };
|
||||
const data = (await this.core.apis.UserApi.getQzoneCookies());
|
||||
const Bkn = this.core.apis.WebApi.getBknFromCookie(data.p_skey);
|
||||
|
||||
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + this.core.selfInfo.uin + '; uin=o' + this.core.selfInfo.uin;
|
||||
|
||||
const signurl = 'https://h5.qzone.qq.com/v2/vip/tx/trpc/ark-share/GenNewSignedArk?g_tk=' + Bkn + '&ark=' + encodeURIComponent(JSON.stringify(signCard));
|
||||
let signed_ark = '';
|
||||
try {
|
||||
const retData = await RequestUtil.HttpGetJson<{
|
||||
code: number,
|
||||
data: { signed_ark: string }
|
||||
}>(signurl, 'GET', undefined, { Cookie: CookieValue });
|
||||
//logDebug('MiniApp JSON 消息生成成功', retData);
|
||||
signed_ark = retData.data.signed_ark;
|
||||
} catch (error) {
|
||||
this.context.logger.logDebug('MiniApp JSON 消息生成失败', error);
|
||||
}
|
||||
return signed_ark;
|
||||
}
|
||||
|
||||
async signInternal(songname: string, singer: string, cover: string, songmid: string, songmusic: string) {
|
||||
//curl -X POST 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003' -H 'Content-Type: application/json' -H 'Cookie: uin=o10086' -d '{"app":"com.tencent.qqreader.share","config":{"ctime":1718634110,"forward":1,"token":"9a63343c32d5a16bcde653eb97faa25d","type":"normal"},"extra":{"app_type":1,"appid":100497308,"msg_seq":14386738075403815000.0,"uin":1733139081},"meta":{"music":{"action":"","android_pkg_name":"","app_type":1,"appid":100497308,"ctime":1718634110,"desc":"周杰伦","jumpUrl":"https://i.y.qq.com/v8/playsong.html?songmid=0039MnYb0qxYhV&type=0","musicUrl":"http://ws.stream.qqmusic.qq.com/http://isure6.stream.qqmusic.qq.com/M800002202B43Cq4V4.mp3?fromtag=810033622&guid=br_xzg&trace=23fe7bcbe2336bbf&uin=553&vkey=CF0F5CE8B0FA16F3001F8A88D877A217EB5E4F00BDCEF1021EB6C48969CA33C6303987AEECE9CC840122DD2F917A59D6130D8A8CA4577C87","preview":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","cover":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","sourceMsgId":"0","source_icon":"https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0","source_url":"","tag":"QQ音乐","title":"晴天","uin":10086}},"prompt":"[分享]晴天","ver":"0.0.0.1","view":"music"}'
|
||||
const signurl = 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003';
|
||||
//let = "https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg";
|
||||
const signCard = {
|
||||
app: 'com.tencent.qqreader.share',
|
||||
config: {
|
||||
ctime: 1718634110,
|
||||
forward: 1,
|
||||
token: '9a63343c32d5a16bcde653eb97faa25d',
|
||||
type: 'normal',
|
||||
},
|
||||
extra: {
|
||||
app_type: 1,
|
||||
appid: 100497308,
|
||||
msg_seq: 14386738075403815000,
|
||||
uin: 1733139081,
|
||||
},
|
||||
meta: {
|
||||
music: {
|
||||
action: '',
|
||||
android_pkg_name: '',
|
||||
app_type: 1,
|
||||
appid: 100497308,
|
||||
ctime: 1718634110,
|
||||
desc: singer,
|
||||
jumpUrl: 'https://i.y.qq.com/v8/playsong.html?songmid=' + songmid + '&type=0',
|
||||
musicUrl: songmusic,
|
||||
preview: cover,
|
||||
cover: cover,
|
||||
sourceMsgId: '0',
|
||||
source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
|
||||
source_url: '',
|
||||
tag: 'QQ音乐',
|
||||
title: songname,
|
||||
uin: 10086,
|
||||
},
|
||||
},
|
||||
prompt: '[分享]' + songname,
|
||||
ver: '0.0.0.1',
|
||||
view: 'music',
|
||||
};
|
||||
//console.log(JSON.stringify(signCard, null, 2));
|
||||
const data = await RequestUtil.HttpGetJson<{ code: number, data: { arkResult: string } }>
|
||||
(signurl, 'POST', signCard, { 'Cookie': 'uin=o10086', 'Content-Type': 'application/json' });
|
||||
return data;
|
||||
}
|
||||
|
||||
//注意处理错误
|
||||
async signWay03(id: string = '', mid: string = '') {
|
||||
let signedMid;
|
||||
if (mid == '') {
|
||||
const MusicInfo = await RequestUtil.HttpGetJson<{
|
||||
songinfo?: {
|
||||
data?: {
|
||||
track_info: {
|
||||
mid: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}>(
|
||||
'https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"songinfo":{"method":"get_song_detail_yqq","param":{"song_type":0,"song_mid":"","song_id":' + id + '},"module":"music.pf_song_detail_svr"}}',
|
||||
'GET',
|
||||
undefined,
|
||||
);
|
||||
signedMid = MusicInfo.songinfo?.data?.track_info.mid;
|
||||
}
|
||||
//第三方接口 存在速率限制 现在勉强用
|
||||
const MusicReal = await RequestUtil.HttpGetJson<{
|
||||
code: number,
|
||||
data?: {
|
||||
name: string,
|
||||
singer: string,
|
||||
url: string,
|
||||
cover: string
|
||||
}
|
||||
}>('https://api.leafone.cn/api/qqmusic?id=' + signedMid + '&type=8', 'GET');
|
||||
//console.log(MusicReal);
|
||||
return { ...MusicReal.data, mid: signedMid };
|
||||
}
|
||||
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
|
||||
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
|
||||
|
||||
@@ -227,10 +21,5 @@ export class NTQQMusicSignApi {
|
||||
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
|
||||
|
||||
//还有一处公告上传可以上传高质量图片 持久为qq域名
|
||||
async SignMusicWrapper(id: string = '') {
|
||||
const MusicInfo = await this.signWay03(id)!;
|
||||
return await this.signInternal(MusicInfo.name!, MusicInfo.singer!, MusicInfo.cover!, MusicInfo.mid!, 'https://ws.stream.qqmusic.qq.com/' + MusicInfo.url!);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@@ -212,108 +212,65 @@ export class NTQQWebApi {
|
||||
}
|
||||
}
|
||||
|
||||
private async getDataInternal(cookieObject: any, groupCode: string, type: number) {
|
||||
let resJson;
|
||||
try {
|
||||
const res = await RequestUtil.HttpGetText(
|
||||
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
||||
gc: groupCode,
|
||||
type: type.toString(),
|
||||
}).toString()}`,
|
||||
'GET',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||||
if (match) {
|
||||
resJson = JSON.parse(match[1].trim());
|
||||
}
|
||||
return type === 1 ? resJson?.talkativeList : resJson?.actorList;
|
||||
} catch (e) {
|
||||
this.context.logger.logDebug('获取当前群荣耀失败', e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async getHonorList(cookieObject: any, groupCode: string, type: number) {
|
||||
const data = await this.getDataInternal(cookieObject, groupCode, type);
|
||||
if (!data) {
|
||||
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
|
||||
return [];
|
||||
}
|
||||
return data.map((item: any) => ({
|
||||
user_id: item?.uin,
|
||||
nickname: item?.name,
|
||||
avatar: item?.avatar,
|
||||
description: item?.desc,
|
||||
}));
|
||||
}
|
||||
|
||||
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
|
||||
let resJson;
|
||||
try {
|
||||
const res = await RequestUtil.HttpGetText(
|
||||
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
||||
gc: Internal_groupCode,
|
||||
type: Internal_type.toString(),
|
||||
}).toString()}`,
|
||||
'GET',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||||
if (match) {
|
||||
resJson = JSON.parse(match[1].trim());
|
||||
}
|
||||
if (Internal_type === 1) {
|
||||
return resJson?.talkativeList;
|
||||
} else {
|
||||
return resJson?.actorList;
|
||||
}
|
||||
} catch (e) {
|
||||
this.context.logger.logDebug('获取当前群荣耀失败', e);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const HonorInfo: any = { group_id: groupCode };
|
||||
|
||||
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
|
||||
const RetInternal = await getDataInternal(groupCode, 1);
|
||||
if (RetInternal) {
|
||||
HonorInfo.current_talkative = {
|
||||
user_id: RetInternal[0]?.uin,
|
||||
avatar: RetInternal[0]?.avatar,
|
||||
nickname: RetInternal[0]?.name,
|
||||
day_count: 0,
|
||||
description: RetInternal[0]?.desc,
|
||||
};
|
||||
HonorInfo.talkative_list = [];
|
||||
for (const talkative_ele of RetInternal) {
|
||||
HonorInfo.talkative_list.push({
|
||||
user_id: talkative_ele?.uin,
|
||||
avatar: talkative_ele?.avatar,
|
||||
description: talkative_ele?.desc,
|
||||
day_count: 0,
|
||||
nickname: talkative_ele?.name,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取龙王信息失败');
|
||||
const talkativeList = await this.getHonorList(cookieObject, groupCode, 1);
|
||||
if (talkativeList.length > 0) {
|
||||
HonorInfo.current_talkative = talkativeList[0];
|
||||
HonorInfo.talkative_list = talkativeList;
|
||||
}
|
||||
}
|
||||
|
||||
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
||||
const RetInternal = await getDataInternal(groupCode, 2);
|
||||
if (RetInternal) {
|
||||
HonorInfo.performer_list = [];
|
||||
for (const performer_ele of RetInternal) {
|
||||
HonorInfo.performer_list.push({
|
||||
user_id: performer_ele?.uin,
|
||||
nickname: performer_ele?.name,
|
||||
avatar: performer_ele?.avatar,
|
||||
description: performer_ele?.desc,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取群聊之火失败');
|
||||
}
|
||||
HonorInfo.performer_list = await this.getHonorList(cookieObject, groupCode, 2);
|
||||
}
|
||||
|
||||
if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) {
|
||||
const RetInternal = await getDataInternal(groupCode, 3);
|
||||
if (RetInternal) {
|
||||
HonorInfo.legend_list = [];
|
||||
for (const legend_ele of RetInternal) {
|
||||
HonorInfo.legend_list.push({
|
||||
user_id: legend_ele?.uin,
|
||||
nickname: legend_ele?.name,
|
||||
avatar: legend_ele?.avatar,
|
||||
desc: legend_ele?.description,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取群聊炽焰失败');
|
||||
}
|
||||
HonorInfo.legend_list = await this.getHonorList(cookieObject, groupCode, 3);
|
||||
}
|
||||
|
||||
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
||||
const RetInternal = await getDataInternal(groupCode, 6);
|
||||
if (RetInternal) {
|
||||
HonorInfo.emotion_list = [];
|
||||
for (const emotion_ele of RetInternal) {
|
||||
HonorInfo.emotion_list.push({
|
||||
user_id: emotion_ele.uin,
|
||||
nickname: emotion_ele.name,
|
||||
avatar: emotion_ele.avatar,
|
||||
desc: emotion_ele.description,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取快乐源泉失败');
|
||||
}
|
||||
HonorInfo.emotion_list = await this.getHonorList(cookieObject, groupCode, 6);
|
||||
}
|
||||
|
||||
// 冒尖小春笋好像已经被tx扬了 R.I.P.
|
||||
|
@@ -23,7 +23,7 @@ export interface ChatCacheList {
|
||||
export interface ChatCacheListItem {
|
||||
chatType: ChatType;
|
||||
basicChatCacheInfo: ChatCacheListItemBasic;
|
||||
guildChatCacheInfo: unknown[]; // TODO: 没用过频道所以不知道这里边的详细内容
|
||||
guildChatCacheInfo: unknown[]; // work: 没用过频道所以不知道这里边的详细内容
|
||||
}
|
||||
|
||||
export interface ChatCacheListItemBasic {
|
||||
|
@@ -117,7 +117,7 @@ export enum GroupMemberRole {
|
||||
}
|
||||
|
||||
export interface GroupMember {
|
||||
memberRealLevel: string | undefined;
|
||||
memberRealLevel: number | undefined;
|
||||
memberSpecialTitle?: string;
|
||||
avatarPath: string;
|
||||
cardName: string;
|
||||
|
@@ -175,8 +175,8 @@ export interface SimpleInfo {
|
||||
status: UserStatus | null;
|
||||
vasInfo: VasInfo | null;
|
||||
relationFlags: RelationFlags | null;
|
||||
otherFlags: any | null;
|
||||
intimate: any | null;
|
||||
otherFlags: any;
|
||||
intimate: any;
|
||||
}
|
||||
|
||||
export type FriendV2 = SimpleInfo;
|
||||
|
14
src/core/external/appid.json
vendored
14
src/core/external/appid.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
40
src/core/external/offset.json
vendored
40
src/core/external/offset.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
31
src/core/external/proto/EmojiLikeToOthers.proto
vendored
31
src/core/external/proto/EmojiLikeToOthers.proto
vendored
@@ -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;
|
||||
}
|
9
src/core/external/proto/GreyTipWrapper.proto
vendored
9
src/core/external/proto/GreyTipWrapper.proto
vendored
@@ -1,9 +0,0 @@
|
||||
syntax = 'proto3';
|
||||
package SysMessage;
|
||||
|
||||
message GreyTipWrapper {
|
||||
uint32 subTypeId = 1;
|
||||
uint32 groupCode = 4;
|
||||
uint32 subTypeIdMinusOne = 13;
|
||||
bytes rest = 44;
|
||||
}
|
18
src/core/external/proto/ProfileLikeTip.proto
vendored
18
src/core/external/proto/ProfileLikeTip.proto
vendored
@@ -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;
|
||||
}
|
36
src/core/external/proto/SysMessage.proto
vendored
36
src/core/external/proto/SysMessage.proto
vendored
@@ -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;
|
||||
}
|
61
src/core/helper/adaptDecoder.ts
Normal file
61
src/core/helper/adaptDecoder.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// work: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);
|
||||
}
|
49
src/core/helper/adaptSysMessageDecoder.ts
Normal file
49
src/core/helper/adaptSysMessageDecoder.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// work: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);
|
||||
}
|
@@ -84,11 +84,10 @@ export function getMajorPath(QQVersion: string): string {
|
||||
}
|
||||
export class NapCatCore {
|
||||
readonly context: InstanceContext;
|
||||
readonly apis: StableNTApiWrapper;
|
||||
readonly eventWrapper: NTEventWrapper;
|
||||
// readonly eventChannel: NTEventChannel;
|
||||
NapCatDataPath: string;
|
||||
NapCatTempPath: string;
|
||||
NapCatDataPath: string = '';
|
||||
NapCatTempPath: string = '';
|
||||
apis: StableNTApiWrapper;
|
||||
// runtime info, not readonly
|
||||
selfInfo: SelfInfo;
|
||||
util: NodeQQNTWrapperUtil;
|
||||
@@ -112,6 +111,8 @@ export class NapCatCore {
|
||||
UserApi: new NTQQUserApi(this.context, this),
|
||||
GroupApi: new NTQQGroupApi(this.context, this),
|
||||
};
|
||||
}
|
||||
async initCore() {
|
||||
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
|
||||
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
|
||||
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
|
||||
@@ -119,7 +120,13 @@ export class NapCatCore {
|
||||
if (!fs.existsSync(this.NapCatTempPath)) {
|
||||
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
||||
}
|
||||
|
||||
//遍历this.apis[i].initApi 如果存在该函数进行async 调用
|
||||
for (const apiKey in this.apis) {
|
||||
const api = this.apis[apiKey as keyof StableNTApiWrapper];
|
||||
if ('initApi' in api && typeof api.initApi === 'function') {
|
||||
await api.initApi();
|
||||
}
|
||||
}
|
||||
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||
|
||||
this.context.logger.setFileLogEnabled(
|
||||
@@ -133,7 +140,6 @@ export class NapCatCore {
|
||||
this.configLoader.configData.consoleLogLevel as LogLevel,
|
||||
);
|
||||
}
|
||||
|
||||
get dataPath(): string {
|
||||
let result = this.context.wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
|
||||
if (!result) {
|
||||
@@ -204,7 +210,7 @@ export class NapCatCore {
|
||||
});
|
||||
};
|
||||
groupListener.onMemberListChange = (arg) => {
|
||||
// todo: 应该加一个内部自己维护的成员变动callback,用于判断成员变化通知
|
||||
// work:应该加一个内部自己维护的成员变动callback,用于判断成员变化通知
|
||||
const groupCode = arg.sceneId.split('_')[0];
|
||||
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
|
||||
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
|
||||
@@ -214,7 +220,7 @@ export class NapCatCore {
|
||||
if (existMember) {
|
||||
Object.assign(existMember, member);
|
||||
} else {
|
||||
existMembers!.set(uid, member);
|
||||
existMembers.set(uid, member);
|
||||
}
|
||||
//移除成员
|
||||
if (member.isDelete) {
|
||||
|
@@ -3,57 +3,57 @@ import { BuddyCategoryType, FriendRequestNotify } from '@/core/entities';
|
||||
export type OnBuddyChangeParams = BuddyCategoryType[];
|
||||
|
||||
export class NodeIKernelBuddyListener {
|
||||
onBuddyListChangedV2(arg: unknown): void {
|
||||
onBuddyListChangedV2(arg: unknown): any {
|
||||
}
|
||||
|
||||
onAddBuddyNeedVerify(arg: unknown) {
|
||||
onAddBuddyNeedVerify(arg: unknown): any {
|
||||
}
|
||||
|
||||
onAddMeSettingChanged(arg: unknown) {
|
||||
onAddMeSettingChanged(arg: unknown): any {
|
||||
}
|
||||
|
||||
onAvatarUrlUpdated(arg: unknown) {
|
||||
onAvatarUrlUpdated(arg: unknown): any {
|
||||
}
|
||||
|
||||
onBlockChanged(arg: unknown) {
|
||||
onBlockChanged(arg: unknown): any {
|
||||
}
|
||||
|
||||
onBuddyDetailInfoChange(arg: unknown) {
|
||||
onBuddyDetailInfoChange(arg: unknown): any {
|
||||
}
|
||||
|
||||
onBuddyInfoChange(arg: unknown) {
|
||||
onBuddyInfoChange(arg: unknown): any {
|
||||
}
|
||||
|
||||
onBuddyListChange(arg: OnBuddyChangeParams): void {
|
||||
onBuddyListChange(arg: OnBuddyChangeParams): any {
|
||||
}
|
||||
|
||||
onBuddyRemarkUpdated(arg: unknown): void {
|
||||
onBuddyRemarkUpdated(arg: unknown): any {
|
||||
}
|
||||
|
||||
onBuddyReqChange(arg: FriendRequestNotify): void {
|
||||
onBuddyReqChange(arg: FriendRequestNotify): any {
|
||||
}
|
||||
|
||||
onBuddyReqUnreadCntChange(arg: unknown): void {
|
||||
onBuddyReqUnreadCntChange(arg: unknown): any {
|
||||
}
|
||||
|
||||
onCheckBuddySettingResult(arg: unknown): void {
|
||||
onCheckBuddySettingResult(arg: unknown): any {
|
||||
}
|
||||
|
||||
onDelBatchBuddyInfos(arg: unknown): void {
|
||||
onDelBatchBuddyInfos(arg: unknown): any {
|
||||
}
|
||||
|
||||
onDoubtBuddyReqChange(arg: unknown): void {
|
||||
onDoubtBuddyReqChange(arg: unknown): any {
|
||||
}
|
||||
|
||||
onDoubtBuddyReqUnreadNumChange(arg: unknown): void {
|
||||
onDoubtBuddyReqUnreadNumChange(arg: unknown): any {
|
||||
}
|
||||
|
||||
onNickUpdated(arg: unknown): void {
|
||||
onNickUpdated(arg: unknown): any {
|
||||
}
|
||||
|
||||
onSmartInfos(arg: unknown): void {
|
||||
onSmartInfos(arg: unknown): any {
|
||||
}
|
||||
|
||||
onSpacePermissionInfos(arg: unknown): void {
|
||||
onSpacePermissionInfos(arg: unknown): any {
|
||||
}
|
||||
}
|
||||
|
@@ -7,19 +7,19 @@ export class NodeIKernelFileAssistantListener {
|
||||
fileSpeed: number,
|
||||
thumbPath: string | null,
|
||||
filePath: string | null,
|
||||
}) {
|
||||
}): any {
|
||||
}
|
||||
|
||||
onSessionListChanged(...args: unknown[]) {
|
||||
onSessionListChanged(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onSessionChanged(...args: unknown[]) {
|
||||
onSessionChanged(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onFileListChanged(...args: unknown[]) {
|
||||
onFileListChanged(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onFileSearch(searchResult: SearchResultWrapper) {
|
||||
onFileSearch(searchResult: SearchResultWrapper): any {
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,70 +1,70 @@
|
||||
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/entities';
|
||||
|
||||
export class NodeIKernelGroupListener {
|
||||
onGroupListInited(listEmpty: boolean): void { }
|
||||
onGroupListInited(listEmpty: boolean): any { }
|
||||
// 发现于Win 9.9.9 23159
|
||||
onGroupMemberLevelInfoChange(...args: unknown[]): void {
|
||||
onGroupMemberLevelInfoChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGetGroupBulletinListResult(...args: unknown[]) {
|
||||
onGetGroupBulletinListResult(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupAllInfoChange(...args: unknown[]) {
|
||||
onGroupAllInfoChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupBulletinChange(...args: unknown[]) {
|
||||
onGroupBulletinChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupBulletinRemindNotify(...args: unknown[]) {
|
||||
onGroupBulletinRemindNotify(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupArkInviteStateResult(...args: unknown[]) {
|
||||
onGroupArkInviteStateResult(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
|
||||
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupConfMemberChange(...args: unknown[]) {
|
||||
onGroupConfMemberChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupDetailInfoChange(...args: unknown[]) {
|
||||
onGroupDetailInfoChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupExtListUpdate(...args: unknown[]) {
|
||||
onGroupExtListUpdate(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupFirstBulletinNotify(...args: unknown[]) {
|
||||
onGroupFirstBulletinNotify(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
|
||||
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): any {
|
||||
}
|
||||
|
||||
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
|
||||
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]): any {
|
||||
}
|
||||
|
||||
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
|
||||
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
|
||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
|
||||
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]): any {
|
||||
}
|
||||
|
||||
onGroupsMsgMaskResult(...args: unknown[]) {
|
||||
onGroupsMsgMaskResult(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupStatisticInfoChange(...args: unknown[]) {
|
||||
onGroupStatisticInfoChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onJoinGroupNotify(...args: unknown[]) {
|
||||
onJoinGroupNotify(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onJoinGroupNoVerifyFlag(...args: unknown[]) {
|
||||
onJoinGroupNoVerifyFlag(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onMemberInfoChange(groupCode: string, dateSource: DataSource, members: Map<string, GroupMember>) {
|
||||
onMemberInfoChange(groupCode: string, dateSource: DataSource, members: Map<string, GroupMember>): any {
|
||||
}
|
||||
|
||||
onMemberListChange(arg: {
|
||||
@@ -74,12 +74,12 @@ export class NodeIKernelGroupListener {
|
||||
hasPrev: boolean,
|
||||
hasNext: boolean,
|
||||
hasRobot: boolean
|
||||
}) {
|
||||
}): any {
|
||||
}
|
||||
|
||||
onSearchMemberChange(...args: unknown[]) {
|
||||
onSearchMemberChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>) {
|
||||
onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>): any {
|
||||
}
|
||||
}
|
@@ -1,57 +1,57 @@
|
||||
export class NodeIKernelLoginListener {
|
||||
onLoginConnected(...args: any[]): void {
|
||||
onLoginConnected(...args: any[]): any {
|
||||
}
|
||||
|
||||
onLoginDisConnected(...args: any[]): void {
|
||||
onLoginDisConnected(...args: any[]): any {
|
||||
}
|
||||
|
||||
onLoginConnecting(...args: any[]): void {
|
||||
onLoginConnecting(...args: any[]): any {
|
||||
}
|
||||
|
||||
onQRCodeGetPicture(arg: { pngBase64QrcodeData: string, qrcodeUrl: string }): void {
|
||||
onQRCodeGetPicture(arg: { pngBase64QrcodeData: string, qrcodeUrl: string }): any {
|
||||
// let base64Data: string = arg.pngBase64QrcodeData
|
||||
// base64Data = base64Data.split("data:image/png;base64,")[1]
|
||||
// let buffer = Buffer.from(base64Data, 'base64')
|
||||
// console.log("onQRCodeGetPicture", arg);
|
||||
}
|
||||
|
||||
onQRCodeLoginPollingStarted(...args: any[]): void {
|
||||
onQRCodeLoginPollingStarted(...args: any[]): any {
|
||||
}
|
||||
|
||||
onQRCodeSessionUserScaned(...args: any[]): void {
|
||||
onQRCodeSessionUserScaned(...args: any[]): any {
|
||||
}
|
||||
|
||||
onQRCodeLoginSucceed(arg: QRCodeLoginSucceedResult): void {
|
||||
onQRCodeLoginSucceed(arg: QRCodeLoginSucceedResult): any {
|
||||
}
|
||||
|
||||
onQRCodeSessionFailed(...args: any[]): void {
|
||||
onQRCodeSessionFailed(...args: any[]): any {
|
||||
}
|
||||
|
||||
onLoginFailed(...args: any[]): void {
|
||||
onLoginFailed(...args: any[]): any {
|
||||
}
|
||||
|
||||
onLogoutSucceed(...args: any[]): void {
|
||||
onLogoutSucceed(...args: any[]): any {
|
||||
}
|
||||
|
||||
onLogoutFailed(...args: any[]): void {
|
||||
onLogoutFailed(...args: any[]): any {
|
||||
}
|
||||
|
||||
onUserLoggedIn(...args: any[]): void {
|
||||
onUserLoggedIn(...args: any[]): any {
|
||||
}
|
||||
|
||||
onQRCodeSessionQuickLoginFailed(...args: any[]): void {
|
||||
onQRCodeSessionQuickLoginFailed(...args: any[]): any {
|
||||
}
|
||||
|
||||
onPasswordLoginFailed(...args: any[]): void {
|
||||
onPasswordLoginFailed(...args: any[]): any {
|
||||
}
|
||||
|
||||
OnConfirmUnusualDeviceFailed(...args: any[]): void {
|
||||
OnConfirmUnusualDeviceFailed(...args: any[]): any {
|
||||
}
|
||||
|
||||
onQQLoginNumLimited(...args: any[]): void {
|
||||
onQQLoginNumLimited(...args: any[]): any {
|
||||
}
|
||||
|
||||
onLoginState(...args: any[]): void {
|
||||
onLoginState(...args: any[]): any {
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ChatType, RawMessage } from '@/core/entities';
|
||||
import { ChatType, KickedOffLineInfo, RawMessage } from '@/core/entities';
|
||||
import { CommonFileInfo } from '@/core';
|
||||
|
||||
export interface OnRichMediaDownloadCompleteParams {
|
||||
@@ -20,8 +20,8 @@ export interface OnRichMediaDownloadCompleteParams {
|
||||
fileSrvErrCode: string,
|
||||
clientMsg: string,
|
||||
businessId: number,
|
||||
userTotalSpacePerDay: unknown | null,
|
||||
userUsedSpacePerDay: unknown | null
|
||||
userTotalSpacePerDay: unknown,
|
||||
userUsedSpacePerDay: unknown
|
||||
}
|
||||
|
||||
export interface GroupFileInfoUpdateParamType {
|
||||
@@ -94,108 +94,108 @@ export interface TempOnRecvParams {
|
||||
}
|
||||
|
||||
export class NodeIKernelMsgListener {
|
||||
onAddSendMsg(msgRecord: RawMessage) {
|
||||
onAddSendMsg(msgRecord: RawMessage): any {
|
||||
|
||||
}
|
||||
|
||||
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown) {
|
||||
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown) {
|
||||
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown) {
|
||||
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onContactUnreadCntUpdate(hashMap: unknown) {
|
||||
onContactUnreadCntUpdate(hashMap: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown) {
|
||||
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown) {
|
||||
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onEmojiDownloadComplete(emojiNotifyInfo: unknown) {
|
||||
onEmojiDownloadComplete(emojiNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onEmojiResourceUpdate(emojiResourceInfo: unknown) {
|
||||
onEmojiResourceUpdate(emojiResourceInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
|
||||
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onFileMsgCome(arrayList: unknown) {
|
||||
onFileMsgCome(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown) {
|
||||
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onFirstViewGroupGuildMapping(arrayList: unknown) {
|
||||
onFirstViewGroupGuildMapping(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown) {
|
||||
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupFileInfoAdd(groupItem: unknown) {
|
||||
onGroupFileInfoAdd(groupItem: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType) {
|
||||
onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupGuildUpdate(groupGuildNotifyInfo: unknown) {
|
||||
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
|
||||
onGroupTransferInfoAdd(groupItem: unknown) {
|
||||
onGroupTransferInfoAdd(groupItem: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupTransferInfoUpdate(groupFileListResult: unknown) {
|
||||
onGroupTransferInfoUpdate(groupFileListResult: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown) {
|
||||
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown) {
|
||||
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown) {
|
||||
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown) {
|
||||
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown) {
|
||||
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown) {
|
||||
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown) {
|
||||
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
@@ -208,176 +208,176 @@ export class NodeIKernelMsgListener {
|
||||
statusText: string;
|
||||
timestamp: string;
|
||||
toUin: string;
|
||||
}) {
|
||||
}): any {
|
||||
|
||||
}
|
||||
|
||||
onKickedOffLine(kickedInfo: unknown) {
|
||||
onKickedOffLine(kickedInfo: KickedOffLineInfo): any {
|
||||
|
||||
}
|
||||
|
||||
onLineDev(arrayList: unknown) {
|
||||
onLineDev(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onLogLevelChanged(j2: unknown) {
|
||||
onLogLevelChanged(j2: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgAbstractUpdate(arrayList: unknown) {
|
||||
onMsgAbstractUpdate(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgBoxChanged(arrayList: unknown) {
|
||||
onMsgBoxChanged(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgDelete(contact: unknown, arrayList: unknown) {
|
||||
onMsgDelete(contact: unknown, arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgEventListUpdate(hashMap: unknown) {
|
||||
onMsgEventListUpdate(hashMap: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgInfoListAdd(arrayList: unknown) {
|
||||
onMsgInfoListAdd(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgInfoListUpdate(msgList: RawMessage[]) {
|
||||
onMsgInfoListUpdate(msgList: RawMessage[]): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgQRCodeStatusChanged(i2: unknown) {
|
||||
onMsgQRCodeStatusChanged(i2: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgRecall(i2: unknown, str: unknown, j2: unknown) {
|
||||
onMsgRecall(i2: unknown, str: unknown, j2: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgSecurityNotify(msgRecord: unknown) {
|
||||
onMsgSecurityNotify(msgRecord: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgSettingUpdate(msgSetting: unknown) {
|
||||
onMsgSettingUpdate(msgSetting: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onNtFirstViewMsgSyncEnd() {
|
||||
onNtFirstViewMsgSyncEnd(): any {
|
||||
|
||||
}
|
||||
|
||||
onNtMsgSyncEnd() {
|
||||
onNtMsgSyncEnd(): any {
|
||||
|
||||
}
|
||||
|
||||
onNtMsgSyncStart() {
|
||||
onNtMsgSyncStart(): any {
|
||||
|
||||
}
|
||||
|
||||
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
|
||||
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onRecvGroupGuildFlag(i2: unknown) {
|
||||
onRecvGroupGuildFlag(i2: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onRecvMsg(arrayList: RawMessage[]) {
|
||||
onRecvMsg(arrayList: RawMessage[]): any {
|
||||
|
||||
}
|
||||
|
||||
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown) {
|
||||
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onRecvOnlineFileMsg(arrayList: unknown) {
|
||||
onRecvOnlineFileMsg(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onRecvS2CMsg(arrayList: unknown) {
|
||||
onRecvS2CMsg(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onRecvSysMsg(arrayList: Array<number>) {
|
||||
onRecvSysMsg(arrayList: Array<number>): any {
|
||||
|
||||
}
|
||||
|
||||
onRecvUDCFlag(i2: unknown) {
|
||||
onRecvUDCFlag(i2: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) {
|
||||
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): any {
|
||||
}
|
||||
|
||||
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown) {
|
||||
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onRichMediaUploadComplete(fileTransNotifyInfo: unknown) {
|
||||
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown) {
|
||||
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown) {
|
||||
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown) {
|
||||
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams) {
|
||||
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): any {
|
||||
|
||||
}
|
||||
|
||||
onUnreadCntAfterFirstView(hashMap: unknown) {
|
||||
onUnreadCntAfterFirstView(hashMap: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onUnreadCntUpdate(hashMap: unknown) {
|
||||
onUnreadCntUpdate(hashMap: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onUserChannelTabStatusChanged(z: unknown) {
|
||||
onUserChannelTabStatusChanged(z: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onUserOnlineStatusChanged(z: unknown) {
|
||||
onUserOnlineStatusChanged(z: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onUserTabStatusChanged(arrayList: unknown) {
|
||||
onUserTabStatusChanged(arrayList: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
|
||||
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
|
||||
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
// 第一次发现于Linux
|
||||
onUserSecQualityChanged(...args: unknown[]) {
|
||||
onUserSecQualityChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgWithRichLinkInfoUpdate(...args: unknown[]) {
|
||||
onMsgWithRichLinkInfoUpdate(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onRedTouchChanged(...args: unknown[]) {
|
||||
onRedTouchChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
// 第一次发现于Win 9.9.9-23159
|
||||
onBroadcastHelperProgerssUpdate(...args: unknown[]) {
|
||||
onBroadcastHelperProgerssUpdate(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -5,67 +5,67 @@ export class NodeIKernelProfileListener {
|
||||
|
||||
}
|
||||
|
||||
onProfileSimpleChanged(...args: unknown[]) {
|
||||
onProfileSimpleChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onProfileDetailInfoChanged(profile: User) {
|
||||
onProfileDetailInfoChanged(profile: User): any {
|
||||
|
||||
}
|
||||
|
||||
onStatusUpdate(...args: unknown[]) {
|
||||
onStatusUpdate(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onSelfStatusChanged(...args: unknown[]) {
|
||||
onSelfStatusChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onStrangerRemarkChanged(...args: unknown[]) {
|
||||
onStrangerRemarkChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onMemberListChange(...args: unknown[]) {
|
||||
onMemberListChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onMemberInfoChange(...args: unknown[]) {
|
||||
onMemberInfoChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupListUpdate(...args: unknown[]) {
|
||||
onGroupListUpdate(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupAllInfoChange(...args: unknown[]) {
|
||||
onGroupAllInfoChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupDetailInfoChange(...args: unknown[]) {
|
||||
onGroupDetailInfoChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupConfMemberChange(...args: unknown[]) {
|
||||
onGroupConfMemberChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupExtListUpdate(...args: unknown[]) {
|
||||
onGroupExtListUpdate(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupNotifiesUpdated(...args: unknown[]) {
|
||||
onGroupNotifiesUpdated(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
|
||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupMemberLevelInfoChange(...args: unknown[]) {
|
||||
onGroupMemberLevelInfoChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGroupBulletinChange(...args: unknown[]) {
|
||||
onGroupBulletinChange(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +1,25 @@
|
||||
export class NodeIKernelRecentContactListener {
|
||||
onDeletedContactsNotify(...args: unknown[]) {
|
||||
onDeletedContactsNotify(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onRecentContactNotification(msgList: any, arg0: { msgListUnreadCnt: string }, arg1: number) {
|
||||
onRecentContactNotification(msgList: any, arg0: { msgListUnreadCnt: string }, arg1: number): any {
|
||||
|
||||
}
|
||||
|
||||
onMsgUnreadCountUpdate(...args: unknown[]) {
|
||||
onMsgUnreadCountUpdate(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onGuildDisplayRecentContactListChanged(...args: unknown[]) {
|
||||
onGuildDisplayRecentContactListChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onRecentContactListChanged(...args: unknown[]) {
|
||||
onRecentContactListChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onRecentContactListChangedVer2(...args: unknown[]) {
|
||||
onRecentContactListChangedVer2(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
export class NodeIKernelRobotListener {
|
||||
onRobotFriendListChanged(...args: unknown[]) {
|
||||
onRobotFriendListChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onRobotListChanged(...args: unknown[]) {
|
||||
onRobotListChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
|
||||
onRobotProfileChanged(...args: unknown[]) {
|
||||
onRobotProfileChanged(...args: unknown[]): any {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ export interface GroupSearchResult {
|
||||
}
|
||||
export interface NodeIKernelSearchListener {
|
||||
|
||||
onSearchGroupResult(params: GroupSearchResult): void;
|
||||
onSearchGroupResult(params: GroupSearchResult): any;
|
||||
|
||||
onSearchFileKeywordsResult(params: {
|
||||
searchId: string,
|
||||
@@ -93,5 +93,5 @@ export interface NodeIKernelSearchListener {
|
||||
end: number
|
||||
}[]
|
||||
}[]
|
||||
}): void;
|
||||
}): any;
|
||||
}
|
||||
|
@@ -1,25 +1,25 @@
|
||||
export class NodeIKernelSessionListener {
|
||||
onNTSessionCreate(args: unknown) {
|
||||
onNTSessionCreate(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGProSessionCreate(args: unknown) {
|
||||
onGProSessionCreate(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onSessionInitComplete(args: unknown) {
|
||||
onSessionInitComplete(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onOpentelemetryInit(args: unknown) {
|
||||
onOpentelemetryInit(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onUserOnlineResult(args: unknown) {
|
||||
onUserOnlineResult(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onGetSelfTinyId(args: unknown) {
|
||||
onGetSelfTinyId(args: unknown): any {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,21 +1,21 @@
|
||||
export class NodeIKernelStorageCleanListener {
|
||||
onCleanCacheProgressChanged(args: unknown) {
|
||||
onCleanCacheProgressChanged(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onScanCacheProgressChanged(args: unknown) {
|
||||
onScanCacheProgressChanged(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onCleanCacheStorageChanged(args: unknown) {
|
||||
onCleanCacheStorageChanged(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onFinishScan(args: unknown) {
|
||||
onFinishScan(args: unknown): any {
|
||||
|
||||
}
|
||||
|
||||
onChatCleanDone(args: unknown) {
|
||||
onChatCleanDone(args: unknown): any {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,2 +1,5 @@
|
||||
export class NodeIKernelTicketListener {
|
||||
listener(): any {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
export class NodeIO3MiscListener {
|
||||
getOnAmgomDataPiece(...arg: unknown[]) {
|
||||
getOnAmgomDataPiece(...arg: unknown[]): any {
|
||||
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
@@ -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 readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
|
||||
private readonly 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();
|
||||
}
|
||||
}
|
||||
|
@@ -1,98 +1,101 @@
|
||||
import { Data, WebSocket } from "ws";
|
||||
import { NapCatCore } from "@/core";
|
||||
import { PacketClient, RecvPacket } from "@/core/packet/client/client";
|
||||
import { Data, WebSocket, ErrorEvent } from "ws";
|
||||
import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
|
||||
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 = (event: ErrorEvent) => {
|
||||
this.available = false;
|
||||
this.context.logger.error(`WebSocket 出错: ${event.message}`);
|
||||
this.websocket?.close();
|
||||
reject(new Error(`WebSocket 出错: ${event.message}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
31
src/core/packet/clientSession.ts
Normal file
31
src/core/packet/clientSession.ts
Normal 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;
|
||||
}
|
||||
|
||||
// work: global message element adapter (?
|
||||
get msgConverter() {
|
||||
return this.context.msgConverter;
|
||||
}
|
||||
}
|
126
src/core/packet/context/clientContext.ts
Normal file
126
src/core/packet/context/clientContext.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
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 readonly 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?.check()) {
|
||||
throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!");
|
||||
}
|
||||
if (!client) {
|
||||
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;
|
||||
}
|
||||
}
|
35
src/core/packet/context/loggerContext.ts
Normal file
35
src/core/packet/context/loggerContext.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { LogLevel, LogWrapper } from "@/common/log";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
|
||||
// work: 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);
|
||||
}
|
||||
}
|
36
src/core/packet/context/napCoreContext.ts
Normal file
36
src/core/packet/context/napCoreContext.ts
Normal 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);
|
||||
}
|
151
src/core/packet/context/operationContext.ts
Normal file
151
src/core/packet/context/operationContext.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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 readonly 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 chatType = groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C;
|
||||
const peerUid = groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid;
|
||||
const reqList = msg.flatMap(m =>
|
||||
m.msg.map(e => {
|
||||
if (e instanceof PacketMsgPicElement) {
|
||||
return this.context.highway.uploadImage({ chatType, peerUid }, e);
|
||||
} else if (e instanceof PacketMsgVideoElement) {
|
||||
return this.context.highway.uploadVideo({ chatType, peerUid }, e);
|
||||
} else if (e instanceof PacketMsgPttElement) {
|
||||
return this.context.highway.uploadPtt({ chatType, peerUid }, e);
|
||||
} else if (e instanceof PacketMsgFileElement) {
|
||||
return this.context.highway.uploadFile({ chatType, peerUid }, e);
|
||||
}
|
||||
return null;
|
||||
}).filter(Boolean)
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
25
src/core/packet/context/packetContext.ts
Normal file
25
src/core/packet/context/packetContext.ts
Normal 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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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 readonly 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: {
|
||||
@@ -242,26 +223,26 @@ export class PacketHighwaySession {
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: await calculateSha1StreamBytes(video.filePath!)
|
||||
fileSha1: await calculateSha1StreamBytes(video.filePath)
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
await this.hwClient.upload(
|
||||
1005,
|
||||
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
|
||||
fs.createReadStream(video.filePath, { highWaterMark: BlockSize }),
|
||||
+video.fileSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
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,35 +254,32 @@ export class PacketHighwaySession {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
await this.hwClient.upload(
|
||||
1006,
|
||||
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
|
||||
fs.createReadStream(video.thumbPath, { highWaterMark: BlockSize }),
|
||||
+video.thumbSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
|
||||
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: {
|
||||
@@ -310,26 +288,26 @@ export class PacketHighwaySession {
|
||||
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
||||
blockSize: BlockSize,
|
||||
hash: {
|
||||
fileSha1: await calculateSha1StreamBytes(video.filePath!)
|
||||
fileSha1: await calculateSha1StreamBytes(video.filePath)
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
await this.hwClient.upload(
|
||||
1001,
|
||||
fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }),
|
||||
fs.createReadStream(video.filePath, { highWaterMark: BlockSize }),
|
||||
+video.fileSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||
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,34 +319,31 @@ export class PacketHighwaySession {
|
||||
fileSha1: [sha1]
|
||||
}
|
||||
});
|
||||
await this.packetHighwayClient.upload(
|
||||
await this.hwClient.upload(
|
||||
1002,
|
||||
fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }),
|
||||
fs.createReadStream(video.thumbPath, { highWaterMark: BlockSize }),
|
||||
+video.thumbSize!,
|
||||
md5,
|
||||
extend
|
||||
);
|
||||
} else {
|
||||
this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`);
|
||||
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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
75
src/core/packet/highway/uploader/highwayHttpUploader.ts
Normal file
75
src/core/packet/highway/uploader/highwayHttpUploader.ts
Normal 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: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
reject(new Error((error as Error).message));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
85
src/core/packet/highway/uploader/highwayTcpUploader.ts
Normal file
85
src/core/packet/highway/uploader/highwayTcpUploader.ts
Normal 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]);
|
||||
}
|
||||
}
|
63
src/core/packet/highway/uploader/highwayUploader.ts
Normal file
63
src/core/packet/highway/uploader/highwayUploader.ts
Normal 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>;
|
||||
}
|
@@ -1,20 +1,20 @@
|
||||
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: {
|
||||
isEnable: true,
|
||||
ip: int32ip2str(ip.outIP!),
|
||||
ip: int32ip2str(ip.outIP ?? 0),
|
||||
},
|
||||
port: ip.outPort!
|
||||
} as NapProtoEncodeStructType<typeof NTHighwayIPv4>;
|
||||
} as NapProtoEncodeStructType<typeof proto.NTHighwayIPv4>;
|
||||
});
|
||||
};
|
||||
|
@@ -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
|
||||
);
|
||||
|
||||
@@ -23,11 +16,10 @@ export class PacketMsgBuilder {
|
||||
return element.map((node): NapProtoEncodeStructType<typeof PushMsgBody> => {
|
||||
const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`;
|
||||
const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement<PacketSendMsgElement>) => {
|
||||
return acc !== undefined ? acc : msg.buildContent();
|
||||
return acc ?? msg.buildContent();
|
||||
}, 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 {
|
||||
|
@@ -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,51 +76,13 @@ export type rawMsgWithSendMsg = {
|
||||
msg: PacketSendMsgElement[]
|
||||
}
|
||||
|
||||
// work: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 = {
|
||||
private readonly rawToPacketMsgConverters: ElementToPacketMsgConverters = {
|
||||
[ElementType.TEXT]: (element) => {
|
||||
if (element.textElement?.atType) {
|
||||
return new PacketMsgAtElement(element as SendTextElement);
|
||||
@@ -155,9 +116,42 @@ export class PacketMsgConverter {
|
||||
[ElementType.MARKDOWN]: (element) => {
|
||||
return new PacketMsgMarkDownElement(element as SendMarkdownElement);
|
||||
},
|
||||
// TODO: check this logic, move it in arkElement?
|
||||
// work:check this logic, move it in arkElement?
|
||||
[ElementType.STRUCTLONGMSG]: (element) => {
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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,14 +28,11 @@ 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
|
||||
// work:SendStructLongMsgElement
|
||||
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
||||
protected constructor(rawElement: T) {
|
||||
}
|
||||
@@ -117,7 +118,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
this.targetUin = +(element.replyElement.senderUin ?? 0);
|
||||
this.targetUid = element.replyElement.senderUidStr ?? '';
|
||||
this.time = +(element.replyElement.replyMsgTime ?? 0);
|
||||
this.elems = []; // TODO: in replyElement.sourceMsgTextElems
|
||||
this.elems = []; // work:in replyElement.sourceMsgTextElems
|
||||
}
|
||||
|
||||
get isGroupReply(): boolean {
|
||||
@@ -130,7 +131,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq],
|
||||
senderUin: BigInt(this.targetUin),
|
||||
time: this.time,
|
||||
elems: [], // TODO: in replyElement.sourceMsgTextElems
|
||||
elems: [], // work:in replyElement.sourceMsgTextElems
|
||||
pbReserve: {
|
||||
messageId: this.messageId,
|
||||
},
|
||||
@@ -345,9 +346,9 @@ export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
|
||||
constructor(element: SendPttElement) {
|
||||
super(element);
|
||||
this.filePath = element.pttElement.filePath;
|
||||
this.fileSize = +element.pttElement.fileSize; // TODO: cc
|
||||
this.fileSize = +element.pttElement.fileSize; // work:cc
|
||||
this.fileMd5 = element.pttElement.md5HexStr;
|
||||
this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc
|
||||
this.fileDuration = Math.round(element.pttElement.duration); // work:cc
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
|
@@ -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
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -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) });
|
||||
}
|
@@ -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) });
|
||||
}
|
@@ -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] 未知的PacketBackend ${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;
|
||||
}
|
||||
}
|
26
src/core/packet/transformer/action/FetchAiVoiceList.ts
Normal file
26
src/core/packet/transformer/action/FetchAiVoiceList.ts
Normal 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();
|
31
src/core/packet/transformer/action/GetAiVoice.ts
Normal file
31
src/core/packet/transformer/action/GetAiVoice.ts
Normal 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();
|
@@ -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();
|
25
src/core/packet/transformer/action/GetStrangerInfo.ts
Normal file
25
src/core/packet/transformer/action/GetStrangerInfo.ts
Normal 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();
|
29
src/core/packet/transformer/action/GroupSign.ts
Normal file
29
src/core/packet/transformer/action/GroupSign.ts
Normal 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();
|
26
src/core/packet/transformer/action/SendPoke.ts
Normal file
26
src/core/packet/transformer/action/SendPoke.ts
Normal 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();
|
30
src/core/packet/transformer/action/SetSpecialTitle.ts
Normal file
30
src/core/packet/transformer/action/SetSpecialTitle.ts
Normal 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();
|
7
src/core/packet/transformer/action/index.ts
Normal file
7
src/core/packet/transformer/action/index.ts
Normal 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';
|
25
src/core/packet/transformer/base.ts
Normal file
25
src/core/packet/transformer/base.ts
Normal 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>;
|
||||
}
|
33
src/core/packet/transformer/highway/DownloadGroupFile.ts
Normal file
33
src/core/packet/transformer/highway/DownloadGroupFile.ts
Normal 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();
|
49
src/core/packet/transformer/highway/DownloadGroupPtt.ts
Normal file
49
src/core/packet/transformer/highway/DownloadGroupPtt.ts
Normal 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();
|
35
src/core/packet/transformer/highway/DownloadOfflineFile.ts
Normal file
35
src/core/packet/transformer/highway/DownloadOfflineFile.ts
Normal 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);
|
||||
}
|
||||
|
||||
// work:check
|
||||
parse(data: Buffer) {
|
||||
const oidbBody = OidbBase.parse(data).body;
|
||||
return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37Response).decode(oidbBody);
|
||||
}
|
||||
}
|
||||
|
||||
export default new DownloadOfflineFile();
|
36
src/core/packet/transformer/highway/DownloadPrivateFile.ts
Normal file
36
src/core/packet/transformer/highway/DownloadPrivateFile.ts
Normal 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();
|
37
src/core/packet/transformer/highway/FetchSessionKey.ts
Normal file
37
src/core/packet/transformer/highway/FetchSessionKey.ts
Normal 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: "", // work: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();
|
38
src/core/packet/transformer/highway/UploadGroupFile.ts
Normal file
38
src/core/packet/transformer/highway/UploadGroupFile.ts
Normal 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: '/', // work:
|
||||
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();
|
87
src/core/packet/transformer/highway/UploadGroupImage.ts
Normal file
87
src/core/packet/transformer/highway/UploadGroupImage.ts
Normal 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, //work: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~", // work:
|
||||
},
|
||||
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();
|
84
src/core/packet/transformer/highway/UploadGroupPtt.ts
Normal file
84
src/core/packet/transformer/highway/UploadGroupPtt.ts
Normal 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();
|
104
src/core/packet/transformer/highway/UploadGroupVideo.ts
Normal file
104
src/core/packet/transformer/highway/UploadGroupVideo.ts
Normal 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();
|
41
src/core/packet/transformer/highway/UploadPrivateFile.ts
Normal file
41
src/core/packet/transformer/highway/UploadPrivateFile.ts
Normal 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();
|
87
src/core/packet/transformer/highway/UploadPrivateImage.ts
Normal file
87
src/core/packet/transformer/highway/UploadPrivateImage.ts
Normal 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, //work: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~", // work:
|
||||
},
|
||||
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();
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user