diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 253be20b..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -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'], - } -}; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bedb58ed..c1f673f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,7 @@ name: "Build Action" on: + push: + pull_request: workflow_dispatch: permissions: write-all @@ -8,54 +10,38 @@ jobs: Build-LiteLoader: runs-on: ubuntu-latest steps: - - name: Clone Main Repository - uses: actions/checkout@v4 - with: - repository: 'NapNeko/NapCatQQ' - submodules: true - ref: main - token: ${{ secrets.NAPCAT_BUILD }} - - name: Use Node.js 20.X - uses: actions/setup-node@v4 - with: - node-version: 20.x - - name: Build NuCat Framework - run: | - npm i - npm run build:framework - cd dist - npm i --omit=dev + - name: Clone Main Repository + uses: actions/checkout@v4 + - name: Use Node.js 20.X + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Build NapCat.Framework + run: | + npm i && cd napcat.webui && npm i && cd .. + npm run build:framework && npm run depend rm package-lock.json - cd .. - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: NapCat.Framework - path: dist + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: NapCat.Framework + path: dist Build-Shell: runs-on: ubuntu-latest steps: - - name: Clone Main Repository - uses: actions/checkout@v4 - with: - repository: 'NapNeko/NapCatQQ' - submodules: true - ref: main - token: ${{ secrets.NAPCAT_BUILD }} - - name: Use Node.js 20.X - uses: actions/setup-node@v4 - with: - node-version: 20.x - - name: Build NuCat LiteLoader - run: | - npm i - npm run build:shell - cd dist - npm i --omit=dev - rm package-lock.json - cd .. - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: NapCat.Shell - path: dist + - name: Clone Main Repository + uses: actions/checkout@v4 + - name: Use Node.js 20.X + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Build NapCat.Shell + run: | + npm i && cd napcat.webui && npm i && cd .. + npm run build:shell && npm run depend + rm package-lock.json + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: NapCat.Shell + path: dist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fef0ec8a..a76fcff2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,9 @@ jobs: - name: Build NuCat Framework run: | npm i + cd napcat.webui + npm i + cd .. npm run build:framework cd dist npm i --omit=dev @@ -78,6 +81,9 @@ jobs: - name: Build NuCat Shell run: | npm i + cd napcat.webui + npm i + cd .. npm run build:shell cd dist npm i --omit=dev @@ -127,10 +133,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 +149,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 diff --git a/README.md b/README.md index 86315c3e..7e254631 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,16 @@ --- -## 欢迎回来 -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/) 页面下载最新版本 @@ -30,25 +26,34 @@ NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现 [Cloudflare.HKServer](https://napcat.napneko.icu/) +[Github.IO](https://napneko.github.io/) + [Cloudflare.Pages](https://napneko.pages.dev/) -[Github.IO](https://napneko.github.io/) +[Server.China](https://napneko.com/) + +[Server.Other](https://napcat.cyou/) + + ## 回家旅途 [QQ Group](https://qm.qq.com/q/VfjAq5HIMS) -[Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl) +## 感谢他们 +感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 -## 猫猫朋友 -感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot) - -感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 +感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi 不过最最重要的 还是需要感谢屏幕前的你哦~ --- -## 约法三章 -> [!CAUTION]\ -> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本项目存在相关性的信息** +## 延缓Native模块与NapCat对新版QQ适配 +为未来持续与高效的使用Native模块 模块代码转为完全非Git仓库的本地保存源码 并进行相关重构 -任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经授权二次分发或基于 NapCat 代码开发。** +同时为了保证稳定 NapCat 本体通常会在3 Week+的周期进行新版本适配 + +因此此时推荐使用release指定版本 + +## 开源附加 + +任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。** diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..adf72e5e --- /dev/null +++ b/eslint.config.mjs @@ -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", + }, +}]; diff --git a/external/LiteLoaderWrapper.zip b/external/LiteLoaderWrapper.zip index a6edb42f..1cca691c 100644 Binary files a/external/LiteLoaderWrapper.zip and b/external/LiteLoaderWrapper.zip differ diff --git a/external/packet/napcat.packet.arm64 b/external/packet/napcat.packet.arm64 deleted file mode 100644 index 670466f3..00000000 Binary files a/external/packet/napcat.packet.arm64 and /dev/null differ diff --git a/external/packet/napcat.packet.exe b/external/packet/napcat.packet.exe deleted file mode 100644 index d2a54204..00000000 Binary files a/external/packet/napcat.packet.exe and /dev/null differ diff --git a/external/packet/napcat.packet.linux b/external/packet/napcat.packet.linux deleted file mode 100644 index e21b381d..00000000 Binary files a/external/packet/napcat.packet.linux and /dev/null differ diff --git a/external/packet/napcat.packet.production.py b/external/packet/napcat.packet.production.py deleted file mode 100644 index 918cc948..00000000 --- a/external/packet/napcat.packet.production.py +++ /dev/null @@ -1,102 +0,0 @@ -import asyncio #line:1 -import datetime #line:2 -import json #line:3 -import hashlib #line:4 -import sys #line:5 -import websockets #line:6 -import frida #line:7 -clients ={}#line:9 -event_loop =None #line:10 -def generate_md5_hash (O000OO00OOO00O0OO :str )->str :#line:12 - return hashlib .md5 (O000OO00OOO00O0OO .encode ()).hexdigest ()#line:13 -async def on_message (O000O0000OO0O0000 ,O0O00OO0OOOO000O0 ):#line:15 - try :#line:16 - if O000O0000OO0O0000 ['type']=='send':#line:17 - O000O0O00OO0O0OOO =O000O0000OO0O0000 ['payload']#line:18 - if not O0O00OO0OOOO000O0 :#line:19 - return #line:20 - if O000O0O00OO0O0OOO ['type']=='recv':#line:21 - await O0O00OO0OOOO000O0 .send (json .dumps ({"type":"recv","trace_id_md5":O000O0O00OO0O0OOO ['trace_id_md5'],"data":{"seq":O000O0O00OO0O0OOO ['seq'],"cmd":O000O0O00OO0O0OOO ['cmd'],"hex_data":O000O0O00OO0O0OOO ['hex_data']}}))#line:30 - elif O000O0O00OO0O0OOO ['type']=='send':#line:31 - print ("send data: ",O000O0O00OO0O0OOO )#line:32 - await O0O00OO0OOOO000O0 .send (json .dumps ({"type":"send","trace_id_md5":O000O0O00OO0O0OOO ['trace_id_md5'],"data":{}}))#line:37 - except Exception as O0O00O0O00O00OO0O :#line:38 - print (f"Error in on_message: {O0O00O0O00O00OO0O}")#line:39 - print (O000O0000OO0O0000 ['payload'])#line:40 -def on_frida_message (OO0O00OOO00000OOO ,OOOOOO0OOO0OOO00O ,O0OO000OOOO00O000 ,O00O000OOO000O0O0 ):#line:42 - asyncio .run_coroutine_threadsafe (on_message (OO0O00OOO00000OOO ,O0OO000OOOO00O000 ),O00O000OOO000O0O0 )#line:43 -async def initialize_frida (OO000OOO000O0OO00 ,O00OOOO0O0OOOO000 ):#line:45 - global clients ,event_loop #line:46 - O0O0OOOO00O000O00 =int (OO000OOO000O0OO00 ['pid'])#line:47 - O00OOO0O000O0OOO0 =hex (int (OO000OOO000O0OO00 ['recv'],16 )>>1 )#line:48 - O0O000OO0O0OOOO00 =hex (int (OO000OOO000O0OO00 ['send'],16 )>>1 )#line:49 - print ("init frida with pid: %d"%O0O0OOOO00O000O00 )#line:50 - OO0OOO0OOOO000O00 =frida .attach (O0O0OOOO00O000O00 )#line:51 - O0OOOOOOO0OO00O00 =""" - const _0x171b=['attach','toString','add','now','recv','event\x20add!','charCodeAt','readPointer','log','set','send','length','startsWith','push','wrapper.node','event\x20send!\x20','findBaseAddress','join','delete','stringify','current_time','sended','seq','readUtf8String','event\x20clear!\x20','trace_id','hook_send\x20napcat!\x20','input','readByteArray'];const _0x5b84=function(_0x171b04,_0x5b84f0){_0x171b04=_0x171b04-0x0;let _0x379e29=_0x171b[_0x171b04];return _0x379e29;};let offset_recv=FRIDA_RECV_OFFSET;let offset_send=FRIDA_SEND_OFFSET;let eventlist=new Map();let isListened=!![];recv(_0x5b84('0x1b'),_0x443b1a=>{isListened=![];if(_0x443b1a&&_0x443b1a[_0x5b84('0x19')]){_0x443b1a['sended']=![];console[_0x5b84('0x8')](_0x5b84('0x5'),JSON['stringify'](_0x443b1a,null,0x2));eventlist[_0x5b84('0x9')](_0x443b1a[_0x5b84('0x19')],_0x443b1a);send({'type':_0x5b84('0xa'),'trace_id_md5':_0x443b1a['trace_id_md5']});}let _0x53dd5f=Date[_0x5b84('0x3')]()/0x3e8;for(let [_0x5981c6,_0x5a5f37]of eventlist){if(_0x5a5f37[_0x5b84('0x14')]+0x3c<_0x53dd5f){console['log'](_0x5b84('0x18'),JSON['stringify'](_0x5a5f37,null,0x2));eventlist[_0x5b84('0x12')](_0x5981c6);}}});function bytesToHex(_0x317040){var _0x475d58=new Uint8Array(_0x317040);for(var _0x247ab6=[],_0x4f61a3=0x0;_0x4f61a3<_0x475d58[_0x5b84('0xb')];_0x4f61a3++){_0x247ab6[_0x5b84('0xd')]((_0x475d58[_0x4f61a3]>>>0x4)['toString'](0x10));_0x247ab6['push']((_0x475d58[_0x4f61a3]&0xf)[_0x5b84('0x1')](0x10));}return _0x247ab6[_0x5b84('0x11')]('');}function HexToBytes(_0x4a226e){var _0x335e51=[];for(var _0x46bc7a=0x0;_0x46bc7a<_0x4a226e['length'];_0x46bc7a+=0x2)_0x335e51[_0x5b84('0xd')](parseInt(_0x4a226e['substr'](_0x46bc7a,0x2),0x10));return _0x335e51;}function String2HexText(_0xba6082){var _0x20ceff=[];for(var _0x28780a=0x0;_0x28780a<_0xba6082['length'];_0x28780a++){_0x20ceff[_0x28780a]=_0xba6082[_0x5b84('0x6')](_0x28780a)['toString'](0x10);}return _0x20ceff[_0x5b84('0x11')]('');}async function main(){let _0x112236=Module[_0x5b84('0x10')]('wrapper.node');while(_0x112236==null){_0x112236=Module[_0x5b84('0x10')](_0x5b84('0xe'));}let _0xce97d=_0x112236['add'](offset_recv);console['log']('hook_recv\x20napcat!\x20');Interceptor[_0x5b84('0x0')](_0xce97d,{'onEnter'(_0x58d330){let _0x366bab=Memory['readPointer'](_0x58d330[0x1])['add'](0x20);let _0x4fc7ff=new Uint8Array(_0x366bab['readByteArray'](0x1))[0x0]&0x1;let _0x28a912=Memory['readPointer'](_0x58d330[0x1])['add'](0x18);let _0x13dd09=Memory[_0x5b84('0x7')](_0x58d330[0x1]);let _0x50360e=new Uint8Array(_0x13dd09[_0x5b84('0x1c')](0x1))[0x0]&0x1;let _0x196dbb=Memory['readPointer'](Memory['readPointer'](_0x58d330[0x1])[_0x5b84('0x2')](0x38));let _0x42cfc8=Memory['readPointer'](_0x196dbb);let _0xca3db8=Memory[_0x5b84('0x7')](_0x196dbb[_0x5b84('0x2')](0x8));let _0x21181a=_0xca3db8-_0x42cfc8;let _0x169a6e=_0x4fc7ff==0x0?Memory[_0x5b84('0x17')](_0x366bab['add'](0x1)):Memory['readUtf8String'](Memory[_0x5b84('0x7')](_0x366bab['add'](0x10)));let _0x3e9755=_0x50360e==0x0?Memory[_0x5b84('0x17')](_0x13dd09['add'](0x1)):Memory[_0x5b84('0x17')](Memory[_0x5b84('0x7')](_0x13dd09[_0x5b84('0x2')](0x10)));let _0x12eb97=Memory['readU32'](_0x28a912);let _0x177b45=bytesToHex(_0x42cfc8['readByteArray'](_0x21181a));let _0xb9a9db='';for(let [_0x7418bd,_0x53b726]of eventlist){if(_0x53b726[_0x5b84('0x16')]==_0x12eb97){_0xb9a9db=_0x53b726['trace_id_md5'];eventlist['delete'](_0x7418bd);break;}}send({'type':_0x5b84('0x4'),'trace_id_md5':_0xb9a9db,'seq':_0x12eb97,'hex_data':_0x177b45,'cmd':_0x169a6e});},'onLeave'(_0x18676d){}});let _0x50e030=_0x112236[_0x5b84('0x2')](offset_send);console['log'](_0x5b84('0x1a'));Interceptor[_0x5b84('0x0')](_0x50e030,{'onEnter'(_0x2c50e5){let _0x43ef68=Memory[_0x5b84('0x7')](_0x2c50e5[0x1])[_0x5b84('0x2')](0x40);let _0x11494e=Memory['readPointer'](_0x2c50e5[0x1])['add'](0x20);let _0x160870=new Uint8Array(_0x11494e['readByteArray'](0x1))[0x0]&0x1;let _0x48c8c0=Memory[_0x5b84('0x7')](Memory['readPointer'](_0x2c50e5[0x1]));let _0x551e15=new Uint8Array(_0x48c8c0['readByteArray'](0x1))[0x0]&0x1;let _0x3d1fb7=Memory[_0x5b84('0x7')](Memory['readPointer'](Memory[_0x5b84('0x7')](_0x2c50e5[0x1]))['add'](0x20));let _0x532d5f=Memory['readPointer'](_0x3d1fb7);let _0xc16bfa=Memory[_0x5b84('0x7')](_0x3d1fb7['add'](0x8));let _0x388601=_0xc16bfa-_0x532d5f;let _0x2f6658=_0x551e15==0x0?Memory['readUtf8String'](_0x48c8c0['add'](0x1)):Memory['readUtf8String'](Memory['readPointer'](_0x48c8c0['add'](0x10)));let _0xa730b0=_0x160870==0x0?Memory[_0x5b84('0x17')](_0x11494e['add'](0x1)):Memory['readUtf8String'](Memory['readPointer'](_0x11494e[_0x5b84('0x2')](0x10)));let _0xd6be27=Memory['readU32'](_0x43ef68);let _0x46ad70=bytesToHex(_0x532d5f[_0x5b84('0x1c')](_0x388601));for(let [_0xecd6b,_0x5390a3]of eventlist){if(_0x46ad70[_0x5b84('0xc')](String2HexText(_0x5390a3[_0x5b84('0x19')]))&&!_0x5390a3['sended']){_0x532d5f['writeByteArray'](HexToBytes(_0x5390a3['hex_data']));_0x5390a3[_0x5b84('0x15')]=!![];_0x5390a3[_0x5b84('0x16')]=_0xd6be27;console[_0x5b84('0x8')](_0x5b84('0xf'),JSON['stringify'](_0x5390a3,null,0x2));break;}}if(!isListened){recv(_0x5b84('0x1b'),_0x17512b=>{isListened=![];if(_0x17512b&&_0x17512b[_0x5b84('0x19')]){_0x17512b[_0x5b84('0x15')]=![];console[_0x5b84('0x8')]('event\x20add!',JSON['stringify'](_0x17512b,null,0x2));eventlist[_0x5b84('0x9')](_0x17512b['trace_id'],_0x17512b);send({'type':'send','trace_id_md5':_0x17512b['trace_id_md5']});}let _0x1e9c5e=Date[_0x5b84('0x3')]()/0x3e8;for(let [_0x1b9261,_0x44053a]of eventlist){if(_0x44053a[_0x5b84('0x14')]+0x3c<_0x1e9c5e){console['log'](_0x5b84('0x18'),JSON[_0x5b84('0x13')](_0x44053a,null,0x2));eventlist[_0x5b84('0x12')](_0x1b9261);}}});}},'onLeave'(_0xf6dcf2){}});}main()['then'](); - """#line:55 - O0OOOOOOO0OO00O00 =O0OOOOOOO0OO00O00 .replace ("FRIDA_RECV_OFFSET",O00OOO0O000O0OOO0 )#line:56 - O0OOOOOOO0OO00O00 =O0OOOOOOO0OO00O00 .replace ("FRIDA_SEND_OFFSET",O0O000OO0O0OOOO00 )#line:57 - OOO0000O00O0O0000 =OO0OOO0OOOO000O00 .create_script (O0OOOOOOO0OO00O00 )#line:58 - OOO0000O00O0O0000 .on ('message',lambda O00OO0000O0OO000O ,O0O0000OO00OO0OO0 :on_frida_message (O00OO0000O0OO000O ,O0O0000OO00OO0OO0 ,O00OOOO0O0OOOO000 ,event_loop ))#line:59 - OOO0000O00O0O0000 .load ()#line:60 - clients [O00OOOO0O0OOOO000 ]={'pid':O0O0OOOO00O000O00 ,'frida_script':OOO0000O00O0O0000 ,'session':OO0OOO0OOOO000O00 }#line:66 - await O00OOOO0O0OOOO000 .send (json .dumps ({"type":"init",'trace_id':'init',"data":{}}))#line:68 -async def handle_send (OO0O00000O0000O00 ,O0O0000OOOOO00000 ):#line:70 - global clients #line:71 - OO0O00OO0OO0OOO0O =OO0O00000O0000O00 ['data']#line:72 - OOOO00000O00OOO00 =OO0O00000O0000O00 ['trace_id']#line:73 - O000O0000000O00O0 =OO0O00000O0000O00 ['cmd']#line:74 - O00OOOO0OOOOO0O0O =int (datetime .datetime .now ().timestamp ())#line:75 - O00OOO00000O000OO =clients [O0O0000OOOOO00000 ]['frida_script']#line:76 - O00OOO00000O000OO .post ({'type':'input','cmd':O000O0000000O00O0 ,'hex_data':OO0O00OO0OO0OOO0O ,'trace_id':OOOO00000O00OOO00 ,'current_time':O00OOOO0OOOOO0O0O ,'trace_id_md5':generate_md5_hash (OOOO00000O00OOO00 )})#line:84 -async def websocket_handler (O000OO0OO0OO0OO0O ,OO0O0OO0O00OOOO0O ):#line:86 - global clients #line:87 - try :#line:88 - async for O0OOOOO0O00O0OO00 in O000OO0OO0OO0OO0O :#line:89 - O00O0O0O000OO0O0O =json .loads (O0OOOOO0O00O0OO00 )#line:90 - O000O0000O00OO00O =O00O0O0O000OO0O0O .get ('action')#line:91 - if O000O0000O00OO00O =='init':#line:92 - O0OO0000OOOOO000O =False #line:93 - for OO00OO000O00O0OOO in frida .get_local_device ().enumerate_processes ():#line:94 - if OO00OO000O00O0OOO .pid ==clients .get (O000OO0OO0OO0OO0O ,{}).get ('pid'):#line:95 - O0OO0000OOOOO000O =True #line:96 - if not O0OO0000OOOOO000O :#line:97 - await initialize_frida (O00O0O0O000OO0O0O ,O000OO0OO0OO0OO0O )#line:98 - elif O000O0000O00OO00O =='send'and O000OO0OO0OO0OO0O in clients :#line:99 - await handle_send (O00O0O0O000OO0O0O ,O000OO0OO0OO0OO0O )#line:100 - except Exception as O0OOOO0O00O000O0O :#line:101 - print (f"WebSocket connection closed: {O0OOOO0O00O000O0O}")#line:102 - finally :#line:103 - if O000OO0OO0OO0OO0O in clients :#line:104 - clients [O000OO0OO0OO0OO0O ]['session'].detach ()#line:105 - del clients [O000OO0OO0OO0OO0O ]#line:106 -async def main ():#line:108 - global event_loop #line:109 - event_loop =asyncio .get_running_loop ()#line:110 - OOOOOO0OOO0000O0O =None #line:111 - OOO000OO0O0OOOO00 =None #line:112 - if '-ip'in sys .argv and '-port'in sys .argv :#line:114 - O00000OOO00OO000O =sys .argv .index ('-ip')+1 #line:115 - OOO0O00O0OO0000OO =sys .argv .index ('-port')+1 #line:116 - if O00000OOO00OO000O {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%" + +"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1 + +pause \ No newline at end of file diff --git a/launcher/launcher-win10-user.bat b/launcher/launcher-win10-user.bat new file mode 100644 index 00000000..debf0acb --- /dev/null +++ b/launcher/launcher-win10-user.bat @@ -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 \ No newline at end of file diff --git a/launcher/qqnt.json b/launcher/qqnt.json index 007fb7f3..50c711a4 100644 --- a/launcher/qqnt.json +++ b/launcher/qqnt.json @@ -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", diff --git a/manifest.json b/manifest.json index d1ffbaf3..32671e74 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "NapCatQQ", "slug": "NapCat.Framework", "description": "高性能的 OneBot 11 协议实现", - "version": "3.4.3", + "version": "4.1.5", "icon": "./logo.png", "authors": [ { diff --git a/napcat.webui/.gitignore b/napcat.webui/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/napcat.webui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/napcat.webui/.vscode/extensions.json b/napcat.webui/.vscode/extensions.json new file mode 100644 index 00000000..a7cea0b0 --- /dev/null +++ b/napcat.webui/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/napcat.webui/README.md b/napcat.webui/README.md new file mode 100644 index 00000000..33895ab2 --- /dev/null +++ b/napcat.webui/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/napcat.webui/package.json b/napcat.webui/package.json new file mode 100644 index 00000000..3651cf16 --- /dev/null +++ b/napcat.webui/package.json @@ -0,0 +1,31 @@ +{ + "name": "napcat.webui", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "webui:lint": "eslint --fix src/**/*.{js,ts,vue}", + "webui:dev": "vite", + "webui:build": "vue-tsc -b && vite build", + "webui:preview": "vite preview" + }, + "dependencies": { + "eslint-plugin-prettier": "^5.2.1", + "qrcode": "^1.5.4", + "tdesign-vue-next": "^1.10.3", + "vue": "^3.5.12", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.14.0", + "@types/qrcode": "^1.5.5", + "@vitejs/plugin-vue": "^5.1.4", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-vue": "^9.31.0", + "globals": "^15.12.0", + "typescript": "~5.6.2", + "vite": "^5.4.10", + "vue-tsc": "^2.1.8" + } +} diff --git a/napcat.webui/public/logo.png b/napcat.webui/public/logo.png new file mode 100644 index 00000000..0eef6b84 Binary files /dev/null and b/napcat.webui/public/logo.png differ diff --git a/napcat.webui/public/vite.svg b/napcat.webui/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/napcat.webui/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/napcat.webui/src/App.vue b/napcat.webui/src/App.vue new file mode 100644 index 00000000..e5fe963f --- /dev/null +++ b/napcat.webui/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/napcat.webui/src/assets/Sotheby.ttf b/napcat.webui/src/assets/Sotheby.ttf new file mode 100644 index 00000000..bb9630f2 Binary files /dev/null and b/napcat.webui/src/assets/Sotheby.ttf differ diff --git a/napcat.webui/src/assets/logo.png b/napcat.webui/src/assets/logo.png new file mode 100644 index 00000000..0eef6b84 Binary files /dev/null and b/napcat.webui/src/assets/logo.png differ diff --git a/napcat.webui/src/assets/vue.svg b/napcat.webui/src/assets/vue.svg new file mode 100644 index 00000000..770e9d33 --- /dev/null +++ b/napcat.webui/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/napcat.webui/src/backend/shell.ts b/napcat.webui/src/backend/shell.ts new file mode 100644 index 00000000..efc7f013 --- /dev/null +++ b/napcat.webui/src/backend/shell.ts @@ -0,0 +1,185 @@ +import { OneBotConfig } from '../../../src/onebot/config/config'; + +export class QQLoginManager { + private retCredential: string; + private readonly apiPrefix: string; + + //调试时http://127.0.0.1:6099/api 打包时 ../api + constructor(retCredential: string, apiPrefix: string = '../api') { + this.retCredential = retCredential; + this.apiPrefix = apiPrefix; + } + + // TODO: + public async GetOB11Config(): Promise { + try { + const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/GetConfig`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.retCredential, + 'Content-Type': 'application/json', + }, + }); + if (ConfigResponse.status == 200) { + const ConfigResponseJson = await ConfigResponse.json(); + if (ConfigResponseJson.code == 0) { + return ConfigResponseJson?.data as OneBotConfig; + } + } + } catch (error) { + console.error('Error getting OB11 config:', error); + } + return {} as OneBotConfig; + } + + public async SetOB11Config(config: OneBotConfig): Promise { + try { + const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.retCredential, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ config: JSON.stringify(config) }), + }); + if (ConfigResponse.status == 200) { + const ConfigResponseJson = await ConfigResponse.json(); + if (ConfigResponseJson.code == 0) { + return true; + } + } + } catch (error) { + console.error('Error setting OB11 config:', error); + } + return false; + } + + public async checkQQLoginStatus(): Promise { + try { + const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.retCredential, + 'Content-Type': 'application/json', + }, + }); + if (QQLoginResponse.status == 200) { + const QQLoginResponseJson = await QQLoginResponse.json(); + if (QQLoginResponseJson.code == 0) { + return QQLoginResponseJson.data.isLogin; + } + } + } catch (error) { + console.error('Error checking QQ login status:', error); + } + return false; + } + + public async checkWebUiLogined(): Promise { + try { + const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.retCredential, + 'Content-Type': 'application/json', + }, + }); + if (LoginResponse.status == 200) { + const LoginResponseJson = await LoginResponse.json(); + if (LoginResponseJson.code == 0) { + return true; + } + } + } catch (error) { + console.error('Error checking web UI login status:', error); + } + return false; + } + + public async loginWithToken(token: string): Promise { + try { + const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token: token }), + }); + const loginResponseJson = await loginResponse.json(); + const retCode = loginResponseJson.code; + if (retCode === 0) { + this.retCredential = loginResponseJson.data.Credential; + return this.retCredential; + } + } catch (error) { + console.error('Error logging in with token:', error); + } + return null; + } + + public async getQQLoginQrcode(): Promise { + try { + const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.retCredential, + 'Content-Type': 'application/json', + }, + }); + if (QQLoginResponse.status == 200) { + const QQLoginResponseJson = await QQLoginResponse.json(); + if (QQLoginResponseJson.code == 0) { + return QQLoginResponseJson.data.qrcode || ''; + } + } + } catch (error) { + console.error('Error getting QQ login QR code:', error); + } + return ''; + } + + public async getQQQuickLoginList(): Promise { + try { + const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.retCredential, + 'Content-Type': 'application/json', + }, + }); + if (QQLoginResponse.status == 200) { + const QQLoginResponseJson = await QQLoginResponse.json(); + if (QQLoginResponseJson.code == 0) { + return QQLoginResponseJson.data || []; + } + } + } catch (error) { + console.error('Error getting QQ quick login list:', error); + } + return []; + } + + public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> { + try { + const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + this.retCredential, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ uin: uin }), + }); + if (QQLoginResponse.status == 200) { + const QQLoginResponseJson = await QQLoginResponse.json(); + if (QQLoginResponseJson.code == 0) { + return { result: true, errMsg: '' }; + } else { + return { result: false, errMsg: QQLoginResponseJson.message }; + } + } + } catch (error) { + console.error('Error setting quick login:', error); + } + return { result: false, errMsg: '接口异常' }; + } +} diff --git a/napcat.webui/src/components/Dashboard.vue b/napcat.webui/src/components/Dashboard.vue new file mode 100644 index 00000000..7d8fcb13 --- /dev/null +++ b/napcat.webui/src/components/Dashboard.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/napcat.webui/src/components/QQLogin.vue b/napcat.webui/src/components/QQLogin.vue new file mode 100644 index 00000000..13d4b674 --- /dev/null +++ b/napcat.webui/src/components/QQLogin.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/napcat.webui/src/components/WebUiLogin.vue b/napcat.webui/src/components/WebUiLogin.vue new file mode 100644 index 00000000..956f3d60 --- /dev/null +++ b/napcat.webui/src/components/WebUiLogin.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/napcat.webui/src/components/webui/Nav.vue b/napcat.webui/src/components/webui/Nav.vue new file mode 100644 index 00000000..ddf489bd --- /dev/null +++ b/napcat.webui/src/components/webui/Nav.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/napcat.webui/src/css/font.css b/napcat.webui/src/css/font.css new file mode 100644 index 00000000..e00fdda5 --- /dev/null +++ b/napcat.webui/src/css/font.css @@ -0,0 +1,6 @@ +@font-face { + font-family: 'Sotheby'; + src: url('../assets/Sotheby.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} \ No newline at end of file diff --git a/napcat.webui/src/css/style.css b/napcat.webui/src/css/style.css new file mode 100644 index 00000000..6305f260 --- /dev/null +++ b/napcat.webui/src/css/style.css @@ -0,0 +1,84 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.card { + padding: 2em; +} + +#app { + height: 100%; + width: 100%; + margin: 0; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} \ No newline at end of file diff --git a/napcat.webui/src/main.ts b/napcat.webui/src/main.ts new file mode 100644 index 00000000..7e071636 --- /dev/null +++ b/napcat.webui/src/main.ts @@ -0,0 +1,62 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import { + Button as TButton, + Input as TInput, + Form as TForm, + FormItem as TFormItem, + Select as TSelect, + Option as TOption, + Menu as TMenu, + MenuItem as TMenuItem, + Icon as TIcon, + Submenu as TSubmenu, + Col as TCol, + Row as TRow, + Card as TCard, + Divider as TDivider, + Link as TLink, + List as TList, + Alert as TAlert, + Tag as TTag, + ListItem as TListItem, + Tabs as TTabs, + TabPanel as TTabPanel, + Space as TSpace, + Checkbox as TCheckbox, + Popup as TPopup, + Dialog as TDialog, + Switch as TSwitch, +} from 'tdesign-vue-next'; +import { router } from './router'; +import 'tdesign-vue-next/es/style/index.css'; + +const app = createApp(App); +app.use(router); +app.use(TButton); +app.use(TInput); +app.use(TForm); +app.use(TFormItem); +app.use(TSelect); +app.use(TOption); +app.use(TMenu); +app.use(TMenuItem); +app.use(TIcon); +app.use(TSubmenu); +app.use(TCol); +app.use(TRow); +app.use(TCard); +app.use(TDivider); +app.use(TLink); +app.use(TList); +app.use(TAlert); +app.use(TTag); +app.use(TListItem); +app.use(TTabs); +app.use(TTabPanel); +app.use(TSpace); +app.use(TCheckbox); +app.use(TPopup); +app.use(TDialog); +app.use(TSwitch); +app.mount('#app'); diff --git a/napcat.webui/src/pages/AboutUs.vue b/napcat.webui/src/pages/AboutUs.vue new file mode 100644 index 00000000..04bf2876 --- /dev/null +++ b/napcat.webui/src/pages/AboutUs.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/napcat.webui/src/pages/BasicInfo.vue b/napcat.webui/src/pages/BasicInfo.vue new file mode 100644 index 00000000..1f9e8418 --- /dev/null +++ b/napcat.webui/src/pages/BasicInfo.vue @@ -0,0 +1,6 @@ + diff --git a/napcat.webui/src/pages/Log.vue b/napcat.webui/src/pages/Log.vue new file mode 100644 index 00000000..bc19a827 --- /dev/null +++ b/napcat.webui/src/pages/Log.vue @@ -0,0 +1,6 @@ + diff --git a/napcat.webui/src/pages/NetWork.vue b/napcat.webui/src/pages/NetWork.vue new file mode 100644 index 00000000..fbe7c1bd --- /dev/null +++ b/napcat.webui/src/pages/NetWork.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/napcat.webui/src/pages/OtherConfig.vue b/napcat.webui/src/pages/OtherConfig.vue new file mode 100644 index 00000000..685c2dd5 --- /dev/null +++ b/napcat.webui/src/pages/OtherConfig.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/napcat.webui/src/pages/network/EmptyStateComponent.vue b/napcat.webui/src/pages/network/EmptyStateComponent.vue new file mode 100644 index 00000000..29fb8e9b --- /dev/null +++ b/napcat.webui/src/pages/network/EmptyStateComponent.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/napcat.webui/src/pages/network/HttpClientComponent.vue b/napcat.webui/src/pages/network/HttpClientComponent.vue new file mode 100644 index 00000000..71cd8d14 --- /dev/null +++ b/napcat.webui/src/pages/network/HttpClientComponent.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/napcat.webui/src/pages/network/HttpServerComponent.vue b/napcat.webui/src/pages/network/HttpServerComponent.vue new file mode 100644 index 00000000..5e61b253 --- /dev/null +++ b/napcat.webui/src/pages/network/HttpServerComponent.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/napcat.webui/src/pages/network/WebsocketClientComponent.vue b/napcat.webui/src/pages/network/WebsocketClientComponent.vue new file mode 100644 index 00000000..d7c2f657 --- /dev/null +++ b/napcat.webui/src/pages/network/WebsocketClientComponent.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/napcat.webui/src/pages/network/WebsocketServerComponent.vue b/napcat.webui/src/pages/network/WebsocketServerComponent.vue new file mode 100644 index 00000000..88830ffa --- /dev/null +++ b/napcat.webui/src/pages/network/WebsocketServerComponent.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/napcat.webui/src/router/index.ts b/napcat.webui/src/router/index.ts new file mode 100644 index 00000000..50f67c11 --- /dev/null +++ b/napcat.webui/src/router/index.ts @@ -0,0 +1,32 @@ +import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; +import Dashboard from '../components/Dashboard.vue'; +import BasicInfo from '../pages/BasicInfo.vue'; +import AboutUs from '../pages/AboutUs.vue'; +import LogView from '../pages/Log.vue'; +import NetWork from '../pages/NetWork.vue'; +import QQLogin from '../components/QQLogin.vue'; +import WebUiLogin from '../components/WebUiLogin.vue'; +import OtherConfig from '../pages/OtherConfig.vue'; + +const routes: Array = [ + { path: '/', redirect: '/webui' }, + { path: '/webui', component: WebUiLogin, name: 'WebUiLogin' }, + { path: '/qqlogin', component: QQLogin, name: 'QQLogin' }, + { + path: '/dashboard', + component: Dashboard, + children: [ + { path: '', redirect: 'basic-info' }, + { path: 'basic-info', component: BasicInfo, name: 'BasicInfo' }, + { path: 'network-config', component: NetWork, name: 'NetWork' }, + { path: 'log-view', component: LogView, name: 'LogView' }, + { path: 'other-config', component: OtherConfig, name: 'OtherConfig' }, + { path: 'about-us', component: AboutUs, name: 'AboutUs' }, + ], + }, +]; + +export const router = createRouter({ + history: createWebHashHistory(), + routes, +}); diff --git a/napcat.webui/src/vite-env.d.ts b/napcat.webui/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/napcat.webui/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/napcat.webui/tsconfig.json b/napcat.webui/tsconfig.json new file mode 100644 index 00000000..bf12aee2 --- /dev/null +++ b/napcat.webui/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ESNext", + "jsx": "preserve", + "jsxImportSource": "vue", + "lib": [ + "DOM", + "DOM.Iterable" + ], + "baseUrl": ".", + "module": "esnext", + "moduleResolution": "bundler", + "paths": { + "@/*": [ + "src/*" + ] + }, + "resolveJsonModule": true, + "types": [ + "vite/client" + ], + "strict": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "experimentalDecorators": true, + "useDefineForClassFields": true + }, + "include": ["src"], + "exclude": ["node_modules"], + "references": [{"path": "./tsconfig.node.json"}] +} diff --git a/napcat.webui/tsconfig.node.json b/napcat.webui/tsconfig.node.json new file mode 100644 index 00000000..f026910e --- /dev/null +++ b/napcat.webui/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strictNullChecks": true + }, + "include": ["vite.config.ts"] +} diff --git a/napcat.webui/vite.config.ts b/napcat.webui/vite.config.ts new file mode 100644 index 00000000..5517677e --- /dev/null +++ b/napcat.webui/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import path from 'path'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + base: './', + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + server: { + proxy: { + '/api': 'http://localhost:6099', + }, + }, + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0].toString(); + } + } + } + } + } +}); \ No newline at end of file diff --git a/package.json b/package.json index 561456b9..82b743ed 100644 --- a/package.json +++ b/package.json @@ -2,18 +2,24 @@ "name": "napcat", "private": true, "type": "module", - "version": "3.4.3", + "version": "4.1.5", "scripts": { - "build:framework": "vite build --mode framework", - "build:shell": "vite build --mode shell", - "build:webui": "cd ./src/webui && vite build", - "lint": "eslint --fix src/**/*.{js,ts}", + "build:framework": "npm run build:webui && vite build --mode framework", + "build:shell": "npm run build:webui && vite build --mode shell", + "build:webui": "cd napcat.webui && vite build", + "dev:framework": "vite build --mode framework", + "dev:shell": "vite build --mode shell", + "dev:webui": "cd napcat.webui && npm run webui:dev", + "lint": "eslint --fix src/**/*.{js,ts,vue}", "depend": "cd dist && npm install --omit=dev" }, "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", - "@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", @@ -26,27 +32,29 @@ "@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": "^4.3.2" + "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", + "piscina": "^4.7.0" } } diff --git a/script/checkVersion.cjs b/script/checkVersion.cjs index 2b8f1d8f..40d53f4d 100644 --- a/script/checkVersion.cjs +++ b/script/checkVersion.cjs @@ -45,7 +45,6 @@ try { sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts - sed -i "s/SettingButton(\\"V.*\\", \\"napcat-update-button\\", \\"secondary\\")/SettingButton(\\"V${targetVersion}\\", \\"napcat-update-button\\", \\"secondary\\")/g" ./static/assets/renderer.js git add . git commit -m "release: v${targetVersion}" git push -u origin main`; diff --git a/src/common/audio-worker.ts b/src/common/audio-worker.ts new file mode 100644 index 00000000..8d84d3b2 --- /dev/null +++ b/src/common/audio-worker.ts @@ -0,0 +1,9 @@ +import { encode } from "silk-wasm"; + +export interface EncodeArgs { + input: ArrayBufferView | ArrayBuffer + sampleRate: number +} +export default async ({ input, sampleRate }: EncodeArgs) => { + return await encode(input, sampleRate); +}; diff --git a/src/common/audio.ts b/src/common/audio.ts index a89a873f..6f849d6c 100644 --- a/src/common/audio.ts +++ b/src/common/audio.ts @@ -1,13 +1,23 @@ +import Piscina from 'piscina'; import fsPromise from 'fs/promises'; import path from 'node:path'; import { randomUUID } from 'crypto'; import { spawn } from 'node:child_process'; -import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm'; +import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm'; import { LogWrapper } from './log'; +import { EncodeArgs } from "@/common/audio-worker"; const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; const EXIT_CODES = [0, 255]; -const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg'; +const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg'; + +async function getWorkerPath() { + return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href; +} + +const piscina = new Piscina({ + filename: await getWorkerPath(), +}); async function guessDuration(pttPath: string, logger: LogWrapper) { const pttFileInfo = await fsPromise.stat(pttPath); @@ -41,8 +51,11 @@ async function convert(filePath: string, pcmPath: string, logger: LogWrapper): P } async function handleWavFile( - file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper -): Promise<{input: Buffer, sampleRate: number}> { + file: Buffer, + filePath: string, + pcmPath: string, + logger: LogWrapper +): Promise<{ input: Buffer; sampleRate: number }> { const { fmt } = getWavFileInfo(file); if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) { return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 }; @@ -60,8 +73,8 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log const { input, sampleRate } = isWav(file) ? (await handleWavFile(file, filePath, pcmPath, logger)) : { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 }; - const silk = await encode(input, sampleRate); - await fsPromise.writeFile(pttPath, silk.data); + const silk = await piscina.run({ input: input, sampleRate: sampleRate }); + await fsPromise.writeFile(pttPath, Buffer.from(silk.data)); logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); return { converted: true, @@ -86,4 +99,4 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log logger.logError.bind(logger)('convert silk failed', error.stack); return {}; } -} \ No newline at end of file +} diff --git a/src/common/config-base.ts b/src/common/config-base.ts index 8070c79b..4593c78c 100644 --- a/src/common/config-base.ts +++ b/src/common/config-base.ts @@ -8,12 +8,12 @@ export abstract class ConfigBase { configPath: string; configData: T = {} as T; - protected constructor(name: string, core: NapCatCore, configPath: string) { + protected constructor(name: string, core: NapCatCore, configPath: string, copy_default: boolean = true) { this.name = name; this.core = core; this.configPath = configPath; fs.mkdirSync(this.configPath, { recursive: true }); - this.read(); + this.read(copy_default); } protected getKeys(): string[] | null { @@ -32,16 +32,18 @@ export abstract class ConfigBase { } } - read(): T { + read(copy_default: boolean = true): T { const logger = this.core.context.logger; const configPath = this.getConfigPath(this.core.selfInfo.uin); - if (!fs.existsSync(configPath)) { + if (!fs.existsSync(configPath) && copy_default) { try { fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8')); logger.log(`[Core] [Config] 配置文件创建成功!\n`); } catch (e: any) { logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message); } + } else if (!fs.existsSync(configPath) && !copy_default) { + fs.writeFileSync(configPath, '{}'); } try { this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8')); diff --git a/src/common/event.ts b/src/common/event.ts index 98686a5b..821b0eb2 100644 --- a/src/common/event.ts +++ b/src/common/event.ts @@ -21,9 +21,9 @@ type FuncKeys = Extract< export type ListenerClassBase = Record; export class NTEventWrapper { - private WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession - private listenerManager: Map = new Map(); //ListenerName-Unique -> Listener实例 - private EventTask = new Map>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func} + private readonly WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession + private readonly listenerManager: Map = new Map(); //ListenerName-Unique -> Listener实例 + private readonly EventTask = new Map>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func} constructor( wrapperSession: NodeIQQNTWrapperSession, @@ -120,9 +120,9 @@ export class NTEventWrapper { ListenerType extends (...args: any) => any = EnsureFunc, >( listenerAndMethod: `${Listener}/${ListenerMethod}`, + checker: (...args: Parameters) => boolean, waitTimes = 1, timeout = 5000, - checker: (...args: Parameters) => boolean, ) { return new Promise>((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 | 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>, ...retData!]); + } + } + + const ListenerNameList = listenerAndMethod.split('/'); + const ListenerMainName = ListenerNameList[0]; + const ListenerSubName = ListenerNameList[1]; + return new Promise<[EventRet: Awaited>, ...Parameters]>( - async (resolve, reject) => { - const id = randomUUID(); - let complete = 0; - let retData: Parameters | 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>, ...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; 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); }, ); } diff --git a/src/common/file.ts b/src/common/file.ts index 4fb308a4..ad1217e9 100644 --- a/src/common/file.ts +++ b/src/common/file.ts @@ -215,7 +215,7 @@ export async function checkUriType(Uri: string) { } if (uri.startsWith('file://')) { let filePath: string; - const pathname = decodeURIComponent(new URL(uri).pathname); + const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash); if (process.platform === 'win32') { filePath = pathname.slice(1); } else { @@ -242,7 +242,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und //解析Http和Https协议 if (UriType == FileUriType.Unknown) { - return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' }; + return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' }; } //解析File协议和本地文件 if (UriType == FileUriType.Local) { @@ -289,5 +289,5 @@ export async function uri2local(dir: string, uri: string, filename: string | und } return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath }; } - return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' }; + return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' }; } diff --git a/src/common/forward-msg-builder.ts b/src/common/forward-msg-builder.ts index 17c164e9..c091c542 100644 --- a/src/common/forward-msg-builder.ts +++ b/src/common/forward-msg-builder.ts @@ -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); } diff --git a/src/common/log.ts b/src/common/log.ts index e836dfe7..42741beb 100644 --- a/src/common/log.ts +++ b/src/common/log.ts @@ -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.setLogSelfInfo({ nick: '', uin: '', uid: '' }); + + this.logger = winston.createLogger({ + level: 'debug', + format: format.combine( + format.timestamp({ format: '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: '', 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); + setLogSelfInfo(selfInfo: { nick: string, uid: string }) { + const userInfo = `${selfInfo.nick}`; + 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})` + }]`; +} \ No newline at end of file diff --git a/src/common/lru-cache.ts b/src/common/lru-cache.ts index 0e537b6a..ba2cef1d 100644 --- a/src/common/lru-cache.ts +++ b/src/common/lru-cache.ts @@ -1,6 +1,6 @@ export class LRUCache { private capacity: number; - private cache: Map; + public cache: Map; constructor(capacity: number) { this.capacity = capacity; @@ -30,4 +30,13 @@ export class LRUCache { } 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); + } + } + } } \ No newline at end of file diff --git a/src/common/message-unique.ts b/src/common/message-unique.ts index db980a94..d6ca43fc 100644 --- a/src/common/message-unique.ts +++ b/src/common/message-unique.ts @@ -2,8 +2,8 @@ import { Peer } from '@/core'; import crypto from 'crypto'; export class LimitedHashTable { - private keyToValue: Map = new Map(); - private valueToKey: Map = new Map(); + private readonly keyToValue: Map = new Map(); + private readonly valueToKey: Map = new Map(); private maxSize: number; constructor(maxSize: number) { @@ -75,8 +75,8 @@ export class LimitedHashTable { } class MessageUniqueWrapper { - private msgDataMap: LimitedHashTable; - private msgIdMap: LimitedHashTable; + private readonly msgDataMap: LimitedHashTable; + private readonly msgIdMap: LimitedHashTable; constructor(maxMap: number = 1000) { this.msgIdMap = new LimitedHashTable(maxMap); diff --git a/src/common/qq-basic-info.ts b/src/common/qq-basic-info.ts index 126ec37d..91f431e1 100644 --- a/src/common/qq-basic-info.ts +++ b/src/common/qq-basic-info.ts @@ -84,7 +84,7 @@ export class QQBasicInfoWrapper { } // 通过Major拉取 性能差 try { - let majorAppid = this.getAppidV2ByMajor(fullVersion); + const majorAppid = this.getAppidV2ByMajor(fullVersion); if (majorAppid) { this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`); return { appid: majorAppid, qua: this.getQUAFallback() }; @@ -98,8 +98,8 @@ export class QQBasicInfoWrapper { return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() }; } getAppidV2ByMajor(QQVersion: string) { - let majorPath = getMajorPath(QQVersion); - let appid = parseAppidFromMajor(majorPath); + const majorPath = getMajorPath(QQVersion); + const appid = parseAppidFromMajor(majorPath); return appid; } diff --git a/src/common/request.ts b/src/common/request.ts index f3e1cebe..9e23cfd0 100644 --- a/src/common/request.ts +++ b/src/common/request.ts @@ -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(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 { - 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; - }); - } } diff --git a/src/common/version.ts b/src/common/version.ts index 29949b41..9eddc4d7 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const napCatVersion = '3.4.3'; +export const napCatVersion = '4.1.5'; diff --git a/src/common/video.ts b/src/common/video.ts index 559ef44c..906f7e6a 100644 --- a/src/common/video.ts +++ b/src/common/video.ts @@ -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) { diff --git a/src/core/adapters/NodeIDependsAdapter.ts b/src/core/adapters/NodeIDependsAdapter.ts index 87baf540..72723f56 100644 --- a/src/core/adapters/NodeIDependsAdapter.ts +++ b/src/core/adapters/NodeIDependsAdapter.ts @@ -6,8 +6,10 @@ export class NodeIDependsAdapter { } onMSFSsoError(args: unknown) { + } getGroupCode(args: unknown) { + } } diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 122fa946..5954b657 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -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 { 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,83 @@ export class NTQQFileApi { return fileData.filePath!; } - async getImageUrl(element: PicElement) { + async getImageUrl(element: PicElement): Promise { 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 = await this.getRkeyData(); + return this.getImageUrlFromParsedUrl(parsedUrl, rkeyData); + } - const rkeyData = { - private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4', - group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds', - online_rkey: false - }; + 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 { - 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); + 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 (!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}`; - } - } - //到这里说明可能是旧客户端 + + return rkeyData; + } + + private getImageUrlFromParsedUrl(parsedUrl: URL, rkeyData: any): string { + const imageAppid = parsedUrl.searchParams.get('appid'); + const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid); + const imageFileId = parsedUrl.searchParams.get('fileid'); + 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`; + 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 ''; } } diff --git a/src/core/apis/friend.ts b/src/core/apis/friend.ts index b805c729..f128b035 100644 --- a/src/core/apis/friend.ts +++ b/src/core/apis/friend.ts @@ -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 = 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( diff --git a/src/core/apis/group.ts b/src/core/apis/group.ts index 6b238d5b..0380186c 100644 --- a/src/core/apis/group.ts +++ b/src/core/apis/group.ts @@ -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) { - let 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; 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> { - //console.log('getGroupMembers -->', groupQQ); + async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise> { + 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> { 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) { - return this.context.session.getRichMediaService().batchGetGroupFileCount(Gids); + async getGroupFileCount(group_ids: Array) { + return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids); } async getArkJsonGroupShare(GroupCode: string) { diff --git a/src/core/apis/index.ts b/src/core/apis/index.ts index 4c0619e3..27e42258 100644 --- a/src/core/apis/index.ts +++ b/src/core/apis/index.ts @@ -4,5 +4,4 @@ export * from './group'; export * from './msg'; export * from './user'; export * from './webapi'; -export * from './sign'; export * from './system'; \ No newline at end of file diff --git a/src/core/apis/msg.ts b/src/core/apis/msg.ts index 37041dd4..120d3351 100644 --- a/src/core/apis/msg.ts +++ b/src/core/apis/msg.ts @@ -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); } diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 4794f7be..e29dcee9 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -1,26 +1,9 @@ import * as os from 'os'; -import { ChatType, InstanceContext, NapCatCore } from '..'; import offset from '@/core/external/offset.json'; -import { PacketClient, RecvPacketData } from '@/core/packet/client'; -import { PacketSession } from "@/core/packet/session"; -import { OidbPacket, PacketHexStr } from "@/core/packet/packer"; -import { NapProtoMsg } from '@/core/packet/proto/NapProto'; -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 { PacketClientSession } from "@/core/packet/clientSession"; +import { napCatVersion } from "@/common/version"; interface OffsetType { [key: string]: { @@ -35,163 +18,49 @@ export class NTQQPacketApi { context: InstanceContext; core: NapCatCore; logger: LogWrapper; - serverUrl: string | undefined; 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; - const config = this.core.configLoader.configData; - if (config && config.packetServer && config.packetServer.length > 0) { - const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086'; - this.InitSendPacket(serverUrl, this.context.basicInfoWrapper.getFullQQVesion()) - .then() - .catch(this.core.context.logger.logError.bind(this.core.context.logger)); - } else { - this.core.context.logger.logWarn('PacketServer未配置,NapCat.Packet将不会加载!'); - } } - + async initApi() { + await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion()) + .then() + .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(serverUrl: string, qqversion: string) { - this.serverUrl = serverUrl; - this.qqVersion = qqversion; - const offsetTable: OffsetType = offset; - const table = offsetTable[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('PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch()); + const err = `[Core] [Packet] PacketBackend 不支持当前QQ版本架构:${qqVer}-${os.arch()}, + 请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本!`; + this.logger.logError(err); + this.errStack.push(err); return false; } - const url = 'ws://' + this.serverUrl + '/ws'; - this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, 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); + if (this.core.configLoader.configData.packetBackend === 'disable') { + const err = '[Core] [Packet] 已禁用PacketBackend,NapCat.Packet将不会加载!'; + this.logger.logError(err); + this.errStack.push(err); + return false; + } + 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 { - return this.packetSession!.client.sendPacket(cmd, data, rsp); - } - - async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { - 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 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; - } } diff --git a/src/core/apis/sign.ts b/src/core/apis/sign.ts index 9ac71d80..c8e2ad6f 100644 --- a/src/core/apis/sign.ts +++ b/src/core/apis/sign.ts @@ -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!); - } - } diff --git a/src/core/apis/webapi.ts b/src/core/apis/webapi.ts index 63cf2247..8cb147b1 100644 --- a/src/core/apis/webapi.ts +++ b/src/core/apis/webapi.ts @@ -8,6 +8,9 @@ import { WebHonorType, } from '@/core'; import { NapCatCore } from '..'; +import { createReadStream, readFileSync, statSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { basename } from 'node:path'; export class NTQQWebApi { context: InstanceContext; @@ -212,108 +215,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.PERFORMER || 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)('获取群聊炽焰失败'); - } + + if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) { + 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. @@ -346,4 +306,110 @@ export class NTQQWebApi { } return (hash & 0x7FFFFFFF).toString(); } + async createQunAlbumSession(gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, uin: string) { + const img = readFileSync(path); + const img_md5 = createHash('md5').update(img).digest('hex'); + const img_size = img.length; + const img_name = basename(path); + const time = Math.floor(Date.now() / 1000); + const GTK = this.getBknFromSKey(pskey); + const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`; + const body = { + control_req: [{ + uin: uin, + token: { + type: 4, + data: pskey, + appid: 5 + }, + appid: "qun", + checksum: img_md5, + check_type: 0, + file_len: img_size, + env: { + refer: "qzone", + deviceInfo: "h5" + }, + model: 0, + biz_req: { + sPicTitle: img_name, + sPicDesc: "", + sAlbumName: sAlbumName, + sAlbumID: sAlbumID, + iAlbumTypeID: 0, + iBitmap: 0, + iUploadType: 0, + iUpPicType: 0, + iBatchID: time, + sPicPath: "", + iPicWidth: 0, + iPicHight: 0, + iWaterType: 0, + iDistinctUse: 0, + iNeedFeeds: 1, + iUploadTime: time, + mapExt: { + appid: "qun", + userid: gc + } + }, + session: "", + asy_upload: 0, + cmd: "FileUpload" + }] + }; + const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`; + const post = await RequestUtil.HttpGetJson(api, 'POST', body, { + "Cookie": cookie, + "Content-Type": "application/json" + }); + + return post; + } + + async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) { + const img_size = statSync(path).size; + const img_name = basename(path); + let seq = 0; + let offset = 0; + const GTK = this.getBknFromSKey(pskey); + const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`; + + const stream = createReadStream(path, { highWaterMark: slice_size }); + + for await (const chunk of stream) { + const end = Math.min(offset + chunk.length, img_size); + const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; + const formData = await RequestUtil.createFormData(boundary, path); + + const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`; + const body = { + uin: uin, + appid: "qun", + session: session, + offset: offset, + data: formData, + checksum: "", + check_type: 0, + retry: 0, + seq: seq, + end: end, + cmd: "FileUpload", + slice_size: slice_size, + "biz_req.iUploadType": 0 + }; + + const post = await RequestUtil.HttpGetJson(api, 'POST', body, { + "Cookie": cookie, + "Content-Type": `multipart/form-data; boundary=${boundary}` + }); + + offset += chunk.length; + seq++; + } + } + async uploadQunAlbum(path: string, albumId: string, group: string, skey: string, pskey: string, uin: string) { + const session = (await this.createQunAlbumSession(group, albumId, group, path, skey, pskey, uin) as { data: { session: string } }).data.session; + return await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 1024 * 1024); + } } diff --git a/src/core/entities/group.ts b/src/core/entities/group.ts index f486f9ec..780b9f9a 100644 --- a/src/core/entities/group.ts +++ b/src/core/entities/group.ts @@ -117,7 +117,7 @@ export enum GroupMemberRole { } export interface GroupMember { - memberRealLevel: string | undefined; + memberRealLevel: number | undefined; memberSpecialTitle?: string; avatarPath: string; cardName: string; diff --git a/src/core/entities/msg.ts b/src/core/entities/msg.ts index 0a254c53..6519861b 100644 --- a/src/core/entities/msg.ts +++ b/src/core/entities/msg.ts @@ -822,6 +822,8 @@ export interface RawMessage { elements: MessageElement[]; sourceType: MsgSourceType; + + isOnlineMsg: boolean; } export interface QueryMsgsParams { chatInfo: Peer; diff --git a/src/core/entities/user.ts b/src/core/entities/user.ts index 6730ab5a..77455cce 100644 --- a/src/core/entities/user.ts +++ b/src/core/entities/user.ts @@ -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; diff --git a/src/core/external/appid.json b/src/core/external/appid.json index 2280b5d5..59f630d7 100644 --- a/src/core/external/appid.json +++ b/src/core/external/appid.json @@ -51,7 +51,7 @@ "appid": 537249739, "qua": "V1_WIN_NQ_9.9.16_28788_GW_B" }, - "9.9.16-28971":{ + "9.9.16-28971": { "appid": 537249775, "qua": "V1_WIN_NQ_9.9.16_28971_GW_B" }, @@ -62,5 +62,29 @@ "6.9.58-28971": { "appid": 537249826, "qua": "V1_MAC_NQ_6.9.58_28971_GW_B" + }, + "9.9.16-29271": { + "appid": 537249813, + "qua": "V1_WIN_NQ_9.9.16_29271_GW_B" + }, + "3.2.13-29271": { + "appid": 537249913, + "qua": "V1_LNX_NQ_3.2.13_29271_GW_B" + }, + "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" } -} \ No newline at end of file +} diff --git a/src/core/external/napcat.json b/src/core/external/napcat.json index 92432677..44952ac2 100644 --- a/src/core/external/napcat.json +++ b/src/core/external/napcat.json @@ -1,7 +1,8 @@ { - "fileLog": true, + "fileLog": false, "consoleLog": true, "fileLogLevel": "debug", "consoleLogLevel": "info", + "packetBackend": "auto", "packetServer": "" -} \ No newline at end of file +} diff --git a/src/core/external/offset.json b/src/core/external/offset.json index 7652878a..2744cf87 100644 --- a/src/core/external/offset.json +++ b/src/core/external/offset.json @@ -7,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" @@ -35,8 +43,44 @@ "send": "6E91318", "recv": "6E94B50" }, - "6.9.56-28418-arm64": { - "send": "4471360", - "recv": "4473BCC" + "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" + }, + "3.2.13-29271-x64": { + "send": "A11E680", + "recv": "A121F80" + }, + "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" } -} \ No newline at end of file +} diff --git a/src/core/external/proto/EmojiLikeToOthers.proto b/src/core/external/proto/EmojiLikeToOthers.proto deleted file mode 100644 index 3c4e2893..00000000 --- a/src/core/external/proto/EmojiLikeToOthers.proto +++ /dev/null @@ -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; -} diff --git a/src/core/external/proto/GreyTipWrapper.proto b/src/core/external/proto/GreyTipWrapper.proto deleted file mode 100644 index a498edf0..00000000 --- a/src/core/external/proto/GreyTipWrapper.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = 'proto3'; -package SysMessage; - -message GreyTipWrapper { - uint32 subTypeId = 1; - uint32 groupCode = 4; - uint32 subTypeIdMinusOne = 13; - bytes rest = 44; -} diff --git a/src/core/external/proto/ProfileLikeTip.proto b/src/core/external/proto/ProfileLikeTip.proto deleted file mode 100644 index 887f045c..00000000 --- a/src/core/external/proto/ProfileLikeTip.proto +++ /dev/null @@ -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; -} diff --git a/src/core/external/proto/SysMessage.proto b/src/core/external/proto/SysMessage.proto deleted file mode 100644 index c15d920f..00000000 --- a/src/core/external/proto/SysMessage.proto +++ /dev/null @@ -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; -} diff --git a/src/core/helper/adaptDecoder.ts b/src/core/helper/adaptDecoder.ts new file mode 100644 index 00000000..f3910363 --- /dev/null +++ b/src/core/helper/adaptDecoder.ts @@ -0,0 +1,61 @@ +// TODO: further refactor in NapCat.Packet v2 +import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core"; + +const LikeDetail = { + txt: ProtoField(1, ScalarType.STRING), + uin: ProtoField(3, ScalarType.INT64), + nickname: ProtoField(5, ScalarType.STRING) +}; + +const LikeMsg = { + times: ProtoField(1, ScalarType.INT32), + time: ProtoField(2, ScalarType.INT32), + detail: ProtoField(3, () => LikeDetail) +}; + +const ProfileLikeSubTip = { + msg: ProtoField(14, () => LikeMsg) +}; + +const ProfileLikeTip = { + msgType: ProtoField(1, ScalarType.INT32), + subType: ProtoField(2, ScalarType.INT32), + content: ProtoField(203, () => ProfileLikeSubTip) +}; + +const SysMessageHeader = { + PeerNumber: ProtoField(1, ScalarType.UINT32), + PeerString: ProtoField(2, ScalarType.STRING), + Uin: ProtoField(5, ScalarType.UINT32), + Uid: ProtoField(6, ScalarType.STRING, true) +}; + +const SysMessageMsgSpec = { + msgType: ProtoField(1, ScalarType.UINT32), + subType: ProtoField(2, ScalarType.UINT32), + subSubType: ProtoField(3, ScalarType.UINT32), + msgSeq: ProtoField(5, ScalarType.UINT32), + time: ProtoField(6, ScalarType.UINT32), + msgId: ProtoField(12, ScalarType.UINT64), + other: ProtoField(13, ScalarType.UINT32) +}; + +const SysMessageBodyWrapper = { + wrappedBody: ProtoField(2, ScalarType.BYTES) +}; + +const SysMessage = { + header: ProtoField(1, () => SysMessageHeader, false, true), + msgSpec: ProtoField(2, () => SysMessageMsgSpec, false, true), + bodyWrapper: ProtoField(3, () => SysMessageBodyWrapper) +}; + +export function decodeProfileLikeTip(buffer: Uint8Array) { + const msg = new NapProtoMsg(ProfileLikeTip); + return msg.decode(buffer); +} + +export function decodeSysMessage(buffer: Uint8Array) { + const msg = new NapProtoMsg(SysMessage); + return msg.decode(buffer); +} diff --git a/src/core/helper/adaptSysMessageDecoder.ts b/src/core/helper/adaptSysMessageDecoder.ts new file mode 100644 index 00000000..6bd9122a --- /dev/null +++ b/src/core/helper/adaptSysMessageDecoder.ts @@ -0,0 +1,49 @@ +// TODO: further refactor in NapCat.Packet v2 +import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core"; + +const BodyInner = { + msgType: ProtoField(1, ScalarType.UINT32, true), + subType: ProtoField(2, ScalarType.UINT32, true) +}; + +const NoifyData = { + skip: ProtoField(1, ScalarType.BYTES, true), + innerData: ProtoField(2, ScalarType.BYTES, true) +}; + +const MsgHead = { + bodyInner: ProtoField(2, () => BodyInner, true), + noifyData: ProtoField(3, () => NoifyData, true) +}; + +const Message = { + msgHead: ProtoField(1, () => MsgHead) +}; + +const SubDetail = { + msgSeq: ProtoField(1, ScalarType.UINT32), + msgTime: ProtoField(2, ScalarType.UINT32), + senderUid: ProtoField(6, ScalarType.STRING) +}; + +const RecallDetails = { + operatorUid: ProtoField(1, ScalarType.STRING), + subDetail: ProtoField(3, () => SubDetail) +}; + +const RecallGroup = { + type: ProtoField(1, ScalarType.INT32), + peerUid: ProtoField(4, ScalarType.UINT32), + recallDetails: ProtoField(11, () => RecallDetails), + grayTipsSeq: ProtoField(37, ScalarType.UINT32) +}; + +export function decodeMessage(buffer: Uint8Array) { + const msg = new NapProtoMsg(Message); + return msg.decode(buffer); +} + +export function decodeRecallGroup(buffer: Uint8Array){ + const msg = new NapProtoMsg(RecallGroup); + return msg.decode(buffer); +} diff --git a/src/core/index.ts b/src/core/index.ts index a9f11647..123aa9f6 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -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,用于判断成员变化通知 + // TODO: 应该加一个内部自己维护的成员变动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) { diff --git a/src/core/listeners/NodeIKernelBuddyListener.ts b/src/core/listeners/NodeIKernelBuddyListener.ts index fa445bce..94cce080 100644 --- a/src/core/listeners/NodeIKernelBuddyListener.ts +++ b/src/core/listeners/NodeIKernelBuddyListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelFileAssistantListener.ts b/src/core/listeners/NodeIKernelFileAssistantListener.ts index e21f9e0d..47256103 100644 --- a/src/core/listeners/NodeIKernelFileAssistantListener.ts +++ b/src/core/listeners/NodeIKernelFileAssistantListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelGroupListener.ts b/src/core/listeners/NodeIKernelGroupListener.ts index 88a9a14c..eada28ab 100644 --- a/src/core/listeners/NodeIKernelGroupListener.ts +++ b/src/core/listeners/NodeIKernelGroupListener.ts @@ -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) { + onMemberInfoChange(groupCode: string, dateSource: DataSource, members: Map): 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) { + onShutUpMemberListChanged(groupCode: string, members: Array): any { } } \ No newline at end of file diff --git a/src/core/listeners/NodeIKernelLoginListener.ts b/src/core/listeners/NodeIKernelLoginListener.ts index ffabeb3b..91046e0b 100644 --- a/src/core/listeners/NodeIKernelLoginListener.ts +++ b/src/core/listeners/NodeIKernelLoginListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelMsgListener.ts b/src/core/listeners/NodeIKernelMsgListener.ts index 08636920..43ce4466 100644 --- a/src/core/listeners/NodeIKernelMsgListener.ts +++ b/src/core/listeners/NodeIKernelMsgListener.ts @@ -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) { + onRecvSysMsg(arrayList: Array): 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 { } } diff --git a/src/core/listeners/NodeIKernelProfileListener.ts b/src/core/listeners/NodeIKernelProfileListener.ts index cb88c6df..4b8fad1c 100644 --- a/src/core/listeners/NodeIKernelProfileListener.ts +++ b/src/core/listeners/NodeIKernelProfileListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelRecentContactListener.ts b/src/core/listeners/NodeIKernelRecentContactListener.ts index 4faad7be..9797cabe 100644 --- a/src/core/listeners/NodeIKernelRecentContactListener.ts +++ b/src/core/listeners/NodeIKernelRecentContactListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelRobotListener.ts b/src/core/listeners/NodeIKernelRobotListener.ts index e823044d..b8f817a4 100644 --- a/src/core/listeners/NodeIKernelRobotListener.ts +++ b/src/core/listeners/NodeIKernelRobotListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelSearchListener.ts b/src/core/listeners/NodeIKernelSearchListener.ts index 19e35d1e..cd7d3a10 100644 --- a/src/core/listeners/NodeIKernelSearchListener.ts +++ b/src/core/listeners/NodeIKernelSearchListener.ts @@ -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; } diff --git a/src/core/listeners/NodeIKernelSessionListener.ts b/src/core/listeners/NodeIKernelSessionListener.ts index 24866ce2..49d853ba 100644 --- a/src/core/listeners/NodeIKernelSessionListener.ts +++ b/src/core/listeners/NodeIKernelSessionListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelStorageCleanListener.ts b/src/core/listeners/NodeIKernelStorageCleanListener.ts index 392bd5df..37d23db2 100644 --- a/src/core/listeners/NodeIKernelStorageCleanListener.ts +++ b/src/core/listeners/NodeIKernelStorageCleanListener.ts @@ -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 { } } diff --git a/src/core/listeners/NodeIKernelTicketListener.ts b/src/core/listeners/NodeIKernelTicketListener.ts index 15a18c86..3763de5b 100644 --- a/src/core/listeners/NodeIKernelTicketListener.ts +++ b/src/core/listeners/NodeIKernelTicketListener.ts @@ -1,2 +1,5 @@ export class NodeIKernelTicketListener { + listener(): any { + + } } diff --git a/src/core/listeners/NodeIO3MiscListener.ts b/src/core/listeners/NodeIO3MiscListener.ts index f7583d1e..bc7a6ebb 100644 --- a/src/core/listeners/NodeIO3MiscListener.ts +++ b/src/core/listeners/NodeIO3MiscListener.ts @@ -1,5 +1,5 @@ export class NodeIO3MiscListener { - getOnAmgomDataPiece(...arg: unknown[]) { + getOnAmgomDataPiece(...arg: unknown[]): any { } } \ No newline at end of file diff --git a/src/core/packet/client.ts b/src/core/packet/client.ts deleted file mode 100644 index 49400798..00000000 --- a/src/core/packet/client.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { LogWrapper } from "@/common/log"; -import { LRUCache } from "@/common/lru-cache"; -import WebSocket, { Data } from "ws"; -import crypto, { createHash } from "crypto"; -import { NapCatCore } from "@/core"; -import { OidbPacket, PacketHexStr } from "@/core/packet/packer"; - -export interface RecvPacket { - type: string, // 仅recv - trace_id_md5?: string, - data: RecvPacketData -} - -export interface RecvPacketData { - seq: number - cmd: string - hex_data: string -} - -export class PacketClient { - private websocket: WebSocket | undefined; - private isConnected: boolean = false; - private reconnectAttempts: number = 0; - private readonly maxReconnectAttempts: number = 60;//现在暂时不可配置 - private readonly cb = new LRUCache Promise>(500); // trace_id-type callback - private readonly clientUrl: string = ''; - readonly napCatCore: NapCatCore; - private readonly logger: LogWrapper; - - constructor(url: string, core: NapCatCore) { - this.clientUrl = url; - this.napCatCore = core; - this.logger = core.context.logger; - } - - get available(): boolean { - return this.isConnected && this.websocket !== undefined; - } - - private randText(len: number) { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < len; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; - } - - connect(cb: any): Promise { - 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)*/); - - this.websocket.onopen = () => { - this.isConnected = true; - this.reconnectAttempts = 0; - this.logger.log.bind(this.logger)(`[Core] [Packet Server] 已连接到 ${this.clientUrl}`); - cb(); - 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.isConnected = false; - //this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`); - this.attemptReconnect(cb); - }; - }); - } - - 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}`); - } - } - - private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise): Promise { - this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback); - } - - async init(pid: number, recv: string, send: string): Promise { - if (!this.isConnected || !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)); - } - - private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => { - }): Promise { - return new Promise((resolve, reject) => { - if (!this.isConnected || !this.websocket) { - throw new Error("WebSocket is not connected"); - } - const commandMessage = { - action: 'send', - cmd: cmd, - data: data, - trace_id: trace_id - }; - this.websocket.send(JSON.stringify(commandMessage)); - if (rsp) { - this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => { - clearTimeout(timeoutHandle); - resolve(json); - }); - } - this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => { - sendcb(json); - if (!rsp) { - clearTimeout(timeoutHandle); - resolve(json); - } - }); - const timeoutHandle = setTimeout(() => { - reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`)); - }, timeout); - }); - } - - private async handleMessage(message: Data): Promise { - try { - 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); - } catch (error) { - this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`); - } - } - - async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise { - // wtfk tx - // 校验失败和异常 可能返回undefined - 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 () => { - // await sleep(10); - await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id); - }).then((res) => resolve(res)).catch((e: Error) => reject(e)); - }); - } - - async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { - return this.sendPacket(pkt.cmd, pkt.data, rsp); - } -} diff --git a/src/core/packet/client/baseClient.ts b/src/core/packet/client/baseClient.ts new file mode 100644 index 00000000..5516550a --- /dev/null +++ b/src/core/packet/client/baseClient.ts @@ -0,0 +1,91 @@ +import { LRUCache } from "@/common/lru-cache"; +import crypto, { createHash } from "crypto"; +import { PacketContext } from "@/core/packet/context/packetContext"; +import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base"; +import { LogStack } from "@/core/packet/context/clientContext"; + +export interface RecvPacket { + type: string, // 仅recv + trace_id_md5?: string, + data: RecvPacketData +} + +export interface RecvPacketData { + seq: number + cmd: string + hex_data: string +} + +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 Promise>(500); // trace_id-type callback + logStack: LogStack; + available: boolean = false; + + protected constructor(context: PacketContext, logStack: LogStack) { + this.context = context; + this.logStack = logStack; + } + + abstract check(): boolean; + + abstract init(pid: number, recv: string, send: string): Promise; + + abstract sendCommandImpl(cmd: string, data: string, trace_id: string): void; + + private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise): Promise { + this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback); + } + + private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => { + }): Promise { + return new Promise((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) { + clearTimeout(timeoutHandle); + 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 { + 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); + }); + } + + async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { + return this.sendPacket(pkt.cmd, pkt.data, rsp); + } +} diff --git a/src/core/packet/client/nativeClient.ts b/src/core/packet/client/nativeClient.ts new file mode 100644 index 00000000..c4d8bd8d --- /dev/null +++ b/src/core/packet/client/nativeClient.ts @@ -0,0 +1,66 @@ +import { createHash } from "crypto"; +import path, { dirname } from "path"; +import { fileURLToPath } from "url"; +import fs from "fs"; +import { IPacketClient } from "@/core/packet/client/baseClient"; +import { constants } from "node:os"; +import { LRUCache } from "@/common/lru-cache"; +import { PacketContext } from "@/core/packet/context/packetContext"; +import { LogStack } from "@/core/packet/context/clientContext"; + +// 0 send 1 recv +export interface NativePacketExportType { + 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 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(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.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.logStack.pushLogWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`); + return false; + } + return true; + } + + async init(pid: number, recv: string, send: string): Promise { + const platform = process.platform + '.' + process.arch; + const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node'); + process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY); + this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, uin: string, cmd: string, seq: number, hex_data: string) => { + const trace_id = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex'); + if (type === 0 && this.cb.get(trace_id + 'recv')) { + //此时为send 提取seq + this.sendEvent.put(seq, trace_id); + } + if (type === 1 && this.sendEvent.get(seq)) { + //此时为recv 调用callback + const trace_id = this.sendEvent.get(seq); + const callback = this.cb.get(trace_id + 'recv'); + // console.log('callback:', callback, trace_id); + callback?.({ seq, cmd, hex_data }); + } + }); + this.available = true; + } + + sendCommandImpl(cmd: string, data: string, trace_id: string): void { + const trace_id_md5 = createHash('md5').update(trace_id).digest('hex'); + this.MoeHooExport.exports.SendPacket?.(cmd, data, trace_id_md5); + this.cb.get(trace_id_md5 + 'send')?.({ seq: 0, cmd, hex_data: '' }); + } +} diff --git a/src/core/packet/client/wsClient.ts b/src/core/packet/client/wsClient.ts new file mode 100644 index 00000000..8d195e9a --- /dev/null +++ b/src/core/packet/client/wsClient.ts @@ -0,0 +1,112 @@ +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 IPacketClient { + private websocket: WebSocket | null = null; + private reconnectAttempts: number = 0; + private readonly maxReconnectAttempts: number = 60; // 现在暂时不可配置 + private readonly clientUrl: string; + private readonly clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`; + + 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.context.napcore.config.packetServer) { + this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`); + return false; + } + return true; + } + + async init(pid: number, recv: string, send: string): Promise { + 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 { + 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 { + return new Promise((resolve, reject) => { + this.websocket = new WebSocket(this.clientUrl); + this.websocket.onopen = () => { + this.available = true; + this.reconnectAttempts = 0; + 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.onclose = () => { + 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 delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + private async handleMessage(message: Data): Promise { + try { + 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); + } catch (error) { + this.context.logger.error(`解析ws消息时出错: ${(error as Error).message}`); + } + } +} diff --git a/src/core/packet/clientSession.ts b/src/core/packet/clientSession.ts new file mode 100644 index 00000000..3faea314 --- /dev/null +++ b/src/core/packet/clientSession.ts @@ -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 { + return this.context.client.init(pid, recv, send); + } + + get clientLogStack() { + return this.context.client.clientLogStack; + } + + get available() { + return this.context.client.available; + } + + get operation() { + return this.context.operation; + } + + // TODO: global message element adapter (? + get msgConverter() { + return this.context.msgConverter; + } +} diff --git a/src/core/packet/context/clientContext.ts b/src/core/packet/context/clientContext.ts new file mode 100644 index 00000000..1b060b82 --- /dev/null +++ b/src/core/packet/context/clientContext.ts @@ -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 { + await this._client.init(pid, recv, send); + } + + async sendOidbPacket(pkt: OidbPacket, rsp?: T): Promise { + 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; + } +} diff --git a/src/core/packet/context/loggerContext.ts b/src/core/packet/context/loggerContext.ts new file mode 100644 index 00000000..3d6d35da --- /dev/null +++ b/src/core/packet/context/loggerContext.ts @@ -0,0 +1,35 @@ +import { LogLevel, LogWrapper } from "@/common/log"; +import { PacketContext } from "@/core/packet/context/packetContext"; + +// TODO: check bind? +export class PacketLogger { + private readonly napLogger: LogWrapper; + + constructor(context: PacketContext) { + this.napLogger = context.napcore.logger; + } + + private _log(level: LogLevel, ...msg: any[]): void { + this.napLogger._log(level, "[Core] [Packet] " + msg); + } + + debug(...msg: any[]): void { + this._log(LogLevel.DEBUG, msg); + } + + info(...msg: any[]): void { + this._log(LogLevel.INFO, msg); + } + + warn(...msg: any[]): void { + this._log(LogLevel.WARN, msg); + } + + error(...msg: any[]): void { + this._log(LogLevel.ERROR, msg); + } + + fatal(...msg: any[]): void { + this._log(LogLevel.FATAL, msg); + } +} diff --git a/src/core/packet/context/napCoreContext.ts b/src/core/packet/context/napCoreContext.ts new file mode 100644 index 00000000..d19c8616 --- /dev/null +++ b/src/core/packet/context/napCoreContext.ts @@ -0,0 +1,36 @@ +import { NapCatCore } from "@/core"; + +export interface NapCoreCompatBasicInfo { + readonly uin: number; + readonly uid: string; + readonly uin2uid: (uin: number) => Promise; + readonly uid2uin: (uid: string) => Promise; + readonly sendSsoCmdReqByContend: (cmd: string, trace_id: string) => Promise; +} + +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); +} diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts new file mode 100644 index 00000000..3cf21980 --- /dev/null +++ b/src/core/packet/context/operationContext.ts @@ -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) { + 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> { + 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; + } + } +} diff --git a/src/core/packet/context/packetContext.ts b/src/core/packet/context/packetContext.ts new file mode 100644 index 00000000..51804fae --- /dev/null +++ b/src/core/packet/context/packetContext.ts @@ -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); + } +} diff --git a/src/core/packet/entities/aiChat.ts b/src/core/packet/entities/aiChat.ts new file mode 100644 index 00000000..08f1dfda --- /dev/null +++ b/src/core/packet/entities/aiChat.ts @@ -0,0 +1,16 @@ +export enum AIVoiceChatType { + Unknown = 0, + Sound = 1, + Sing = 2 +} + +export interface AIVoiceItem { + voiceId: string; + voiceDisplayName: string; + voiceExampleUrl: string; +} + +export interface AIVoiceItemList { + category: string; + voices: AIVoiceItem[]; +} diff --git a/src/core/packet/highway/client.ts b/src/core/packet/highway/client.ts index 4c788568..e4c4d7fc 100644 --- a/src/core/packet/highway/client.ts +++ b/src/core/packet/highway/client.ts @@ -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; } } diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/highwayContext.ts similarity index 60% rename from src/core/packet/highway/session.ts rename to src/core/packet/highway/highwayContext.ts index 997c5aa9..f02e375c 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/highwayContext.ts @@ -1,24 +1,21 @@ -import * as fs from "node:fs"; -import { ChatType, Peer } from "@/core"; -import { LogWrapper } from "@/common/log"; -import { PacketClient } from "@/core/packet/client"; -import { PacketPacker } from "@/core/packet/packer"; -import { NapProtoMsg } from "@/core/packet/proto/NapProto"; -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 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 | 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('packetServer不可用,请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置'); - } 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 { - 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 { 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 { 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 { 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 { + private async uploadGroupImage(groupUin: number, img: PacketMsgPicElement): Promise { 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 { + private async uploadC2CImage(peerUid: string, img: PacketMsgPicElement): Promise { 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 { + private async uploadGroupVideo(groupUin: number, video: PacketMsgVideoElement): Promise { 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 { + private async uploadC2CVideo(peerUid: string, video: PacketMsgVideoElement): Promise { 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 { + private async uploadGroupPtt(groupUin: number, ptt: PacketMsgPttElement): Promise { 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 { + private async uploadC2CPtt(peerUid: string, ptt: PacketMsgPttElement): Promise { 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 { + private async uploadGroupFile(groupUin: number, file: PacketMsgFileElement): Promise { 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 { + private async uploadC2CFile(peerUid: string, file: PacketMsgFileElement): Promise { 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; } diff --git a/src/core/packet/highway/uploader.ts b/src/core/packet/highway/uploader.ts deleted file mode 100644 index 49ee5fe7..00000000 --- a/src/core/packet/highway/uploader.ts +++ /dev/null @@ -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 "@/core/packet/proto/NapProto"; -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 { - return new Promise((_, 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; -} - -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 { - const controller = new AbortController(); - const { signal } = controller; - const upload = new Promise((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 { - 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 { - 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 { - 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); - } - }); - } -} diff --git a/src/core/packet/highway/uploader/highwayHttpUploader.ts b/src/core/packet/highway/uploader/highwayHttpUploader.ts new file mode 100644 index 00000000..962261a2 --- /dev/null +++ b/src/core/packet/highway/uploader/highwayHttpUploader.ts @@ -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 { + 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 { + 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 { + 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)); + } + }); + } +} diff --git a/src/core/packet/highway/uploader/highwayTcpUploader.ts b/src/core/packet/highway/uploader/highwayTcpUploader.ts new file mode 100644 index 00000000..69e02ed2 --- /dev/null +++ b/src/core/packet/highway/uploader/highwayTcpUploader.ts @@ -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 { + const controller = new AbortController(); + const { signal } = controller; + const upload = new Promise((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]); + } +} diff --git a/src/core/packet/highway/uploader/highwayUploader.ts b/src/core/packet/highway/uploader/highwayUploader.ts new file mode 100644 index 00000000..c8902dff --- /dev/null +++ b/src/core/packet/highway/uploader/highwayUploader.ts @@ -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 { + return new Promise((_, 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; +} diff --git a/src/core/packet/highway/utils.ts b/src/core/packet/highway/utils.ts index e786afbd..64b84cef 100644 --- a/src/core/packet/highway/utils.ts +++ b/src/core/packet/highway/utils.ts @@ -1,20 +1,20 @@ -import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; -import { IPv4 } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; -import { NTHighwayIPv4 } from "@/core/packet/proto/highway/highway"; +import { NapProtoEncodeStructType } from "@napneko/nap-proto-core"; +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[]): NapProtoEncodeStructType[] =>{ +export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType[]): NapProtoEncodeStructType[] => { return ipv4s.map((ip) => { return { domain: { isEnable: true, - ip: int32ip2str(ip.outIP!), + ip: int32ip2str(ip.outIP ?? 0), }, port: ip.outPort! - } as NapProtoEncodeStructType; + } as NapProtoEncodeStructType; }); }; diff --git a/src/core/packet/message/builder.ts b/src/core/packet/message/builder.ts index 63f92593..addb8771 100644 --- a/src/core/packet/message/builder.ts +++ b/src/core/packet/message/builder.ts @@ -1,21 +1,14 @@ import * as crypto from "crypto"; -import { PushMsgBody } from "@/core/packet/proto/message/message"; -import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; -import { LogWrapper } from "@/common/log"; +import { PushMsgBody } from "@/core/packet/transformer/proto"; +import { NapProtoEncodeStructType } from "@napneko/nap-proto-core"; 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 => { 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) => { - 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 { diff --git a/src/core/packet/message/converter.ts b/src/core/packet/message/converter.ts index 2414e946..cb193ed4 100644 --- a/src/core/packet/message/converter.ts +++ b/src/core/packet/message/converter.ts @@ -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[] } +// TODO: make it become adapter? export class PacketMsgConverter { - private logger: LogWrapper; - - constructor(logger: LogWrapper) { - this.logger = logger; - } - private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters { return SupportedElementTypes.includes(type); } - rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg { - return { - senderUid: msg.senderUid ?? '', - senderUin: msg.senderUin, - senderName: msg.senderName, - groupId: msg.groupId, - time: msg.time, - msg: msg.msg.map((element) => { - if (!this.isValidElementType(element.elementType)) return null; - return this.rawToPacketMsgConverters[element.elementType](element as MessageElement); - }).filter((e) => e !== null) - }; - } - - rawMsgToPacketMsg(msg: RawMessage, ctxPeer: Peer): PacketMsg { - return { - seq: +msg.msgSeq, - groupId: ctxPeer.chatType === ChatType.KCHATTYPEGROUP ? +msg.peerUid : undefined, - senderUid: msg.senderUid, - senderUin: +msg.senderUin, - senderName: msg.sendMemberName && msg.sendMemberName !== '' - ? msg.sendMemberName - : msg.sendNickName && msg.sendNickName !== '' - ? msg.sendNickName - : "QQ用户", - time: +msg.msgTime, - msg: msg.elements.map((element) => { - if (!this.isValidElementType(element.elementType)) return null; - return this.rawToPacketMsgConverters[element.elementType](element); - }).filter((e) => e !== null) - }; - } - - private rawToPacketMsgConverters: ElementToPacketMsgConverters = { + private readonly rawToPacketMsgConverters: ElementToPacketMsgConverters = { [ElementType.TEXT]: (element) => { if (element.textElement?.atType) { return new PacketMsgAtElement(element as SendTextElement); @@ -160,4 +121,37 @@ export class PacketMsgConverter { return new PacketMultiMsgElement(element as SendStructLongMsgElement); } }; + + rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg { + return { + senderUid: msg.senderUid ?? '', + senderUin: msg.senderUin, + senderName: msg.senderName, + groupId: msg.groupId, + time: msg.time, + msg: msg.msg.map((element) => { + if (!this.isValidElementType(element.elementType)) return null; + return this.rawToPacketMsgConverters[element.elementType](element as MessageElement); + }).filter((e) => e !== null) + }; + } + + rawMsgToPacketMsg(msg: RawMessage, ctxPeer: Peer): PacketMsg { + return { + seq: +msg.msgSeq, + groupId: ctxPeer.chatType === ChatType.KCHATTYPEGROUP ? +msg.peerUid : undefined, + senderUid: msg.senderUid, + senderUin: +msg.senderUin, + senderName: msg.sendMemberName && msg.sendMemberName !== '' + ? msg.sendMemberName + : msg.sendNickName && msg.sendNickName !== '' + ? msg.sendNickName + : "QQ用户", + time: +msg.msgTime, + msg: msg.elements.map((element) => { + if (!this.isValidElementType(element.elementType)) return null; + return this.rawToPacketMsgConverters[element.elementType](element); + }).filter((e) => e !== null) + }; + } } diff --git a/src/core/packet/message/element.ts b/src/core/packet/message/element.ts index 9e6853dc..8142f1cb 100644 --- a/src/core/packet/message/element.ts +++ b/src/core/packet/message/element.ts @@ -1,5 +1,5 @@ import * as zlib from "node:zlib"; -import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto"; +import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core"; import { CustomFace, Elem, @@ -7,8 +7,12 @@ import { MentionExtra, NotOnlineImage, QBigFaceExtra, - QSmallFaceExtra -} from "@/core/packet/proto/message/element"; + QSmallFaceExtra, + MsgInfo, + OidbSvcTrpcTcp0XE37_800Response, + FileExtra, + GroupFileExtra +} from "@/core/packet/transformer/proto"; import { AtType, PicType, @@ -24,11 +28,8 @@ import { SendTextElement, SendVideoElement } from "@/core"; -import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; -import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message"; import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; -import { FileExtra, GroupFileExtra } from "@/core/packet/proto/message/component"; -import { OidbSvcTrpcTcp0XE37_800Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; +import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message"; // raw <-> packet // TODO: SendStructLongMsgElement @@ -241,7 +242,7 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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]) - }) - ); - } - - 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: "" - } - } - ) - ); - } -} diff --git a/src/core/packet/proto/NapProto.ts b/src/core/packet/proto/NapProto.ts deleted file mode 100644 index 7a42b22f..00000000 --- a/src/core/packet/proto/NapProto.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { MessageType, PartialMessage, RepeatType, ScalarType } from '@protobuf-ts/runtime'; -import { PartialFieldInfo } from "@protobuf-ts/runtime/build/types/reflection-info"; - -type LowerCamelCase = CamelCaseHelper; - -type CamelCaseHelper< - S extends string, - CapNext extends boolean, - IsFirstChar extends boolean -> = S extends `${infer F}${infer R}` - ? F extends '_' - ? CamelCaseHelper - : F extends `${number}` - ? `${F}${CamelCaseHelper}` - : CapNext extends true - ? `${Uppercase}${CamelCaseHelper}` - : IsFirstChar extends true - ? `${Lowercase}${CamelCaseHelper}` - : `${F}${CamelCaseHelper}` - : ''; - -type ScalarTypeToTsType = - T extends ScalarType.DOUBLE | ScalarType.FLOAT | ScalarType.INT32 | ScalarType.FIXED32 | ScalarType.UINT32 | ScalarType.SFIXED32 | ScalarType.SINT32 ? number : - T extends ScalarType.INT64 | ScalarType.UINT64 | ScalarType.FIXED64 | ScalarType.SFIXED64 | ScalarType.SINT64 ? bigint : - T extends ScalarType.BOOL ? boolean : - T extends ScalarType.STRING ? string : - T extends ScalarType.BYTES ? Uint8Array : - never; - -interface BaseProtoFieldType { - kind: 'scalar' | 'message'; - no: number; - type: T; - optional: O; - repeat: R; -} - -interface ScalarProtoFieldType extends BaseProtoFieldType { - kind: 'scalar'; -} - -interface MessageProtoFieldType ProtoMessageType, O extends boolean, R extends O extends true ? false : boolean> extends BaseProtoFieldType { - kind: 'message'; -} - -type ProtoFieldType = - | ScalarProtoFieldType - | MessageProtoFieldType<() => ProtoMessageType, boolean, boolean>; - -type ProtoMessageType = { - [key: string]: ProtoFieldType; -}; - -export function ProtoField(no: number, type: T, optional?: O, repeat?: R): ScalarProtoFieldType; -export function ProtoField ProtoMessageType, O extends boolean = false, R extends O extends true ? false : boolean = false>(no: number, type: T, optional?: O, repeat?: R): MessageProtoFieldType; -export function ProtoField(no: number, type: ScalarType | (() => ProtoMessageType), optional?: boolean, repeat?: boolean): ProtoFieldType { - if (typeof type === 'function') { - return { kind: 'message', no: no, type: type, optional: optional ?? false, repeat: repeat ?? false }; - } else { - return { kind: 'scalar', no: no, type: type, optional: optional ?? false, repeat: repeat ?? false }; - } -} - -type ProtoFieldReturnType = NonNullable extends ScalarProtoFieldType - ? ScalarTypeToTsType - : T extends NonNullable> - ? NonNullable, E>> - : never; - -type RequiredFieldsBaseType = { - [K in keyof T as T[K] extends { optional: true } ? never : LowerCamelCase]: - T[K] extends { repeat: true } - ? ProtoFieldReturnType[] - : ProtoFieldReturnType -} - -type OptionalFieldsBaseType = { - [K in keyof T as T[K] extends { optional: true } ? LowerCamelCase : never]?: - T[K] extends { repeat: true } - ? ProtoFieldReturnType[] - : ProtoFieldReturnType -} - -type RequiredFieldsType = E extends true ? Partial> : RequiredFieldsBaseType; - -type OptionalFieldsType = E extends true ? Partial> : OptionalFieldsBaseType; - -type NapProtoStructType = RequiredFieldsType & OptionalFieldsType; - -export type NapProtoEncodeStructType = NapProtoStructType; - -export type NapProtoDecodeStructType = NapProtoStructType; - -const NapProtoMsgCache = new Map>>(); - -export class NapProtoMsg { - private readonly _msg: T; - private readonly _field: PartialFieldInfo[]; - private readonly _proto_msg: MessageType>; - - constructor(fields: T) { - this._msg = fields; - this._field = Object.keys(fields).map(key => { - const field = fields[key]; - if (field.kind === 'scalar') { - const repeatType = field.repeat - ? [ScalarType.STRING, ScalarType.BYTES].includes(field.type) - ? RepeatType.UNPACKED - : RepeatType.PACKED - : RepeatType.NO; - return { - no: field.no, - name: key, - kind: 'scalar', - T: field.type, - opt: field.optional, - repeat: repeatType, - }; - } else if (field.kind === 'message') { - return { - no: field.no, - name: key, - kind: 'message', - repeat: field.repeat ? RepeatType.PACKED : RepeatType.NO, - T: () => new NapProtoMsg(field.type())._proto_msg, - }; - } - }) as PartialFieldInfo[]; - this._proto_msg = new MessageType>('nya', this._field); - } - - encode(data: NapProtoEncodeStructType): Uint8Array { - return this._proto_msg.toBinary(this._proto_msg.create(data as PartialMessage>)); - } - - decode(data: Uint8Array): NapProtoDecodeStructType { - return this._proto_msg.fromBinary(data) as NapProtoDecodeStructType; - } -} diff --git a/src/core/packet/proto/old/Message.ts b/src/core/packet/proto/old/Message.ts deleted file mode 100644 index 8eb7d3b4..00000000 --- a/src/core/packet/proto/old/Message.ts +++ /dev/null @@ -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) }); -} diff --git a/src/core/packet/proto/old/ProfileLike.ts b/src/core/packet/proto/old/ProfileLike.ts deleted file mode 100644 index e79f089f..00000000 --- a/src/core/packet/proto/old/ProfileLike.ts +++ /dev/null @@ -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) }); -} diff --git a/src/core/packet/session.ts b/src/core/packet/session.ts deleted file mode 100644 index 7e636281..00000000 --- a/src/core/packet/session.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PacketClient } from "@/core/packet/client"; -import { PacketHighwaySession } from "@/core/packet/highway/session"; -import { LogWrapper } from "@/common/log"; -import { PacketPacker } from "@/core/packet/packer"; - -export class PacketSession { - readonly logger: LogWrapper; - readonly client: PacketClient; - readonly packer: PacketPacker; - readonly highwaySession: PacketHighwaySession; - - constructor(logger: LogWrapper, client: PacketClient) { - this.logger = logger; - this.client = client; - this.packer = new PacketPacker(this.logger, this.client); - this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer); - } -} diff --git a/src/core/packet/transformer/action/FetchAiVoiceList.ts b/src/core/packet/transformer/action/FetchAiVoiceList.ts new file mode 100644 index 00000000..3617b81f --- /dev/null +++ b/src/core/packet/transformer/action/FetchAiVoiceList.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/action/GetAiVoice.ts b/src/core/packet/transformer/action/GetAiVoice.ts new file mode 100644 index 00000000..d5b9c6c1 --- /dev/null +++ b/src/core/packet/transformer/action/GetAiVoice.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts b/src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts new file mode 100644 index 00000000..e9333f05 --- /dev/null +++ b/src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/action/GetStrangerInfo.ts b/src/core/packet/transformer/action/GetStrangerInfo.ts new file mode 100644 index 00000000..8ed74260 --- /dev/null +++ b/src/core/packet/transformer/action/GetStrangerInfo.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/action/GroupSign.ts b/src/core/packet/transformer/action/GroupSign.ts new file mode 100644 index 00000000..e8379279 --- /dev/null +++ b/src/core/packet/transformer/action/GroupSign.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/action/SendPoke.ts b/src/core/packet/transformer/action/SendPoke.ts new file mode 100644 index 00000000..ba5ea446 --- /dev/null +++ b/src/core/packet/transformer/action/SendPoke.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/action/SetSpecialTitle.ts b/src/core/packet/transformer/action/SetSpecialTitle.ts new file mode 100644 index 00000000..ce99e023 --- /dev/null +++ b/src/core/packet/transformer/action/SetSpecialTitle.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/action/index.ts b/src/core/packet/transformer/action/index.ts new file mode 100644 index 00000000..a92c7321 --- /dev/null +++ b/src/core/packet/transformer/action/index.ts @@ -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'; diff --git a/src/core/packet/transformer/base.ts b/src/core/packet/transformer/base.ts new file mode 100644 index 00000000..eda5b730 --- /dev/null +++ b/src/core/packet/transformer/base.ts @@ -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 { + protected msgBuilder: PacketMsgBuilder; + + protected constructor() { + this.msgBuilder = new PacketMsgBuilder(); + } + + abstract build(...args: any[]): OidbPacket | Promise; + + abstract parse(data: Buffer): NapProtoDecodeStructType; +} diff --git a/src/core/packet/transformer/highway/DownloadGroupFile.ts b/src/core/packet/transformer/highway/DownloadGroupFile.ts new file mode 100644 index 00000000..00e077ff --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadGroupFile.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/highway/DownloadGroupPtt.ts b/src/core/packet/transformer/highway/DownloadGroupPtt.ts new file mode 100644 index 00000000..c724868a --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadGroupPtt.ts @@ -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 { + constructor() { + super(); + } + + build(groupUin: number, node: NapProtoEncodeStructType): 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(); diff --git a/src/core/packet/transformer/highway/DownloadOfflineFile.ts b/src/core/packet/transformer/highway/DownloadOfflineFile.ts new file mode 100644 index 00000000..1ca3af7b --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadOfflineFile.ts @@ -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 { + constructor() { + super(); + } + + build(fileUUID: string, fileHash: string, senderUid: string, receiverUid: string): OidbPacket { + const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37_800).encode({ + subCommand: 800, + field2: 0, + body: { + senderUid: senderUid, + receiverUid: receiverUid, + fileUuid: fileUUID, + fileHash: fileHash, + }, + field101: 3, + field102: 1, + field200: 1, + }); + return OidbBase.build(0xE37, 800, body, false, false); + } + + // TODO:check + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37Response).decode(oidbBody); + } +} + +export default new DownloadOfflineFile(); diff --git a/src/core/packet/transformer/highway/DownloadPrivateFile.ts b/src/core/packet/transformer/highway/DownloadPrivateFile.ts new file mode 100644 index 00000000..6355c08e --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadPrivateFile.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/highway/FetchSessionKey.ts b/src/core/packet/transformer/highway/FetchSessionKey.ts new file mode 100644 index 00000000..324968e9 --- /dev/null +++ b/src/core/packet/transformer/highway/FetchSessionKey.ts @@ -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 { + constructor() { + super(); + } + + build(): OidbPacket { + const req = new NapProtoMsg(proto.HttpConn0x6ff_501).encode({ + httpConn: { + field1: 0, + field2: 0, + field3: 16, + field4: 1, + field6: 3, + serviceTypes: [1, 5, 10, 21], + // tgt: "", // TODO: do we really need tgt? seems not + field9: 2, + field10: 9, + field11: 8, + ver: "1.0.1" + } + }); + return { + cmd: "HttpConn.0x6ff_501", + data: PacketHexStrBuilder(req) + }; + } + + parse(data: Buffer) { + return new NapProtoMsg(proto.HttpConn0x6ff_501Response).decode(data); + } +} + +export default new FetchSessionKey(); diff --git a/src/core/packet/transformer/highway/UploadGroupFile.ts b/src/core/packet/transformer/highway/UploadGroupFile.ts new file mode 100644 index 00000000..f5e91b82 --- /dev/null +++ b/src/core/packet/transformer/highway/UploadGroupFile.ts @@ -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 { + constructor() { + super(); + } + + build(groupUin: number, file: PacketMsgFileElement): OidbPacket { + const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({ + file: { + groupUin: groupUin, + appId: 4, + busId: 102, + entrance: 6, + targetDirectory: '/', // TODO: + fileName: file.fileName, + localDirectory: `/${file.fileName}`, + fileSize: BigInt(file.fileSize), + fileMd5: file.fileMd5, + fileSha1: file.fileSha1, + fileSha3: Buffer.alloc(0), + field15: true + } + }); + return OidbBase.build(0x6D6, 0, body, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody); + } +} + +export default new UploadGroupFile(); diff --git a/src/core/packet/transformer/highway/UploadGroupImage.ts b/src/core/packet/transformer/highway/UploadGroupImage.ts new file mode 100644 index 00000000..6c38cfbd --- /dev/null +++ b/src/core/packet/transformer/highway/UploadGroupImage.ts @@ -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 { + constructor() { + super(); + } + + build(groupUin: number, img: PacketMsgPicElement): OidbPacket { + const data = new NapProtoMsg(proto.NTV2RichMediaReq).encode( + { + reqHead: { + common: { + requestId: 1, + command: 100 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 2, + group: { + groupUin: groupUin + }, + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: +img.size, + fileHash: img.md5, + fileSha1: img.sha1!, + fileName: img.name, + type: { + type: 1, + picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa + videoFormat: 0, + voiceFormat: 0, + }, + width: img.width, + height: img.height, + time: 0, + original: 1 + }, + subFileType: 0, + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic: { + bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), + textSummary: "Nya~", // TODO: + }, + video: { + bytesPbReserve: Buffer.alloc(0), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false, + } + } + ); + return OidbBase.build(0x11C4, 100, data, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new UploadGroupImage(); diff --git a/src/core/packet/transformer/highway/UploadGroupPtt.ts b/src/core/packet/transformer/highway/UploadGroupPtt.ts new file mode 100644 index 00000000..820c56de --- /dev/null +++ b/src/core/packet/transformer/highway/UploadGroupPtt.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/highway/UploadGroupVideo.ts b/src/core/packet/transformer/highway/UploadGroupVideo.ts new file mode 100644 index 00000000..0f8e12b8 --- /dev/null +++ b/src/core/packet/transformer/highway/UploadGroupVideo.ts @@ -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 { + 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(); diff --git a/src/core/packet/transformer/highway/UploadPrivateFile.ts b/src/core/packet/transformer/highway/UploadPrivateFile.ts new file mode 100644 index 00000000..30a94f4c --- /dev/null +++ b/src/core/packet/transformer/highway/UploadPrivateFile.ts @@ -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 { + constructor() { + super(); + } + + async build(selfUid: string, peerUid: string, file: PacketMsgFileElement): Promise { + 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(); diff --git a/src/core/packet/transformer/highway/UploadPrivateImage.ts b/src/core/packet/transformer/highway/UploadPrivateImage.ts new file mode 100644 index 00000000..9b9b708c --- /dev/null +++ b/src/core/packet/transformer/highway/UploadPrivateImage.ts @@ -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 { + constructor() { + super(); + } + + build(peerUin: string, img: PacketMsgPicElement): OidbPacket { + const data = new NapProtoMsg(proto.NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 100 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + }, + }, + client: { + agentType: 2, + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: +img.size, + fileHash: img.md5, + fileSha1: img.sha1!, + fileName: img.name, + type: { + type: 1, + picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa + videoFormat: 0, + voiceFormat: 0, + }, + width: img.width, + height: img.height, + time: 0, + original: 1 + }, + subFileType: 0, + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 1, + extBizInfo: { + pic: { + bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), + textSummary: "Nya~", // TODO: + }, + video: { + bytesPbReserve: Buffer.alloc(0), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false, + } + } + ); + return OidbBase.build(0x11C5, 100, data,true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new UploadPrivateImage(); diff --git a/src/core/packet/transformer/highway/UploadPrivatePtt.ts b/src/core/packet/transformer/highway/UploadPrivatePtt.ts new file mode 100644 index 00000000..e943bfd1 --- /dev/null +++ b/src/core/packet/transformer/highway/UploadPrivatePtt.ts @@ -0,0 +1,81 @@ +import * as proto from "@/core/packet/transformer/proto"; +import { NapProtoMsg } from "@napneko/nap-proto-core"; +import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base"; +import OidbBase from "@/core/packet/transformer/oidb/oidbBase"; +import crypto from "node:crypto"; +import { PacketMsgPttElement } from "@/core/packet/message/element"; + +class UploadPrivatePtt extends PacketTransformer { + constructor() { + super(); + } + + build(peerUin: string, ptt: PacketMsgPttElement): OidbPacket { + const data = new NapProtoMsg(proto.NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 4, + command: 100 + }, + scene: { + requestType: 2, + businessType: 3, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + } + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: ptt.fileSize, + fileHash: ptt.fileMd5, + fileSha1: ptt.fileSha1, + fileName: `${ptt.fileMd5}.amr`, + type: { + type: 3, + picFormat: 0, + videoFormat: 0, + voiceFormat: 1 + }, + height: 0, + width: 0, + time: ptt.fileDuration, + original: 0 + }, + subFileType: 0 + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 1, + extBizInfo: { + pic: { + textSummary: "Nya~", + }, + ptt: { + bytesReserve: Buffer.from([0x08, 0x00, 0x38, 0x00]), + bytesGeneralFlags: Buffer.from([0x9a, 0x01, 0x0b, 0xaa, 0x03, 0x08, 0x08, 0x04, 0x12, 0x04, 0x00, 0x00, 0x00, 0x00]), + } + }, + clientSeq: 0, + noNeedCompatMsg: false + } + }); + return OidbBase.build(0x126D, 100, data, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new UploadPrivatePtt(); diff --git a/src/core/packet/transformer/highway/UploadPrivateVideo.ts b/src/core/packet/transformer/highway/UploadPrivateVideo.ts new file mode 100644 index 00000000..f47312c5 --- /dev/null +++ b/src/core/packet/transformer/highway/UploadPrivateVideo.ts @@ -0,0 +1,105 @@ +import * as proto from "@/core/packet/transformer/proto"; +import { NapProtoMsg } from "@napneko/nap-proto-core"; +import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base"; +import OidbBase from "@/core/packet/transformer/oidb/oidbBase"; +import crypto from "node:crypto"; +import { PacketMsgVideoElement } from "@/core/packet/message/element"; + +class UploadPrivateVideo extends PacketTransformer { + constructor() { + super(); + } + + build(peerUin: string, video: PacketMsgVideoElement): OidbPacket { + if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); + const data = new NapProtoMsg(proto.NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 3, + command: 100 + }, + scene: { + requestType: 2, + businessType: 2, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin + } + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: +video.fileSize, + fileHash: video.fileMd5, + fileSha1: video.fileSha1, + fileName: "nya.mp4", + type: { + type: 2, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: 0, + width: 0, + time: 0, + original: 0 + }, + subFileType: 0 + }, { + fileInfo: { + fileSize: +video.thumbSize, + fileHash: video.thumbMd5, + fileSha1: video.thumbSha1, + fileName: "nya.jpg", + type: { + type: 1, + picFormat: 0, + videoFormat: 0, + voiceFormat: 0 + }, + height: video.thumbHeight, + width: video.thumbWidth, + time: 0, + original: 0 + }, + subFileType: 100 + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic: { + bizType: 0, + textSummary: "Nya~", + }, + video: { + bytesPbReserve: Buffer.from([0x80, 0x01, 0x00]), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false + } + }); + return OidbBase.build(0x11E9, 100, data, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new UploadPrivateVideo(); diff --git a/src/core/packet/transformer/highway/index.ts b/src/core/packet/transformer/highway/index.ts new file mode 100644 index 00000000..9444789d --- /dev/null +++ b/src/core/packet/transformer/highway/index.ts @@ -0,0 +1,13 @@ +export { default as DownloadGroupFile } from './DownloadGroupFile'; +export { default as DownloadGroupPtt } from './DownloadGroupPtt'; +export { default as DownloadOfflineFile } from './DownloadOfflineFile'; +export { default as DownloadPrivateFile } from './DownloadPrivateFile'; +export { default as FetchSessionKey } from './FetchSessionKey'; +export { default as UploadGroupFile } from './UploadGroupFile'; +export { default as UploadGroupImage } from './UploadGroupImage'; +export { default as UploadGroupPtt } from './UploadGroupPtt'; +export { default as UploadGroupVideo } from './UploadGroupVideo'; +export { default as UploadPrivateFile } from './UploadPrivateFile'; +export { default as UploadPrivateImage } from './UploadPrivateImage'; +export { default as UploadPrivatePtt } from './UploadPrivatePtt'; +export { default as UploadPrivateVideo } from './UploadPrivateVideo'; diff --git a/src/core/packet/transformer/index.ts b/src/core/packet/transformer/index.ts new file mode 100644 index 00000000..e4693da8 --- /dev/null +++ b/src/core/packet/transformer/index.ts @@ -0,0 +1,4 @@ +export * from './action'; +export * from './highway'; +export * from './message'; +export * from './system'; diff --git a/src/core/packet/transformer/message/UploadForwardMsg.ts b/src/core/packet/transformer/message/UploadForwardMsg.ts new file mode 100644 index 00000000..7b88da1a --- /dev/null +++ b/src/core/packet/transformer/message/UploadForwardMsg.ts @@ -0,0 +1,51 @@ +import zlib from "node:zlib"; +import * as proto from "@/core/packet/transformer/proto"; +import { NapProtoMsg } from "@napneko/nap-proto-core"; +import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from "@/core/packet/transformer/base"; +import { PacketMsg } from "@/core/packet/message/message"; + +class UploadForwardMsg extends PacketTransformer { + constructor() { + super(); + } + + build(selfUid: string, msg: PacketMsg[], groupUin: number = 0): OidbPacket { + const msgBody = this.msgBuilder.buildFakeMsg(selfUid, msg); + const longMsgResultData = new NapProtoMsg(proto.LongMsgResult).encode( + { + action: { + actionCommand: "MultiMsg", + actionData: { + msgBody: msgBody + } + } + } + ); + const payload = zlib.gzipSync(Buffer.from(longMsgResultData)); + const req = new NapProtoMsg(proto.SendLongMsgReq).encode( + { + info: { + type: groupUin === 0 ? 1 : 3, + uid: { + uid: groupUin === 0 ? selfUid : groupUin.toString(), + }, + groupUin: groupUin, + payload: payload + }, + settings: { + field1: 4, field2: 1, field3: 7, field4: 0 + } + } + ); + return { + cmd: "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", + data: PacketHexStrBuilder(req) + }; + } + + parse(data: Buffer) { + return new NapProtoMsg(proto.SendLongMsgResp).decode(data); + } +} + +export default new UploadForwardMsg(); diff --git a/src/core/packet/transformer/message/index.ts b/src/core/packet/transformer/message/index.ts new file mode 100644 index 00000000..88148753 --- /dev/null +++ b/src/core/packet/transformer/message/index.ts @@ -0,0 +1 @@ +export { default as UploadForwardMsg } from './UploadForwardMsg'; diff --git a/src/core/packet/transformer/oidb/oidbBase.ts b/src/core/packet/transformer/oidb/oidbBase.ts new file mode 100644 index 00000000..23cffefc --- /dev/null +++ b/src/core/packet/transformer/oidb/oidbBase.ts @@ -0,0 +1,32 @@ +import * as proto from "@/core/packet/transformer/proto"; +import { NapProtoMsg } from "@napneko/nap-proto-core"; +import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from "@/core/packet/transformer/base"; + +class OidbBase extends PacketTransformer { + constructor() { + super(); + } + + build(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket { + const data = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).encode({ + command: cmd, + subCommand: subCmd, + body: body, + isReserved: isUid ? 1 : 0 + }); + return { + cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`, + data: PacketHexStrBuilder(data), + }; + } + + parse(data: Buffer) { + const res = new NapProtoMsg(proto.OidbSvcTrpcTcpBase).decode(data); + if (res.errorCode !== 0) { + throw new Error(`OidbSvcTrpcTcpBase parse error: ${res.errorMsg} (code=${res.errorCode})`); + } + return res; + } +} + +export default new OidbBase(); diff --git a/src/core/packet/proto/action/action.ts b/src/core/packet/transformer/proto/action/action.ts similarity index 97% rename from src/core/packet/proto/action/action.ts rename to src/core/packet/transformer/proto/action/action.ts index af6b45d6..90f5add8 100644 --- a/src/core/packet/proto/action/action.ts +++ b/src/core/packet/transformer/proto/action/action.ts @@ -1,6 +1,6 @@ import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; -import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/proto/message/message"; +import { ProtoField } from "@napneko/nap-proto-core"; +import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/transformer/proto"; export const FaceRoamRequest = { comm: ProtoField(1, () => PlatInfo, true), diff --git a/src/core/packet/proto/action/miniAppAdaptShareInfo.ts b/src/core/packet/transformer/proto/action/miniAppAdaptShareInfo.ts similarity index 94% rename from src/core/packet/proto/action/miniAppAdaptShareInfo.ts rename to src/core/packet/transformer/proto/action/miniAppAdaptShareInfo.ts index 76bb2829..c931992d 100644 --- a/src/core/packet/proto/action/miniAppAdaptShareInfo.ts +++ b/src/core/packet/transformer/proto/action/miniAppAdaptShareInfo.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const MiniAppAdaptShareInfoReq = { appId: ProtoField(2, ScalarType.STRING), diff --git a/src/core/packet/proto/highway/highway.ts b/src/core/packet/transformer/proto/highway/highway.ts similarity index 96% rename from src/core/packet/proto/highway/highway.ts rename to src/core/packet/transformer/proto/highway/highway.ts index f69b9e4b..faeef73a 100644 --- a/src/core/packet/proto/highway/highway.ts +++ b/src/core/packet/transformer/proto/highway/highway.ts @@ -1,6 +1,5 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; -import { MsgInfo, MsgInfoBody } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { MsgInfoBody } from "@/core/packet/transformer/proto"; export const DataHighwayHead = { version: ProtoField(1, ScalarType.UINT32), diff --git a/src/core/packet/transformer/proto/index.ts b/src/core/packet/transformer/proto/index.ts new file mode 100644 index 00000000..da8f1293 --- /dev/null +++ b/src/core/packet/transformer/proto/index.ts @@ -0,0 +1,31 @@ +// action folder +export * from "./action/action"; +export * from "./action/miniAppAdaptShareInfo"; + +// highway folder +export * from "./highway/highway"; + +// message folder +export * from "./message/action"; +export * from "./message/c2c"; +export * from "./message/component"; +export * from "./message/element"; +export * from "./message/group"; +export * from "./message/message"; +export * from "./message/notify"; +export * from "./message/routing"; + +// oidb folder +export * from "./oidb/common/Ntv2.RichMediaReq"; +export * from "./oidb/common/Ntv2.RichMediaResp"; +export * from "./oidb/Oidb.0x6D6"; +export * from "./oidb/Oidb.0x8FC_2"; +export * from "./oidb/Oidb.0x9067_202"; +export * from "./oidb/Oidb.0x929"; +export * from "./oidb/Oidb.0xE37_1200"; +export * from "./oidb/Oidb.0xE37_1700"; +export * from "./oidb/Oidb.0XE37_800"; +export * from "./oidb/Oidb.0xEB7"; +export * from "./oidb/Oidb.0xED3_1"; +export * from "./oidb/Oidb.0XFE1_2"; +export * from "./oidb/OidbBase"; diff --git a/src/core/packet/proto/message/action.ts b/src/core/packet/transformer/proto/message/action.ts similarity index 95% rename from src/core/packet/proto/message/action.ts rename to src/core/packet/transformer/proto/message/action.ts index 29d7945f..369c57d4 100644 --- a/src/core/packet/proto/message/action.ts +++ b/src/core/packet/transformer/proto/message/action.ts @@ -1,6 +1,5 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; -import { PushMsgBody } from "@/core/packet/proto/message/message"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { PushMsgBody } from "@/core/packet/transformer/proto"; export const LongMsgResult = { action: ProtoField(2, () => LongMsgAction) diff --git a/src/core/packet/proto/message/c2c.ts b/src/core/packet/transformer/proto/message/c2c.ts similarity index 78% rename from src/core/packet/proto/message/c2c.ts rename to src/core/packet/transformer/proto/message/c2c.ts index 2a93e5eb..e3025754 100644 --- a/src/core/packet/proto/message/c2c.ts +++ b/src/core/packet/transformer/proto/message/c2c.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const C2C = { uin: ProtoField(1, ScalarType.UINT32, true), diff --git a/src/core/packet/proto/message/component.ts b/src/core/packet/transformer/proto/message/component.ts similarity index 97% rename from src/core/packet/proto/message/component.ts rename to src/core/packet/transformer/proto/message/component.ts index 0981a73e..a6bb7749 100644 --- a/src/core/packet/proto/message/component.ts +++ b/src/core/packet/transformer/proto/message/component.ts @@ -1,6 +1,5 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; -import { Elem } from "@/core/packet/proto/message/element"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { Elem } from "@/core/packet/transformer/proto"; export const Attr = { codePage: ProtoField(1, ScalarType.INT32), diff --git a/src/core/packet/proto/message/element.ts b/src/core/packet/transformer/proto/message/element.ts similarity index 99% rename from src/core/packet/proto/message/element.ts rename to src/core/packet/transformer/proto/message/element.ts index 3ed728e3..c68a2ae8 100644 --- a/src/core/packet/proto/message/element.ts +++ b/src/core/packet/transformer/proto/message/element.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const Elem = { text: ProtoField(1, () => Text, true), diff --git a/src/core/packet/proto/message/group.ts b/src/core/packet/transformer/proto/message/group.ts similarity index 84% rename from src/core/packet/proto/message/group.ts rename to src/core/packet/transformer/proto/message/group.ts index 3416f1e5..c483850f 100644 --- a/src/core/packet/proto/message/group.ts +++ b/src/core/packet/transformer/proto/message/group.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const GroupRecallMsg = { type: ProtoField(1, ScalarType.UINT32), diff --git a/src/core/packet/proto/message/message.ts b/src/core/packet/transformer/proto/message/message.ts similarity index 88% rename from src/core/packet/proto/message/message.ts rename to src/core/packet/transformer/proto/message/message.ts index 5203f285..c5916e86 100644 --- a/src/core/packet/proto/message/message.ts +++ b/src/core/packet/transformer/proto/message/message.ts @@ -1,8 +1,14 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; -import { ForwardHead, Grp, GrpTmp, ResponseForward, ResponseGrp, Trans0X211, WPATmp } from "@/core/packet/proto/message/routing"; -import { RichText } from "@/core/packet/proto/message/component"; -import { C2C } from "@/core/packet/proto/message/c2c"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { + C2C, + ForwardHead, + Grp, + GrpTmp, + ResponseForward, + ResponseGrp, RichText, + Trans0X211, + WPATmp +} from "@/core/packet/transformer/proto"; export const ContentHead = { type: ProtoField(1, ScalarType.UINT32), diff --git a/src/core/packet/proto/message/notify.ts b/src/core/packet/transformer/proto/message/notify.ts similarity index 88% rename from src/core/packet/proto/message/notify.ts rename to src/core/packet/transformer/proto/message/notify.ts index e739e151..3b246780 100644 --- a/src/core/packet/proto/message/notify.ts +++ b/src/core/packet/transformer/proto/message/notify.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const FriendRecall = { info: ProtoField(1, () => FriendRecallInfo), diff --git a/src/core/packet/proto/message/routing.ts b/src/core/packet/transformer/proto/message/routing.ts similarity index 92% rename from src/core/packet/proto/message/routing.ts rename to src/core/packet/transformer/proto/message/routing.ts index 7de44012..619e395b 100644 --- a/src/core/packet/proto/message/routing.ts +++ b/src/core/packet/transformer/proto/message/routing.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const ForwardHead = { field1: ProtoField(1, ScalarType.UINT32, true), diff --git a/src/core/packet/proto/oidb/Oidb.0XE37_800.ts b/src/core/packet/transformer/proto/oidb/Oidb.0XE37_800.ts similarity index 95% rename from src/core/packet/proto/oidb/Oidb.0XE37_800.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0XE37_800.ts index dc708071..8496fd5d 100644 --- a/src/core/packet/proto/oidb/Oidb.0XE37_800.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0XE37_800.ts @@ -1,6 +1,5 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; -import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/transformer/proto"; export const OidbSvcTrpcTcp0XE37_800 = { subCommand: ProtoField(1, ScalarType.UINT32), diff --git a/src/core/packet/proto/oidb/Oidb.0XFE1_2.ts b/src/core/packet/transformer/proto/oidb/Oidb.0XFE1_2.ts similarity index 86% rename from src/core/packet/proto/oidb/Oidb.0XFE1_2.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0XFE1_2.ts index 7c6b37be..679fcd69 100644 --- a/src/core/packet/proto/oidb/Oidb.0XFE1_2.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0XFE1_2.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcp0XFE1_2 = { uin: ProtoField(1, ScalarType.UINT32), diff --git a/src/core/packet/proto/oidb/Oidb.0x6D6.ts b/src/core/packet/transformer/proto/oidb/Oidb.0x6D6.ts similarity index 97% rename from src/core/packet/proto/oidb/Oidb.0x6D6.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0x6D6.ts index be16da98..657cea98 100644 --- a/src/core/packet/proto/oidb/Oidb.0x6D6.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0x6D6.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcp0x6D6 = { file: ProtoField(1, () => OidbSvcTrpcTcp0x6D6Upload, true), diff --git a/src/core/packet/proto/oidb/Oidb.0x8FC_2.ts b/src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts similarity index 83% rename from src/core/packet/proto/oidb/Oidb.0x8FC_2.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts index 3721ee8b..2fc08f6e 100644 --- a/src/core/packet/proto/oidb/Oidb.0x8FC_2.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; //设置群头衔 OidbSvcTrpcTcp.0x8fc_2 @@ -13,4 +12,4 @@ export const OidbSvcTrpcTcp0X8FC_2_Body = { export const OidbSvcTrpcTcp0X8FC_2 = { groupUin: ProtoField(1, ScalarType.UINT32), body: ProtoField(3, ScalarType.BYTES), -}; \ No newline at end of file +}; diff --git a/src/core/packet/proto/oidb/Oidb.0x9067_202.ts b/src/core/packet/transformer/proto/oidb/Oidb.0x9067_202.ts similarity index 89% rename from src/core/packet/proto/oidb/Oidb.0x9067_202.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0x9067_202.ts index 2ee31688..b500bcc8 100644 --- a/src/core/packet/proto/oidb/Oidb.0x9067_202.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0x9067_202.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; import { MultiMediaReqHead } from "./common/Ntv2.RichMediaReq"; //Req diff --git a/src/core/packet/transformer/proto/oidb/Oidb.0x929.ts b/src/core/packet/transformer/proto/oidb/Oidb.0x929.ts new file mode 100644 index 00000000..7bcd587f --- /dev/null +++ b/src/core/packet/transformer/proto/oidb/Oidb.0x929.ts @@ -0,0 +1,42 @@ +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { MsgInfo } from "@/core/packet/transformer/proto"; + + +export const OidbSvcTrpcTcp0X929D_0 = { + groupUin: ProtoField(1, ScalarType.UINT32), + chatType: ProtoField(2, ScalarType.UINT32), +}; + +export const OidbSvcTrpcTcp0X929D_0Resp = { + content: ProtoField(1, () => OidbSvcTrpcTcp0X929D_0RespContent, false, true), +}; + +export const OidbSvcTrpcTcp0X929D_0RespContent = { + category: ProtoField(1, ScalarType.STRING), + voices: ProtoField(2, () => OidbSvcTrpcTcp0X929D_0RespContentVoice, false, true), +}; + +export const OidbSvcTrpcTcp0X929D_0RespContentVoice = { + voiceId: ProtoField(1, ScalarType.STRING), + voiceDisplayName: ProtoField(2, ScalarType.STRING), + voiceExampleUrl: ProtoField(3, ScalarType.STRING), +}; + +export const OidbSvcTrpcTcp0X929B_0 = { + groupUin: ProtoField(1, ScalarType.UINT32), + voiceId: ProtoField(2, ScalarType.STRING), + text: ProtoField(3, ScalarType.STRING), + chatType: ProtoField(4, ScalarType.UINT32), + session: ProtoField(5, () => OidbSvcTrpcTcp0X929B_0_Session), +}; + +export const OidbSvcTrpcTcp0X929B_0_Session = { + sessionId: ProtoField(1, ScalarType.UINT32), +}; + +export const OidbSvcTrpcTcp0X929B_0Resp = { + statusCode: ProtoField(1, ScalarType.UINT32), + field2: ProtoField(2, ScalarType.UINT32, true), + field3: ProtoField(3, ScalarType.UINT32), + msgInfo: ProtoField(4, () => MsgInfo, true), +}; diff --git a/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts b/src/core/packet/transformer/proto/oidb/Oidb.0xE37_1200.ts similarity index 96% rename from src/core/packet/proto/oidb/Oidb.0xE37_1200.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0xE37_1200.ts index ba2b0f16..1c0e55d3 100644 --- a/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0xE37_1200.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcp0XE37_1200 = { subCommand: ProtoField(1, ScalarType.UINT32, true), diff --git a/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts b/src/core/packet/transformer/proto/oidb/Oidb.0xE37_1700.ts similarity index 91% rename from src/core/packet/proto/oidb/Oidb.0xE37_1700.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0xE37_1700.ts index 046dff8f..12fb9c5c 100644 --- a/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0xE37_1700.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcp0XE37_1700 = { command: ProtoField(1, ScalarType.UINT32, true), diff --git a/src/core/packet/proto/oidb/Oidb.0xEB7.ts b/src/core/packet/transformer/proto/oidb/Oidb.0xEB7.ts similarity index 74% rename from src/core/packet/proto/oidb/Oidb.0xEB7.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0xEB7.ts index 27cf8cee..43eec544 100644 --- a/src/core/packet/proto/oidb/Oidb.0xEB7.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0xEB7.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcp0XEB7_Body = { uin: ProtoField(1, ScalarType.STRING), @@ -9,4 +8,4 @@ export const OidbSvcTrpcTcp0XEB7_Body = { export const OidbSvcTrpcTcp0XEB7 = { body: ProtoField(2, () => OidbSvcTrpcTcp0XEB7_Body), -}; \ No newline at end of file +}; diff --git a/src/core/packet/proto/oidb/Oidb.0xED3_1.ts b/src/core/packet/transformer/proto/oidb/Oidb.0xED3_1.ts similarity index 72% rename from src/core/packet/proto/oidb/Oidb.0xED3_1.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0xED3_1.ts index 5e644c50..46c47957 100644 --- a/src/core/packet/proto/oidb/Oidb.0xED3_1.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0xED3_1.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; // Send Poke export const OidbSvcTrpcTcp0XED3_1 = { diff --git a/src/core/packet/proto/oidb/OidbBase.ts b/src/core/packet/transformer/proto/oidb/OidbBase.ts similarity index 75% rename from src/core/packet/proto/oidb/OidbBase.ts rename to src/core/packet/transformer/proto/oidb/OidbBase.ts index 86b62b07..fb5c94eb 100644 --- a/src/core/packet/proto/oidb/OidbBase.ts +++ b/src/core/packet/transformer/proto/oidb/OidbBase.ts @@ -1,13 +1,13 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcpBase = { command: ProtoField(1, ScalarType.UINT32), subCommand: ProtoField(2, ScalarType.UINT32), + errorCode: ProtoField(3, ScalarType.UINT32), body: ProtoField(4, ScalarType.BYTES), errorMsg: ProtoField(5, ScalarType.STRING, true), isReserved: ProtoField(12, ScalarType.UINT32) }; export const OidbSvcTrpcTcpBaseRsp = { body: ProtoField(4, ScalarType.BYTES) -}; \ No newline at end of file +}; diff --git a/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts b/src/core/packet/transformer/proto/oidb/common/Ntv2.RichMediaReq.ts similarity index 98% rename from src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts rename to src/core/packet/transformer/proto/oidb/common/Ntv2.RichMediaReq.ts index 2f117520..482f7489 100644 --- a/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts +++ b/src/core/packet/transformer/proto/oidb/common/Ntv2.RichMediaReq.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../../NapProto"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const NTV2RichMediaReq = { ReqHead: ProtoField(1, () => MultiMediaReqHead), diff --git a/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts b/src/core/packet/transformer/proto/oidb/common/Ntv2.RichMediaResp.ts similarity index 96% rename from src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts rename to src/core/packet/transformer/proto/oidb/common/Ntv2.RichMediaResp.ts index e1d16556..72c12ed7 100644 --- a/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts +++ b/src/core/packet/transformer/proto/oidb/common/Ntv2.RichMediaResp.ts @@ -1,6 +1,6 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../../NapProto"; -import { CommonHead, MsgInfo, PicUrlExtInfo, VideoExtInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { CommonHead, MsgInfo, PicUrlExtInfo, VideoExtInfo } from "@/core/packet/transformer/proto"; + export const NTV2RichMediaResp = { respHead: ProtoField(1, () => MultiMediaRespHead), diff --git a/src/core/packet/transformer/system/FetchRkey.ts b/src/core/packet/transformer/system/FetchRkey.ts new file mode 100644 index 00000000..76507bc6 --- /dev/null +++ b/src/core/packet/transformer/system/FetchRkey.ts @@ -0,0 +1,40 @@ +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 FetchRkey extends PacketTransformer { + constructor() { + super(); + } + + build(): OidbPacket { + const data = new NapProtoMsg(proto.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 OidbBase.build(0x9067, 202, data); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(oidbBody); + } +} + +export default new FetchRkey(); diff --git a/src/core/packet/transformer/system/index.ts b/src/core/packet/transformer/system/index.ts new file mode 100644 index 00000000..a4387d8e --- /dev/null +++ b/src/core/packet/transformer/system/index.ts @@ -0,0 +1 @@ +export { default as FetchRkey } from './FetchRkey'; diff --git a/src/core/packet/utils/crypto/hash.ts b/src/core/packet/utils/crypto/hash.ts index 56ab7524..6f2eae88 100644 --- a/src/core/packet/utils/crypto/hash.ts +++ b/src/core/packet/utils/crypto/hash.ts @@ -5,17 +5,17 @@ import * as fs from 'fs'; import { CalculateStreamBytesTransform } from "@/core/packet/utils/crypto/sha1StreamBytesTransform"; function sha1Stream(readable: stream.Readable) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { readable.on('error', reject); readable.pipe(crypto.createHash('sha1').on('error', reject).on('data', resolve)); - }) as Promise; + }); } function md5Stream(readable: stream.Readable) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { readable.on('error', reject); readable.pipe(crypto.createHash('md5').on('error', reject).on('data', resolve)); - }) as Promise; + }); } export function calculateSha1(filePath: string): Promise { @@ -39,7 +39,7 @@ export function calculateSha1StreamBytes(filePath: string): Promise { calculateStreamBytes.on('end', () => { resolve(byteArrayList); }); - calculateStreamBytes.on('error', (err) => { + calculateStreamBytes.on('error', (err: Error) => { reject(err); }); readable.pipe(calculateStreamBytes); diff --git a/src/core/packet/utils/crypto/sha1Stream.ts b/src/core/packet/utils/crypto/sha1Stream.ts index 7eb5421c..18604e44 100644 --- a/src/core/packet/utils/crypto/sha1Stream.ts +++ b/src/core/packet/utils/crypto/sha1Stream.ts @@ -45,11 +45,17 @@ export class Sha1Stream { let e = this._state[4]; for (let i = 0; i < 80; i++) { - const [f, k] = (i < 20) ? [(b & c) | ((~b) & d), 0x5A827999] : - (i < 40) ? [b ^ c ^ d, 0x6ED9EBA1] : - (i < 60) ? [(b & c) | (b & d) | (c & d), 0x8F1BBCDC] : - [b ^ c ^ d, 0xCA62C1D6]; - const temp = (this.rotateLeft(a, 5) + f + k + e + w[i]) >>> 0; + let temp; + if (i < 20) { + temp = ((b & c) | (~b & d)) + 0x5A827999; + } else if (i < 40) { + temp = (b ^ c ^ d) + 0x6ED9EBA1; + } else if (i < 60) { + temp = ((b & c) | (b & d) | (c & d)) + 0x8F1BBCDC; + } else { + temp = (b ^ c ^ d) + 0xCA62C1D6; + } + temp += ((this.rotateLeft(a, 5) + e + w[i]) >>> 0); e = d; d = c; c = this.rotateLeft(b, 30) >>> 0; diff --git a/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts index c6e229db..95f743fa 100644 --- a/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts +++ b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts @@ -3,7 +3,7 @@ import { Sha1Stream } from "@/core/packet/utils/crypto/sha1Stream"; export class CalculateStreamBytesTransform extends stream.Transform { private readonly blockSize = 1024 * 1024; - private sha1: Sha1Stream; + private readonly sha1: Sha1Stream; private buffer: Buffer; private bytesRead: number; private readonly byteArrayList: Buffer[]; diff --git a/src/core/packet/helper/miniAppHelper.ts b/src/core/packet/utils/helper/miniAppHelper.ts similarity index 90% rename from src/core/packet/helper/miniAppHelper.ts rename to src/core/packet/utils/helper/miniAppHelper.ts index a65f4e3c..66b9642a 100644 --- a/src/core/packet/helper/miniAppHelper.ts +++ b/src/core/packet/utils/helper/miniAppHelper.ts @@ -9,10 +9,10 @@ import { type MiniAppTemplateNameList = "bili" | "weibo"; export abstract class MiniAppInfo { - static sdkId: string = "V1_PC_MINISDK_99.99.99_1_APP_A"; + static readonly sdkId: string = "V1_PC_MINISDK_99.99.99_1_APP_A"; template: MiniAppReqTemplateParams; - private static appMap = new Map(); + private static readonly appMap = new Map(); protected constructor(template: MiniAppReqTemplateParams) { this.template = template; @@ -22,7 +22,7 @@ export abstract class MiniAppInfo { return this.appMap.get(name); } - static Bili = new class extends MiniAppInfo { + static readonly Bili = new class extends MiniAppInfo { constructor() { super({ sdkId: MiniAppInfo.sdkId, @@ -40,7 +40,7 @@ export abstract class MiniAppInfo { } }; - static WeiBo = new class extends MiniAppInfo { + static readonly WeiBo = new class extends MiniAppInfo { constructor() { super({ sdkId: MiniAppInfo.sdkId, diff --git a/src/core/services/NodeIKernelGroupService.ts b/src/core/services/NodeIKernelGroupService.ts index dcc3ebab..d5684800 100644 --- a/src/core/services/NodeIKernelGroupService.ts +++ b/src/core/services/NodeIKernelGroupService.ts @@ -89,7 +89,7 @@ export interface NodeIKernelGroupService { isEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise; - queryCachedEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise; + queryCachedEssenceMsg(req: { groupCode: string, msgRandom: number, msgSeq: number }): Promise<{ items: Array }>; fetchGroupEssenceList(req: { groupCode: string, diff --git a/src/core/wrapper.ts b/src/core/wrapper.ts index 63978b0c..9d569ccf 100644 --- a/src/core/wrapper.ts +++ b/src/core/wrapper.ts @@ -29,10 +29,7 @@ import { NodeIKernelECDHService } from './services/NodeIKernelECDHService'; import { NodeIO3MiscService } from './services/NodeIO3MiscService'; export interface NodeQQNTWrapperUtil { - get(): unknown; - - // eslint-disable-next-line @typescript-eslint/no-misused-new - new(): NodeQQNTWrapperUtil; + get(): NodeQQNTWrapperUtil; getNTUserDataInfoConfig(): string; @@ -153,7 +150,7 @@ export interface NodeIQQNTWrapperSession { nodeIKernelSessionListener: NodeIKernelSessionListener, ): void; - startNT(n: 0): void; + startNT(session: number): void; startNT(): void; diff --git a/src/framework/napcat.ts b/src/framework/napcat.ts index 60607d0f..a01397ba 100644 --- a/src/framework/napcat.ts +++ b/src/framework/napcat.ts @@ -23,6 +23,14 @@ export async function NCoreInitFramework( ) { //在进入本层前是否登录未进行判断 console.log('NapCat Framework App Loading...'); + + process.on('uncaughtException', (err) => { + console.log('[NapCat] [Error] Unhandled Exception:', err.message); + }); + process.on('unhandledRejection', (reason, promise) => { + console.log('[NapCat] [Error] unhandledRejection:', reason); + }); + const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); @@ -47,11 +55,12 @@ export async function NCoreInitFramework( // await sleep(2500); // 初始化 NapCatFramework const loaderObject = new NapCatFramework(wrapper, session, logger, loginService, selfInfo, basicInfoWrapper, pathWrapper); + await loaderObject.core.initCore(); //启动WebUi InitWebUi(logger, pathWrapper).then().catch(logger.logError.bind(logger)); //初始化LLNC的Onebot实现 - new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper); + await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot(); } export class NapCatFramework { diff --git a/src/native/external/MoeHoo.win32.node b/src/native/external/MoeHoo.win32.node deleted file mode 100644 index 89703140..00000000 Binary files a/src/native/external/MoeHoo.win32.node and /dev/null differ diff --git a/src/native/index.ts b/src/native/index.ts deleted file mode 100644 index 6369cd1a..00000000 --- a/src/native/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { constants } from "node:os"; -import path from "path"; -import { dlopen } from "process"; -import fs from "fs"; -export class Native { - platform: string; - supportedPlatforms = ['']; - MoeHooExport: any = { exports: {} }; - recallHookEnabled: boolean = false; - inited = true; - constructor(nodePath: string, platform: string = process.platform) { - this.platform = platform; - try { - if (!this.supportedPlatforms.includes(this.platform)) { - throw new Error(`Platform ${this.platform} is not supported`); - } - const nativeNode = path.join(nodePath, './native/MoeHoo.win32.node'); - if (fs.existsSync(nativeNode)) { - dlopen(this.MoeHooExport, nativeNode, constants.dlopen.RTLD_LAZY); - } - } catch (error) { - this.inited = false; - } - - } - isSetReCallEnabled(): boolean { - return this.recallHookEnabled && this.inited; - } - registerRecallCallback(callback: (hex: string) => any): void { - try { - if (!this.inited) throw new Error('Native Not Init'); - if (this.MoeHooExport.exports?.registMsgPush) { - this.MoeHooExport.exports.registMsgPush(callback); - this.recallHookEnabled = true; - } - } catch (error) { - this.recallHookEnabled = false; - } - } -} \ No newline at end of file diff --git a/src/native/packet/MoeHoo.darwin.arm64.node b/src/native/packet/MoeHoo.darwin.arm64.node new file mode 100644 index 00000000..829749c4 Binary files /dev/null and b/src/native/packet/MoeHoo.darwin.arm64.node differ diff --git a/src/native/packet/MoeHoo.darwin.x64.node b/src/native/packet/MoeHoo.darwin.x64.node new file mode 100644 index 00000000..66cfff98 Binary files /dev/null and b/src/native/packet/MoeHoo.darwin.x64.node differ diff --git a/src/native/packet/MoeHoo.linux.arm64.node b/src/native/packet/MoeHoo.linux.arm64.node new file mode 100644 index 00000000..73d9a012 Binary files /dev/null and b/src/native/packet/MoeHoo.linux.arm64.node differ diff --git a/src/native/packet/MoeHoo.linux.x64.node b/src/native/packet/MoeHoo.linux.x64.node new file mode 100644 index 00000000..70ca6681 Binary files /dev/null and b/src/native/packet/MoeHoo.linux.x64.node differ diff --git a/src/native/packet/MoeHoo.win32.x64.node b/src/native/packet/MoeHoo.win32.x64.node new file mode 100644 index 00000000..ab1701c0 Binary files /dev/null and b/src/native/packet/MoeHoo.win32.x64.node differ diff --git a/src/onebot/action/BaseAction.ts b/src/onebot/action/BaseAction.ts index 92e46f8a..cace2723 100644 --- a/src/onebot/action/BaseAction.ts +++ b/src/onebot/action/BaseAction.ts @@ -29,7 +29,7 @@ abstract class BaseAction { }); return { valid: false, - message: errorMessages.join('\n') as string || '未知错误', + message: errorMessages.join('\n') ?? '未知错误', }; } return { @@ -37,27 +37,27 @@ abstract class BaseAction { }; } - public async handle(payload: PayloadType): Promise> { + public async handle(payload: PayloadType, adaptername: string): Promise> { const result = await this.check(payload); if (!result.valid) { return OB11Response.error(result.message, 400); } try { - const resData = await this._handle(payload); + const resData = await this._handle(payload, adaptername); return OB11Response.ok(resData); } catch (e: any) { this.core.context.logger.logError.bind(this.core.context.logger)('发生错误', e); - return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200); + return OB11Response.error(e?.stack?.toString() || e?.toString() || '未知错误,可能操作超时', 200); } } - public async websocketHandle(payload: PayloadType, echo: any): Promise> { + public async websocketHandle(payload: PayloadType, echo: any, adaptername: string): Promise> { const result = await this.check(payload); if (!result.valid) { return OB11Response.error(result.message, 1400, echo); } try { - const resData = await this._handle(payload); + const resData = await this._handle(payload, adaptername); return OB11Response.ok(resData, echo); } catch (e: any) { this.core.context.logger.logError.bind(this.core.context.logger)('发生错误', e); @@ -65,7 +65,7 @@ abstract class BaseAction { } } - abstract _handle(payload: PayloadType): PromiseLike; + abstract _handle(payload: PayloadType, adaptername: string): PromiseLike; } export default BaseAction; diff --git a/src/onebot/action/extends/GetAiCharacters.ts b/src/onebot/action/extends/GetAiCharacters.ts new file mode 100644 index 00000000..bc6e61e7 --- /dev/null +++ b/src/onebot/action/extends/GetAiCharacters.ts @@ -0,0 +1,41 @@ +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; +import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; + +const SchemaData = { + type: 'object', + properties: { + group_id: { type: ['number', 'string'] }, + chat_type: { type: ['number', 'string'] }, + }, + required: ['group_id'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +interface GetAiCharactersResponse { + type: string; + characters: { + character_id: string; + character_name: string; + preview_url: string; + }[]; +} + +export class GetAiCharacters extends GetPacketStatusDepends { + actionName = ActionName.GetAiCharacters; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +(payload.chat_type ?? 1) as AIVoiceChatType); + return rawList?.map((item) => ({ + type: item.category, + characters: item.voices.map((voice) => ({ + character_id: voice.voiceId, + character_name: voice.voiceDisplayName, + preview_url: voice.voiceExampleUrl, + })), + })) ?? []; + } +} diff --git a/src/onebot/action/extends/GetFriendWithCategory.ts b/src/onebot/action/extends/GetFriendWithCategory.ts index 5fca6445..0a5a67f5 100644 --- a/src/onebot/action/extends/GetFriendWithCategory.ts +++ b/src/onebot/action/extends/GetFriendWithCategory.ts @@ -6,7 +6,7 @@ export class GetFriendWithCategory extends BaseAction { actionName = ActionName.GetFriendsWithCategory; async _handle(payload: void) { - return (await this.core.apis.FriendApi.getBuddyV2ExWithCate(true)).map(category => ({ + return (await this.core.apis.FriendApi.getBuddyV2ExWithCate()).map(category => ({ ...category, buddyList: OB11Entities.friendsV2(category.buddyList), })); diff --git a/src/onebot/action/extends/GetMiniAppArk.ts b/src/onebot/action/extends/GetMiniAppArk.ts index 6baafb80..83f24dc8 100644 --- a/src/onebot/action/extends/GetMiniAppArk.ts +++ b/src/onebot/action/extends/GetMiniAppArk.ts @@ -1,8 +1,8 @@ import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; +import { MiniAppInfo, MiniAppInfoHelper } from "@/core/packet/utils/helper/miniAppHelper"; import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from "@/core/packet/entities/miniApp"; -import { MiniAppInfo, MiniAppInfoHelper } from "@/core/packet/helper/miniAppHelper"; const SchemaData = { type: 'object', @@ -60,7 +60,7 @@ export class GetMiniAppArk extends GetPacketStatusDepends; + const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload; reqParam = MiniAppInfoHelper.generateReq( customParams, { @@ -77,7 +77,7 @@ export class GetMiniAppArk extends GetPacketStatusDepends { const start = payload.start ? Number(payload.start) : 0; const count = payload.count ? Number(payload.count) : 10; const ret = await this.core.apis.UserApi.getProfileLike(this.core.selfInfo.uid, start, count); - const listdata: any[] = ret.info.userLikeInfos[0].voteInfo.userInfos; - for (let i = 0; i < listdata.length; i++) { - listdata[i].uin = parseInt((await this.core.apis.UserApi.getUinByUidV2(listdata[i].uid)) || ''); + const listdata = ret.info.userLikeInfos[0].voteInfo.userInfos; + for (const item of listdata) { + item.uin = parseInt((await this.core.apis.UserApi.getUinByUidV2(item.uid)) || ''); } return ret.info.userLikeInfos[0].voteInfo; } diff --git a/src/onebot/action/extends/GetRkey.ts b/src/onebot/action/extends/GetRkey.ts index 8ec95b22..79a22626 100644 --- a/src/onebot/action/extends/GetRkey.ts +++ b/src/onebot/action/extends/GetRkey.ts @@ -6,6 +6,6 @@ export class GetRkey extends GetPacketStatusDepends> { actionName = ActionName.GetRkey; async _handle() { - return await this.core.apis.PacketApi.sendRkeyPacket(); + return await this.core.apis.PacketApi.pkt.operation.FetchRkey(); } } diff --git a/src/onebot/action/extends/GetUserStatus.ts b/src/onebot/action/extends/GetUserStatus.ts index 3e7774b7..1fe392e9 100644 --- a/src/onebot/action/extends/GetUserStatus.ts +++ b/src/onebot/action/extends/GetUserStatus.ts @@ -17,6 +17,6 @@ export class GetUserStatus extends GetPacketStatusDepends { async _handle(payload: Payload) { const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.image)); if (!success) { - throw `OCR ${payload.image}失败,image字段可能格式不正确`; + throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`); } if (path) { await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 @@ -29,12 +29,12 @@ export class OCRImage extends BaseAction { fs.unlink(path, () => { }); if (!ret) { - throw `OCR ${payload.file}失败`; + throw new Error(`OCR ${payload.file}失败`); } return ret.result; } fs.unlink(path, () => { }); - throw `OCR ${payload.file}失败,文件可能不存在`; + throw new Error(`OCR ${payload.file}失败,文件可能不存在`); } } diff --git a/src/onebot/action/extends/SetGroupSign.ts b/src/onebot/action/extends/SetGroupSign.ts index 9e87446b..7b314e51 100644 --- a/src/onebot/action/extends/SetGroupSign.ts +++ b/src/onebot/action/extends/SetGroupSign.ts @@ -1,22 +1,25 @@ -import BaseAction from '../BaseAction'; +import { GetPacketStatusDepends } from '../packet/GetPacketStatus'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; const SchemaData = { type: 'object', properties: { - group_id: { type: 'string' }, + group_id: { type: ['string', 'number'] }, }, required: ['group_id'], } as const satisfies JSONSchema; type Payload = FromSchema; -export class SetGroupSign extends BaseAction { +export class SetGroupSign extends GetPacketStatusDepends { actionName = ActionName.SetGroupSign; payloadSchema = SchemaData; async _handle(payload: Payload) { - return await this.core.apis.PacketApi.sendGroupSignPacket(payload.group_id); + return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id); } } +export class SendGroupSign extends SetGroupSign { + actionName = ActionName.SendGroupSign; +} diff --git a/src/onebot/action/extends/SetInputStatus.ts b/src/onebot/action/extends/SetInputStatus.ts index 9890cb40..db694b07 100644 --- a/src/onebot/action/extends/SetInputStatus.ts +++ b/src/onebot/action/extends/SetInputStatus.ts @@ -1,7 +1,7 @@ import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import BaseAction from '../BaseAction'; -import { ActionName, BaseCheckResult } from '../types'; -import { ChatType, Peer } from '@/core'; +import { ActionName } from '../types'; +import { ChatType } from '@/core'; const SchemaData = { type: 'object', @@ -9,7 +9,7 @@ const SchemaData = { event_type: { type: 'number' }, user_id: { type: ['number', 'string'] }, }, - required: ['event_type','user_id'], + required: ['event_type', 'user_id'], } as const satisfies JSONSchema; type Payload = FromSchema; diff --git a/src/onebot/action/extends/SetQQAvatar.ts b/src/onebot/action/extends/SetQQAvatar.ts index 940e852e..0f149b92 100644 --- a/src/onebot/action/extends/SetQQAvatar.ts +++ b/src/onebot/action/extends/SetQQAvatar.ts @@ -26,7 +26,7 @@ export default class SetAvatar extends BaseAction { async _handle(payload: Payload): Promise { const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file)); if (!success) { - throw `头像${payload.file}设置失败,file字段可能格式不正确`; + throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`); } if (path) { await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 @@ -35,18 +35,18 @@ export default class SetAvatar extends BaseAction { }); if (!ret) { - throw `头像${payload.file}设置失败,api无返回`; + throw new Error(`头像${payload.file}设置失败,api无返回`); } // log(`头像设置返回:${JSON.stringify(ret)}`) if (ret.result as number == 1004022) { - throw `头像${payload.file}设置失败,文件可能不是图片格式`; + throw new Error(`头像${payload.file}设置失败,文件可能不是图片格式`); } else if (ret.result != 0) { - throw `头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`; + throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`); } } else { fs.unlink(path, () => { }); - throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`; + throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`); } return null; } diff --git a/src/onebot/action/extends/SetSpecialTittle.ts b/src/onebot/action/extends/SetSpecialTittle.ts index 2c54d501..d962e014 100644 --- a/src/onebot/action/extends/SetSpecialTittle.ts +++ b/src/onebot/action/extends/SetSpecialTittle.ts @@ -20,6 +20,6 @@ export class SetSpecialTittle extends GetPacketStatusDepends { async _handle(payload: Payload) { const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); if(!uid) throw new Error('User not found'); - await this.core.apis.PacketApi.sendSetSpecialTittlePacket(payload.group_id.toString(), uid, payload.special_title); + await this.core.apis.PacketApi.pkt.operation.SetGroupSpecialTitle(+payload.group_id, uid, payload.special_title); } } diff --git a/src/onebot/action/file/GetGroupFileUrl.ts b/src/onebot/action/file/GetGroupFileUrl.ts index fade88ca..d0a53892 100644 --- a/src/onebot/action/file/GetGroupFileUrl.ts +++ b/src/onebot/action/file/GetGroupFileUrl.ts @@ -26,7 +26,7 @@ export class GetGroupFileUrl extends GetPacketStatusDepends { } const rootMsgId = MessageUnique.getShortIdByMsgId(msgId); - const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId || parseInt(msgId)); + const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId); if (!rootMsg) { throw new Error('msg not found'); } @@ -72,17 +72,15 @@ export class GoCQHTTPGetForwardMsgAction extends BaseAction { } const singleMsg = data.msgList[0]; - const resMsg = await this.obContext.apis.MsgApi.parseMessage(singleMsg, 'array');//强制array 以便处理 - if (!resMsg) { + const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理 + if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) { throw new Error('找不到相关的聊天记录'); } - //if (this.obContext.configLoader.configData.messagePostFormat === 'array') { - //提取 - const realmsg = ((await this.parseForward([resMsg]))[0].data.message as OB11MessageNode[])[0].data.message; - //里面都是offline消息 id都是0 没得说话 - return { message: realmsg }; + return { + messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content + }; //} // return { message: resMsg }; } -} \ No newline at end of file +} diff --git a/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts index f6444456..dd72e1bd 100644 --- a/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts +++ b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts @@ -4,6 +4,7 @@ import { ActionName } from '../types'; import { ChatType } from '@/core/entities'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { MessageUnique } from '@/common/message-unique'; +import { AdapterConfigWrap } from '@/onebot/config/config'; interface Response { messages: OB11Message[]; @@ -26,28 +27,29 @@ export default class GetFriendMsgHistory extends BaseAction { actionName = ActionName.GetFriendMsgHistory; payloadSchema = SchemaData; - async _handle(payload: Payload): Promise { + async _handle(payload: Payload, adapter: string): Promise { //处理参数 const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const MsgCount = +(payload.count ?? 20); const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder; - if (!uid) throw `记录${payload.user_id}不存在`; + if (!uid) throw new Error(`记录${payload.user_id}不存在`); const friend = await this.core.apis.FriendApi.isBuddy(uid); const peer = { chatType: friend ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid: uid }; const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0'); const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0'; const msgList = hasMessageSeq ? (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, MsgCount)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, MsgCount)).msgList; - if (msgList.length === 0) throw `消息${payload.message_seq}不存在`; + if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`); //翻转消息 if (isReverseOrder) msgList.reverse(); //转换序号 await Promise.all(msgList.map(async msg => { msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId); })); + const network = Object.values(this.obContext.configLoader.configData.network) as Array; //烘焙消息 const ob11MsgList = (await Promise.all( - msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg))) + msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array'))) ).filter(msg => msg !== undefined); return { 'messages': ob11MsgList }; } diff --git a/src/onebot/action/go-cqhttp/GetGroupFileSystemInfo.ts b/src/onebot/action/go-cqhttp/GetGroupFileSystemInfo.ts index 79b2572c..9f819e20 100644 --- a/src/onebot/action/go-cqhttp/GetGroupFileSystemInfo.ts +++ b/src/onebot/action/go-cqhttp/GetGroupFileSystemInfo.ts @@ -15,7 +15,7 @@ type Payload = FromSchema; export class GetGroupFileSystemInfo extends BaseAction { actionName = ActionName.GoCQHTTP_GetGroupFileSystemInfo; diff --git a/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts index 69f27f62..3020a15e 100644 --- a/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts +++ b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts @@ -4,6 +4,7 @@ import { ActionName } from '../types'; import { ChatType, Peer } from '@/core/entities'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { MessageUnique } from '@/common/message-unique'; +import { AdapterConfigWrap } from '@/onebot/config/config'; interface Response { messages: OB11Message[]; @@ -26,7 +27,7 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction { + async _handle(payload: Payload, adapter: string): Promise { //处理参数 const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder; const MsgCount = +(payload.count ?? 20); @@ -36,16 +37,18 @@ export default class GoCQHTTPGetGroupMsgHistory extends BaseAction { msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId); })); + const network = Object.values(this.obContext.configLoader.configData.network) as Array; //烘焙消息 + const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array'; const ob11MsgList = (await Promise.all( - msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg))) + msgList.map(msg => this.obContext.apis.MsgApi.parseMessage(msg, msgFormat))) ).filter(msg => msg !== undefined); return { 'messages': ob11MsgList }; } diff --git a/src/onebot/action/go-cqhttp/GoCQHTTPDeleteFriend.ts b/src/onebot/action/go-cqhttp/GoCQHTTPDeleteFriend.ts index e770cb09..ea1b4c53 100644 --- a/src/onebot/action/go-cqhttp/GoCQHTTPDeleteFriend.ts +++ b/src/onebot/action/go-cqhttp/GoCQHTTPDeleteFriend.ts @@ -6,10 +6,15 @@ const SchemaData = { type: 'object', properties: { friend_id: { type: ['string', 'number'] }, + user_id: { type: ['string', 'number'] }, temp_block: { type: 'boolean' }, temp_both_del: { type: 'boolean' }, }, - required: ['friend_id'], + oneOf: [ + { required: ['friend_id'] }, + { required: ['user_id'] }, + ], + } as const satisfies JSONSchema; type Payload = FromSchema; @@ -18,7 +23,8 @@ export class GoCQHTTPDeleteFriend extends BaseAction { payloadSchema = SchemaData; async _handle(payload: Payload) { - const uid = await this.core.apis.UserApi.getUidByUinV2(payload.friend_id.toString()); + const uin = payload.friend_id ?? payload.user_id ?? ''; + const uid = await this.core.apis.UserApi.getUidByUinV2(uin.toString()); if (!uid) { return { diff --git a/src/onebot/action/go-cqhttp/SendGroupNotice.ts b/src/onebot/action/go-cqhttp/SendGroupNotice.ts index 55fa973c..81dbf9af 100644 --- a/src/onebot/action/go-cqhttp/SendGroupNotice.ts +++ b/src/onebot/action/go-cqhttp/SendGroupNotice.ts @@ -34,15 +34,15 @@ export class SendGroupNotice extends BaseAction { success, } = (await uri2local(this.core.NapCatTempPath, payload.image)); if (!success) { - throw `群公告${payload.image}设置失败,image字段可能格式不正确`; + throw new Error(`群公告${payload.image}设置失败,image字段可能格式不正确`); } if (!path) { - throw `群公告${payload.image}设置失败,获取资源失败`; + throw new Error(`群公告${payload.image}设置失败,获取资源失败`); } await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 const ImageUploadResult = await this.core.apis.GroupApi.uploadGroupBulletinPic(payload.group_id.toString(), path); if (ImageUploadResult.errCode != 0) { - throw `群公告${payload.image}设置失败,图片上传失败`; + throw new Error(`群公告${payload.image}设置失败,图片上传失败`); } unlink(path, () => { diff --git a/src/onebot/action/go-cqhttp/SetGroupPortrait.ts b/src/onebot/action/go-cqhttp/SetGroupPortrait.ts index 9a68b7e0..2fc9e818 100644 --- a/src/onebot/action/go-cqhttp/SetGroupPortrait.ts +++ b/src/onebot/action/go-cqhttp/SetGroupPortrait.ts @@ -27,24 +27,24 @@ export default class SetGroupPortrait extends BaseAction { async _handle(payload: Payload): Promise { const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file)); if (!success) { - throw `头像${payload.file}设置失败,file字段可能格式不正确`; + throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`); } if (path) { await checkFileReceived(path, 5000); // 文件不存在QQ会崩溃,需要提前判断 const ret = await this.core.apis.GroupApi.setGroupAvatar(payload.group_id.toString(), path); fs.unlink(path, () => { }); if (!ret) { - throw `头像${payload.file}设置失败,api无返回`; + throw new Error(`头像${payload.file}设置失败,api无返回`); } if (ret.result as number == 1004022) { - throw `头像${payload.file}设置失败,文件可能不是图片格式或权限不足`; + throw new Error(`头像${payload.file}设置失败,文件可能不是图片格式或权限不足`); } else if (ret.result != 0) { - throw `头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`; + throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`); } return ret; } else { - fs.unlink(path, () => {}); - throw `头像${payload.file}设置失败,无法获取头像,文件可能不存在`; + fs.unlink(path, () => { }); + throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`); } } } diff --git a/src/onebot/action/go-cqhttp/SetQQProfile.ts b/src/onebot/action/go-cqhttp/SetQQProfile.ts index e2ac98e4..e010d6cc 100644 --- a/src/onebot/action/go-cqhttp/SetQQProfile.ts +++ b/src/onebot/action/go-cqhttp/SetQQProfile.ts @@ -14,7 +14,7 @@ const SchemaData = { type Payload = FromSchema; -export class SetQQProfile extends BaseAction { +export class SetQQProfile extends BaseAction { actionName = ActionName.SetQQProfile; payloadSchema = SchemaData; diff --git a/src/onebot/action/go-cqhttp/UploadGroupFile.ts b/src/onebot/action/go-cqhttp/UploadGroupFile.ts index 79efa989..6645ed5e 100644 --- a/src/onebot/action/go-cqhttp/UploadGroupFile.ts +++ b/src/onebot/action/go-cqhttp/UploadGroupFile.ts @@ -42,7 +42,7 @@ export default class GoCQHTTPUploadGroupFile extends BaseAction { deleteAfterSentFiles: [] }; const sendFileEle = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name, payload.folder ?? payload.folder_id); - await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [sendFileEle], [], true); + await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [sendFileEle], msgContext.deleteAfterSentFiles, true); return null; } } diff --git a/src/onebot/action/go-cqhttp/UploadPrivateFile.ts b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts index 856e32b2..dfbaf0cc 100644 --- a/src/onebot/action/go-cqhttp/UploadPrivateFile.ts +++ b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts @@ -27,7 +27,7 @@ export default class GoCQHTTPUploadPrivateFile extends BaseAction if (payload.user_id) { const peerUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); if (!peerUid) { - throw `私聊${payload.user_id}不存在`; + throw new Error( `私聊${payload.user_id}不存在`); } const isBuddy = await this.core.apis.FriendApi.isBuddy(peerUid); return { chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid }; @@ -53,7 +53,7 @@ export default class GoCQHTTPUploadPrivateFile extends BaseAction deleteAfterSentFiles: [] }; const sendFileEle: SendFileElement = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name); - await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(await this.getPeer(payload), [sendFileEle], [], true); + await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(await this.getPeer(payload), [sendFileEle], msgContext.deleteAfterSentFiles, true); return null; } } diff --git a/src/onebot/action/group/GetAiRecord.ts b/src/onebot/action/group/GetAiRecord.ts new file mode 100644 index 00000000..d8f829c8 --- /dev/null +++ b/src/onebot/action/group/GetAiRecord.ts @@ -0,0 +1,26 @@ +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; +import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; + +const SchemaData = { + type: 'object', + properties: { + character: { type: ['string'] }, + group_id: { type: ['number', 'string'] }, + text: { type: 'string' }, + }, + required: ['character', 'group_id', 'text'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class GetAiRecord extends GetPacketStatusDepends { + actionName = ActionName.GetAiRecord; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + const rawRsp = await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound); + return await this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+payload.group_id, rawRsp.msgInfoBody[0].index); + } +} diff --git a/src/onebot/action/group/GetGroupEssence.ts b/src/onebot/action/group/GetGroupEssence.ts index 9e809ab4..1895bda1 100644 --- a/src/onebot/action/group/GetGroupEssence.ts +++ b/src/onebot/action/group/GetGroupEssence.ts @@ -4,6 +4,7 @@ import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { MessageUnique } from '@/common/message-unique'; import crypto from 'crypto'; +import { AdapterConfigWrap } from '@/onebot/config/config'; const SchemaData = { type: 'object', @@ -30,7 +31,9 @@ export class GetGroupEssence extends BaseAction { }; } - async _handle(payload: Payload) { + async _handle(payload: Payload, adapter: string) { + const network = Object.values(this.obContext.configLoader.configData.network) as Array; + const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array'; const msglist = (await this.core.apis.WebApi.getGroupEssenceMsgAll(payload.group_id.toString())).flatMap((e) => e.data.msg_list); if (!msglist) { throw new Error('获取失败'); @@ -51,7 +54,7 @@ export class GetGroupEssence extends BaseAction { operator_nick: msg.add_digest_nick, message_id: message_id, operator_time: msg.add_digest_time, - content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage))?.message + content: (await this.obContext.apis.MsgApi.parseMessage(rawMessage, msgFormat))?.message }; } const msgTempData = JSON.stringify({ diff --git a/src/onebot/action/group/GetGroupMemberInfo.ts b/src/onebot/action/group/GetGroupMemberInfo.ts index 82f734ef..3dbf606d 100644 --- a/src/onebot/action/group/GetGroupMemberInfo.ts +++ b/src/onebot/action/group/GetGroupMemberInfo.ts @@ -3,7 +3,6 @@ import { OB11Entities } from '@/onebot/entities'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { GroupMember } from '@/core'; const SchemaData = { type: 'object', @@ -44,7 +43,7 @@ class GetGroupMemberInfo extends BaseAction { } else { this.core.context.logger.logDebug(`获取群成员详细信息失败, 只能返回基础信息`); } - return OB11Entities.groupMember(payload.group_id.toString(), member as GroupMember); + return OB11Entities.groupMember(payload.group_id.toString(), member); } } diff --git a/src/onebot/action/group/GetGroupMemberList.ts b/src/onebot/action/group/GetGroupMemberList.ts index 018a6f4d..eb4a62a1 100644 --- a/src/onebot/action/group/GetGroupMemberList.ts +++ b/src/onebot/action/group/GetGroupMemberList.ts @@ -24,12 +24,11 @@ export class GetGroupMemberList extends BaseAction { const noCache = payload.no_cache ? this.stringToBoolean(payload.no_cache) : false; const memberCache = this.core.apis.GroupApi.groupMemberCache; let groupMembers; - if (noCache) { - groupMembers = await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr); - } else { + try { + groupMembers = await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr, 3000, noCache); + } catch (error) { groupMembers = memberCache.get(groupIdStr) ?? await this.core.apis.GroupApi.getGroupMembersV2(groupIdStr); } - const memberPromises = Array.from(groupMembers.values()).map(item => OB11Entities.groupMember(groupIdStr, item) ); diff --git a/src/onebot/action/group/GetGroupShutList.ts b/src/onebot/action/group/GetGroupShutList.ts index 8d63e054..f7aa70ec 100644 --- a/src/onebot/action/group/GetGroupShutList.ts +++ b/src/onebot/action/group/GetGroupShutList.ts @@ -1,4 +1,3 @@ -import { OB11Group } from '@/onebot'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; diff --git a/src/onebot/action/group/GroupPoke.ts b/src/onebot/action/group/GroupPoke.ts index 1c502e2b..d85eb842 100644 --- a/src/onebot/action/group/GroupPoke.ts +++ b/src/onebot/action/group/GroupPoke.ts @@ -18,6 +18,6 @@ export class GroupPoke extends GetPacketStatusDepends { payloadSchema = SchemaData; async _handle(payload: Payload) { - await this.core.apis.PacketApi.sendPokePacket(+payload.user_id, +payload.group_id); + await this.core.apis.PacketApi.pkt.operation.GroupPoke(+payload.user_id, +payload.group_id); } } diff --git a/src/onebot/action/group/SendGroupAiRecord.ts b/src/onebot/action/group/SendGroupAiRecord.ts new file mode 100644 index 00000000..74f9d43f --- /dev/null +++ b/src/onebot/action/group/SendGroupAiRecord.ts @@ -0,0 +1,38 @@ +import { ActionName } from '../types'; +import { FromSchema, JSONSchema } from 'json-schema-to-ts'; +import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; +import { uri2local } from "@/common/file"; +import { ChatType, Peer } from "@/core"; +import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; + +const SchemaData = { + type: 'object', + properties: { + character: { type: ['string'] }, + group_id: { type: ['number', 'string'] }, + text: { type: 'string' }, + }, + required: ['character', 'group_id', 'text'], +} as const satisfies JSONSchema; + +type Payload = FromSchema; + +export class SendGroupAiRecord extends GetPacketStatusDepends { + actionName = ActionName.SendGroupAiRecord; + payloadSchema = SchemaData; + + async _handle(payload: Payload) { + const rawRsp = await this.core.apis.PacketApi.pkt.operation.GetAiVoice(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound); + const url = await this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+payload.group_id, rawRsp.msgInfoBody[0].index); + const { path, errMsg, success } = (await uri2local(this.core.NapCatTempPath, url)); + if (!success) { + throw new Error(errMsg); + } + const peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() } as Peer; + const element = await this.core.apis.FileApi.createValidSendPttElement(path); + const sendRes = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [element], [path]); + return { message_id: sendRes.id ?? -1 }; + } +} diff --git a/src/onebot/action/index.ts b/src/onebot/action/index.ts index 12ea7127..ecdd2418 100644 --- a/src/onebot/action/index.ts +++ b/src/onebot/action/index.ts @@ -92,13 +92,16 @@ import { GetGroupFileUrl } from "@/onebot/action/file/GetGroupFileUrl"; import { GetPacketStatus } from "@/onebot/action/packet/GetPacketStatus"; import { FriendPoke } from "@/onebot/action/user/FriendPoke"; import { GetCredentials } from './system/GetCredentials'; -import { SetGroupSign } from './extends/SetGroupSign'; +import { SendGroupSign, SetGroupSign } from './extends/SetGroupSign'; import { GoCQHTTPGetGroupAtAllRemain } from './go-cqhttp/GetGroupAtAllRemain'; import { GoCQHTTPCheckUrlSafely } from './go-cqhttp/GoCQHTTPCheckUrlSafely'; import { GoCQHTTPGetModelShow } from './go-cqhttp/GoCQHTTPGetModelShow'; import { GoCQHTTPSetModelShow } from './go-cqhttp/GoCQHTTPSetModelShow'; import { GoCQHTTPDeleteFriend } from './go-cqhttp/GoCQHTTPDeleteFriend'; import { GetMiniAppArk } from "@/onebot/action/extends/GetMiniAppArk"; +import { GetAiRecord } from "@/onebot/action/group/GetAiRecord"; +import { SendGroupAiRecord } from "@/onebot/action/group/SendGroupAiRecord"; +import { GetAiCharacters } from "@/onebot/action/extends/GetAiCharacters"; export type ActionMap = Map>; @@ -122,6 +125,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo new TranslateEnWordToZn(obContext, core), new GetGroupRootFiles(obContext, core), new SetGroupSign(obContext, core), + new SendGroupSign(obContext, core), // onebot11 new SendLike(obContext, core), new GetMsg(obContext, core), @@ -212,6 +216,9 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo new GetGroupShutList(obContext, core), new GetGroupFileUrl(obContext, core), new GetMiniAppArk(obContext, core), + new GetAiRecord(obContext, core), + new SendGroupAiRecord(obContext, core), + new GetAiCharacters(obContext, core), ]; const actionMap = new Map(); for (const action of actionHandlers) { diff --git a/src/onebot/action/msg/GetMsg.ts b/src/onebot/action/msg/GetMsg.ts index 1764e465..f5a85814 100644 --- a/src/onebot/action/msg/GetMsg.ts +++ b/src/onebot/action/msg/GetMsg.ts @@ -4,6 +4,7 @@ import { ActionName } from '../types'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { MessageUnique } from '@/common/message-unique'; import { RawMessage } from '@/core'; +import { AdapterConfigWrap } from '@/onebot/config/config'; export type ReturnDataType = OB11Message @@ -22,13 +23,15 @@ class GetMsg extends BaseAction { actionName = ActionName.GetMsg; payloadSchema = SchemaData; - async _handle(payload: Payload) { + async _handle(payload: Payload, adapter: string) { // log("history msg ids", Object.keys(msgHistory)); + const network = Object.values(this.obContext.configLoader.configData.network) as Array; + const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array'; if (!payload.message_id) { throw Error('参数message_id不能为空'); } const MsgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString()); - const msgIdWithPeer = MessageUnique.getMsgIdAndPeerByShortId(MsgShortId || parseInt(payload.message_id.toString())); + const msgIdWithPeer = MessageUnique.getMsgIdAndPeerByShortId(MsgShortId ?? +payload.message_id); if (!msgIdWithPeer) { throw new Error('消息不存在'); } @@ -40,7 +43,7 @@ class GetMsg extends BaseAction { } else { msg = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()])).msgList[0]; } - const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg); + const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, msgFormat); if (!retMsg) throw Error('消息为空'); try { retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!; diff --git a/src/onebot/action/msg/MarkMsgAsRead.ts b/src/onebot/action/msg/MarkMsgAsRead.ts index 279f4966..188d80e0 100644 --- a/src/onebot/action/msg/MarkMsgAsRead.ts +++ b/src/onebot/action/msg/MarkMsgAsRead.ts @@ -2,12 +2,14 @@ import { ChatType, Peer } from '@/core/entities'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; +import { MessageUnique } from '@/common/message-unique'; const SchemaData = { type: 'object', properties: { user_id: { type: ['number', 'string'] }, group_id: { type: ['number', 'string'] }, + message_id: { type: ['number', 'string'] }, }, } as const satisfies JSONSchema; @@ -15,10 +17,20 @@ type PlayloadType = FromSchema; class MarkMsgAsRead extends BaseAction { async getPeer(payload: PlayloadType): Promise { + if (payload.message_id) { + const s_peer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)?.Peer; + if (s_peer) { + return s_peer; + } + const l_peer = MessageUnique.getPeerByMsgId(payload.message_id.toString())?.Peer; + if (l_peer) { + return l_peer; + } + } if (payload.user_id) { const peerUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); if (!peerUid) { - throw `私聊${payload.user_id}不存在`; + throw new Error( `私聊${payload.user_id}不存在`); } const isBuddy = await this.core.apis.FriendApi.isBuddy(peerUid); return { chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid }; diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts index b46ac58d..58fbc807 100644 --- a/src/onebot/action/msg/SendMsg.ts +++ b/src/onebot/action/msg/SendMsg.ts @@ -11,10 +11,10 @@ import { decodeCQCode } from '@/onebot/cqcode'; import { MessageUnique } from '@/common/message-unique'; import { ChatType, ElementType, NapCatCore, Peer, RawMessage, SendArkElement, SendMessageElement } from '@/core'; import BaseAction from '../BaseAction'; -import { rawMsgWithSendMsg } from "@/core/packet/message/converter"; -import { PacketMsg } from "@/core/packet/message/message"; import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; import { stringifyWithBigInt } from "@/common/helper"; +import { PacketMsg } from "@/core/packet/message/message"; +import { rawMsgWithSendMsg } from "@/core/packet/message/converter"; export interface ReturnDataType { message_id: number; @@ -68,7 +68,7 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext, } return { chatType: ChatType.KCHATTYPEC2C, - peerUid: Uid!, + peerUid: Uid, guildId: '', }; } @@ -122,8 +122,8 @@ export class SendMsg extends BaseAction { returnMsgAndResId = packetMode ? await this.handleForwardedNodesPacket(peer, messages as OB11MessageNode[], payload.source, payload.news, payload.summary, payload.prompt) : await this.handleForwardedNodes(peer, messages as OB11MessageNode[]); - } catch (e) { - throw Error(packetMode ? `发送伪造合并转发消息失败: ${e}` : `发送合并转发消息失败: ${e}`); + } catch (e: any) { + throw Error(packetMode ? `发送伪造合并转发消息失败: ${e?.stack}` : `发送合并转发消息失败: ${e?.stack}`); } if (!returnMsgAndResId) { throw Error('发送合并转发消息失败:returnMsgAndResId 为空!'); @@ -133,7 +133,7 @@ export class SendMsg extends BaseAction { guildId: '', peerUid: peer.peerUid, chatType: peer.chatType, - }, (returnMsgAndResId.message)!.msgId); + }, (returnMsgAndResId.message).msgId); return { message_id: msgShortId!, res_id: returnMsgAndResId.res_id }; } else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) { throw Error(`发送转发消息(res_id:${returnMsgAndResId.res_id} 失败`); @@ -150,7 +150,7 @@ export class SendMsg extends BaseAction { const { sendElements, deleteAfterSentFiles } = await this.obContext.apis.MsgApi .createSendElements(messages, peer); const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles); - return { message_id: returnMsg!.id! }; + return { message_id: returnMsg.id! }; } private async uploadForwardedNodesPacket(msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: { @@ -175,7 +175,7 @@ export class SendMsg extends BaseAction { if (getSpecialMsgNum({ message: OB11Data }, OB11MessageDataType.node)) { const uploadReturnData = await this.uploadForwardedNodesPacket(msgPeer, OB11Data as OB11MessageNode[], node.data.source, node.data.news, node.data.summary, node.data.prompt, { - user_id: (node.data.user_id || node.data.uin)?.toString() ?? parentMeta?.user_id ?? this.core.selfInfo.uin, + user_id: (node.data.user_id ?? node.data.uin)?.toString() ?? parentMeta?.user_id ?? this.core.selfInfo.uin, nickname: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? "QQ用户", }, dp + 1); sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : []; @@ -185,16 +185,16 @@ export class SendMsg extends BaseAction { } const packetMsgElements: rawMsgWithSendMsg = { - senderUin: Number((node.data.user_id || node.data.uin) ?? parentMeta?.user_id) || +this.core.selfInfo.uin, + senderUin: Number((node.data.user_id ?? node.data.uin) ?? parentMeta?.user_id) || +this.core.selfInfo.uin, senderName: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? "QQ用户", groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : undefined, time: Number(node.data.time) || Date.now(), msg: sendElements, }; logger.logDebug(`handleForwardedNodesPacket[SendRaw] 开始转换 ${stringifyWithBigInt(packetMsgElements)}`); - const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements); + const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements); logger.logDebug(`handleForwardedNodesPacket[SendRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`); - packetMsg.push(transformedMsg!); + packetMsg.push(transformedMsg); } else if (node.data.id) { const id = node.data.id; const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(+id) || MessageUnique.getPeerByMsgId(id); @@ -205,9 +205,9 @@ export class SendMsg extends BaseAction { const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0]; logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`); await this.core.apis.FileApi.downloadRawMsgMedia([msg]); - const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgToPacketMsg(msg, msgPeer); + const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgToPacketMsg(msg, msgPeer); logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`); - packetMsg.push(transformedMsg!); + packetMsg.push(transformedMsg); } else { logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${stringifyWithBigInt(node)}`); } @@ -216,7 +216,7 @@ export class SendMsg extends BaseAction { logger.logWarn('handleForwardedNodesPacket 元素为空!'); return null; } - const resid = await this.core.apis.PacketApi.sendUploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); + const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0); const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt); return { finallySendElements: { @@ -308,8 +308,8 @@ export class SendMsg extends BaseAction { MessageUnique.createUniqueMsgId(selfPeer, result.value.msgId); } }); - } catch (e) { - logger.logDebug('生成转发消息节点失败', e); + } catch (e: any) { + logger.logDebug('生成转发消息节点失败', e?.stack); } } } @@ -350,8 +350,8 @@ export class SendMsg extends BaseAction { return { message: await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds) }; - } catch (e) { - logger.logError.bind(this.core.context.logger)('forward failed', e); + } catch (e: any) { + logger.logError.bind(this.core.context.logger)('forward failed', e?.stack); return { message: null }; @@ -376,8 +376,8 @@ export class SendMsg extends BaseAction { } try { return await this.core.apis.MsgApi.sendMsg(selfPeer, sendElements, true); - } catch (e) { - logger.logError.bind(this.core.context.logger)(e, '克隆转发消息失败,将忽略本条消息', msg); + } catch (e: any) { + logger.logError.bind(this.core.context.logger)(e?.stack, '克隆转发消息失败,将忽略本条消息', msg); } } } diff --git a/src/onebot/action/msg/SetMsgEmojiLike.ts b/src/onebot/action/msg/SetMsgEmojiLike.ts index 8ceb32bc..065538f0 100644 --- a/src/onebot/action/msg/SetMsgEmojiLike.ts +++ b/src/onebot/action/msg/SetMsgEmojiLike.ts @@ -8,6 +8,7 @@ const SchemaData = { properties: { message_id: { type: ['string', 'number'] }, emoji_id: { type: ['string', 'number'] }, + set: { type: ['boolean', 'string'] } }, required: ['message_id', 'emoji_id'], } as const satisfies JSONSchema; @@ -26,10 +27,19 @@ export class SetMsgEmojiLike extends BaseAction { if (!payload.emoji_id) { throw new Error('emojiId not found'); } + if (!payload.set) { + payload.set = true; + } + const msgData = (await this.core.apis.MsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList; if (!msgData || msgData.length == 0 || !msgData[0].msgSeq) { throw new Error('find msg by msgid error'); } - return await this.core.apis.MsgApi.setEmojiLike(msg.Peer, msgData[0].msgSeq, payload.emoji_id.toString(), true); + return await this.core.apis.MsgApi.setEmojiLike( + msg.Peer, + msgData[0].msgSeq, + payload.emoji_id.toString(), + typeof payload.set == 'string' ? payload.set === 'true' : !!payload + ); } } diff --git a/src/onebot/action/packet/GetPacketStatus.ts b/src/onebot/action/packet/GetPacketStatus.ts index 265e7126..63c3020e 100644 --- a/src/onebot/action/packet/GetPacketStatus.ts +++ b/src/onebot/action/packet/GetPacketStatus.ts @@ -9,7 +9,8 @@ export abstract class GetPacketStatusDepends extends BaseAction if (!this.core.apis.PacketApi.available) { return { valid: false, - message: "packetServer不可用,请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置!", + message: "packetBackend不可用,请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置!" + + "错误堆栈信息:" + this.core.apis.PacketApi.clientLogStack, }; } return await super.check(payload); diff --git a/src/onebot/action/types.ts b/src/onebot/action/types.ts index 5c998a47..05907312 100644 --- a/src/onebot/action/types.ts +++ b/src/onebot/action/types.ts @@ -136,6 +136,11 @@ export enum ActionName { GetGroupIgnoredNotifies = 'get_group_ignored_notifies', SetGroupSign = "set_group_sign", + SendGroupSign = "send_group_sign", + GetMiniAppArk = "get_mini_app_ark", // UploadForwardMsg = "upload_forward_msg", + GetAiRecord = "get_ai_record", + GetAiCharacters = "get_ai_characters", + SendGroupAiRecord = "send_group_ai_record", } diff --git a/src/onebot/action/user/FriendPoke.ts b/src/onebot/action/user/FriendPoke.ts index 267c6367..620d9a73 100644 --- a/src/onebot/action/user/FriendPoke.ts +++ b/src/onebot/action/user/FriendPoke.ts @@ -17,6 +17,6 @@ export class FriendPoke extends GetPacketStatusDepends { payloadSchema = SchemaData; async _handle(payload: Payload) { - await this.core.apis.PacketApi.sendPokePacket(+payload.user_id); + await this.core.apis.PacketApi.pkt.operation.FriendPoke(+payload.user_id); } } diff --git a/src/onebot/action/user/GetRecentContact.ts b/src/onebot/action/user/GetRecentContact.ts index 9a9487fe..05df160a 100644 --- a/src/onebot/action/user/GetRecentContact.ts +++ b/src/onebot/action/user/GetRecentContact.ts @@ -1,6 +1,7 @@ import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import BaseAction from '../BaseAction'; import { ActionName } from '../types'; +import { AdapterConfigWrap } from '@/onebot/config/config'; const SchemaData = { type: 'object', @@ -15,13 +16,16 @@ export default class GetRecentContact extends BaseAction { actionName = ActionName.GetRecentContact; payloadSchema = SchemaData; - async _handle(payload: Payload) { + async _handle(payload: Payload, adapter: string) { const ret = await this.core.apis.UserApi.getRecentContactListSnapShot(+(payload.count || 10)); + const network = Object.values(this.obContext.configLoader.configData.network) as Array; + //烘焙消息 + const msgFormat = network.flat().find(e => e.name === adapter)?.messagePostFormat ?? 'array'; return await Promise.all(ret.info.changedList.map(async (t) => { const FastMsg = await this.core.apis.MsgApi.getMsgsByMsgId({ chatType: t.chatType, peerUid: t.peerUid }, [t.msgId]); if (FastMsg.msgList.length > 0) { //扩展ret.info.changedList - const lastestMsg = await this.obContext.apis.MsgApi.parseMessage(FastMsg.msgList[0]); + const lastestMsg = await this.obContext.apis.MsgApi.parseMessage(FastMsg.msgList[0], msgFormat); return { lastestMsg: lastestMsg, peerUin: t.peerUin, diff --git a/src/onebot/action/user/SendLike.ts b/src/onebot/action/user/SendLike.ts index e2934a0f..e63409b4 100644 --- a/src/onebot/action/user/SendLike.ts +++ b/src/onebot/action/user/SendLike.ts @@ -18,13 +18,11 @@ export default class SendLike extends BaseAction { payloadSchema = SchemaData; async _handle(payload: Payload): Promise { - //logDebug('点赞参数', payload); const qq = payload.user_id.toString(); - const uid: string = await this.core.apis.UserApi.getUidByUinV2(qq) || ''; + const uid: string = await this.core.apis.UserApi.getUidByUinV2(qq) ?? ''; const result = await this.core.apis.UserApi.like(uid, parseInt(payload.times?.toString()) || 1); - //logDebug('点赞结果', result); if (result.result !== 0) { - throw `点赞失败 ${result.errMsg}`; + throw new Error(`点赞失败 ${result.errMsg}`); } return null; } diff --git a/src/onebot/api/group.ts b/src/onebot/api/group.ts index ca5ed920..c8124f8d 100644 --- a/src/onebot/api/group.ts +++ b/src/onebot/api/group.ts @@ -40,7 +40,7 @@ export class OneBotGroupApi { if (msg.senderUin && msg.senderUin !== '0') { const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin); if (member && member.cardName !== msg.sendMemberName) { - const newCardName = msg.sendMemberName || ''; + const newCardName = msg.sendMemberName ?? ''; const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName); member.cardName = newCardName; return event; @@ -48,7 +48,7 @@ export class OneBotGroupApi { } for (const element of msg.elements) { - if (element.grayTipElement && element.grayTipElement.groupElement) { + if (element.grayTipElement?.groupElement) { const groupElement = element.grayTipElement.groupElement; if (groupElement.type == TipGroupElementType.memberIncrease) { const MemberIncreaseEvent = await this.obContext.apis.GroupApi.parseGroupMemberIncreaseEvent(msg.peerUid, element.grayTipElement); @@ -83,7 +83,7 @@ export class OneBotGroupApi { url: pathToFileURL(element.fileElement.filePath).href, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize), - busid: element.fileElement.fileBizId || 0, + busid: element.fileElement.fileBizId ?? 0, }, ); } @@ -109,8 +109,8 @@ export class OneBotGroupApi { return new OB11GroupPokeEvent( this.core, parseInt(msg.peerUid), - parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid))!), - parseInt((await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid))!), + +await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid), + +await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid), pokedetail, ); } @@ -165,13 +165,13 @@ export class OneBotGroupApi { async parseGroupBanEvent(GroupCode: string, grayTipElement: GrayTipElement) { const groupElement = grayTipElement?.groupElement; if (!groupElement?.shutUp) return undefined; - const memberUid = groupElement.shutUp!.member.uid; - const adminUid = groupElement.shutUp!.admin.uid; + const memberUid = groupElement.shutUp.member.uid; + const adminUid = groupElement.shutUp.admin.uid; let memberUin: string; - let duration = parseInt(groupElement.shutUp!.duration); + let duration = parseInt(groupElement.shutUp.duration); const subType: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban'; if (memberUid) { - memberUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, memberUid))?.uin || ''; + memberUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, memberUid))?.uin ?? ''; } else { memberUin = '0'; // 0表示全员禁言 if (duration > 0) { @@ -225,7 +225,7 @@ export class OneBotGroupApi { const memberUin = member?.uin; const adminMember = await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid); if (memberUin) { - const operatorUin = adminMember?.uin || memberUin; + const operatorUin = adminMember?.uin ?? memberUin; return new OB11GroupIncreaseEvent( this.core, parseInt(GroupCode), @@ -240,7 +240,7 @@ export class OneBotGroupApi { async parseGroupKickEvent(GroupCode: string, grayTipElement: GrayTipElement) { const groupElement = grayTipElement?.groupElement; if (!groupElement) return undefined; - const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid))?.uin || (await this.core.apis.UserApi.getUidByUinV2(groupElement.adminUid)); + const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, groupElement.adminUid))?.uin ?? (await this.core.apis.UserApi.getUidByUinV2(groupElement.adminUid)); if (adminUin) { return new OB11GroupDecreaseEvent( this.core, diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index bce77da1..85299bb5 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -18,13 +18,7 @@ import { SendTextElement, } from '@/core'; import faceConfig from '@/core/external/face_config.json'; -import { - NapCatOneBot11Adapter, - OB11Message, - OB11MessageData, - OB11MessageDataType, - OB11MessageFileBase, -} from '@/onebot'; +import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, } from '@/onebot'; import { OB11Entities } from '@/onebot/entities'; import { EventType } from '@/onebot/event/OB11BaseEvent'; import { encodeCQCode } from '@/onebot/cqcode'; @@ -33,8 +27,9 @@ import { RequestUtil } from '@/common/request'; import fs from 'node:fs'; import fsPromise from 'node:fs/promises'; import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; -import { decodeSysMessage } from '@/core/packet/proto/old/ProfileLike'; +// import { decodeSysMessage } from '@/core/packet/proto/old/ProfileLike'; import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; +import { decodeSysMessage } from "@/core/helper/adaptDecoder"; type RawToOb11Converters = { [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( @@ -190,6 +185,9 @@ export class OneBotMsgApi { file_id: FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, "", "." + _.key + ".jpg"), path: url, url: url, + key: _.key, + emoji_id: _.emojiId, + emoji_package_id: _.emojiPackageId, file_unique: _.key }, }; @@ -370,7 +368,7 @@ export class OneBotMsgApi { multiMsgItem.parentMsgPeer = parentMsgPeer; multiMsgItem.parentMsgIdList = msg.parentMsgIdList; multiMsgItem.id = MessageUnique.createUniqueMsgId(parentMsgPeer, multiMsgItem.msgId); //该ID仅用查看 无法调用 - return await this.parseMessage(multiMsgItem); + return await this.parseMessage(multiMsgItem, 'array'); }, ))).filter(item => item !== undefined), }, @@ -506,13 +504,12 @@ export class OneBotMsgApi { // File service [OB11MessageDataType.image]: async (sendMsg, context) => { - const sendPicElement = await this.core.apis.FileApi.createValidSendPicElement( + return await this.core.apis.FileApi.createValidSendPicElement( context, (await this.handleOb11FileLikeMessage(sendMsg, context)).path, sendMsg.data.summary, sendMsg.data.sub_type, ); - return sendPicElement; }, [OB11MessageDataType.file]: async (sendMsg, context) => { @@ -696,29 +693,38 @@ export class OneBotMsgApi { async parseMessage( msg: RawMessage, - messagePostFormat: string = this.obContext.configLoader.configData.messagePostFormat, + messagePostFormat: string, + ) { + if (messagePostFormat === 'string') { + return (await this.parseMessageV2(msg))?.stringMsg; + } + return (await this.parseMessageV2(msg))?.arrayMsg; + } + + async parseMessageV2( + msg: RawMessage, ) { if (msg.senderUin == '0' || msg.senderUin == '') return; if (msg.peerUin == '0' || msg.peerUin == '') return; //跳过空消息 const resMsg: OB11Message = { self_id: parseInt(this.core.selfInfo.uin), - user_id: parseInt(msg.senderUin!), + user_id: parseInt(msg.senderUin), time: parseInt(msg.msgTime) || Date.now(), message_id: msg.id!, message_seq: msg.id!, real_id: msg.id!, message_type: msg.chatType == ChatType.KCHATTYPEGROUP ? 'group' : 'private', sender: { - user_id: parseInt(msg.senderUin || '0'), + user_id: +(msg.senderUin ?? 0), nickname: msg.sendNickName, - card: msg.sendMemberName || '', + card: msg.sendMemberName ?? '', }, raw_message: '', font: 14, sub_type: 'friend', - message: messagePostFormat === 'string' ? '' : [], - message_format: messagePostFormat === 'string' ? 'string' : 'array', + message: [], + message_format: 'array', post_type: this.core.selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE, }; if (this.core.selfInfo.uin == msg.senderUin) { @@ -788,17 +794,17 @@ export class OneBotMsgApi { }).map((entry) => (>entry).value).filter(value => value != null); const msgAsCQCode = validSegments.map(msg => encodeCQCode(msg)).join('').trim(); - - if (messagePostFormat === 'string') { - resMsg.message = msgAsCQCode; - resMsg.raw_message = msgAsCQCode; - } else { - resMsg.message = validSegments; - resMsg.raw_message = msgAsCQCode; - } - return resMsg; + resMsg.message = validSegments; + resMsg.raw_message = msgAsCQCode; + let stringMsg = structuredClone(resMsg); + stringMsg = await this.importArrayTostringMsg(stringMsg); + return { stringMsg: stringMsg, arrayMsg: resMsg }; + } + async importArrayTostringMsg(msg: OB11Message) { + msg.message_format = 'string'; + msg.message = msg.raw_message; + return msg; } - async createSendElements( messageData: OB11MessageData[], peer: Peer, @@ -892,6 +898,7 @@ export class OneBotMsgApi { return { path, fileName: inputdata.name ?? fileName }; } + async parseSysMessage(msg: number[]) { const sysMsg = decodeSysMessage(Uint8Array.from(msg)); if (sysMsg.msgSpec.length === 0) { @@ -900,8 +907,7 @@ export class OneBotMsgApi { const { msgType, subType, subSubType } = sysMsg.msgSpec[0]; if (msgType === 528 && subType === 39 && subSubType === 39) { if (!sysMsg.bodyWrapper) return; - const event = await this.obContext.apis.UserApi.parseLikeEvent(sysMsg.bodyWrapper.wrappedBody); - return event; + return await this.obContext.apis.UserApi.parseLikeEvent(sysMsg.bodyWrapper.wrappedBody); } /* if (msgType === 732 && subType === 16 && subSubType === 16) { diff --git a/src/onebot/api/user.ts b/src/onebot/api/user.ts index 7c3c09d1..d22add6a 100644 --- a/src/onebot/api/user.ts +++ b/src/onebot/api/user.ts @@ -1,8 +1,7 @@ import { NapCatCore } from '@/core'; -import { decodeProfileLikeTip } from '@/core/packet/proto/old/ProfileLike'; - import { NapCatOneBot11Adapter } from '@/onebot'; import { OB11ProfileLikeEvent } from '../event/notice/OB11ProfileLikeEvent'; +import { decodeProfileLikeTip } from "@/core/helper/adaptDecoder"; export class OneBotUserApi { obContext: NapCatOneBot11Adapter; @@ -12,6 +11,7 @@ export class OneBotUserApi { this.obContext = obContext; this.core = core; } + async parseLikeEvent(wrappedBody: Uint8Array): Promise { const likeTip = decodeProfileLikeTip(Uint8Array.from(wrappedBody)); if (likeTip?.msgType !== 0 || likeTip?.subType !== 203) return; diff --git a/src/onebot/config/config.ts b/src/onebot/config/config.ts new file mode 100644 index 00000000..00e6d55c --- /dev/null +++ b/src/onebot/config/config.ts @@ -0,0 +1,225 @@ +interface v1Config { + http: { + enable: boolean; + host: string; + port: number; + secret: string; + enableHeart: boolean; + enablePost: boolean; + postUrls: string[]; + }; + ws: { + enable: boolean; + host: string; + port: number; + }; + reverseWs: { + enable: boolean; + urls: string[]; + }; + debug: boolean; + heartInterval: number; + messagePostFormat: string; + enableLocalFile2Url: boolean; + musicSignUrl: string; + reportSelfMessage: boolean; + token: string; +} +export interface AdapterConfigInner { + name: string; + enable: boolean; +} +export type AdapterConfigWrap = AdapterConfigInner & Partial; + +export interface AdapterConfig extends AdapterConfigInner { + [key: string]: any; +} + +const createDefaultAdapterConfig = (config: T): T => config; + +export const httpServerDefaultConfigs = createDefaultAdapterConfig({ + name: 'http-server', + enable: false as boolean, + port: 3000, + host: '0.0.0.0', + enableCors: true, + enableWebsocket: true, + messagePostFormat: 'array', + token: '', + debug: false, +}); +export type HttpServerConfig = typeof httpServerDefaultConfigs; + +export const httpClientDefaultConfigs = createDefaultAdapterConfig({ + name: 'http-client', + enable: false as boolean, + url: 'http://localhost:8080', + messagePostFormat: 'array', + reportSelfMessage: false, + token: '', + debug: false, +}); +export type HttpClientConfig = typeof httpClientDefaultConfigs; + +export const websocketServerDefaultConfigs = createDefaultAdapterConfig({ + name: 'websocket-server', + enable: false as boolean, + host: '0.0.0.0', + port: 3001, + messagePostFormat: 'array', + reportSelfMessage: false, + token: '', + enableForcePushEvent: true, + debug: false, + heartInterval: 30000, +}); +export type WebsocketServerConfig = typeof websocketServerDefaultConfigs; + +export const websocketClientDefaultConfigs = createDefaultAdapterConfig({ + name: 'websocket-client', + enable: false as boolean, + url: 'ws://localhost:8082', + messagePostFormat: 'array', + reportSelfMessage: false, + reconnectInterval: 5000, + token: '', + debug: false, + heartInterval: 30000, +}); +export type WebsocketClientConfig = typeof websocketClientDefaultConfigs; + +export interface NetworkConfig { + httpServers: Array; + httpClients: Array; + websocketServers: Array; + websocketClients: Array; +} + +export function mergeConfigs(defaultConfig: T, userConfig: Partial): T { + return { ...defaultConfig, ...userConfig }; +} + +export interface OneBotConfig { + network: NetworkConfig; // 网络配置 + musicSignUrl: string; // 音乐签名地址 + enableLocalFile2Url: boolean; +} + +const createDefaultConfig = (config: T): T => config; + +export const defaultOneBotConfigs = createDefaultConfig({ + network: { + httpServers: [], + httpClients: [], + websocketServers: [], + websocketClients: [], + }, + musicSignUrl: '', + enableLocalFile2Url: false, +}); + +export const mergeNetworkDefaultConfig = { + httpServers: httpServerDefaultConfigs, + httpClients: httpClientDefaultConfigs, + websocketServers: websocketServerDefaultConfigs, + websocketClients: websocketClientDefaultConfigs, +} as const; + +export type NetworkConfigAdapter = HttpServerConfig | HttpClientConfig | WebsocketServerConfig | WebsocketClientConfig; +type NetworkConfigKeys = keyof typeof mergeNetworkDefaultConfig; + +export function mergeOneBotConfigs( + userConfig: Partial, + defaultConfig: OneBotConfig = defaultOneBotConfigs +): OneBotConfig { + const mergedConfig = { ...defaultConfig }; + + if (userConfig.network) { + mergedConfig.network = { ...defaultConfig.network }; + for (const key in userConfig.network) { + const userNetworkConfig = userConfig.network[key as keyof NetworkConfig]; + const defaultNetworkConfig = mergeNetworkDefaultConfig[key as NetworkConfigKeys]; + if (Array.isArray(userNetworkConfig)) { + mergedConfig.network[key as keyof NetworkConfig] = userNetworkConfig.map((e) => + mergeConfigs(defaultNetworkConfig, e) + ); + } + } + } + if (userConfig.musicSignUrl !== undefined) { + mergedConfig.musicSignUrl = userConfig.musicSignUrl; + } + return mergedConfig; +} + +function checkIsOneBotConfigV1(v1Config: Partial): boolean { + return v1Config.http !== undefined || v1Config.ws !== undefined || v1Config.reverseWs !== undefined; +} + +export function migrateOneBotConfigsV1(config: Partial): OneBotConfig { + if (!checkIsOneBotConfigV1(config)) { + return config as OneBotConfig; + } + const mergedConfig = { ...defaultOneBotConfigs }; + if (config.http) { + mergedConfig.network.httpServers = [ + mergeConfigs(httpServerDefaultConfigs, { + enable: config.http.enable, + port: config.http.port, + host: config.http.host, + token: config.http.secret, + debug: config.debug, + messagePostFormat: config.messagePostFormat, + }), + ]; + } + if (config.ws) { + mergedConfig.network.websocketServers = [ + mergeConfigs(websocketServerDefaultConfigs, { + enable: config.ws.enable, + port: config.ws.port, + host: config.ws.host, + token: config.token, + debug: config.debug, + messagePostFormat: config.messagePostFormat, + reportSelfMessage: config.reportSelfMessage, + }), + ]; + } + if (config.reverseWs) { + mergedConfig.network.websocketClients = config.reverseWs.urls.map((url) => + mergeConfigs(websocketClientDefaultConfigs, { + enable: config.reverseWs?.enable, + url: url, + token: config.token, + debug: config.debug, + messagePostFormat: config.messagePostFormat, + reportSelfMessage: config.reportSelfMessage, + }) + ); + } + if (config.heartInterval) { + mergedConfig.network.websocketServers[0].heartInterval = config.heartInterval; + } + if (config.musicSignUrl) { + mergedConfig.musicSignUrl = config.musicSignUrl; + } + if (config.enableLocalFile2Url) { + mergedConfig.enableLocalFile2Url = config.enableLocalFile2Url; + } + return mergedConfig; +} +export function getConfigBoolKey( + configs: Array, + prediction: (config: NetworkConfigAdapter) => boolean +): { positive: Array, negative: Array } { + const result: { positive: string[], negative: string[] } = { positive: [], negative: [] }; + configs.forEach(config => { + if (prediction(config)) { + result.positive.push(config.name); + } else { + result.negative.push(config.name); + } + }); + return result; +} \ No newline at end of file diff --git a/src/onebot/config/index.ts b/src/onebot/config/index.ts index 7d517095..9e80df5a 100644 --- a/src/onebot/config/index.ts +++ b/src/onebot/config/index.ts @@ -1,11 +1,9 @@ import { ConfigBase } from '@/common/config-base'; -import ob11DefaultConfig from './onebot11.json'; import { NapCatCore } from '@/core'; +import { OneBotConfig } from './config'; -export type OB11Config = typeof ob11DefaultConfig; - -export class OB11ConfigLoader extends ConfigBase { +export class OB11ConfigLoader extends ConfigBase { constructor(core: NapCatCore, configPath: string) { - super('onebot11', core, configPath); + super('onebot11', core, configPath, false); } } diff --git a/src/onebot/config/onebot11.json b/src/onebot/config/onebot11.json deleted file mode 100644 index 8ee5a368..00000000 --- a/src/onebot/config/onebot11.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "http": { - "enable": false, - "host": "", - "port": 3000, - "secret": "", - "enableHeart": false, - "enablePost": false, - "postUrls": [] - }, - "ws": { - "enable": false, - "host": "", - "port": 3001 - }, - "reverseWs": { - "enable": false, - "urls": [] - }, - "GroupLocalTime": { - "Record": false, - "RecordList": [] - }, - "debug": false, - "heartInterval": 30000, - "messagePostFormat": "array", - "enableLocalFile2Url": true, - "musicSignUrl": "", - "reportSelfMessage": false, - "token": "" -} diff --git a/src/onebot/entities.ts b/src/onebot/entities.ts index efaf249f..d043e3e0 100644 --- a/src/onebot/entities.ts +++ b/src/onebot/entities.ts @@ -25,7 +25,7 @@ export class OB11Entities { user_id: parseInt(rawFriend.coreInfo.uin), nickname: rawFriend.coreInfo.nick, remark: rawFriend.coreInfo.remark ?? rawFriend.coreInfo.nick, - sex: this.sex(rawFriend.baseInfo.sex!), + sex: this.sex(rawFriend.baseInfo.sex), level: 0, })); } @@ -66,7 +66,7 @@ export class OB11Entities { sex: OB11Entities.sex(member.sex!), age: member.age ?? 0, area: '', - level: member.memberRealLevel ?? '0', + level: member.memberRealLevel?.toString() ?? '0', qq_level: member.qqLevel && calcQQLevel(member.qqLevel) || 0, join_time: +member.joinTime, last_sent_time: +member.lastSpeakTime, @@ -76,7 +76,7 @@ export class OB11Entities { is_robot: member.isRobot, shut_up_timestamp: member.shutUpTime, role: OB11Entities.groupMemberRole(member.role), - title: member.memberSpecialTitle || '', + title: member.memberSpecialTitle ?? '', }; } diff --git a/src/onebot/event/meta/OB11HeartbeatEvent.ts b/src/onebot/event/meta/OB11HeartbeatEvent.ts index 41d16ba9..7171d9cc 100644 --- a/src/onebot/event/meta/OB11HeartbeatEvent.ts +++ b/src/onebot/event/meta/OB11HeartbeatEvent.ts @@ -11,11 +11,11 @@ export class OB11HeartbeatEvent extends OB11BaseMetaEvent { status: HeartbeatStatus; interval: number; - public constructor(core: NapCatCore, interval: number, isOnline: boolean | undefined, isGood: boolean) { + public constructor(core: NapCatCore, interval: number, isOnline: boolean, isGood: boolean) { super(core); this.interval = interval; this.status = { - online: isOnline && true, + online: isOnline, good: isGood, }; } diff --git a/src/onebot/event/notice/BotOfflineEvent.ts b/src/onebot/event/notice/BotOfflineEvent.ts new file mode 100644 index 00000000..6718a561 --- /dev/null +++ b/src/onebot/event/notice/BotOfflineEvent.ts @@ -0,0 +1,16 @@ +import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; +import { NapCatCore } from '@/core'; + +export class BotOfflineEvent extends OB11BaseNoticeEvent { + notice_type = 'bot_offline'; + user_id: number; + tag: string = 'BotOfflineEvent'; + message: string = 'BotOfflineEvent'; + + public constructor(core: NapCatCore, tag: string, message: string) { + super(core); + this.user_id = +core.selfInfo.uin; + this.tag = tag; + this.message = message; + } +} diff --git a/src/onebot/event/notice/OB11InputStatusEvent.ts b/src/onebot/event/notice/OB11InputStatusEvent.ts index 7ae538e2..4b5777ac 100644 --- a/src/onebot/event/notice/OB11InputStatusEvent.ts +++ b/src/onebot/event/notice/OB11InputStatusEvent.ts @@ -1,7 +1,7 @@ import { OB11BaseNoticeEvent } from './OB11BaseNoticeEvent'; import { NapCatCore } from '@/core'; -//输入状态事件 初步完成 Mlikio wa Todo 需要做一些过滤 +//TODO: 输入状态事件 初步完成 Mlikiowa 需要做一些过滤 export class OB11InputStatusEvent extends OB11BaseNoticeEvent { notice_type = 'notify'; sub_type = 'input_status'; diff --git a/src/onebot/event/notice/OB11PokeEvent.ts b/src/onebot/event/notice/OB11PokeEvent.ts index adf44da9..80a2e760 100644 --- a/src/onebot/event/notice/OB11PokeEvent.ts +++ b/src/onebot/event/notice/OB11PokeEvent.ts @@ -25,7 +25,7 @@ export class OB11GroupPokeEvent extends OB11PokeEvent { raw_info: any; //raw_message nb等框架标准为string - constructor(core: NapCatCore, group_id: number, user_id: number = 0, target_id: number = 0, raw_message: any) { + constructor(core: NapCatCore, group_id: number, user_id: number, target_id: number, raw_message: any) { super(core); this.group_id = group_id; this.target_id = target_id; diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 9d3b2176..ca86f059 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -6,7 +6,6 @@ import { GroupNotifyMsgStatus, GroupNotifyMsgType, InstanceContext, - MsgSourceType, NapCatCore, NodeIKernelBuddyListener, NodeIKernelGroupListener, @@ -15,11 +14,13 @@ import { RawMessage, SendStatusType, } from '@/core'; -import { OB11Config, OB11ConfigLoader } from '@/onebot/config'; +import { OB11ConfigLoader } from '@/onebot/config'; import { + IOB11NetworkAdapter, OB11ActiveHttpAdapter, OB11ActiveWebSocketAdapter, OB11NetworkManager, + OB11NetworkReloadType, OB11PassiveHttpAdapter, OB11PassiveWebSocketAdapter, } from '@/onebot/network'; @@ -45,8 +46,9 @@ import { OB11FriendRecallNoticeEvent } from '@/onebot/event/notice/OB11FriendRec import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecallNoticeEvent'; import { LRUCache } from '@/common/lru-cache'; import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener'; -import { Native } from '@/native'; -import { decodeMessage, decodeRecallGroup } from '@/core/packet/proto/old/Message'; +import { BotOfflineEvent } from './event/notice/BotOfflineEvent'; +import { AdapterConfigWrap, mergeOneBotConfigs, migrateOneBotConfigsV1, NetworkConfigAdapter, OneBotConfig } from './config/config'; +import { OB11Message } from './types'; //OneBot实现类 export class NapCatOneBot11Adapter { @@ -57,14 +59,15 @@ export class NapCatOneBot11Adapter { apis: StableOneBotApiWrapper; networkManager: OB11NetworkManager; actions: ActionMap; - nativeCore: Native | undefined; - private bootTime = Date.now() / 1000; + private readonly bootTime = Date.now() / 1000; recallMsgCache = new LRUCache(100); constructor(core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) { this.core = core; this.context = context; this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath); + this.configLoader.save(migrateOneBotConfigsV1(this.configLoader.configData)); + this.configLoader.save(mergeOneBotConfigs(this.configLoader.configData)); this.apis = { GroupApi: new OneBotGroupApi(this, core), UserApi: new OneBotUserApi(this, core), @@ -74,94 +77,85 @@ export class NapCatOneBot11Adapter { }; this.actions = createActionMap(this, core); this.networkManager = new OB11NetworkManager(); - this.registerNative(core, context).catch(e => this.context.logger.logWarn.bind(this.context.logger)('初始化Native失败', e)).then(); - this.InitOneBot() - .catch(e => this.context.logger.logError.bind(this.context.logger)('初始化OneBot失败', e)); - } - async registerNative(core: NapCatCore, context: InstanceContext) { - try { - this.nativeCore = new Native(context.pathWrapper.binaryPath); - if (!this.nativeCore.inited) throw new Error('Native Not Init'); - this.nativeCore.registerRecallCallback(async (hex: string) => { - try { - const data = decodeMessage(Buffer.from(hex, 'hex')); - //data.MsgHead.BodyInner.MsgType SubType - const bodyInner = data.msgHead?.bodyInner; - //context.logger.log("[appNative] Parse MsgType:" + bodyInner.msgType + " / SubType:" + bodyInner.subType); - if (bodyInner && bodyInner.msgType == 732 && bodyInner.subType == 17) { - const RecallData = Buffer.from(data.msgHead.noifyData.innerData); - //跳过 4字节 群号 + 不知道的1字节 +2字节 长度 - const uid = RecallData.readUint32BE(); - const buffer = Buffer.from(RecallData.toString('hex').slice(14), 'hex'); - const seq: number = decodeRecallGroup(buffer).recallDetails.subDetail.msgSeq; - const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: uid.toString() }; - context.logger.log("[Native] 群消息撤回 Peer: " + uid.toString() + " / MsgSeq:" + seq); - const msgs = await core.apis.MsgApi.queryMsgsWithFilterExWithSeq(peer, seq.toString()); - this.recallMsgCache.put(msgs.msgList[0].msgId, msgs.msgList[0]); - } - } catch (error: any) { - context.logger.logWarn("[Native] Error:", (error as Error).message, ' HEX:', hex); - } - }); - } catch (error) { - context.logger.logWarn("[Native] Error:", (error as Error).message); - return; + async creatOneBotLog(ob11Config: OneBotConfig) { + let log = `[network] 配置加载\n`; + for (const key of ob11Config.network.httpServers) { + log += `HTTP服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`; } + for (const key of ob11Config.network.httpClients) { + log += `HTTP上报服务: ${key.url}, : ${key.enable ? '已启动' : '未启动'}\n`; + } + for (const key of ob11Config.network.websocketServers) { + log += `WebSocket服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`; + } + for (const key of ob11Config.network.websocketClients) { + log += `WebSocket反向服务: ${key.url}, : ${key.enable ? '已启动' : '未启动'}\n`; + } + return log; } async InitOneBot() { const selfInfo = this.core.selfInfo; const ob11Config = this.configLoader.configData; - const serviceInfo = ` - HTTP服务 ${ob11Config.http.enable ? '已启动' : '未启动'}, ${ob11Config.http.host}:${ob11Config.http.port} - HTTP上报服务 ${ob11Config.http.enablePost ? '已启动' : '未启动'}, 上报地址: ${ob11Config.http.postUrls} - WebSocket服务 ${ob11Config.ws.enable ? '已启动' : '未启动'}, ${ob11Config.ws.host}:${ob11Config.ws.port} - WebSocket反向服务 ${ob11Config.reverseWs.enable ? '已启动' : '未启动'}, 反向地址: ${ob11Config.reverseWs.urls}`; + this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid) + .then((user) => { + selfInfo.nick = user.nick; + this.context.logger.setLogSelfInfo(selfInfo); + }) + .catch(this.context.logger.logError.bind(this.context.logger)); - this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid).then(user => { - selfInfo.nick = user.nick; - this.context.logger.setLogSelfInfo(selfInfo); - }).catch(this.context.logger.logError.bind(this.context.logger)); + const serviceInfo = await this.creatOneBotLog(ob11Config); this.context.logger.log(`[Notice] [OneBot11] ${serviceInfo}`); - //创建NetWork服务 - if (ob11Config.http.enable) { - this.networkManager.registerAdapter(new OB11PassiveHttpAdapter( - ob11Config.http.port, ob11Config.token, this.core, this.actions, - )); + // //创建NetWork服务 + for (const key of ob11Config.network.httpServers) { + if (key.enable) { + this.networkManager.registerAdapter( + new OB11PassiveHttpAdapter(key.name, key, this.core, this.actions) + ); + } } - if (ob11Config.http.enablePost) { - ob11Config.http.postUrls.forEach(url => { - this.networkManager.registerAdapter(new OB11ActiveHttpAdapter( - url, ob11Config.http.secret, this.core, this, - )); - }); + for (const key of ob11Config.network.httpClients) { + if (key.enable) { + this.networkManager.registerAdapter( + new OB11ActiveHttpAdapter(key.name, key, this.core, this, this.actions) + ); + } } - if (ob11Config.ws.enable) { - const OBPassiveWebSocketAdapter = new OB11PassiveWebSocketAdapter( - ob11Config.ws.host, ob11Config.ws.port, ob11Config.heartInterval, ob11Config.token, this.core, this.actions, - ); - this.networkManager.registerAdapter(OBPassiveWebSocketAdapter); + for (const key of ob11Config.network.websocketServers) { + if (key.enable) { + this.networkManager.registerAdapter( + new OB11PassiveWebSocketAdapter( + key.name, + key, + this.core, + this.actions + ) + ); + } } - if (ob11Config.reverseWs.enable) { - ob11Config.reverseWs.urls.forEach(url => { - this.networkManager.registerAdapter(new OB11ActiveWebSocketAdapter( - url, 5000, ob11Config.heartInterval, ob11Config.token, this.core, this.actions, - )); - }); + for (const key of ob11Config.network.websocketClients) { + if (key.enable) { + this.networkManager.registerAdapter( + new OB11ActiveWebSocketAdapter( + key.name, + key, + this.core, + this.actions + ) + ); + } } - await this.networkManager.openAllAdapters(); this.initMsgListener(); this.initBuddyListener(); this.initGroupListener(); - //this.initRecentContactListener(); await WebUiDataRuntime.setQQLoginUin(selfInfo.uin.toString()); await WebUiDataRuntime.setQQLoginStatus(true); - await WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig: OB11Config) => { + await WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => { const prev = this.configLoader.configData; this.configLoader.save(newConfig); this.context.logger.log(`OneBot11 配置更改:${JSON.stringify(prev)} -> ${JSON.stringify(newConfig)}`); @@ -171,7 +165,9 @@ export class NapCatOneBot11Adapter { initRecentContactListener() { const recentContactListener = new NodeIKernelRecentContactListener(); - recentContactListener.onRecentContactNotification = function (msgList: any[] /* arg0: { msgListUnreadCnt: string }, arg1: number */) { + recentContactListener.onRecentContactNotification = function ( + msgList: any[] /* arg0: { msgListUnreadCnt: string }, arg1: number */ + ) { msgList.forEach((msg) => { if (msg.chatType == ChatType.KCHATTYPEGROUP) { // log("recent contact", msgList, arg0, arg1); @@ -180,117 +176,93 @@ export class NapCatOneBot11Adapter { }; } - private async reloadNetwork(prev: OB11Config, now: OB11Config) { - const serviceInfo = ` - HTTP服务 ${now.http.enable ? '已启动' : '未启动'}, ${now.http.host}:${now.http.port} - HTTP上报服务 ${now.http.enablePost ? '已启动' : '未启动'}, 上报地址: ${now.http.postUrls} - WebSocket服务 ${now.ws.enable ? '已启动' : '未启动'}, ${now.ws.host}:${now.ws.port} - WebSocket反向服务 ${now.reverseWs.enable ? '已启动' : '未启动'}, 反向地址: ${now.reverseWs.urls}`; - this.context.logger.log(`[Notice] [OneBot11] 热重载 ${serviceInfo}`); + private async reloadNetwork(prev: OneBotConfig, now: OneBotConfig) { + const prevLog = await this.creatOneBotLog(prev); + const newLog = await this.creatOneBotLog(now); + this.context.logger.log(`[Notice] [OneBot11] 配置变更前:\n${prevLog}`); + this.context.logger.log(`[Notice] [OneBot11] 配置变更后:\n${newLog}`); - // check difference in passive http (Http) - if (prev.http.enable !== now.http.enable) { - if (now.http.enable) { - await this.networkManager.registerAdapterAndOpen(new OB11PassiveHttpAdapter( - now.http.port, now.token, this.core, this.actions, - )); - } else { - await this.networkManager.closeAdapterByPredicate(adapter => adapter instanceof OB11PassiveHttpAdapter); - } - } + const { added: addedHttpServers, removed: removedHttpServers } = this.findDifference(prev.network.httpServers, now.network.httpServers); + const { added: addedHttpClients, removed: removedHttpClients } = this.findDifference(prev.network.httpClients, now.network.httpClients); + const { added: addedWebSocketServers, removed: removedWebSocketServers } = this.findDifference(prev.network.websocketServers, now.network.websocketServers); + const { added: addedWebSocketClients, removed: removedWebSocketClients } = this.findDifference(prev.network.websocketClients, now.network.websocketClients); - // check difference in active http (HttpPost) - if (prev.http.enablePost !== now.http.enablePost) { - if (now.http.enablePost) { - now.http.postUrls.forEach(url => { - this.networkManager.registerAdapterAndOpen(new OB11ActiveHttpAdapter( - url, now.http.secret, this.core, this, - )); - }); - } else { - await this.networkManager.closeAdapterByPredicate(adapter => adapter instanceof OB11ActiveHttpAdapter); - } - } else { - if (now.http.enablePost) { - const { added, removed } = this.findDifference(prev.http.postUrls, now.http.postUrls); - await this.networkManager.closeAdapterByPredicate( - adapter => adapter instanceof OB11ActiveHttpAdapter && removed.includes(adapter.url), - ); - for (const url of added) { - await this.networkManager.registerAdapterAndOpen(new OB11ActiveHttpAdapter( - url, now.http.secret, this.core, this, - )); - } - } - } + await this.handleRemovedAdapters(removedHttpServers); + await this.handleRemovedAdapters(removedHttpClients); + await this.handleRemovedAdapters(removedWebSocketServers); + await this.handleRemovedAdapters(removedWebSocketClients); - // check difference in passive websocket (Ws) - if (prev.ws.enable !== now.ws.enable) { - if (now.ws.enable) { - await this.networkManager.registerAdapterAndOpen(new OB11PassiveWebSocketAdapter( - now.ws.host, now.ws.port, now.heartInterval, now.token, this.core, this.actions, - )); - } else { - await this.networkManager.closeAdapterByPredicate( - adapter => adapter instanceof OB11PassiveWebSocketAdapter, - ); - } - } + await this.handlerConfigChange(now.network.httpServers); + await this.handlerConfigChange(now.network.httpClients); + await this.handlerConfigChange(now.network.websocketServers); + await this.handlerConfigChange(now.network.websocketClients); + + await this.handleAddedAdapters(addedHttpServers, OB11PassiveHttpAdapter); + await this.handleAddedAdapters(addedHttpClients, OB11ActiveHttpAdapter); + await this.handleAddedAdapters(addedWebSocketServers, OB11PassiveWebSocketAdapter); + await this.handleAddedAdapters(addedWebSocketClients, OB11ActiveWebSocketAdapter); + } + + private async handlerConfigChange(adapters: Array) { + for (const adapterConfig of adapters) { + const existingAdapter = this.networkManager.findSomeAdapter(adapterConfig.name); + if (existingAdapter) { + const networkChange = await existingAdapter.reload(adapterConfig); + if (networkChange === OB11NetworkReloadType.NetWorkClose) { + this.networkManager.closeSomeAdapters([existingAdapter]); - // check difference in active websocket (ReverseWs) - if (prev.reverseWs.enable !== now.reverseWs.enable) { - if (now.reverseWs.enable) { - now.reverseWs.urls.forEach(url => { - this.networkManager.registerAdapterAndOpen(new OB11ActiveWebSocketAdapter( - url, 5000, now.heartInterval, now.token, this.core, this.actions, - )); - }); - } else { - await this.networkManager.closeAdapterByPredicate( - adapter => adapter instanceof OB11ActiveWebSocketAdapter, - ); - } - } else { - if (now.reverseWs.enable) { - const { added, removed } = this.findDifference(prev.reverseWs.urls, now.reverseWs.urls); - await this.networkManager.closeAdapterByPredicate( - adapter => adapter instanceof OB11ActiveWebSocketAdapter && removed.includes(adapter.url), - ); - for (const url of added) { - await this.networkManager.registerAdapterAndOpen(new OB11ActiveWebSocketAdapter( - url, 5000, now.heartInterval, now.token, this.core, this.actions, - )); } } } } - private findDifference(prev: T[], now: T[]): { added: T[], removed: T[] } { - const added = now.filter(item => !prev.includes(item)); - const removed = prev.filter(item => !now.includes(item)); + private async handleRemovedAdapters(adapters: Array<{ name: string }>): Promise { + for (const adapter of adapters) { + await this.networkManager.closeAdapterByPredicate((existingAdapter) => existingAdapter.name === adapter.name); + } + } + + private async handleAddedAdapters IOB11NetworkAdapter>(addedAdapters: Array, AdapterClass: T) { + for (const adapter of addedAdapters) { + if (adapter.enable) { + const newAdapter = new AdapterClass(adapter.name, adapter, this.core, this.actions); + await newAdapter.open(); + this.networkManager.registerAdapter(newAdapter); + } + } + } + private findDifference(prev: T[], now: T[]): { added: T[]; removed: T[] } { + const added = now.filter((item) => !prev.includes(item)); + const removed = prev.filter((item) => !now.includes(item)); return { added, removed }; } private initMsgListener() { const msgListener = new NodeIKernelMsgListener(); msgListener.onRecvSysMsg = (msg) => { - this.apis.MsgApi.parseSysMessage(msg).then((event) => { - if (event) this.networkManager.emitEvent(event); - }).catch(e => this.context.logger.logError.bind(this.context.logger)('constructSysMessage error: ', e, '\n Parse Hex:', Buffer.from(msg).toString('hex'))); + this.apis.MsgApi.parseSysMessage(msg) + .then((event) => { + if (event) this.networkManager.emitEvent(event); + }) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)( + 'constructSysMessage error: ', + e, + '\n Parse Hex:', + Buffer.from(msg).toString('hex') + ) + ); }; - msgListener.onInputStatusPush = async data => { + msgListener.onInputStatusPush = async (data) => { const uin = await this.core.apis.UserApi.getUinByUidV2(data.fromUin); this.context.logger.log(`[Notice] [输入状态] ${uin} ${data.statusText}`); - await this.networkManager.emitEvent(new OB11InputStatusEvent( - this.core, - parseInt(uin), - data.eventType, - data.statusText, - )); + await this.networkManager.emitEvent( + new OB11InputStatusEvent(this.core, parseInt(uin), data.eventType, data.statusText) + ); }; - msgListener.onRecvMsg = async msg => { + msgListener.onRecvMsg = async (msg) => { for (const m of msg) { if (this.bootTime > parseInt(m.msgTime)) { this.context.logger.logDebug(`消息时间${m.msgTime}早于启动时间${this.bootTime},忽略上报`); @@ -302,53 +274,54 @@ export class NapCatOneBot11Adapter { peerUid: m.peerUid, guildId: '', }, - m.msgId, + m.msgId + ); + await this.emitMsg(m).catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理消息失败', e) ); - // if (m.sourceType == MsgSourceType.K_DOWN_SOURCETYPE_AIOINNER) { - await this.emitMsg(m) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理消息失败', e)); - // } } }; - const msgIdSend = new LRUCache(100); + const msgIdSend = new LRUCache(100); const recallMsgs = new LRUCache(100); - msgListener.onMsgInfoListUpdate = async msgList => { - this.emitRecallMsg(msgList, recallMsgs) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理消息失败', e)); - - for (const msg of msgList.filter(e => e.senderUin == this.core.selfInfo.uin)) { - if (msg.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS && !msgIdSend.get(msg.msgId)) { - msgIdSend.put(msg.msgId, true); + msgListener.onAddSendMsg = async (msg) => { + if (msg.sendStatus == SendStatusType.KSEND_STATUS_SENDING) { + msgIdSend.put(msg.msgId, 0); + } + }; + msgListener.onMsgInfoListUpdate = async (msgList) => { + this.emitRecallMsg(msgList, recallMsgs).catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理消息失败', e) + ); + for (const msg of msgList.filter((e) => e.senderUin == this.core.selfInfo.uin)) { + if (msg.sendStatus == SendStatusType.KSEND_STATUS_SUCCESS && msgIdSend.get(msg.msgId) == 0) { + msgIdSend.put(msg.msgId, 1); // 完成后再post - this.apis.MsgApi.parseMessage(msg) - .then((ob11Msg) => { - if (!ob11Msg) return; - ob11Msg.target_id = parseInt(msg.peerUin); - if (this.configLoader.configData.reportSelfMessage) { - msg.id = MessageUnique.createUniqueMsgId({ - chatType: msg.chatType, - peerUid: msg.peerUid, - guildId: '', - }, msg.msgId); - this.emitMsg(msg); - } else { - // logOB11Message(this.core, ob11Msg); - } - }); + msg.id = MessageUnique.createUniqueMsgId( + { + chatType: msg.chatType, + peerUid: msg.peerUid, + guildId: '', + }, + msg.msgId + ); + this.emitMsg(msg); } } }; - - this.context.session.getMsgService().addKernelMsgListener( - proxiedListenerOf(msgListener, this.context.logger), - ); + msgListener.onKickedOffLine = async (kick) => { + const event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc); + this.networkManager + .emitEvent(event) + .catch((e) => this.context.logger.logError.bind(this.context.logger)('处理Bot掉线失败', e)); + }; + this.context.session.getMsgService().addKernelMsgListener(proxiedListenerOf(msgListener, this.context.logger)); } private initBuddyListener() { const buddyListener = new NodeIKernelBuddyListener(); - buddyListener.onBuddyReqChange = async reqs => { + buddyListener.onBuddyReqChange = async (reqs) => { this.core.apis.FriendApi.clearBuddyReqUnreadCnt(); for (let i = 0; i < reqs.unreadNums; i++) { const req = reqs.buddyReqs[i]; @@ -357,21 +330,23 @@ export class NapCatOneBot11Adapter { } try { const requesterUin = await this.core.apis.UserApi.getUinByUidV2(req.friendUid); - await this.networkManager.emitEvent(new OB11FriendRequestEvent( - this.core, - parseInt(requesterUin!), - req.extWords, - req.friendUid + '|' + req.reqTime, - )); + await this.networkManager.emitEvent( + new OB11FriendRequestEvent( + this.core, + +requesterUin, + req.extWords, + req.friendUid + '|' + req.reqTime + ) + ); } catch (e) { this.context.logger.logDebug('获取加好友者QQ号失败', e); } } }; - this.context.session.getBuddyService().addKernelBuddyListener( - proxiedListenerOf(buddyListener, this.context.logger), - ); + this.context.session + .getBuddyService() + .addKernelBuddyListener(proxiedListenerOf(buddyListener, this.context.logger)); } private initGroupListener() { @@ -380,11 +355,13 @@ export class NapCatOneBot11Adapter { groupListener.onGroupNotifiesUpdated = async (_, notifies) => { //console.log('ob11 onGroupNotifiesUpdated', notifies[0]); await this.core.apis.GroupApi.clearGroupNotifiesUnreadCount(false); - if (![ - GroupNotifyMsgType.SET_ADMIN, - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED, - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN, - ].includes(notifies[0]?.type)) { + if ( + ![ + GroupNotifyMsgType.SET_ADMIN, + GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED, + GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN, + ].includes(notifies[0]?.type) + ) { for (const notify of notifies) { const notifyTime = parseInt(notify.seq) / 1000 / 1000; // log(`群通知时间${notifyTime}`, `启动时间${this.bootTime}`); @@ -395,15 +372,19 @@ export class NapCatOneBot11Adapter { const flag = notify.group.groupCode + '|' + notify.seq + '|' + notify.type; this.context.logger.logDebug('收到群通知', notify); - if ([ - GroupNotifyMsgType.SET_ADMIN, - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED, - GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN, - ].includes(notify.type)) { - const member1 = await this.core.apis.GroupApi.getGroupMember(notify.group.groupCode, notify.user1.uid); + if ( + [ + GroupNotifyMsgType.SET_ADMIN, + GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED, + GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN, + ].includes(notify.type) + ) { + const member1 = await this.core.apis.GroupApi.getGroupMember( + notify.group.groupCode, + notify.user1.uid + ); this.context.logger.logDebug('有管理员变动通知'); // refreshGroupMembers(notify.group.groupCode).then(); - this.context.logger.logDebug('开始获取变动的管理员'); if (member1) { this.context.logger.logDebug('变动管理员获取成功'); @@ -414,16 +395,28 @@ export class NapCatOneBot11Adapter { [ GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_CANCELED, GroupNotifyMsgType.CANCEL_ADMIN_NOTIFY_ADMIN, - ].includes(notify.type) ? 'unset' : 'set', + ].includes(notify.type) + ? 'unset' + : 'set' ); - this.networkManager.emitEvent(groupAdminNoticeEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e)); + this.networkManager + .emitEvent(groupAdminNoticeEvent) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e) + ); } else { - this.context.logger.logDebug('获取群通知的成员信息失败', notify, this.core.apis.GroupApi.getGroup(notify.group.groupCode)); + this.context.logger.logDebug( + '获取群通知的成员信息失败', + notify, + this.core.apis.GroupApi.getGroup(notify.group.groupCode) + ); } - } else if (notify.type == GroupNotifyMsgType.MEMBER_LEAVE_NOTIFY_ADMIN || notify.type == GroupNotifyMsgType.KICK_MEMBER_NOTIFY_ADMIN) { + } else if ( + notify.type == GroupNotifyMsgType.MEMBER_LEAVE_NOTIFY_ADMIN || + notify.type == GroupNotifyMsgType.KICK_MEMBER_NOTIFY_ADMIN + ) { this.context.logger.logDebug('有成员退出通知', notify); - const member1Uin = (await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid))!; + const member1Uin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid); let operatorId = member1Uin; let subType: GroupDecreaseSubType = 'leave'; if (notify.user2.uid) { @@ -439,17 +432,21 @@ export class NapCatOneBot11Adapter { parseInt(notify.group.groupCode), parseInt(member1Uin), parseInt(operatorId), - subType, + subType ); - this.networkManager.emitEvent(groupDecreaseEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理群成员退出失败', e)); + this.networkManager + .emitEvent(groupDecreaseEvent) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理群成员退出失败', e) + ); // notify.status == 1 表示未处理 2表示处理完成 - } else if ([ - GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS, - ].includes(notify.type) && notify.status == GroupNotifyMsgStatus.KUNHANDLE) { + } else if ( + [GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS].includes(notify.type) && + notify.status == GroupNotifyMsgStatus.KUNHANDLE + ) { this.context.logger.logDebug('有加群请求'); try { - let requestUin = (await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid))!; + let requestUin = await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid); if (isNaN(parseInt(requestUin))) { requestUin = (await this.core.apis.UserApi.getUserDetailInfo(notify.user1.uid)).uin; } @@ -459,14 +456,24 @@ export class NapCatOneBot11Adapter { parseInt(requestUin), 'add', notify.postscript, - flag, + flag ); - this.networkManager.emitEvent(groupRequestEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理加群请求失败', e)); + this.networkManager + .emitEvent(groupRequestEvent) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理加群请求失败', e) + ); } catch (e) { - this.context.logger.logError.bind(this.context.logger)('获取加群人QQ号失败 Uid:', notify.user1.uid, e); + this.context.logger.logError.bind(this.context.logger)( + '获取加群人QQ号失败 Uid:', + notify.user1.uid, + e + ); } - } else if (notify.type == GroupNotifyMsgType.INVITED_BY_MEMBER && notify.status == GroupNotifyMsgStatus.KUNHANDLE) { + } else if ( + notify.type == GroupNotifyMsgType.INVITED_BY_MEMBER && + notify.status == GroupNotifyMsgStatus.KUNHANDLE + ) { this.context.logger.logDebug(`收到邀请我加群通知:${notify}`); const groupInviteEvent = new OB11GroupRequestEvent( this.core, @@ -474,11 +481,17 @@ export class NapCatOneBot11Adapter { parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user2.uid)), 'invite', notify.postscript, - flag, + flag ); - this.networkManager.emitEvent(groupInviteEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e)); - } else if (notify.type == GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS && notify.status == GroupNotifyMsgStatus.KUNHANDLE) { + this.networkManager + .emitEvent(groupInviteEvent) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e) + ); + } else if ( + notify.type == GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS && + notify.status == GroupNotifyMsgStatus.KUNHANDLE + ) { this.context.logger.logDebug(`收到群员邀请加群通知:${notify}`); const groupInviteEvent = new OB11GroupRequestEvent( this.core, @@ -486,10 +499,13 @@ export class NapCatOneBot11Adapter { parseInt(await this.core.apis.UserApi.getUinByUidV2(notify.user1.uid)), 'add', notify.postscript, - flag, + flag ); - this.networkManager.emitEvent(groupInviteEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e)); + this.networkManager + .emitEvent(groupInviteEvent) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理邀请本人加群失败', e) + ); } } } @@ -508,93 +524,122 @@ export class NapCatOneBot11Adapter { this.core, parseInt(groupCode), parseInt(member.uin), - member.role === GroupMemberRole.admin ? 'set' : 'unset', + member.role === GroupMemberRole.admin ? 'set' : 'unset' ); - this.networkManager.emitEvent(groupAdminNoticeEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e)); + this.networkManager + .emitEvent(groupAdminNoticeEvent) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理群管理员变动失败', e) + ); existMember.isChangeRole = false; this.context.logger.logDebug.bind(this.context.logger)('群管理员变动处理完毕'); }); } }; - this.context.session.getGroupService().addKernelGroupListener( - proxiedListenerOf(groupListener, this.context.logger), - ); + this.context.session + .getGroupService() + .addKernelGroupListener(proxiedListenerOf(groupListener, this.context.logger)); } private async emitMsg(message: RawMessage) { - const { debug, reportSelfMessage, messagePostFormat } = this.configLoader.configData; + const network = Object.values(this.configLoader.configData.network).flat() as Array; this.context.logger.logDebug('收到新消息 RawMessage', message); - this.apis.MsgApi.parseMessage(message, messagePostFormat).then((ob11Msg) => { - if (!ob11Msg) return; - this.context.logger.logDebug('转化为 OB11Message', ob11Msg); - if (debug) { - ob11Msg.raw = message; - } else { - if (ob11Msg.message.length === 0) { - return; - } + await this.handleMsg(message, network); + await this.handleGroupEvent(message); + await this.handlePrivateMsgEvent(message); + } + private async handleMsg(message: RawMessage, network: Array) { + try { + const ob11Msg = await this.apis.MsgApi.parseMessageV2(message); + if (ob11Msg) { + const isSelfMsg = this.isSelfMessage(ob11Msg); + this.context.logger.logDebug('转化为 OB11Message', ob11Msg); + const msgMap = this.createMsgMap(network, ob11Msg, isSelfMsg, message); + this.handleDebugNetwork(network, msgMap, message); + this.handleNotReportSelfNetwork(network, msgMap, isSelfMsg); + this.networkManager.emitEventByNames(msgMap); } - const isSelfMsg = ob11Msg.user_id.toString() == this.core.selfInfo.uin; - if (isSelfMsg && !reportSelfMessage) { - return; + + } catch (e) { + this.context.logger.logError('constructMessage error: ', e); + } + } + + private isSelfMessage(ob11Msg: { + stringMsg: OB11Message; + arrayMsg: OB11Message; + }): boolean { + return ob11Msg.stringMsg.user_id.toString() == this.core.selfInfo.uin || + ob11Msg.arrayMsg.user_id.toString() == this.core.selfInfo.uin; + } + + private createMsgMap(network: Array, ob11Msg: any, isSelfMsg: boolean, message: RawMessage): Map { + const msgMap: Map = new Map(); + network.filter(e => e.enable).forEach(e => { + if (e.messagePostFormat == 'string') { + msgMap.set(e.name, structuredClone(ob11Msg.stringMsg)); + } else { + msgMap.set(e.name, structuredClone(ob11Msg.arrayMsg)); } if (isSelfMsg) { - ob11Msg.target_id = parseInt(message.peerUin); + ob11Msg.stringMsg.target_id = parseInt(message.peerUin); + ob11Msg.arrayMsg.target_id = parseInt(message.peerUin); } - // if (ob11Msg.raw_message.startsWith('!set')) { - // this.core.apis.UserApi.getUidByUinV2(ob11Msg.user_id.toString()).then(uid => { - // if(uid){ - // this.core.apis.PacketApi.sendSetSpecialTittlePacket(message.peerUin, uid, '测试'); - // console.log('set', message.peerUin, uid); - // } + }); + return msgMap; + } - // }); + private handleDebugNetwork(network: Array, msgMap: Map, message: RawMessage) { + const debugNetwork = network.filter(e => e.enable && e.debug); + if (debugNetwork.length > 0) { + debugNetwork.forEach(adapter => { + const msg = msgMap.get(adapter.name); + if (msg) { + msg.raw = message; + } + }); + } else if (msgMap.size === 0) { + return; + } + } - // } - // if (ob11Msg.raw_message.startsWith('!status')) { - // console.log('status', message.peerUin, message.senderUin); - // let delMsg: string[] = []; - // let peer = { - // peerUid: message.peerUin, - // chatType: 2, - // }; - // this.core.apis.PacketApi.sendStatusPacket(+message.senderUin).then(async e => { - // if (e) { - // const { sendElements } = await this.apis.MsgApi.createSendElements([{ - // type: OB11MessageDataType.text, - // data: { - // text: 'status ' + JSON.stringify(e, null, 2), - // } - // }], peer) + private handleNotReportSelfNetwork(network: Array, msgMap: Map, isSelfMsg: boolean) { + if (isSelfMsg) { + const notReportSelfNetwork = network.filter(e => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e))); + notReportSelfNetwork.forEach(adapter => { + msgMap.delete(adapter.name); + }); + } + } - // this.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, delMsg) - // } - // }) - // } - this.networkManager.emitEvent(ob11Msg); - }).catch(e => this.context.logger.logError.bind(this.context.logger)('constructMessage error: ', e)); - - this.apis.GroupApi.parseGroupEvent(message).then(groupEvent => { + private async handleGroupEvent(message: RawMessage) { + try { + const groupEvent = await this.apis.GroupApi.parseGroupEvent(message); if (groupEvent) { - // log("post group event", groupEvent); this.networkManager.emitEvent(groupEvent); } - }).catch(e => this.context.logger.logError.bind(this.context.logger)('constructGroupEvent error: ', e)); + } catch (e) { + this.context.logger.logError('constructGroupEvent error: ', e); + } + } - this.apis.MsgApi.parsePrivateMsgEvent(message).then(privateEvent => { + private async handlePrivateMsgEvent(message: RawMessage) { + try { + const privateEvent = await this.apis.MsgApi.parsePrivateMsgEvent(message); if (privateEvent) { - // log("post private event", privateEvent); this.networkManager.emitEvent(privateEvent); } - }).catch(e => this.context.logger.logError.bind(this.context.logger)('constructPrivateEvent error: ', e)); + } catch (e) { + this.context.logger.logError('constructPrivateEvent error: ', e); + } } private async emitRecallMsg(msgList: RawMessage[], cache: LRUCache) { for (const message of msgList) { // log("message update", message.sendStatus, message.msgId, message.msgSeq) const peer: Peer = { chatType: message.chatType, peerUid: message.peerUid, guildId: '' }; - if (message.recallTime != '0' && !cache.get(message.msgId)) { //todo: 这个判断方法不太好,应该使用灰色消息元素来判断? + if (message.recallTime != '0' && !cache.get(message.msgId)) { + //TODO: 这个判断方法不太好,应该使用灰色消息元素来判断? cache.put(message.msgId, true); // 撤回消息上报 let oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId); @@ -604,28 +649,32 @@ export class NapCatOneBot11Adapter { if (message.chatType == ChatType.KCHATTYPEC2C) { const friendRecallEvent = new OB11FriendRecallNoticeEvent( this.core, - parseInt(message!.senderUin), - oriMessageId, + +message.senderUin, + oriMessageId ); - this.networkManager.emitEvent(friendRecallEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理好友消息撤回失败', e)); + this.networkManager + .emitEvent(friendRecallEvent) + .catch((e) => + this.context.logger.logError.bind(this.context.logger)('处理好友消息撤回失败', e) + ); } else if (message.chatType == ChatType.KCHATTYPEGROUP) { let operatorId = message.senderUin; for (const element of message.elements) { const operatorUid = element.grayTipElement?.revokeElement.operatorUid; if (!operatorUid) return; const operator = await this.core.apis.GroupApi.getGroupMember(message.peerUin, operatorUid); - operatorId = operator?.uin || message.senderUin; + operatorId = operator?.uin ?? message.senderUin; } const groupRecallEvent = new OB11GroupRecallNoticeEvent( this.core, - parseInt(message.peerUin), - parseInt(message.senderUin), - parseInt(operatorId), + +message.peerUin, + +message.senderUin, + +operatorId, oriMessageId ); - this.networkManager.emitEvent(groupRecallEvent) - .catch(e => this.context.logger.logError.bind(this.context.logger)('处理群消息撤回失败', e)); + this.networkManager + .emitEvent(groupRecallEvent) + .catch((e) => this.context.logger.logError.bind(this.context.logger)('处理群消息撤回失败', e)); } } } diff --git a/src/onebot/network/active-http.ts b/src/onebot/network/active-http.ts index 26687929..26975454 100644 --- a/src/onebot/network/active-http.ts +++ b/src/onebot/network/active-http.ts @@ -1,25 +1,31 @@ -import { IOB11NetworkAdapter, OB11EmitEventContent } from '@/onebot/network/index'; +import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index'; import { createHmac } from 'crypto'; import { LogWrapper } from '@/common/log'; import { QuickAction, QuickActionEvent } from '../types'; import { NapCatCore } from '@/core'; import { NapCatOneBot11Adapter } from '..'; +import { RequestUtil } from '@/common/request'; +import { HttpClientConfig } from '../config/config'; +import { ActionMap } from '../action'; export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter { logger: LogWrapper; - isOpen: boolean = false; - + isEnable: boolean = false; + public config: HttpClientConfig; constructor( - public url: string, - public secret: string | undefined, + public name: string, + config: HttpClientConfig, public core: NapCatCore, public obContext: NapCatOneBot11Adapter, + public actions: ActionMap, ) { this.logger = core.context.logger; + this.config = structuredClone(config); } + onEvent(event: T) { - if (!this.isOpen) { + if (!this.isEnable) { return; } const headers: Record = { @@ -27,20 +33,16 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter { 'x-self-id': this.core.selfInfo.uin, }; const msgStr = JSON.stringify(event); - if (this.secret && this.secret.length > 0) { - const hmac = createHmac('sha1', this.secret); + if (this.config.token && this.config.token.length > 0) { + const hmac = createHmac('sha1', this.config.token); hmac.update(msgStr); const sig = hmac.digest('hex'); headers['x-signature'] = 'sha1=' + sig; } - fetch(this.url, { - method: 'POST', - headers, - body: msgStr, - }).then(async (res) => { + RequestUtil.HttpGetText(this.config.url, 'POST', msgStr, headers).then(async (res) => { let resJson: QuickAction; try { - resJson = await res.json(); + resJson = JSON.parse(res); //logDebug('新消息事件HTTP上报返回快速操作: ', JSON.stringify(resJson)); } catch (e) { this.logger.logDebug('[OneBot] [Http Client] 新消息事件HTTP上报没有返回快速操作,不需要处理'); @@ -59,10 +61,26 @@ export class OB11ActiveHttpAdapter implements IOB11NetworkAdapter { } open() { - this.isOpen = true; + this.isEnable = true; } close() { - this.isOpen = false; + this.isEnable = false; + } + async reload(newconfig: HttpClientConfig) { + const wasEnabled = this.isEnable; + const oldUrl = this.config.url; + this.config = newconfig; + if (newconfig.enable && !wasEnabled) { + this.open(); + return OB11NetworkReloadType.NetWorkOpen; + } else if (!newconfig.enable && wasEnabled) { + this.close(); + return OB11NetworkReloadType.NetWorkClose; + } + if (oldUrl !== newconfig.url) { + return OB11NetworkReloadType.NetWorkReload; + } + return OB11NetworkReloadType.Normal; } } diff --git a/src/onebot/network/active-websocket.ts b/src/onebot/network/active-websocket.ts index e35c16c5..ce2a1130 100644 --- a/src/onebot/network/active-websocket.ts +++ b/src/onebot/network/active-websocket.ts @@ -1,4 +1,4 @@ -import { IOB11NetworkAdapter, OB11EmitEventContent } from '@/onebot/network/index'; +import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from '@/onebot/network/index'; import { WebSocket } from 'ws'; import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'; import { NapCatCore } from '@/core'; @@ -7,22 +7,23 @@ import { OB11Response } from '@/onebot/action/OB11Response'; import { LogWrapper } from '@/common/log'; import { ActionMap } from '@/onebot/action'; import { LifeCycleSubType, OB11LifeCycleEvent } from '../event/meta/OB11LifeCycleEvent'; +import { WebsocketClientConfig } from '../config/config'; export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter { - isClosed: boolean = false; + isEnable: boolean = false; logger: LogWrapper; private connection: WebSocket | null = null; private heartbeatRef: NodeJS.Timeout | null = null; + public config: WebsocketClientConfig; constructor( - public url: string, - public reconnectIntervalInMillis: number, - public heartbeatIntervalInMillis: number, - private token: string, + public name: string, + confg: WebsocketClientConfig, public core: NapCatCore, public actions: ActionMap, ) { this.logger = core.context.logger; + this.config = structuredClone(confg); } onEvent(event: T) { @@ -35,20 +36,23 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter { if (this.connection) { return; } - this.heartbeatRef = setInterval(() => { - if (this.connection && this.connection.readyState === WebSocket.OPEN) { - this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatIntervalInMillis, this.core.selfInfo.online, true))); - } - }, this.heartbeatIntervalInMillis); + if (this.config.heartInterval > 0) { + this.heartbeatRef = setInterval(() => { + if (this.connection && this.connection.readyState === WebSocket.OPEN) { + this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.config.heartInterval, this.core.selfInfo.online ?? true, true))); + } + }, this.config.heartInterval); + } + this.isEnable = true; await this.tryConnect(); } close() { - if (this.isClosed) { + if (!this.isEnable) { this.logger.logDebug('Cannot close a closed WebSocket connection'); return; } - this.isClosed = true; + this.isEnable = false; if (this.connection) { this.connection.close(); this.connection = null; @@ -66,16 +70,16 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter { } private async tryConnect() { - if (!this.connection && !this.isClosed) { + if (!this.connection && this.isEnable) { let isClosedByError = false; - this.connection = new WebSocket(this.url, { + this.connection = new WebSocket(this.config.url, { maxPayload: 1024 * 1024 * 1024, handshakeTimeout: 2000, perMessageDeflate: false, headers: { 'X-Self-ID': this.core.selfInfo.uin, - 'Authorization': `Bearer ${this.token}`, + 'Authorization': `Bearer ${this.config.token}`, 'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段 'User-Agent': 'OneBot/11', }, @@ -100,21 +104,21 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter { }); this.connection.once('close', () => { if (!isClosedByError) { - this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.url}) 连接意外关闭`); - this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.reconnectIntervalInMillis / 1000)} 秒后尝试重新连接`); - if (!this.isClosed) { + this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接意外关闭`); + this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`); + if (this.isEnable) { this.connection = null; - setTimeout(() => this.tryConnect(), this.reconnectIntervalInMillis); + setTimeout(() => this.tryConnect(), this.config.reconnectInterval); } } }); this.connection.on('error', (err) => { isClosedByError = true; - this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.url}) 连接错误`, err); - this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.reconnectIntervalInMillis / 1000)} 秒后尝试重新连接`); - if (!this.isClosed) { + this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 反向WebSocket (${this.config.url}) 连接错误`, err); + this.logger.logError.bind(this.logger)(`[OneBot] [WebSocket Client] 在 ${Math.floor(this.config.reconnectInterval / 1000)} 秒后尝试重新连接`); + if (this.isEnable) { this.connection = null; - setTimeout(() => this.tryConnect(), this.reconnectIntervalInMillis); + setTimeout(() => this.tryConnect(), this.config.reconnectInterval); } }); } @@ -147,8 +151,46 @@ export class OB11ActiveWebSocketAdapter implements IOB11NetworkAdapter { this.checkStateAndReply(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo)); return; } - const retdata = await action.websocketHandle(receiveData.params, echo ?? ''); - const packet = Object.assign({}, retdata); - this.checkStateAndReply(packet); + const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name); + this.checkStateAndReply({ ...retdata }); + } + async reload(newConfig: WebsocketClientConfig) { + const wasEnabled = this.isEnable; + const oldUrl = this.config.url; + const oldHeartInterval = this.config.heartInterval; + this.config = newConfig; + + if (newConfig.enable && !wasEnabled) { + this.open(); + return OB11NetworkReloadType.NetWorkOpen; + } else if (!newConfig.enable && wasEnabled) { + this.close(); + return OB11NetworkReloadType.NetWorkClose; + } + + if (oldUrl !== newConfig.url) { + this.close(); + if (newConfig.enable) { + this.open(); + } + return OB11NetworkReloadType.NetWorkReload; + } + + if (oldHeartInterval !== newConfig.heartInterval) { + if (this.heartbeatRef) { + clearInterval(this.heartbeatRef); + this.heartbeatRef = null; + } + if (newConfig.heartInterval > 0 && this.isEnable) { + this.heartbeatRef = setInterval(() => { + if (this.connection && this.connection.readyState === WebSocket.OPEN) { + this.connection.send(JSON.stringify(new OB11HeartbeatEvent(this.core, newConfig.heartInterval, this.core.selfInfo.online ?? true, true))); + } + }, newConfig.heartInterval); + } + return OB11NetworkReloadType.NetWorkReload; + } + + return OB11NetworkReloadType.Normal; } } diff --git a/src/onebot/network/index.ts b/src/onebot/network/index.ts index 9d4b16b5..dc8b8b67 100644 --- a/src/onebot/network/index.ts +++ b/src/onebot/network/index.ts @@ -1,33 +1,60 @@ import { OB11BaseEvent } from '@/onebot/event/OB11BaseEvent'; import { OB11Message } from '@/onebot'; import { ActionMap } from '@/onebot/action'; +import { NetworkConfigAdapter } from '../config/config'; export type OB11EmitEventContent = OB11BaseEvent | OB11Message; - +export enum OB11NetworkReloadType { + Normal = 0, + ConfigChange = 1, + NetWorkReload = 2, + NetWorkClose = 3, + NetWorkOpen = 4 +} export interface IOB11NetworkAdapter { - actions?: ActionMap; - + actions: ActionMap; + name: string; + isEnable: boolean; + config: NetworkConfigAdapter; + onEvent(event: T): void; open(): void | Promise; close(): void | Promise; + + reload(config: any): OB11NetworkReloadType | Promise; } export class OB11NetworkManager { - adapters: IOB11NetworkAdapter[] = []; + adapters: Map = new Map(); async openAllAdapters() { - return Promise.all(this.adapters.map(adapter => adapter.open())); + return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.open())); } async emitEvent(event: OB11EmitEventContent) { - //console.log('adapters', this.adapters.length); - return Promise.all(this.adapters.map(adapter => adapter.onEvent(event))); + return Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.onEvent(event))); } + async emitEventByName(names: string[], event: OB11EmitEventContent) { + return Promise.all(names.map(name => { + const adapter = this.adapters.get(name); + if (adapter) { + return adapter.onEvent(event); + } + })); + } + async emitEventByNames(map: Map) { + return Promise.all(Array.from(map.entries()).map(([name, event]) => { + const adapter = this.adapters.get(name); + if (adapter) { + return adapter.onEvent(event); + } + })); + } registerAdapter(adapter: IOB11NetworkAdapter) { - this.adapters.push(adapter); + this.adapters.set(adapter.name, adapter); } async registerAdapterAndOpen(adapter: IOB11NetworkAdapter) { @@ -36,24 +63,38 @@ export class OB11NetworkManager { } async closeSomeAdapters(adaptersToClose: IOB11NetworkAdapter[]) { - this.adapters = this.adapters.filter(adapter => !adaptersToClose.includes(adapter)); - await Promise.all(adaptersToClose.map(adapter => adapter.close())); + for (const adapter of adaptersToClose) { + this.adapters.delete(adapter.name); + await adapter.close(); + } + } + + findSomeAdapter(name: string) { + return this.adapters.get(name); } - /** - * Close all adapters that satisfy the predicate. - */ async closeAdapterByPredicate(closeFilter: (adapter: IOB11NetworkAdapter) => boolean) { - await this.closeSomeAdapters(this.adapters.filter(closeFilter)); + const adaptersToClose = Array.from(this.adapters.values()).filter(closeFilter); + await this.closeSomeAdapters(adaptersToClose); } async closeAllAdapters() { - await Promise.all(this.adapters.map(adapter => adapter.close())); - this.adapters = []; + await Promise.all(Array.from(this.adapters.values()).map(adapter => adapter.close())); + this.adapters.clear(); + } + + async readloadAdapter(name: string, config: T) { + const adapter = this.adapters.get(name); + if (adapter) { + await adapter.reload(config); + } + } + async readloadSomeAdapters(configMap: Map) { + await Promise.all(Array.from(configMap.entries()).map(([name, config]) => this.readloadAdapter(name, config))); } } export * from './active-http'; export * from './active-websocket'; export * from './passive-http'; -export * from './passive-websocket'; +export * from './passive-websocket'; \ No newline at end of file diff --git a/src/onebot/network/passive-http.ts b/src/onebot/network/passive-http.ts index 2ebaf624..c1ca6afc 100644 --- a/src/onebot/network/passive-http.ts +++ b/src/onebot/network/passive-http.ts @@ -1,22 +1,25 @@ -import { IOB11NetworkAdapter } from './index'; +import { IOB11NetworkAdapter, OB11NetworkReloadType } from './index'; import express, { Express, Request, Response } from 'express'; import http from 'http'; import { NapCatCore } from '@/core'; import { OB11Response } from '../action/OB11Response'; import { ActionMap } from '@/onebot/action'; import cors from 'cors'; +import { HttpServerConfig } from '../config/config'; export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { private app: Express | undefined; private server: http.Server | undefined; - private isOpen: boolean = false; + isEnable: boolean = false; + public config: HttpServerConfig; constructor( - public port: number, - public token: string, + public name: string, + config: HttpServerConfig, public core: NapCatCore, public actions: ActionMap, ) { + this.config = structuredClone(config); } onEvent() { @@ -25,13 +28,13 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { open() { try { - if (this.isOpen) { + if (this.isEnable) { this.core.context.logger.logError('Cannot open a closed HTTP server'); return; } - if (!this.isOpen) { + if (!this.isEnable) { this.initializeServer(); - this.isOpen = true; + this.isEnable = true; } } catch (e) { this.core.context.logger.logError(`[OneBot] [HTTP Server Adapter] Boot Error: ${e}`); @@ -40,7 +43,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { } async close() { - this.isOpen = false; + this.isEnable = false; this.server?.close(); this.app = undefined; } @@ -63,12 +66,12 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { }); }); - this.app.use((req, res, next) => this.authorize(this.token, req, res, next)); + this.app.use((req, res, next) => this.authorize(this.config.token, req, res, next)); this.app.use(async (req, res, _) => { await this.handleRequest(req, res); }); - this.server.listen(this.port, () => { - this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.port}`); + this.server.listen(this.config.port, () => { + this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Start On Port ${this.config.port}`); }); } @@ -85,7 +88,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { } private async handleRequest(req: Request, res: Response) { - if (!this.isOpen) { + if (!this.isEnable) { this.core.context.logger.log(`[OneBot] [HTTP Server Adapter] Server is closed`); return res.json(OB11Response.error('Server is closed', 200)); } @@ -101,7 +104,7 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { const action = this.actions.get(actionName); if (action) { try { - const result = await action.handle(payload); + const result = await action.handle(payload, this.name); return res.json(result); } catch (error: any) { return res.json(OB11Response.error(error?.stack?.toString() || error?.message || 'Error Handle', 200)); @@ -110,4 +113,28 @@ export class OB11PassiveHttpAdapter implements IOB11NetworkAdapter { return res.json(OB11Response.error('不支持的api ' + actionName, 200)); } } + + async reload(newConfig: HttpServerConfig) { + const wasEnabled = this.isEnable; + const oldPort = this.config.port; + this.config = newConfig; + + if (newConfig.enable && !wasEnabled) { + this.open(); + return OB11NetworkReloadType.NetWorkOpen; + } else if (!newConfig.enable && wasEnabled) { + this.close(); + return OB11NetworkReloadType.NetWorkClose; + } + + if (oldPort !== newConfig.port) { + this.close(); + if (newConfig.enable) { + this.open(); + } + return OB11NetworkReloadType.NetWorkReload; + } + + return OB11NetworkReloadType.Normal; + } } diff --git a/src/onebot/network/passive-websocket.ts b/src/onebot/network/passive-websocket.ts index ea18ebd8..1e47cc02 100644 --- a/src/onebot/network/passive-websocket.ts +++ b/src/onebot/network/passive-websocket.ts @@ -1,4 +1,4 @@ -import { IOB11NetworkAdapter, OB11EmitEventContent } from './index'; +import { IOB11NetworkAdapter, OB11EmitEventContent, OB11NetworkReloadType } from './index'; import urlParse from 'url'; import { WebSocket, WebSocketServer } from 'ws'; import { Mutex } from 'async-mutex'; @@ -10,43 +10,43 @@ import { OB11HeartbeatEvent } from '../event/meta/OB11HeartbeatEvent'; import { IncomingMessage } from 'http'; import { ActionMap } from '@/onebot/action'; import { LifeCycleSubType, OB11LifeCycleEvent } from '../event/meta/OB11LifeCycleEvent'; +import { WebsocketServerConfig } from '../config/config'; export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter { wsServer: WebSocketServer; wsClients: WebSocket[] = []; wsClientsMutex = new Mutex(); - isOpen: boolean = false; - hasBeenClosed: boolean = false; + isEnable: boolean = false; heartbeatInterval: number = 0; - core: NapCatCore; logger: LogWrapper; + public config: WebsocketServerConfig; private heartbeatIntervalId: NodeJS.Timeout | null = null; wsClientWithEvent: WebSocket[] = []; constructor( - ip: string, - port: number, - heartbeatInterval: number, - token: string, - core: NapCatCore, + public name: string, + config: WebsocketServerConfig, + public core: NapCatCore, public actions: ActionMap, ) { - this.core = core; + this.config = structuredClone(config); this.logger = core.context.logger; - - this.heartbeatInterval = heartbeatInterval; + if (this.config.host === '0.0.0.0') { + //兼容配置同时处理0.0.0.0逻辑 + this.config.host = ''; + } this.wsServer = new WebSocketServer({ - port: port, - host: ip, + port: this.config.port, + host: this.config.host, maxPayload: 1024 * 1024 * 1024, }); this.wsServer.on('connection', async (wsClient, wsReq) => { - if (!this.isOpen) { + if (!this.isEnable) { wsClient.close(); return; } //鉴权 - this.authorize(token, wsClient, wsReq); + this.authorize(this.config.token, wsClient, wsReq); const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url; const isApiConnect = paramUrl === '/api' || paramUrl === '/api/'; if (!isApiConnect) { @@ -102,23 +102,22 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter { } open() { - if (this.isOpen) { + if (this.isEnable) { this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Cannot open a opened WebSocket server'); return; } - if (this.hasBeenClosed) { - this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Cannot open a WebSocket server that has been closed'); - return; - } const addressInfo = this.wsServer.address(); this.logger.log('[OneBot] [WebSocket Server] Server Started', typeof (addressInfo) === 'string' ? addressInfo : addressInfo?.address + ':' + addressInfo?.port); - this.isOpen = true; - this.registerHeartBeat(); + this.isEnable = true; + if (this.heartbeatInterval > 0) { + this.registerHeartBeat(); + } + } async close() { - this.isOpen = false; + this.isEnable = false; this.wsServer.close((err) => { if (err) { this.logger.logError.bind(this.logger)('[OneBot] [WebSocket Server] Error closing server:', err.message); @@ -145,7 +144,7 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter { this.wsClientsMutex.runExclusive(async () => { this.wsClientWithEvent.forEach((wsClient) => { if (wsClient.readyState === WebSocket.OPEN) { - wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatInterval, this.core.selfInfo.online, true))); + wsClient.send(JSON.stringify(new OB11HeartbeatEvent(this.core, this.heartbeatInterval, this.core.selfInfo.online ?? true, true))); } }); }); @@ -188,9 +187,51 @@ export class OB11PassiveWebSocketAdapter implements IOB11NetworkAdapter { this.checkStateAndReply(OB11Response.error('不支持的api ' + receiveData.action, 1404, echo), wsClient); return; } - const retdata = await action.websocketHandle(receiveData.params, echo ?? ''); - const packet = Object.assign({}, retdata); - this.checkStateAndReply(packet, wsClient); + const retdata = await action.websocketHandle(receiveData.params, echo ?? '', this.name); + this.checkStateAndReply({ ...retdata }, wsClient); + } + + async reload(newConfig: WebsocketServerConfig) { + const wasEnabled = this.isEnable; + const oldPort = this.config.port; + const oldHost = this.config.host; + const oldHeartbeatInterval = this.heartbeatInterval; + this.config = newConfig; + + if (newConfig.enable && !wasEnabled) { + this.open(); + return OB11NetworkReloadType.NetWorkOpen; + } else if (!newConfig.enable && wasEnabled) { + this.close(); + return OB11NetworkReloadType.NetWorkClose; + } + + if (oldPort !== newConfig.port || oldHost !== newConfig.host) { + this.close(); + this.wsServer = new WebSocketServer({ + port: newConfig.port, + host: newConfig.host === '0.0.0.0' ? '' : newConfig.host, + maxPayload: 1024 * 1024 * 1024, + }); + if (newConfig.enable) { + this.open(); + } + return OB11NetworkReloadType.NetWorkReload; + } + + if (oldHeartbeatInterval !== newConfig.heartInterval) { + if (this.heartbeatIntervalId) { + clearInterval(this.heartbeatIntervalId); + this.heartbeatIntervalId = null; + } + this.heartbeatInterval = newConfig.heartInterval; + if (newConfig.heartInterval > 0 && this.isEnable) { + this.registerHeartBeat(); + } + return OB11NetworkReloadType.NetWorkReload; + } + + return OB11NetworkReloadType.Normal; } } diff --git a/src/shell/napcat.ts b/src/shell/napcat.ts index cc8890de..39b82bcf 100644 --- a/src/shell/napcat.ts +++ b/src/shell/napcat.ts @@ -34,49 +34,46 @@ program.option('-q, --qq [number]', 'QQ号').parse(process.argv); const cmdOptions = program.opts(); // NapCat Shell App ES 入口文件 -export async function NCoreInitShell() { - console.log('NapCat Shell App Loading...'); +async function handleUncaughtExceptions(logger: LogWrapper) { + process.on('uncaughtException', (err) => { + logger.logError('[NapCat] [Error] Unhandled Exception:', err.message); + }); + process.on('unhandledRejection', (reason, promise) => { + logger.logError('[NapCat] [Error] unhandledRejection:', reason); + }); +} - const pathWrapper = new NapCatPathWrapper(); - const logger = new LogWrapper(pathWrapper.logsPath); - const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); - const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion()); +function getDataPaths(wrapper: WrapperNodeApi): [string, string] { + if (os.platform() === 'darwin') { + const userPath = os.homedir(); + const appDataPath = path.resolve(userPath, './Library/Application Support/QQ'); + return [appDataPath, path.join(appDataPath, 'global')]; + } + let dataPath = wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig(); + if (!dataPath) { + dataPath = path.resolve(os.homedir(), './.config/QQ'); + fs.mkdirSync(dataPath, { recursive: true }); + } + const dataPathGlobal = path.resolve(dataPath, './nt_qq/global'); + return [dataPath, dataPathGlobal]; +} - const o3Service = wrapper.NodeIO3MiscService.get(); - o3Service.addO3MiscListener(new NodeIO3MiscListener()); - - logger.log(`[NapCat] [Core] NapCat.Core Version: ` + napCatVersion); - InitWebUi(logger, pathWrapper).then().catch(logger.logError.bind(logger)); - - // from constructor - const engine = wrapper.NodeIQQNTWrapperEngine.get(); - //const util = wrapper.NodeQQNTWrapperUtil.get(); - const loginService = wrapper.NodeIKernelLoginService.get(); - - const session = wrapper.NodeIQQNTWrapperSession.create(); - // from get dataPath - const [dataPath, dataPathGlobal] = (() => { - if (os.platform() === 'darwin') { - const userPath = os.homedir(); - const appDataPath = path.resolve(userPath, './Library/Application Support/QQ'); - return [appDataPath, path.join(appDataPath, 'global')]; - } - let dataPath = wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig(); - if (!dataPath) { - dataPath = path.resolve(os.homedir(), './.config/QQ'); - fs.mkdirSync(dataPath, { recursive: true }); - } - const dataPathGlobal = path.resolve(dataPath, './nt_qq/global'); - return [dataPath, dataPathGlobal]; - })(); +function getPlatformType(): PlatformType { const platformMapping: Partial> = { win32: PlatformType.KWINDOWS, darwin: PlatformType.KMAC, linux: PlatformType.KLINUX, }; - const systemPlatform = platformMapping[os.platform()] ?? PlatformType.KWINDOWS; - if (!basicInfoWrapper.QQVersionAppid || !basicInfoWrapper.QQVersionQua) throw new Error('QQVersionAppid or QQVersionQua is not defined'); - // from initConfig + return platformMapping[os.platform()] ?? PlatformType.KWINDOWS; +} + +async function initializeEngine( + engine: any, + basicInfoWrapper: QQBasicInfoWrapper, + dataPathGlobal: string, + systemPlatform: PlatformType, + systemVersion: string +) { engine.initWithDeskTopConfig( { base_path_prefix: '', @@ -93,33 +90,38 @@ export async function NCoreInitShell() { }, new NodeIGlobalAdapter(), ); +} + +async function initializeLoginService( + loginService: NodeIKernelLoginService, + basicInfoWrapper: QQBasicInfoWrapper, + dataPathGlobal: string, + systemVersion: string, + hostname: string +) { loginService.initConfig({ machineId: '', - appid: basicInfoWrapper.QQVersionAppid, + appid: basicInfoWrapper.QQVersionAppid ?? '', platVer: systemVersion, commonPath: dataPathGlobal, clientVer: basicInfoWrapper.getFullQQVesion(), hostName: hostname, }); +} - let quickLoginUin = cmdOptions.qq; // undefined | 'true' | string - const historyLoginList = (await loginService.getLoginList()).LocalLoginInfoList; - if (quickLoginUin == 'true') { - if (historyLoginList.length > 0) { - quickLoginUin = historyLoginList[0].uin; - logger.log(`-q 指令指定使用最近的 QQ ${quickLoginUin} 进行快速登录`); - } else { - quickLoginUin = ''; - } - } - const dataTimestape = new Date().getTime().toString(); - o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']); - const selfInfo = await new Promise((resolve) => { +async function handleLogin( + loginService: NodeIKernelLoginService, + logger: LogWrapper, + pathWrapper: NapCatPathWrapper, + quickLoginUin: string | undefined, + historyLoginList: any[] +): Promise { + return new Promise((resolve) => { const loginListener = new NodeIKernelLoginListener(); let isLogined = false; - // from constructor + loginListener.onUserLoggedIn = (userid: string) => { - logger.logError.bind(logger)(`当前账号(${userid})已登录,无法重复登录`); + logger.logError(`当前账号(${userid})已登录,无法重复登录`); }; loginListener.onQRCodeLoginSucceed = async (loginResult) => { @@ -127,13 +129,12 @@ export async function NCoreInitShell() { resolve({ uid: loginResult.uid, uin: loginResult.uin, - nick: '', // 获取不到 + nick: '', online: true, }); }; loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => { - //设置WebuiQrcode WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl); const realBase64 = pngBase64QrcodeData.replace(/^data:image\/\w+;base64,/, ''); @@ -152,50 +153,46 @@ export async function NCoreInitShell() { }); }); }; + loginListener.onQRCodeSessionFailed = (errType: number, errCode: number, errMsg: string) => { if (!isLogined) { - logger.logError.bind(logger)('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg); + logger.logError('[Core] [Login] Login Error,ErrCode: ', errCode, ' ErrMsg:', errMsg); if (errType == 1 && errCode == 3) { // 二维码过期刷新 } loginService.getQRCodePicture(); } }; + loginListener.onLoginFailed = (args) => { - logger.logError.bind(logger)('[Core] [Login] Login Error , ErrInfo: ', args); + logger.logError('[Core] [Login] Login Error , ErrInfo: ', args); }; loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger)); const isConnect = loginService.connect(); if (!isConnect) { - logger.logError.bind(logger)('核心登录服务连接失败!'); + logger.logError('核心登录服务连接失败!'); return; } + logger.log('核心登录服务连接成功!'); - // 实现WebUi快速登录 + loginService.getLoginList().then((res) => { // 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList WebUiDataRuntime.setQQQuickLoginList(res.LocalLoginInfoList.filter((item) => item.isQuickLogin).map((item) => item.uin.toString())); }); - if (basicInfoWrapper.QQVersionConfig?.curVersion) { - loginService.getLoginMiscData('hotUpdateSign').then((res) => { - if (res.result === 0) { - loginService.setLoginMiscData('hotUpdateSign', res.value); - } - }); - session.getNodeMiscService().writeVersionToRegistry(basicInfoWrapper.QQVersionConfig?.curVersion); - } + WebUiDataRuntime.setQuickLoginCall(async (uin: string) => { return await new Promise((resolve) => { if (uin) { - logger.log.bind(logger)('正在快速登录 ', uin); + logger.log('正在快速登录 ', uin); loginService.quickLoginWithUin(uin).then(res => { if (res.loginErrorInfo.errMsg) { resolve({ result: false, message: res.loginErrorInfo.errMsg }); } resolve({ result: true, message: '' }); }).catch((e) => { - logger.logError.bind(logger)(e); + logger.logError(e); resolve({ result: false, message: '快速登录发生错误' }); }); } else { @@ -211,14 +208,14 @@ export async function NCoreInitShell() { loginService.quickLoginWithUin(quickLoginUin) .then(result => { if (result.loginErrorInfo.errMsg) { - logger.logError.bind(logger)('快速登录错误:', result.loginErrorInfo.errMsg); + logger.logError('快速登录错误:', result.loginErrorInfo.errMsg); if (!isLogined) loginService.getQRCodePicture(); } }) .catch(); }, 1000); } else { - logger.logError.bind(logger)('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式'); + logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式'); if (!isLogined) loginService.getQRCodePicture(); } } else { @@ -232,37 +229,20 @@ export async function NCoreInitShell() { loginService.getQRCodePicture(); } }); - // BEFORE LOGGING IN - const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129'; - o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex'))); - // AFTER LOGGING IN - //99b15bdb4c984fc69d5aa1feb9aa16xx --> 99b15bdb-4c98-4fc6-9d5a-a1feb9aa16xx - //把guid从左向右转换为guid格式 loginService.getMachineGuid() +} - let guid = loginService.getMachineGuid(); - guid = guid.slice(0, 8) + '-' + guid.slice(8, 12) + '-' + guid.slice(12, 16) + '-' + guid.slice(16, 20) + '-' + guid.slice(20); - //console.log('guid:', guid); - //NodeIO3MiscService/reportAmgomWeather login a6 [ '1726748166943', '184', '329' ] - o3Service.reportAmgomWeather('login', 'a6', [dataTimestape, '184', '329']); - // if(session.getUnitedConfigService()){ - // session.getUnitedConfigService().fetchUnitedCommendConfig([]); - // } - // from initSession - await new Promise(async (resolve, reject) => { - const sessionConfig = await genSessionConfig( - guid, - basicInfoWrapper.QQVersionAppid!, - basicInfoWrapper.getFullQQVesion(), - selfInfo.uin, - selfInfo.uid, - dataPath, - ); +async function initializeSession( + session: NodeIQQNTWrapperSession, + sessionConfig: any, + logger: LogWrapper +) { + return new Promise((resolve, reject) => { const sessionListener = new NodeIKernelSessionListener(); sessionListener.onSessionInitComplete = (r: unknown) => { if (r === 0) { resolve(); } else { - reject(r); + reject(new Error('登录异常' + r?.toString())); } }; session.init( @@ -273,21 +253,74 @@ export async function NCoreInitShell() { ); try { session.startNT(0); - } catch (_) { /* Empty */ + } catch (_) { try { session.startNT(); - } catch (e) { - reject('init failed ' + e); + } catch (e: unknown) { + reject(new Error('init failed ' + (e as Error).message)); } } }); - // Initialization end! +} + +export async function NCoreInitShell() { + console.log('NapCat Shell App Loading...'); + const pathWrapper = new NapCatPathWrapper(); + const logger = new LogWrapper(pathWrapper.logsPath); + handleUncaughtExceptions(logger); + + const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); + const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion()); + + const o3Service = wrapper.NodeIO3MiscService.get(); + o3Service.addO3MiscListener(new NodeIO3MiscListener()); + + logger.log(`[NapCat] [Core] NapCat.Core Version: ` + napCatVersion); + InitWebUi(logger, pathWrapper).then().catch(logger.logError.bind(logger)); + + const engine = wrapper.NodeIQQNTWrapperEngine.get(); + const loginService = wrapper.NodeIKernelLoginService.get(); + const session = wrapper.NodeIQQNTWrapperSession.create(); + + const [dataPath, dataPathGlobal] = getDataPaths(wrapper); + const systemPlatform = getPlatformType(); + + if (!basicInfoWrapper.QQVersionAppid || !basicInfoWrapper.QQVersionQua) throw new Error('QQVersionAppid or QQVersionQua is not defined'); + + await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion); + await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname); + + const quickLoginUin = cmdOptions.qq; + const historyLoginList = (await loginService.getLoginList()).LocalLoginInfoList; + + const dataTimestape = new Date().getTime().toString(); + o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']); + + const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList); + + const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129'; + o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex'))); + + let guid = loginService.getMachineGuid(); + guid = guid.slice(0, 8) + '-' + guid.slice(8, 12) + '-' + guid.slice(12, 16) + '-' + guid.slice(16, 20) + '-' + guid.slice(20); + o3Service.reportAmgomWeather('login', 'a6', [dataTimestape, '184', '329']); + + const sessionConfig = await genSessionConfig( + guid, + basicInfoWrapper.QQVersionAppid, + basicInfoWrapper.getFullQQVesion(), + selfInfo.uin, + selfInfo.uid, + dataPath, + ); + + await initializeSession(session, sessionConfig, logger); const accountDataPath = path.resolve(dataPath, './NapCat/data'); fs.mkdirSync(dataPath, { recursive: true }); logger.logDebug('本账号数据/缓存目录:', accountDataPath); - new NapCatShell( + await new NapCatShell( wrapper, session, logger, @@ -295,9 +328,10 @@ export async function NCoreInitShell() { selfInfo, basicInfoWrapper, pathWrapper, - ); + ).InitNapCat(); } + export class NapCatShell { readonly core: NapCatCore; readonly context: InstanceContext; @@ -322,8 +356,13 @@ export class NapCatShell { }; this.core = new NapCatCore(this.context, selfInfo); - // TODO: complete ob11 adapter initialization logic - new NapCatOneBot11Adapter(this.core, this.context, pathWrapper); + + + } + async InitNapCat() { + await this.core.initCore(); + new NapCatOneBot11Adapter(this.core, this.context, this.context.pathWrapper).InitOneBot() + .catch(e => this.context.logger.logError.bind(this.context.logger)('初始化OneBot失败', e)); } } diff --git a/src/webui/index.ts b/src/webui/index.ts index 3d4cae46..68ac18b1 100644 --- a/src/webui/index.ts +++ b/src/webui/index.ts @@ -36,26 +36,34 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp // 配置静态文件服务,提供./static目录下的文件服务,访问路径为/webui app.use(config.prefix + '/webui', express.static(pathWrapper.staticPath)); //挂载API接口 + // 添加CORS支持 + // TODO: + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); + next(); + }); app.use(config.prefix + '/api', ALLRouter); app.listen(config.port, config.host, async () => { log(`[NapCat] [WebUi] Current WebUi is running at http://${config.host}:${config.port}${config.prefix}`); log(`[NapCat] [WebUi] Login Token is ${config.token}`); - log(`[NapCat] [WebUi] WebUi User Panel Url: http://${config.host}:${config.port}${config.prefix}/webui?token=${config.token}`); - log(`[NapCat] [WebUi] WebUi Local Panel Url: http://127.0.0.1:${config.port}${config.prefix}/webui?token=${config.token}`); + log( + `[NapCat] [WebUi] WebUi User Panel Url: http://${config.host}:${config.port}${config.prefix}/webui?token=${config.token}` + ); + log( + `[NapCat] [WebUi] WebUi Local Panel Url: http://127.0.0.1:${config.port}${config.prefix}/webui?token=${config.token}` + ); //获取上网Ip //https://www.ip.cn/api/index?ip&type=0 - RequestUtil.HttpGetJson<{ IP: {IP:string} }>( - 'https://ip.011102.xyz/', - 'GET', - {}, - {}, - true, - true - ).then((data) => { - log(`[NapCat] [WebUi] WebUi Publish Panel Url: http://${data.IP.IP}:${config.port}${config.prefix}/webui/?token=${config.token}`); - }).catch((err) => { - logger.logError.bind(logger)(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`); - }); - + RequestUtil.HttpGetJson<{ IP: { IP: string } }>('https://ip.011102.xyz/', 'GET', {}, {}, true, true) + .then((data) => { + log( + `[NapCat] [WebUi] WebUi Publish Panel Url: http://${data.IP.IP}:${config.port}${config.prefix}/webui/?token=${config.token}` + ); + }) + .catch((err) => { + logger.logError.bind(logger)(`[NapCat] [WebUi] Get Publish Panel Url Error: ${err}`); + }); }); } diff --git a/src/webui/src/api/BaseInfo.ts b/src/webui/src/api/BaseInfo.ts new file mode 100644 index 00000000..d7e9a825 --- /dev/null +++ b/src/webui/src/api/BaseInfo.ts @@ -0,0 +1,15 @@ +import { RequestHandler } from 'express'; +import { WebUiDataRuntime } from '../helper/Data'; + +export const LogFileListHandler: RequestHandler = async (req, res) => { + res.send({ + code: 0, + data: { + uin: 0, + nick: 'NapCat', + avatar: 'https://q1.qlogo.cn/g?b=qq&nk=0&s=640', + status: 'online', + boottime: Date.now() + } + }); +}; diff --git a/src/webui/src/api/OB11Config.ts b/src/webui/src/api/OB11Config.ts index f2fbd145..60de2264 100644 --- a/src/webui/src/api/OB11Config.ts +++ b/src/webui/src/api/OB11Config.ts @@ -1,12 +1,11 @@ import { RequestHandler } from 'express'; import { WebUiDataRuntime } from '../helper/Data'; import { existsSync, readFileSync } from 'node:fs'; -import { OB11Config } from '@/webui/ui/components/WebUiApiOB11Config'; +import { OneBotConfig } from '@/onebot/config/config'; import { resolve } from 'node:path'; import { webUiPathWrapper } from '@/webui'; -const isEmpty = (data: any) => - data === undefined || data === null || data === ''; +const isEmpty = (data: any) => data === undefined || data === null || data === ''; export const OB11GetConfigHandler: RequestHandler = async (req, res) => { const isLogin = await WebUiDataRuntime.getQQLoginStatus(); if (!isLogin) { @@ -19,15 +18,15 @@ export const OB11GetConfigHandler: RequestHandler = async (req, res) => { const uin = await WebUiDataRuntime.getQQLoginUin(); const configFilePath = resolve(webUiPathWrapper.configPath, `./onebot11_${uin}.json`); //console.log(configFilePath); - let data: OB11Config; + let data: OneBotConfig; try { data = JSON.parse( existsSync(configFilePath) ? readFileSync(configFilePath).toString() - : readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString(), + : readFileSync(resolve(webUiPathWrapper.configPath, './onebot11.json')).toString() ); } catch (e) { - data = {} as OB11Config; + data = {} as OneBotConfig; res.send({ code: -1, message: 'Config Get Error', diff --git a/src/webui/src/helper/Data.ts b/src/webui/src/helper/Data.ts index c569d912..9563e6f7 100644 --- a/src/webui/src/helper/Data.ts +++ b/src/webui/src/helper/Data.ts @@ -1,4 +1,4 @@ -import { OB11Config } from '@/onebot/config'; +import { OneBotConfig } from '@/onebot/config/config'; interface LoginRuntimeType { LoginCurrentTime: number; @@ -7,9 +7,9 @@ interface LoginRuntimeType { QQQRCodeURL: string; QQLoginUin: string; NapCatHelper: { - onQuickLoginRequested: (uin: string) => Promise<{ result: boolean, message: string }>; - onOB11ConfigChanged: (ob11: OB11Config) => Promise; - QQLoginList: string[] + onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>; + onOB11ConfigChanged: (ob11: OneBotConfig) => Promise; + QQLoginList: string[]; }; } @@ -31,62 +31,62 @@ const LoginRuntime: LoginRuntimeType = { }; export const WebUiDataRuntime = { - checkLoginRate: async function(RateLimit: number): Promise { + checkLoginRate: async function (RateLimit: number): Promise { LoginRuntime.LoginCurrentRate++; //console.log(RateLimit, LoginRuntime.LoginCurrentRate, Date.now() - LoginRuntime.LoginCurrentTime); if (Date.now() - LoginRuntime.LoginCurrentTime > 1000 * 60) { - LoginRuntime.LoginCurrentRate = 0;//超出时间重置限速 + LoginRuntime.LoginCurrentRate = 0; //超出时间重置限速 LoginRuntime.LoginCurrentTime = Date.now(); return true; } return LoginRuntime.LoginCurrentRate <= RateLimit; }, - getQQLoginStatus: async function(): Promise { + getQQLoginStatus: async function (): Promise { return LoginRuntime.QQLoginStatus; }, - setQQLoginStatus: async function(status: boolean): Promise { + setQQLoginStatus: async function (status: boolean): Promise { LoginRuntime.QQLoginStatus = status; }, - setQQLoginQrcodeURL: async function(url: string): Promise { + setQQLoginQrcodeURL: async function (url: string): Promise { LoginRuntime.QQQRCodeURL = url; }, - getQQLoginQrcodeURL: async function(): Promise { + getQQLoginQrcodeURL: async function (): Promise { return LoginRuntime.QQQRCodeURL; }, - setQQLoginUin: async function(uin: string): Promise { + setQQLoginUin: async function (uin: string): Promise { LoginRuntime.QQLoginUin = uin; }, - getQQLoginUin: async function(): Promise { + getQQLoginUin: async function (): Promise { return LoginRuntime.QQLoginUin; }, - getQQQuickLoginList: async function(): Promise { + getQQQuickLoginList: async function (): Promise { return LoginRuntime.NapCatHelper.QQLoginList; }, - setQQQuickLoginList: async function(list: string[]): Promise { + setQQQuickLoginList: async function (list: string[]): Promise { LoginRuntime.NapCatHelper.QQLoginList = list; }, - setQuickLoginCall(func: (uin: string) => Promise<{ result: boolean, message: string }>): void { + setQuickLoginCall(func: (uin: string) => Promise<{ result: boolean; message: string }>): void { LoginRuntime.NapCatHelper.onQuickLoginRequested = func; }, - requestQuickLogin: async function(uin: string): Promise<{ result: boolean, message: string }> { + requestQuickLogin: async function (uin: string): Promise<{ result: boolean; message: string }> { return await LoginRuntime.NapCatHelper.onQuickLoginRequested(uin); }, - setOnOB11ConfigChanged: async function(func: (ob11: OB11Config) => Promise): Promise { + setOnOB11ConfigChanged: async function (func: (ob11: OneBotConfig) => Promise): Promise { LoginRuntime.NapCatHelper.onOB11ConfigChanged = func; }, - setOB11Config: async function(ob11: OB11Config): Promise { + setOB11Config: async function (ob11: OneBotConfig): Promise { await LoginRuntime.NapCatHelper.onOB11ConfigChanged(ob11); }, }; diff --git a/src/webui/src/helper/SignToken.ts b/src/webui/src/helper/SignToken.ts index 2932e8dc..22cccd70 100644 --- a/src/webui/src/helper/SignToken.ts +++ b/src/webui/src/helper/SignToken.ts @@ -11,7 +11,7 @@ interface WebUiCredentialJson { } export class AuthHelper { - private static secretKey = Math.random().toString(36).slice(2); + private static readonly secretKey = Math.random().toString(36).slice(2); /** * 签名凭证方法。 diff --git a/src/webui/src/helper/config.ts b/src/webui/src/helper/config.ts index e6d8bdf8..24ef6a63 100644 --- a/src/webui/src/helper/config.ts +++ b/src/webui/src/helper/config.ts @@ -8,7 +8,7 @@ import { resolve } from 'node:path'; const MAX_PORT_TRY = 100; async function tryUseHost(host: string): Promise { - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { try { const server = net.createServer(); server.on('listening', () => { @@ -18,9 +18,9 @@ async function tryUseHost(host: string): Promise { server.on('error', (err: any) => { if (err.code === 'EADDRNOTAVAIL') { - reject('主机地址验证失败,可能为非本机地址'); + reject(new Error('主机地址验证失败,可能为非本机地址')); } else { - reject(`遇到错误: ${err.code}`); + reject(new Error(`遇到错误: ${err.code}`)); } }); @@ -28,13 +28,13 @@ async function tryUseHost(host: string): Promise { server.listen(0, host); } catch (error) { // 这里捕获到的错误应该是启动服务器时的同步错误 - reject(`服务器启动时发生错误: ${error}`); + reject(new Error(`服务器启动时发生错误: ${error}`)); } }); } async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise { - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { try { const server = net.createServer(); server.on('listening', () => { @@ -48,10 +48,10 @@ async function tryUsePort(port: number, host: string, tryCount: number = 0): Pro // 使用循环代替递归 resolve(tryUsePort(port + 1, host, tryCount + 1)); } else { - reject(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`); + reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`)); } } else { - reject(`遇到错误: ${err.code}`); + reject(new Error(`遇到错误: ${err.code}`)); } }); @@ -59,7 +59,7 @@ async function tryUsePort(port: number, host: string, tryCount: number = 0): Pro server.listen(port, host); } catch (error) { // 这里捕获到的错误应该是启动服务器时的同步错误 - reject(`服务器启动时发生错误: ${error}`); + reject(new Error(`服务器启动时发生错误: ${error}`)); } }); } @@ -114,14 +114,14 @@ export class WebUiConfigWrapper { // 不希望回写的配置放后面 // 查询主机地址是否可用 - const [host_err, host] = await tryUseHost(parsedConfig.host).then(data => [null, data as string]).catch(err => [err, null]); + const [host_err, host] = await tryUseHost(parsedConfig.host).then(data => [null, data]).catch(err => [err, null]); if (host_err) { console.log('host不可用', host_err); parsedConfig.port = 0; // 设置为0,禁用WebUI } else { parsedConfig.host = host; // 修正端口占用情况 - const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host).then(data => [null, data as number]).catch(err => [err, null]); + const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host).then(data => [null, data]).catch(err => [err, null]); if (port_err) { console.log('port不可用', port_err); parsedConfig.port = 0; // 设置为0,禁用WebUI diff --git a/src/webui/ui/NapCat.ts b/src/webui/ui/NapCat.ts deleted file mode 100644 index 7519f00d..00000000 --- a/src/webui/ui/NapCat.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { SettingList } from './components/SettingList'; -import { SettingItem } from './components/SettingItem'; -import { SettingButton } from './components/SettingButton'; -import { SettingSwitch } from './components/SettingSwitch'; -import { SettingSelect } from './components/SettingSelect'; -import { OB11Config, OB11ConfigWrapper } from './components/WebUiApiOB11Config'; - -async function onSettingWindowCreated(view: Element) { - const isEmpty = (value: any) => value === undefined || false || value === ''; - await OB11ConfigWrapper.Init(localStorage.getItem('auth') as string); - const ob11Config: OB11Config = await OB11ConfigWrapper.GetOB11Config(); - const setOB11Config = (key: string, value: any) => { - const configKey = key.split('.'); - if (configKey.length === 2) { - ob11Config[configKey[1]] = value; - } else if (configKey.length === 3) { - ob11Config[configKey[1]][configKey[2]] = value; - } - // OB11ConfigWrapper.SetOB11Config(ob11Config); // 只有当点保存时才下发配置,而不是在修改值后立即下发 - }; - - const parser = new DOMParser(); - const doc = parser.parseFromString( - [ - '
', - ` -
-
`, - SettingList([ - SettingItem( - 'Napcat', - undefined, - SettingButton('V3.3.12', 'napcat-update-button', 'secondary'), - ), - ]), - SettingList([ - SettingItem( - '启用 HTTP 服务', - undefined, - SettingSwitch('ob11.http.enable', ob11Config.http.enable, { - 'control-display-id': 'config-ob11-http-port', - }), - ), - SettingItem( - 'HTTP 服务监听端口', - undefined, - `
`, - 'config-ob11-http-port', - ob11Config.http.enable, - ), - SettingItem( - '启用 HTTP 心跳', - undefined, - SettingSwitch('ob11.http.enableHeart', ob11Config.http.enableHeart, { - 'control-display-id': 'config-ob11-HTTP.enableHeart', - }), - ), - SettingItem( - '启用 HTTP 事件上报', - undefined, - SettingSwitch('ob11.http.enablePost', ob11Config.http.enablePost, { - 'control-display-id': 'config-ob11-http-postUrls', - }), - ), - `
- -
- HTTP 事件上报密钥 -
-
- -
-
- -
- HTTP 事件上报地址 -
- 添加 -
-
-
`, - SettingItem( - '启用正向 WebSocket 服务', - undefined, - SettingSwitch('ob11.ws.enable', ob11Config.ws.enable, { - 'control-display-id': 'config-ob11-ws-port', - }), - ), - SettingItem( - '正向 WebSocket 服务监听端口', - undefined, - `
`, - 'config-ob11-ws-port', - ob11Config.ws.enable, - ), - SettingItem( - '启用反向 WebSocket 服务', - undefined, - SettingSwitch('ob11.reverseWs.enable', ob11Config.reverseWs.enable, { - 'control-display-id': 'config-ob11-reverseWs-urls', - }), - ), - `
- -
- 反向 WebSocket 监听地址 -
- 添加 -
-
-
`, - SettingItem( - ' WebSocket 服务心跳间隔', - '控制每隔多久发送一个心跳包,单位为毫秒', - `
`, - ), - SettingItem( - 'Access token', - undefined, - `
`, - ), - SettingItem( - '新消息上报格式', - '如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 OneBot v11 文档', - SettingSelect( - [ - { text: '消息段', value: 'array' }, - { text: 'CQ码', value: 'string' }, - ], - 'ob11.messagePostFormat', - ob11Config.messagePostFormat, - ), - ), - SettingItem( - '音乐卡片签名地址', - undefined, - `
`, - 'ob11.musicSignUrl', - ), - SettingItem( - '启用本地进群时间与发言时间记录', - undefined, - SettingSwitch('ob11.GroupLocalTime.Record', ob11Config.GroupLocalTime.Record, { - 'control-display-id': 'config-ob11-GroupLocalTime-RecordList', - }), - ), - `
- -
- 群列表 -
- 添加 -
-
-
`, - SettingItem( - '', - undefined, - SettingButton('保存', 'config-ob11-save', 'primary'), - ), - ]), - SettingList([ - SettingItem( - '上报 Bot 自身发送的消息', - '上报 event 为 message_sent', - SettingSwitch('ob11.reportSelfMessage', ob11Config.reportSelfMessage), - ), - ]), - SettingList([ - SettingItem( - 'GitHub 仓库', - 'https://github.com/NapNeko/NapCatQQ', - SettingButton('点个星星', 'open-github'), - ), - SettingItem('NapCat 文档', '', SettingButton('看看文档', 'open-docs')), - ]), - SettingItem( - 'Telegram 群', - 'https://t.me/+nLZEnpne-pQ1OWFl', - SettingButton('进去逛逛', 'open-telegram'), - ), - SettingItem( - 'QQ 群', - '518662028', - SettingButton('我要进去', 'open-qq-group'), - ), - '
', - ].join(''), - 'text/html', - ); - - // 外链按钮 - doc.querySelector('#open-github')?.addEventListener('click', () => { - window.open('https://github.com/NapNeko/NapCatQQ', '_blank'); - }); - doc.querySelector('#open-docs')?.addEventListener('click', () => { - window.open('https://napneko.github.io/', '_blank'); - }); - doc.querySelector('#open-telegram')?.addEventListener('click', () => { - window.open('https://t.me/+nLZEnpne-pQ1OWFl', '_blank'); - }); - doc.querySelector('#open-qq-group')?.addEventListener('click', () => { - window.open('https://qm.qq.com/q/VfjAq5HIMS', '_blank'); - }); - // 生成反向地址列表 - const buildHostListItem = ( - type: string, - host: string, - index: number, - inputAttrs: any = {}, - ) => { - const dom = { - container: document.createElement('setting-item'), - input: document.createElement('input'), - inputContainer: document.createElement('div'), - deleteBtn: document.createElement('setting-button'), - }; - dom.container.classList.add('setting-host-list-item'); - dom.container.dataset.direction = 'row'; - Object.assign(dom.input, inputAttrs); - dom.input.classList.add('q-input__inner'); - dom.input.type = 'url'; - dom.input.value = host; - dom.input.addEventListener('input', () => { - ob11Config[type.split('-')[0]][type.split('-')[1]][index] = - dom.input.value; - }); - - dom.inputContainer.classList.add('q-input'); - dom.inputContainer.appendChild(dom.input); - - dom.deleteBtn.innerHTML = '删除'; - dom.deleteBtn.dataset.type = 'secondary'; - dom.deleteBtn.addEventListener('click', () => { - ob11Config[type.split('-')[0]][type.split('-')[1]].splice(index, 1); - initReverseHost(type); - }); - - dom.container.appendChild(dom.inputContainer); - dom.container.appendChild(dom.deleteBtn); - - return dom.container; - }; - const buildHostList = ( - hosts: string[], - type: string, - inputAttr: any = {}, - ) => { - const result: HTMLElement[] = []; - - hosts?.forEach((host, index) => { - result.push(buildHostListItem(type, host, index, inputAttr)); - }); - - return result; - }; - const addReverseHost = ( - type: string, - doc: Document = document, - inputAttr: any = {}, - ) => { - type = type.replace(/\./g, '-');//替换操作 - const hostContainerDom = doc.body.querySelector( - `#config-ob11-${type}-list`, - ); - hostContainerDom?.appendChild( - buildHostListItem( - type, - '', - ob11Config[type.split('-')[0]][type.split('-')[1]].length, - inputAttr, - ), - ); - ob11Config[type.split('-')[0]][type.split('-')[1]].push(''); - }; - const initReverseHost = (type: string, doc: Document = document) => { - type = type.replace(/\./g, '-');//替换操作 - const hostContainerDom = doc.body?.querySelector( - `#config-ob11-${type}-list`, - ); - if (hostContainerDom) { - [...hostContainerDom.childNodes].forEach((dom) => dom.remove()); - buildHostList( - ob11Config[type.split('-')[0]][type.split('-')[1]], - type, - ).forEach((dom) => { - hostContainerDom?.appendChild(dom); - }); - } - }; - - initReverseHost('http.postUrls', doc); - initReverseHost('reverseWs.urls', doc); - initReverseHost('GroupLocalTime.RecordList', doc); - - doc - .querySelector('#config-ob11-http-postUrls-add') - ?.addEventListener('click', () => - addReverseHost('http.postUrls', document, { - placeholder: '如:http://127.0.0.1:5140/onebot', - }), - ); - - doc - .querySelector('#config-ob11-reverseWs-urls-add') - ?.addEventListener('click', () => - addReverseHost('reverseWs.urls', document, { - placeholder: '如:ws://127.0.0.1:5140/onebot', - }), - ); - doc - .querySelector('#config-ob11-GroupLocalTime-RecordList-add') - ?.addEventListener('click', () => - addReverseHost('GroupLocalTime.RecordList', document, { - placeholder: '此处填写群号 -1为全部', - }), - ); - doc.querySelector('#config-ffmpeg-select')?.addEventListener('click', () => { - //选择ffmpeg - }); - - doc.querySelector('#config-open-log-path')?.addEventListener('click', () => { - //打开日志 - }); - - // 开关 - doc - .querySelectorAll('setting-switch[data-config-key]') - .forEach((dom: Element) => { - dom.addEventListener('click', () => { - const active = dom.getAttribute('is-active') == undefined; - //@ts-expect-error 等待修复 - setOB11Config(dom.dataset.configKey, active); - if (active) dom.setAttribute('is-active', ''); - else dom.removeAttribute('is-active'); - //@ts-expect-error 等待修复 - if (!isEmpty(dom.dataset.controlDisplayId)) { - const displayDom = document.querySelector( - //@ts-expect-error 等待修复 - `#${dom.dataset.controlDisplayId}`, - ); - if (active) displayDom?.removeAttribute('is-hidden'); - else displayDom?.setAttribute('is-hidden', ''); - } - }); - }); - - // 输入框 - doc - .querySelectorAll( - 'setting-item .q-input input.q-input__inner[data-config-key]', - ) - .forEach((dom: Element) => { - dom.addEventListener('input', () => { - const Type = dom.getAttribute('type'); - //@ts-expect-error等待修复 - const configKey = dom.dataset.configKey; - const configValue = - Type === 'number' - ? parseInt((dom as HTMLInputElement).value) >= 1 - ? parseInt((dom as HTMLInputElement).value) - : 1 - : (dom as HTMLInputElement).value; - - setOB11Config(configKey, configValue); - }); - }); - - // 下拉框 - doc - .querySelectorAll('ob-setting-select[data-config-key]') - .forEach((dom: Element) => { - //@ts-expect-error等待修复 - dom?.addEventListener('selected', (e: CustomEvent) => { - //@ts-expect-error等待修复 - const configKey = dom.dataset.configKey; - const configValue = e.detail.value; - setOB11Config(configKey, configValue); - }); - }); - - // 保存按钮 - doc.querySelector('#config-ob11-save')?.addEventListener('click', () => { - OB11ConfigWrapper.SetOB11Config(ob11Config); - alert('保存成功'); - }); - doc.body.childNodes.forEach((node) => { - view.appendChild(node); - }); -} - -export { onSettingWindowCreated }; diff --git a/src/webui/ui/components/SettingButton.ts b/src/webui/ui/components/SettingButton.ts deleted file mode 100644 index 02132f3c..00000000 --- a/src/webui/ui/components/SettingButton.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const SettingButton = (text: string, id?: string, type: string = 'secondary') => { - return `${text}`; -}; \ No newline at end of file diff --git a/src/webui/ui/components/SettingItem.ts b/src/webui/ui/components/SettingItem.ts deleted file mode 100644 index 11c15426..00000000 --- a/src/webui/ui/components/SettingItem.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const SettingItem = ( - title: string, - subtitle?: string, - action?: string, - id?: string, - visible: boolean = true, -) => { - return ` -
- ${title} - ${subtitle ? `${subtitle}` : ''} -
- ${action ? `
${action}
` : ''} -
`; -}; \ No newline at end of file diff --git a/src/webui/ui/components/SettingList.ts b/src/webui/ui/components/SettingList.ts deleted file mode 100644 index 8db03dc2..00000000 --- a/src/webui/ui/components/SettingList.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const SettingList = ( - items: string[], - title?: string, - isCollapsible: boolean = false, - direction: string = 'column', -) => { - return ` - - - ${items.join('')} - - - `; -}; \ No newline at end of file diff --git a/src/webui/ui/components/SettingOption.ts b/src/webui/ui/components/SettingOption.ts deleted file mode 100644 index ce44e2e4..00000000 --- a/src/webui/ui/components/SettingOption.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const SettingOption = (text: string, value?: string, isSelected: boolean = false) => { - return `${text}`; -}; \ No newline at end of file diff --git a/src/webui/ui/components/SettingSelect.ts b/src/webui/ui/components/SettingSelect.ts deleted file mode 100644 index 461f1388..00000000 --- a/src/webui/ui/components/SettingSelect.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { SettingOption } from './SettingOption'; - -interface MouseEventExtend extends MouseEvent { - target: HTMLElement; -} - -// -const SelectTemplate = document.createElement('template'); -SelectTemplate.innerHTML = ` -
-
- - - - -
- -
`; - -window.customElements.define( - 'ob-setting-select', - class extends HTMLElement { - readonly _button: HTMLDivElement; - readonly _text: HTMLInputElement; - readonly _context: HTMLUListElement; - - constructor() { - super(); - - this.attachShadow({ mode: 'open' }); - this.shadowRoot?.append(SelectTemplate.content.cloneNode(true)); - - this._button = this.shadowRoot!.querySelector('div[part="button"]')!; - this._text = this.shadowRoot!.querySelector('input[part="current-text"]')!; - this._context = this.shadowRoot!.querySelector('ul[part="option-list"]')!; - - const buttonClick = () => { - const isHidden = this._context.classList.toggle('hidden'); - window[`${isHidden ? 'remove' : 'add'}EventListener`]('pointerdown', windowPointerDown); - }; - - const windowPointerDown = ({ target }: any) => { - if (!this.contains(target)) buttonClick(); - }; - - this._button.addEventListener('click', buttonClick); - this._context.addEventListener('click', (event) => { - const { target } = event as MouseEventExtend; - - if (target.tagName !== 'SETTING-OPTION') return; - buttonClick(); - - if (target.hasAttribute('is-selected')) return; - - this.querySelectorAll('setting-option[is-selected]').forEach((dom) => dom.toggleAttribute('is-selected')); - target.toggleAttribute('is-selected'); - - this._text.value = target.textContent as string; - this.dispatchEvent( - new CustomEvent('selected', { - bubbles: true, - composed: true, - detail: { - name: target.textContent, - value: target.dataset.value, - }, - }), - ); - }); - - this._text.value = this.querySelector('setting-option[is-selected]')?.textContent as string; - } - }, -); - -export const SettingSelect = (items: Array<{ text: string; value: string }>, configKey?: string, configValue?: any) => { - return ` - ${items - .map((e, i) => { - return SettingOption(e.text, e.value, configKey && configValue ? configValue === e.value : i === 0); - }) - .join('')} -`; -}; diff --git a/src/webui/ui/components/SettingSwitch.ts b/src/webui/ui/components/SettingSwitch.ts deleted file mode 100644 index 65573b7d..00000000 --- a/src/webui/ui/components/SettingSwitch.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const SettingSwitch = (configKey?: string, isActive: boolean = false, extraData?: Record) => { - return ` `data-${key}="${extraData[key]}"`) : ''} - > - `; -}; \ No newline at end of file diff --git a/src/webui/ui/components/WebUiApiOB11Config.ts b/src/webui/ui/components/WebUiApiOB11Config.ts deleted file mode 100644 index 2d510ecd..00000000 --- a/src/webui/ui/components/WebUiApiOB11Config.ts +++ /dev/null @@ -1,79 +0,0 @@ -export interface OB11Config { - [key: string]: any; - - http: { - enable: boolean; - host: ''; - port: number; - secret: ''; - enableHeart: boolean; - enablePost: boolean; - postUrls: string[]; - }; - ws: { - enable: boolean; - host: ''; - port: number; - }; - reverseWs: { - enable: boolean; - urls: string[]; - }; - GroupLocalTime: { - Record: boolean, - RecordList: Array - }; - debug: boolean; - heartInterval: number; - messagePostFormat: 'array' | 'string'; - enableLocalFile2Url: boolean; - musicSignUrl: ''; - reportSelfMessage: boolean; - token: ''; - -} - -class WebUiApiOB11ConfigWrapper { - private retCredential: string = ''; - - async Init(Credential: string) { - this.retCredential = Credential; - } - - async GetOB11Config(): Promise { - const ConfigResponse = await fetch('../api/OB11Config/GetConfig', { - method: 'POST', - headers: { - Authorization: 'Bearer ' + this.retCredential, - 'Content-Type': 'application/json', - }, - }); - if (ConfigResponse.status == 200) { - const ConfigResponseJson = await ConfigResponse.json(); - if (ConfigResponseJson.code == 0) { - return ConfigResponseJson?.data; - } - } - return {} as OB11Config; - } - - async SetOB11Config(config: OB11Config): Promise { - const ConfigResponse = await fetch('../api/OB11Config/SetConfig', { - method: 'POST', - headers: { - Authorization: 'Bearer ' + this.retCredential, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ config: JSON.stringify(config) }), - }); - if (ConfigResponse.status == 200) { - const ConfigResponseJson = await ConfigResponse.json(); - if (ConfigResponseJson.code == 0) { - return true; - } - } - return false; - } -} - -export const OB11ConfigWrapper = new WebUiApiOB11ConfigWrapper(); diff --git a/src/webui/vite.config.ts b/src/webui/vite.config.ts deleted file mode 100644 index 1cffeba7..00000000 --- a/src/webui/vite.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vite'; - -export default defineConfig({ - build: { - target: 'esnext', - minify: false, - lib: { - entry: 'ui/NapCat.ts', - formats: ['es'], - fileName: () => 'renderer.js', - }, - }, -}); diff --git a/static/QQLogin.html b/static/QQLogin.html deleted file mode 100644 index 0714ebf6..00000000 --- a/static/QQLogin.html +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - - NapCat - WebUi - - - - - - - - - - \ No newline at end of file diff --git a/static/assets/NapCat.css b/static/assets/NapCat.css deleted file mode 100644 index f82825c6..00000000 --- a/static/assets/NapCat.css +++ /dev/null @@ -1,180 +0,0 @@ -setting-item[is-hidden], -setting-item[is-hidden] + setting-divider { - display: none !important; -} - -.config-host-list { - width: 100%; - padding-left: 16px; - box-sizing: border-box; -} -.config-host-list[is-hidden], -.config-host-list[is-hidden] + setting-divider { - display: none !important; -} - -setting-item .q-input { - height: 24px; - width: 100px; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - box-sizing: border-box; - position: relative; - background: var(--bg_bottom_light); - border: 1px solid var(--border_dark); -} - -setting-item .q-input .q-input__inner { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - box-sizing: border-box; - color: var(--text_primary); - font-family: inherit; - font-size: 12px; - height: 24px; - line-height: 24px; - width: 100%; - border: 1px solid transparent; - padding: 0px 8px; -} - -setting-item .q-input input[type='number'].q-input__inner::-webkit-outer-spin-button, -setting-item .q-input input[type='number'].q-input__inner::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.config-host-list setting-item.setting-host-list-item .q-input { - width: 260px; -} - -setting-item a { - color: var(--text-link); -} -setting-item a:hover { - color: var(--hover-link); -} -setting-item a:active, -setting-item a:visited { - color: var(--text-link); -} - -ob-setting-select { - width: 100px; -} - -ob-setting-select, -ob-setting-select::part(parent), -ob-setting-select::part(button) { - display: block; - position: relative; - height: 24px; - font-size: 12px; - line-height: 24px; - box-sizing: border-box; -} - -ob-setting-select::part(button) { - display: flex; - padding: 0px 8px; - background-color: transparent; - border-radius: 4px; - border: 1px solid var(--border_dark); - z-index: 5; - cursor: default; - align-items: center; - flex-direction: row; - flex-wrap: nowrap; -} - -ob-setting-select::part(current-text) { - display: block; - margin-right: 8px; - padding: 0px; - background: none; - background-color: transparent; - font-size: 12px; - color: var(--text_primary); - text-overflow: ellipsis; - border-radius: 0px; - border: none; - outline: none; - overflow: hidden; - appearance: none; - box-sizing: border-box; - cursor: default; - flex: 1; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - -webkit-pointer-events: none; - -moz-pointer-events: none; - -ms-pointer-events: none; - -o-pointer-events: none; - pointer-events: none; -} - -ob-setting-select::part(button-arrow) { - position: relative; - display: block; - width: 16px; - height: 16px; - color: var(--icon_primary); -} - -ob-setting-select::part(option-list) { - display: flex; - position: absolute; - top: 100%; - padding: 4px; - margin: 5px 0px; - width: 100%; - max-height: var(--q-contextmenu-max-height); - background-color: var(--blur_middle_standard); - background-clip: padding-box; - backdrop-filter: blur(8px); - font-size: 12px; - box-shadow: var(--shadow_bg_middle_secondary); - border: 1px solid var(--border_secondary); - border-radius: 4px; - box-sizing: border-box; - app-region: no-drag; - overflow-x: hidden; - overflow-y: auto; - list-style: none; - z-index: 999; - flex-direction: column; - align-items: stretch; - flex-wrap: nowrap; - justify-content: flex-start; - gap: 4px; -} - -#napcat-error { - display: none; -} - -#napcat-error setting-panel { - background: rgba(255, 0, 0, 0.5); - color: white; -} - -#napcat-error setting-panel pre { - margin: 0; - padding: 16px; - box-sizing: border-box; -} - -#napcat-error setting-panel pre code { - font-family: 'FiraCode Nerd Font', 'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace; -} - -#napcat-error.show { - display: block; -} \ No newline at end of file diff --git a/static/assets/color.css b/static/assets/color.css deleted file mode 100644 index 0f77d861..00000000 --- a/static/assets/color.css +++ /dev/null @@ -1,679 +0,0 @@ -:root { - --font-bold: 400; - --font_size_1: 10px; - --font_size_2: 12px; - --font_size_3: 14px; - --font_size_4: 16px; - --font_size_5: 18px; - --avatar_size_1: 20px; - --avatar_size_2: 32px; - --avatar_size_3: 40px; - --font_size_main_1: 12px; - --font_size_main_2: 14px; - --line_height_1: 14px; - --line_height_2: 16px; - --line_height_3: 20px; - --line_height_4: 22px; - --line_height_5: 24px; - --line_height_main_1: 18px; - --line_height_main_2: 22px; - - --shadow_card_rest: 0px 2px 4px rgba(0, 0, 0, 0.12); - --shadow_tooltip: 0px 4px 8px rgba(0, 0, 0, 0.26); - --shadow_flyout: 0px 8px 16px rgba(0, 0, 0, 0.14); - --shadow_dialog: 0px 30px 60px rgba(0, 0, 0, 0.36) 0 2px 20px rgba(0, 0, 0, 0.37); - - --blend_brightness_white_004: 1.04; - --blend_brightness_white_008: 1.08; - --blend_brightness_white_010: 1.10; - --blend_brightness_white_016: 1.16; - --blend_brightness_white_020: 1.20; - --blend_brightness_black_004: 0.96; - --blend_brightness_black_008: 0.92; - --blend_brightness_black_010: 0.90; - --blend_brightness_black_016: 0.84; - --blend_brightness_black_020: 0.80; - --blend_white_004: rgba(255, 255, 255, 0.04); - --blend_white_008: rgba(255, 255, 255, 0.08); - --blend_white_010: rgba(255, 255, 255, 0.10); - --blend_white_016: rgba(255, 255, 255, 0.16); - --blend_white_020: rgba(255, 255, 255, 0.20); - --blend_black_004: rgba(0, 0, 0, 0.04); - --blend_black_008: rgba(0, 0, 0, 0.08); - --blend_black_010: rgba(0, 0, 0, 0.10); - --blend_black_016: rgba(0, 0, 0, 0.16); - --blend_black_020: rgba(0, 0, 0, 0.20); - --blend_transparent: rgba(0, 0, 0, 0); - - --el-color-white: #fff; - --el-color-black: #000; - --el-color-primary: #409eff; - --el-color-primary-rgb: 64,158,255; - --el-color-success-rgb: 103,194,58; - --el-color-warning-rgb: 230,162,60; - --el-color-danger-rgb: 245,108,108; - --el-color-error-rgb: 245,108,108; - --el-color-info-rgb: 144,147,153; - --el-color-primary-light-1: #53a8ff; - --el-color-primary-light-2: #66b1ff; - --el-color-primary-light-3: #79bbff; - --el-color-primary-light-4: #8cc5ff; - --el-color-primary-light-5: #a0cfff; - --el-color-primary-light-6: #b3d8ff; - --el-color-primary-light-7: #c6e2ff; - --el-color-primary-light-8: #d9ecff; - --el-color-primary-light-9: #ecf5ff; - --el-color-primary-dark-2: #337ecc; - --el-color-success: #67c23a; - --el-color-success-light-3: #95d475; - --el-color-success-light-5: #b3e19d; - --el-color-success-light-7: #d1edc4; - --el-color-success-light-8: #e1f3d8; - --el-color-success-light-9: #f0f9eb; - --el-color-success-dark-2: #529b2e; - --el-color-warning: #e6a23c; - --el-color-warning-light-3: #eebe77; - --el-color-warning-light-5: #f3d19e; - --el-color-warning-light-7: #f8e3c5; - --el-color-warning-light-8: #faecd8; - --el-color-warning-light-9: #fdf6ec; - --el-color-warning-dark-2: #b88230; - --el-color-danger: #f56c6c; - --el-color-danger-light-3: #f89898; - --el-color-danger-light-5: #fab6b6; - --el-color-danger-light-7: #fcd3d3; - --el-color-danger-light-8: #fde2e2; - --el-color-danger-light-9: #fef0f0; - --el-color-danger-dark-2: #c45656; - --el-color-error: #f56c6c; - --el-color-error-light-3: #f89898; - --el-color-error-light-5: #fab6b6; - --el-color-error-light-7: #fcd3d3; - --el-color-error-light-8: #fde2e2; - --el-color-error-light-9: #fef0f0; - --el-color-error-dark-2: #c45656; - --el-color-info: #909399; - --el-color-info-light-3: #b1b3b8; - --el-color-info-light-5: #c8c9cc; - --el-color-info-light-7: #dedfe0; - --el-color-info-light-8: #e9e9eb; - --el-color-info-light-9: #f4f4f5; - --el-color-info-dark-2: #73767a; - --el-bg-color: #fff; - --el-bg-color-page: #fff; - --el-bg-color-overlay: #fff; - --el-text-color-primary: #303133; - --el-text-color-regular: #606266; - --el-text-color-secondary: #909399; - --el-text-color-placeholder: #a8abb2; - --el-text-color-disabled: #c0c4cc; - --el-border-color: #dcdfe6; - --el-border-color-light: #e4e7ed; - --el-border-color-lighter: #ebeef5; - --el-border-color-extra-light: #f2f6fc; - --el-border-color-dark: #d4d7de; - --el-border-color-darker: #cdd0d6; - --el-fill-color: #f0f2f5; - --el-fill-color-light: #f5f7fa; - --el-fill-color-lighter: #fafafa; - --el-fill-color-extra-light: #fafcff; - --el-fill-color-dark: #ebedf0; - --el-fill-color-darker: #e6e8eb; - --el-fill-color-blank: #fff; - --el-border-radius-base: 4px; - --el-border-radius-small: 2px; - --el-border-radius-round: 20px; - --el-border-radius-circle: 100%; - --el-font-size-extra-large: 20px; - --el-font-size-large: 18px; - --el-font-size-medium: 16px; - --el-font-size-base: 14px; - --el-font-size-small: 13px; - --el-font-size-extra-small: 12px; - --el-font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; - --el-box-shadow: 0px 12px 32px 4px rgba(0,0,0,.04),0px 8px 20px rgba(0,0,0,.08); - --el-box-shadow-light: 0px 0px 12px rgba(0,0,0,.12); - --el-box-shadow-lighter: 0px 0px 6px rgba(0,0,0,.12); - --el-box-shadow-dark: 0px 16px 48px 16px rgba(0,0,0,.08),0px 12px 32px rgba(0,0,0,.12),0px 8px 16px -8px rgba(0,0,0,.16); - --el-disabled-bg-color: var(--el-fill-color-light); - --el-disabled-text-color: var(--el-text-color-placeholder); - --el-disabled-border-color: var(--el-border-color-light); - --el-index-normal: 1; - --el-index-top: 1000; - --el-index-popper: 2000; - --el-overlay-color: rgba(0,0,0,.8); - --el-overlay-color-light: rgba(0,0,0,.7); - --el-overlay-color-lighter: rgba(0,0,0,.5); - --el-mask-color: hsla(0,0%,100%,.9); - --el-mask-color-extra-light: hsla(0,0%,100%,.3); - --el-border-width: 1px; - --el-border-style: solid; - --el-border-color-hover: var(--el-text-color-disabled); - --el-border: var(--el-border-width) var(--el-border-style) var(--el-border-color); - --el-svg-monochrome-grey: var(--el-border-color); - --el-font-weight-primary: 500; - --el-font-line-height-primary: 24px; - --el-transition-duration: 0.3s; - --el-transition-duration-fast: 0.2s; - --el-transition-function-ease-in-out-bezier: cubic-bezier(0.645,0.045,0.355,1); - --el-transition-function-fast-bezier: cubic-bezier(0.23,1,0.32,1); - --el-transition-all: all var(--el-transition-duration) var(--el-transition-function-ease-in-out-bezier); - --el-transition-fade: opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier); - --el-transition-md-fade: transform var(--el-transition-duration) var(--el-transition-function-fast-bezier),opacity var(--el-transition-duration) var(--el-transition-function-fast-bezier); - --el-transition-fade-linear: opacity var(--el-transition-duration-fast) linear; - --el-transition-border: border-color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier); - --el-transition-box-shadow: box-shadow var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier); - --el-transition-color: color var(--el-transition-duration-fast) var(--el-transition-function-ease-in-out-bezier); - - --nt_brand_standard_2_overlay_hover_brand_2_mix: #008debff; - --nt_brand_standard_2_overlay_pressed_brand_2_mix: #0080d6ff; - --nt_feedback_error_2_overlay_hover_brand_2_mix: #e4462cff; - --nt_feedback_error_2_overlay_pressed_brand_2_mix: #cf4028ff; - --nt_icon_white_2_overlay_hover_2_mix: #f5f5f5ff; - --nt_icon_white_2_overlay_pressed_2_mix: #e0e0e0ff; - --nt_bg_top_light_2_overlay_hover_2_mix: #f5f5f5ff; - --nt_bg_top_light_2_overlay_pressed_2_mix: #e0e0e0ff; - --nt_icon_secondary_02_2_0_2_alpha: rgba(204, 204, 204, 0); - --nt_icon_secondary_02_2_70_2_alpha: rgba(204, 204, 204, 0.7); - --nt_text_link_2_50_2_alpha: rgba(45, 119, 229, 0.5); - --nt_bubble_host_2_overlay_pressed_brand_2_mix: #0080d6ff; - --nt_bg_white_2_overlay_pressed_brand_2_mix: #d6d6d6ff; - --nt_bg_white_2_overlay_hover_2_mix: #f5f5f5ff; - --nt_bg_white_2_overlay_pressed_2_mix: #e0e0e0ff; - --nt_fg_white_2_overlay_hover_2_mix: #f5f5f5ff; - --nt_fg_white_2_overlay_pressed_2_mix: #e0e0e0ff; - --nt_icon_red_2_overlay_hover_2_mix: #f5314fff; - --nt_icon_red_2_overlay_pressed_2_mix: #e02d48ff; - --nt_fg_grey_standard_2_overlay_hover_2_mix: #0000003b; - --nt_fg_grey_standard_2_overlay_pressed_2_mix: #0000004c; - --nt_bubble_guest_2_overlay_pressed_2_mix: #e0e0e0ff; - --nt_icon_primary_2_20_2_alpha: rgba(0, 0, 0, 0.2); - --nt_bg_grey_standard_2_95_2_alpha: rgba(242, 242, 242, 0.95); - --nt_tag_red_2_20_2_alpha: rgba(255, 134, 46, 0.2); - --nt_tag_red_2_25_2_alpha: rgba(255, 134, 46, 0.25); - --nt_tag_blue_2_20_2_alpha: rgba(0, 153, 255, 0.2); - --nt_tag_blue_2_25_2_alpha: rgba(0, 153, 255, 0.25); - --nt_tag_blue_2_10_2_alpha: rgba(0, 153, 255, 0.1); - --nt_brand_standard_2_20_2_alpha: rgba(0, 153, 255, 0.2); - --nt_feedback_error_2_20_2_alpha: rgba(247, 76, 48, 0.2); - --nt_text_white_2_60_2_alpha: rgba(255, 255, 255, 0.6); - --nt_bg_white_2_70_2_alpha: rgba(255, 255, 255, 0.7); - --nt_bg_white_2_90_2_alpha: rgba(255, 255, 255, 0.9); - --nt_bg_white_2_97_2_alpha: rgba(255, 255, 255, 0.97); - --nt_bg_white_2_40_2_alpha: rgba(255, 255, 255, 0.4); - --nt_bg_white_2_30_2_alpha: rgba(255, 255, 255, 0.3); - --nt_text_white_2_80_2_alpha: rgba(255, 255, 255, 0.8); - --nt_brand_standard_2_50_2_alpha: rgba(0, 153, 255, 0.5); - --nt_bg_nav_secondary_2_60_2_alpha: rgba(255, 255, 255, 0.6); - --nt_bg_nav_2_60_2_alpha: rgba(242, 242, 242, 0.6); - --nt_feedback_error_2_10_2_alpha: rgba(247, 76, 48, 0.1); - --nt_brand_standard_2_10_2_alpha: rgba(0, 153, 255, 0.1); - --nt_on_brand_primary_2_40_2_alpha: rgba(255, 255, 255, 0.4); - --nt_text_primary_2_72_2_alpha: rgba(0, 0, 0, 0.72); - --nt_text_white_2_72_2_alpha: rgba(255, 255, 255, 0.72); - - --border_secondary: 1px solid rgba(0, 0, 0, 0.0578); - --border_primary: 1px solid rgba(117, 117, 117, 0.4); - - --shadow_bg_top: 0px 4px 8px rgba(0, 0, 0, 0.14); - --shadow_bg_middle_secondary: 0px 8px 16px rgba(0, 0, 0, 0.14); - --shadow_bg_middle_primary: 0px 32px 64px rgba(0, 0, 0, 0.1876), 0px 2px 21px rgba(0, 0, 0, 0.1474); - --shadow_bg_bottom_inactive: 0px 16px 32px rgba(0, 0, 0, 0.1876), 0px 2px 10.67px rgba(0, 0, 0, 0.1474); - --shadow_bg_bottom_active: 0px 32px 64px rgba(0, 0, 0, 0.28), 0px 2px 21px rgba(0, 0, 0, 0.22); - - --brand_standard: #0099ffff; - --on_brand_primary: #ffffffff; - --on_brand_secondary: #ffffffff; - --text_primary: #000000ff; - --text_primary_light: #00000099; - --text_secondary_02: #ccccccff; - --text_white: #ffffffff; - --text_secondary_01: #999999ff; - --text_black: #000000ff; - --on_bg_text: #999999ff; - --text_link: #2d77e5ff; - --text_secondary: #00000080; - --text_tertiary: #0000004d; - --icon_primary: #000000ff; - --icon_secondary_01: #999999ff; - --icon_secondary_02: #ccccccff; - --icon_white: #ffffffff; - --icon_red: #ff3352ff; - --icon_black: #000000ff; - --icon_secondary: #00000066; - --icon_tertiary: #0000004d; - --feedback_success: #15d173ff; - --feedback_warning: #ffb300ff; - --feedback_error: #f74c30ff; - --bg_grey_standard: #f2f2f2ff; - --bg_white: #ffffffff; - --bg_list: #ffffffff; - --bg_aio_1: #f2f2f2ff; - --bg_aio_2: #f2f2f2ff; - --bg_aio_3: #f2f2f2ff; - --bg_aio_4: #f2f2f2ff; - --bg_nav: #f2f2f2ff; - --mac_bg_nav: #ffffff1a; - --bg_bottom_standard: #f2f2f2ff; - --bg_bottom_light: #ffffffff; - --bg_middle_standard: #f2f2f2ff; - --bg_middle_light: #ffffffff; - --bg_top_standard: #f2f2f2ff; - --bg_top_light: #ffffffff; - --bg_nav_secondary: #ffffffff; - --bubble_host: #0099ffff; - --bubble_guest: #ffffffff; - --bubble_host_text: #ffffff; - --bubble_guest_text: #000000ff; - --bubble_host_1: #0099ffff; - --bubble_host_2: #0099ffff; - --fg_grey_standard: #00000033; - --fg_white: #ffffffff; - --fg_grey_light: #0000000a; - --fill_standard_secondary: #0000000a; - --fill_standard_primary: #00000033; - --fill_light_primary: #ffffffff; - --fill_light_secondary: #ffffffff; - --divider_standard: #0000000a; - --divider_dark: #00000014; - --border_standard: #0000000a; - --border_dark: #00000014; - --overlay_hover: #0000000a; - --overlay_hover_brand: #00000014; - --overlay_pressed_brand: #00000029; - --overlay_active_brand: #0099ffff; - --overlay_top: #0000000f; - --overlay_mask_standard: #00000080; - --overlay_mask_dark: #00000099; - --overlay_pressed: #0000001f; - --overlay_active: #00000014; - --overlay_mask_aio: #00000000; - --blur_standard: #ffffffcc; - --blur_superlight: #ffffff1a; - --blur_middle_standard: #ffffffcc; - --blur_bottom_superlight: #ffffff1a; - --extend_blue: #eaf1ffff; - --svip_red: #ff4222ff; - --tag_sage_green_bg: #a3c4c633; - --tag_sage_green_text: #769698ff; - --tag_red_bg: #ff3f3233; - --tag_red_text: #f74c30ff; - --tag_orange_text: #ff8d40ff; - --tag_orange_bg: #ff862e33; - --tag_purple_text: #aa76f6ff; - --tag_purple_bg: #b27eff33; - --tag_blue_text: #0099ffff; - --tag_blue_bg: #0099ff33; - --tag_blue: #0099ff33; - --tag_red: #ff862e33; - --border_white: #ffffffff; - --border_secondary: #0000000f; - --border_primary: #75757566; - --mac_border_primary: #00000014; - --mac_border_secondary: #0000000a; - --host_bubble_bg_css_value: #0099ff; - --on_bubble_host_text: #ffffffff; - --brand_text: #0099ffff; - - --text-primary: #000; - --text-primary-light: #666; - --text-secondary-01: #999; - --text-secondary-02: #ccc; - --text-white: #fff; - --text-brand: #0099ff; - --text-link: #2d77e5; - --text-success: #12d173; - --text-warning: #ffb300; - --text-error: #ff5967; - --icon-primary: #000; - --icon-secondary-01: #999999; - --icon-secondary-02: #cccccc; - --icon-white: #fff; - --icon-brand: #0099ff; - --icon-success: #15d173; - --icon-warning: #ffb300; - --icon-error: #ff5967; - --button-primary-default: #0099ff; - --button-primary-hover: #4DB7FF; - --button-primary-pressed: #0089E5; - --button-primary-disable: #CCEBFF; - --button-secondary-default: #ccc; - --button-secondary-hover: #E5E5E5; - --button-secondary-pressed: #B2B2B2; - --button-secondary-disable: #F0F0F0; - --button-white-default: #FFFFFF; - --button-white-hover: #E5E5E5; - --button-white-pressed: #B2B2B2; - --button-white-disable: #ffffff4d; - --button-error-default: #FF5967; - --button-error-hover: #FF8B94; - --button-error-pressed: #E6505C; - --button-error-disable: #FFEEEF; - --bubble-host: #0099FF; - --bubble-guest: #EBEBEB; - --divider-standard: #E5E5E5; - --divider-light: #F5F5F5; - --divider-brand: #0099FF; - --background-01: #000; - --background-02: #E5E5E5; - --background-03: #F5F5F5; - --background-04: #FAFAFA; - --background-05: #FFFFFF; - --background-dialogue: #FFFFFF; - --hover-list: #F0F0F0; - --hover-icon: #EBEBEB; - --hover-link: #81ADEF; - --press-list: #E5E5E5; - --press-icon: #E5E5E5; - --press-link: #286BCE; - --badge-brand: #0099FF; - --badge-red: #FF5967; - --badge-grey: #CCCCCC; - --audio-hangup: #FF3350; - --gray-black: #000; - --gray-20: #333333; - --gray-40: #666666; - --gray-60: #999999; - --gray-80: #cccccc; - --gray-90: #e5e5e5; - --gray-96: #f5f5f5; - --gray-white: #ffffff; - --blue-dark: #0089E5; - --blue-standard: #0099FF; - --blue-light: #4DB7FF; - --blue-superlight: #E6F5FF; - --green-dark: #12BC67; - --green-standard: #15D173; - --green-light: #5BDE9D; - --green-superlight: #E8FAF1; - --yellow-dark: #E5A000; - --yellow-standard: #FFB300; - --yellow-light: #FFC94C; - --yellow-superlight: #FFF7E5; - --orange-dark: #E57E39; - --orange-standard: #FF8D40; - --orange-light: #FFAE78; - --orange-superlight: #FFF3EB; - --red-dark: #E6505C; - --red-standard: #FF5967; - --red-light: #FF8B94; - --red-superlight: #FFEEEF; - --pink-dark: #E55BA0; - --pink-standard: #FF66B3; - --pink-light: #FF93C9; - --pink-superlight: #FFEFF7; - --indigo-dark: #775CE6; - --indigo-standard: #8566FF; - --indigo-light: #A994FF; - --indigo-superlight: #F3F0FF; - --list-hover: rgba(243, 243, 243); - --list-pressed: rgba(226, 226, 226); - --background_01: #000000; - --background_02: #E6E6E6; - --background_03: #F5F5F5; - --background_04: #FAFAFA; - --background_05: #FFFFFF; - --background_dialogue: #FFFFFF; - --sidebar_win: #ebebeb; - --sidebar_mac: rgba(255,255,255,0.1); - - --nt_mix_tokens: nt_brand_standard_2_overlay_hover_brand_2_mix,nt_brand_standard_2_overlay_pressed_brand_2_mix,nt_feedback_error_2_overlay_hover_brand_2_mix,nt_feedback_error_2_overlay_pressed_brand_2_mix,nt_icon_white_2_overlay_hover_2_mix,nt_icon_white_2_overlay_pressed_2_mix,nt_bubble_host_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_hover_2_mix,nt_bg_white_2_overlay_pressed_2_mix,nt_fg_white_2_overlay_hover_2_mix,nt_fg_white_2_overlay_pressed_2_mix,nt_icon_red_2_overlay_hover_2_mix,nt_icon_red_2_overlay_pressed_2_mix,nt_fg_grey_standard_2_overlay_hover_2_mix,nt_fg_grey_standard_2_overlay_pressed_2_mix,nt_bubble_guest_2_overlay_pressed_2_mix,nt_icon_primary_2_20_2_alpha,nt_bg_grey_standard_2_95_2_alpha,nt_tag_red_2_20_2_alpha,nt_tag_red_2_25_2_alpha,nt_tag_blue_2_20_2_alpha,nt_tag_blue_2_25_2_alpha,nt_tag_blue_2_10_2_alpha,nt_tag_purple_2_20_2_alpha,nt_brand_standard_2_20_2_alpha,nt_tag_sage_green_2_20_2_alpha,nt_feedback_error_2_20_2_alpha,nt_text_white_2_60_2_alpha,nt_bg_white_2_70_2_alpha,nt_bg_white_2_90_2_alpha,nt_bg_white_2_97_2_alpha,nt_bg_white_2_40_2_alpha,nt_bg_white_2_30_2_alpha,nt_text_white_2_80_2_alpha,nt_brand_standard_2_50_2_alpha,nt_bg_nav_secondary_2_60_2_alpha,nt_bg_nav_2_60_2_alpha,nt_feedback_error_2_10_2_alpha,nt_brand_standard_2_10_2_alpha,nt_on_brand_primary_2_40_2_alpha,nt_text_primary_2_72_2_alpha,nt_text_white_2_72_2_alpha; - - color-scheme: light; - } - - .q-theme-tokens-dark { - --nt_brand_standard_2_overlay_hover_brand_2_mix: #1472d0ff; - --nt_brand_standard_2_overlay_pressed_brand_2_mix: #0056abff; - --nt_feedback_error_2_overlay_hover_brand_2_mix: #f85a40ff; - --nt_feedback_error_2_overlay_pressed_brand_2_mix: #cf4028ff; - --nt_icon_white_2_overlay_hover_2_mix: #ffffffff; - --nt_icon_white_2_overlay_pressed_2_mix: #d6d6d6ff; - --nt_bg_top_light_2_overlay_hover_2_mix: #404040ff; - --nt_bg_top_light_2_overlay_pressed_2_mix: #282828ff; - --nt_icon_secondary_02_2_0_2_alpha: rgba(77, 77, 77, 0); - --nt_icon_secondary_02_2_70_2_alpha: rgba(77, 77, 77, 0.7); - --nt_text_link_2_50_2_alpha: rgba(45, 119, 229, 0.5); - --nt_bubble_host_2_overlay_pressed_brand_2_mix: #202020ff; - --nt_bg_white_2_overlay_pressed_brand_2_mix: #202020ff; - --nt_bg_white_2_overlay_hover_2_mix: #373737ff; - --nt_bg_white_2_overlay_pressed_2_mix: #202020ff; - --nt_fg_white_2_overlay_hover_2_mix: #373737ff; - --nt_fg_white_2_overlay_pressed_2_mix: #202020ff; - --nt_icon_red_2_overlay_hover_2_mix: #ff4360ff; - --nt_icon_red_2_overlay_pressed_2_mix: #d62b45ff; - --nt_fg_grey_standard_2_overlay_hover_2_mix: #ffffff43; - --nt_fg_grey_standard_2_overlay_pressed_2_mix: #82828254; - --nt_bubble_guest_2_overlay_pressed_2_mix: #202020ff; - --nt_icon_primary_2_20_2_alpha: rgba(255, 255, 255, 0.2); - --nt_bg_grey_standard_2_95_2_alpha: rgba(26, 26, 26, 0.95); - --nt_tag_red_2_20_2_alpha: rgba(255, 134, 46, 0.2); - --nt_tag_red_2_25_2_alpha: rgba(255, 134, 46, 0.25); - --nt_tag_blue_2_20_2_alpha: rgba(0, 153, 255, 0.2); - --nt_tag_blue_2_25_2_alpha: rgba(0, 153, 255, 0.25); - --nt_tag_blue_2_10_2_alpha: rgba(0, 153, 255, 0.1); - --nt_brand_standard_2_20_2_alpha: rgba(0, 102, 204, 0.2); - --nt_feedback_error_2_20_2_alpha: rgba(247, 76, 48, 0.2); - --nt_text_white_2_60_2_alpha: rgba(255, 255, 255, 0.6); - --nt_bg_white_2_70_2_alpha: rgba(38, 38, 38, 0.7); - --nt_bg_white_2_90_2_alpha: rgba(38, 38, 38, 0.9); - --nt_bg_white_2_97_2_alpha: rgba(38, 38, 38, 0.97); - --nt_bg_white_2_40_2_alpha: rgba(38, 38, 38, 0.4); - --nt_bg_white_2_30_2_alpha: rgba(38, 38, 38, 0.3); - --nt_text_white_2_80_2_alpha: rgba(255, 255, 255, 0.8); - --nt_brand_standard_2_50_2_alpha: rgba(0, 102, 204, 0.5); - --nt_bg_nav_secondary_2_60_2_alpha: rgba(27, 27, 27, 0.6); - --nt_bg_nav_2_60_2_alpha: rgba(17, 17, 17, 0.6); - --nt_feedback_error_2_10_2_alpha: rgba(247, 76, 48, 0.1); - --nt_brand_standard_2_10_2_alpha: rgba(0, 102, 204, 0.1); - --nt_on_brand_primary_2_40_2_alpha: rgba(255, 255, 255, 0.4); - --nt_text_primary_2_72_2_alpha: rgba(255, 255, 255, 0.72); - --nt_text_white_2_72_2_alpha: rgba(255, 255, 255, 0.72); - - --border_secondary: 1px solid rgba(0, 0, 0, 0.2); - --border_primary: 1px solid rgba(117, 117, 117, 0.4); - - --shadow_bg_top: 0px 4px 8px rgba(0, 0, 0, 0.26); - --shadow_bg_middle_secondary: 0px 8px 16px rgba(0, 0, 0, 0.14); - --shadow_bg_middle_primary: 0px 32px 64px rgba(0, 0, 0, 0.37), 0px 2px 21px rgba(0, 0, 0, 0.37); - --shadow_bg_bottom_inactive: 0px 32px 64px rgba(0, 0, 0, 0.56), 0px 2px 21px rgba(0, 0, 0, 0.55); - --shadow_bg_bottom_active: 0px 32px 64px rgba(0, 0, 0, 0.56), 0px 2px 21px rgba(0, 0, 0, 0.55); - - --brand_standard: #0066ccff; - --on_brand_primary: #ffffffff; - --on_brand_secondary: #ffffffff; - --text_primary: #ffffffe6; - --text_primary_light: #ffffff99; - --text_secondary_02: #666666ff; - --text_white: #ffffffe6; - --text_secondary_01: #808080ff; - --text_black: #000000ff; - --on_bg_text: #808080ff; - --text_link: #2d77e5ff; - --text_secondary: #ffffff99; - --text_tertiary: #ffffff66; - --icon_primary: #ffffffb3; - --icon_secondary_01: #666666ff; - --icon_secondary_02: #4d4d4dff; - --icon_white: #ffffffff; - --icon_red: #ff3352ff; - --icon_black: #000000ff; - --icon_secondary: #ffffff80; - --icon_tertiary: #ffffff66; - --feedback_success: #15d173ff; - --feedback_warning: #ffb300ff; - --feedback_error: #f74c30ff; - --bg_grey_standard: #1a1a1aff; - --bg_white: #262626ff; - --bg_list: #1b1b1bff; - --bg_aio_1: #1a1a1aff; - --bg_aio_2: #1a1a1aff; - --bg_aio_3: #1a1a1aff; - --bg_aio_4: #1a1a1aff; - --bg_nav: #111111ff; - --mac_bg_nav: #0000001a; - --bg_bottom_standard: #111111ff; - --bg_bottom_light: #1b1b1bff; - --bg_middle_standard: #1b1b1bff; - --bg_middle_light: #262626ff; - --bg_top_standard: #262626ff; - --bg_top_light: #303030ff; - --bg_nav_secondary: #1b1b1bff; - --bubble_host: #262626ff; - --bubble_guest: #262626ff; - --bubble_host_text: #f2f2f2; - --bubble_guest_text: #f2f2f2ff; - --bubble_host_1: #262626ff; - --bubble_host_2: #262626ff; - --fg_grey_standard: #ffffff33; - --fg_white: #262626ff; - --fg_grey_light: #00000033; - --fill_standard_secondary: #ffffff0f; - --fill_standard_primary: #ffffff33; - --fill_light_primary: #262626ff; - --fill_light_secondary: #ffffff0f; - --divider_standard: #ffffff0a; - --divider_dark: #ffffff14; - --border_standard: #ffffff0f; - --border_dark: #ffffff14; - --overlay_hover: #ffffff14; - --overlay_hover_brand: #ffffff14; - --overlay_pressed_brand: #00000029; - --overlay_active_brand: #0066ccff; - --overlay_top: #ffffff0f; - --overlay_mask_standard: #00000080; - --overlay_mask_dark: #00000099; - --overlay_pressed: #00000029; - --overlay_active: #ffffff1f; - --overlay_mask_aio: #00000000; - --blur_standard: #000000cc; - --blur_superlight: #0000001a; - --blur_middle_standard: #262626cc; - --blur_bottom_superlight: #0000001a; - --extend_blue: #002f65ff; - --svip_red: #ff4222ff; - --tag_sage_green_bg: #a3c4c633; - --tag_sage_green_text: #769698ff; - --tag_red_bg: #ff3f3233; - --tag_red_text: #f74c30ff; - --tag_orange_text: #ff8d40ff; - --tag_orange_bg: #ff862e33; - --tag_purple_text: #aa76f6ff; - --tag_purple_bg: #b27eff33; - --tag_blue_text: #0066ccff; - --tag_blue_bg: #0099ff33; - --tag_blue: #0099ff40; - --tag_red: #ff862e33; - --border_white: #262626ff; - --border_secondary: #00000033; - --border_primary: #75757566; - --mac_border_primary: #ffffff14; - --mac_border_secondary: #ffffff0a; - --host_bubble_bg_css_value: #262626; - --on_bubble_host_text: #f2f2f2ff; - --brand_text: #0066ccff; - - --text-primary: #FFFFFF; - --text-primary-light: #CCCCCC; - --text-secondary-01: #999; - --text-secondary-02: #666666; - --text-white: #fff; - --text-brand: #0099ff; - --text-link: #2d77e5; - --text-success: #12d173; - --text-warning: #ffb300; - --text-error: #ff5967; - --icon-primary: #999999; - --icon-secondary-01: #999999; - --icon-secondary-02: #999999; - --icon-white: #fff; - --icon-brand: #0099ff; - --icon-success: #15d173; - --icon-warning: #ffb300; - --icon-error: #ff5967; - --button-primary-default: #0066CC; - --button-primary-hover: #19467F; - --button-primary-pressed: #0A1F33; - --button-primary-disable: #232323; - --button-secondary-default: #232323; - --button-secondary-hover: #1F1F1F; - --button-secondary-pressed: #1A1A1A; - --button-secondary-disable: #1A1A1A; - --button-white-default: #FFFFFF; - --button-white-hover: #FAFAFA; - --button-white-pressed: #F5F5F5; - --button-white-disable: #FFFFFF; - --button-error-default: #FF5967; - --button-error-hover: #FF8B94; - --button-error-pressed: #E6505C; - --button-error-disable: #FFEEEF; - --bubble-host: #262626; - --bubble-guest: #262626; - --divider-standard: #242424; - --divider-light: #4D4D4D; - --divider-brand: #0099FF; - --background-01: #FFFFFF; - --background-02: #242424; - --background-03: #181818; - --background-04: #1F1F1F; - --background-05: #000000; - --background-dialogue: #262626; - --hover-list: #292929; - --hover-icon: #333333; - --hover-link: #81ADEF; - --press-list: #383838; - --press-icon: #262626; - --press-link: #286BCE; - --badge-brand: #0099FF; - --badge-red: #FF5967; - --badge-grey: #4D4D4D; - --audio-hangup: #FF3350; - --gray-black: #000; - --gray-20: #333333; - --gray-40: #666666; - --gray-60: #999999; - --gray-80: #cccccc; - --gray-90: #e5e5e5; - --gray-96: #f5f5f5; - --gray-white: #ffffff; - --blue-dark: #0057BD; - --blue-standard: #0066CC; - --blue-light: #0072E4; - --blue-superlight: #E6F5FF; - --green-dark: #12BC67; - --green-standard: #15D173; - --green-light: #5BDE9D; - --green-superlight: #E8FAF1; - --yellow-dark: #E5A000; - --yellow-standard: #FFB300; - --yellow-light: #FFC94C; - --yellow-superlight: #FFF7E5; - --orange-dark: #E57E39; - --orange-standard: #FF8D40; - --orange-light: #FFAE78; - --orange-superlight: #FFF3EB; - --red-dark: #E6505C; - --red-standard: #FF5967; - --red-light: #FF8B94; - --red-superlight: #FFEEEF; - --pink-dark: #E55BA0; - --pink-standard: #FF66B3; - --pink-light: #FF93C9; - --pink-superlight: #FFEFF7; - --indigo-dark: #775CE6; - --indigo-standard: #8566FF; - --indigo-light: #A994FF; - --indigo-superlight: #F3F0FF; - --list-hover: rgba(71, 71, 71); - --list-pressed: rgba(28, 28, 28); - --background_01: #ffffff; - --background_02: #292929; - --background_03: #1A1A1A; - --background_04: #212121; - --background_05: #212121; - --background_dialogue: #292929; - --sidebar_win: rgba(0,0,0,0.8); - --sidebar_mac: rgba(0,0,0,0.1); - - color-scheme: dark; - } \ No newline at end of file diff --git a/static/assets/qrcode.min.js b/static/assets/qrcode.min.js deleted file mode 100644 index 974e0628..00000000 --- a/static/assets/qrcode.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Skipped minification because the original files appears to be already minified. - * Original file: /npm/qrcode@1.5.1/build/qrcode.js - * - * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files - */ -var QRCode=function(t){"use strict";var r,e=function(){return"function"==typeof Promise&&Promise.prototype&&Promise.prototype.then},n=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706],o=function(t){if(!t)throw new Error('"version" cannot be null or undefined');if(t<1||t>40)throw new Error('"version" should be in range from 1 to 40');return 4*t+17},a=function(t){return n[t]},i=function(t){for(var r=0;0!==t;)r++,t>>>=1;return r},u=function(t){if("function"!=typeof t)throw new Error('"toSJISFunc" is not a valid function.');r=t},s=function(){return void 0!==r},f=function(t){return r(t)};function h(t,r){return t(r={exports:{}},r.exports),r.exports}var c=h((function(t,r){r.L={bit:1},r.M={bit:0},r.Q={bit:3},r.H={bit:2},r.isValid=function(t){return t&&void 0!==t.bit&&t.bit>=0&&t.bit<4},r.from=function(t,e){if(r.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"l":case"low":return r.L;case"m":case"medium":return r.M;case"q":case"quartile":return r.Q;case"h":case"high":return r.H;default:throw new Error("Unknown EC Level: "+t)}}(t)}catch(t){return e}}}));function g(){this.buffer=[],this.length=0}c.L,c.M,c.Q,c.H,c.isValid,g.prototype={get:function(t){var r=Math.floor(t/8);return 1==(this.buffer[r]>>>7-t%8&1)},put:function(t,r){for(var e=0;e>>r-e-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var r=Math.floor(this.length/8);this.buffer.length<=r&&this.buffer.push(0),t&&(this.buffer[r]|=128>>>this.length%8),this.length++}};var d=g;function l(t){if(!t||t<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=t,this.data=new Uint8Array(t*t),this.reservedBit=new Uint8Array(t*t)}l.prototype.set=function(t,r,e,n){var o=t*this.size+r;this.data[o]=e,n&&(this.reservedBit[o]=!0)},l.prototype.get=function(t,r){return this.data[t*this.size+r]},l.prototype.xor=function(t,r,e){this.data[t*this.size+r]^=e},l.prototype.isReserved=function(t,r){return this.reservedBit[t*this.size+r]};var v=l,p=h((function(t,r){var e=o;r.getRowColCoords=function(t){if(1===t)return[];for(var r=Math.floor(t/7)+2,n=e(t),o=145===n?26:2*Math.ceil((n-13)/(2*r-2)),a=[n-7],i=1;i=0&&t<=7},r.from=function(t){return r.isValid(t)?parseInt(t,10):void 0},r.getPenaltyN1=function(t){for(var r=t.size,n=0,o=0,a=0,i=null,u=null,s=0;s=5&&(n+=e+(o-5)),i=h,o=1),(h=t.get(f,s))===u?a++:(a>=5&&(n+=e+(a-5)),u=h,a=1)}o>=5&&(n+=e+(o-5)),a>=5&&(n+=e+(a-5))}return n},r.getPenaltyN2=function(t){for(var r=t.size,e=0,o=0;o=10&&(1488===n||93===n)&&e++,a=a<<1&2047|t.get(u,i),u>=10&&(1488===a||93===a)&&e++}return e*o},r.getPenaltyN4=function(t){for(var r=0,e=t.data.length,n=0;n=0;){for(var n=e[0],o=0;o0){var o=new Uint8Array(this.degree);return o.set(e,n),o}return e};var L=T,b=function(t){return!isNaN(t)&&t>=1&&t<=40},U="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+",x="(?:(?![A-Z0-9 $%*+\\-./:]|"+(U=U.replace(/u/g,"\\u"))+")(?:.|[\r\n]))+",k=new RegExp(U,"g"),F=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g"),S=new RegExp(x,"g"),D=new RegExp("[0-9]+","g"),Y=new RegExp("[A-Z $%*+\\-./:]+","g"),_=new RegExp("^"+U+"$"),z=new RegExp("^[0-9]+$"),H=new RegExp("^[A-Z0-9 $%*+\\-./:]+$"),J={KANJI:k,BYTE_KANJI:F,BYTE:S,NUMERIC:D,ALPHANUMERIC:Y,testKanji:function(t){return _.test(t)},testNumeric:function(t){return z.test(t)},testAlphanumeric:function(t){return H.test(t)}},K=h((function(t,r){r.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]},r.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]},r.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]},r.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]},r.MIXED={bit:-1},r.getCharCountIndicator=function(t,r){if(!t.ccBits)throw new Error("Invalid mode: "+t);if(!b(r))throw new Error("Invalid version: "+r);return r>=1&&r<10?t.ccBits[0]:r<27?t.ccBits[1]:t.ccBits[2]},r.getBestModeForData=function(t){return J.testNumeric(t)?r.NUMERIC:J.testAlphanumeric(t)?r.ALPHANUMERIC:J.testKanji(t)?r.KANJI:r.BYTE},r.toString=function(t){if(t&&t.id)return t.id;throw new Error("Invalid mode")},r.isValid=function(t){return t&&t.bit&&t.ccBits},r.from=function(t,e){if(r.isValid(t))return t;try{return function(t){if("string"!=typeof t)throw new Error("Param is not a string");switch(t.toLowerCase()){case"numeric":return r.NUMERIC;case"alphanumeric":return r.ALPHANUMERIC;case"kanji":return r.KANJI;case"byte":return r.BYTE;default:throw new Error("Unknown mode: "+t)}}(t)}catch(t){return e}}}));K.NUMERIC,K.ALPHANUMERIC,K.BYTE,K.KANJI,K.MIXED,K.getCharCountIndicator,K.getBestModeForData,K.isValid;var O=h((function(t,r){var e=i(7973);function n(t,r){return K.getCharCountIndicator(t,r)+4}function o(t,r){var e=0;return t.forEach((function(t){var o=n(t.mode,r);e+=o+t.getBitsLength()})),e}r.from=function(t,r){return b(t)?parseInt(t,10):r},r.getCapacity=function(t,r,e){if(!b(t))throw new Error("Invalid QR Code version");void 0===e&&(e=K.BYTE);var o=8*(a(t)-M(t,r));if(e===K.MIXED)return o;var i=o-n(e,t);switch(e){case K.NUMERIC:return Math.floor(i/10*3);case K.ALPHANUMERIC:return Math.floor(i/11*2);case K.KANJI:return Math.floor(i/13);case K.BYTE:default:return Math.floor(i/8)}},r.getBestVersionForData=function(t,e){var n,a=c.from(e,c.M);if(Array.isArray(t)){if(t.length>1)return function(t,e){for(var n=1;n<=40;n++){if(o(t,n)<=r.getCapacity(n,e,K.MIXED))return n}}(t,a);if(0===t.length)return 1;n=t[0]}else n=t;return function(t,e,n){for(var o=1;o<=40;o++)if(e<=r.getCapacity(o,n,t))return o}(n.mode,n.getLength(),a)},r.getEncodedBits=function(t){if(!b(t)||t<7)throw new Error("Invalid QR Code version");for(var r=t<<12;i(r)-e>=0;)r^=7973<=0;)n^=1335<0&&(e=this.data.substr(r),n=parseInt(e,10),t.put(n,3*o+1))};var j=q,$=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function X(t){this.mode=K.ALPHANUMERIC,this.data=t}X.getBitsLength=function(t){return 11*Math.floor(t/2)+t%2*6},X.prototype.getLength=function(){return this.data.length},X.prototype.getBitsLength=function(){return X.getBitsLength(this.data.length)},X.prototype.write=function(t){var r;for(r=0;r+2<=this.data.length;r+=2){var e=45*$.indexOf(this.data[r]);e+=$.indexOf(this.data[r+1]),t.put(e,11)}this.data.length%2&&t.put($.indexOf(this.data[r]),6)};var Z=X;function W(t){this.mode=K.BYTE,"string"==typeof t&&(t=function(t){for(var r=[],e=t.length,n=0;n=55296&&o<=56319&&e>n+1){var a=t.charCodeAt(n+1);a>=56320&&a<=57343&&(o=1024*(o-55296)+a-56320+65536,n+=1)}o<128?r.push(o):o<2048?(r.push(o>>6|192),r.push(63&o|128)):o<55296||o>=57344&&o<65536?(r.push(o>>12|224),r.push(o>>6&63|128),r.push(63&o|128)):o>=65536&&o<=1114111?(r.push(o>>18|240),r.push(o>>12&63|128),r.push(o>>6&63|128),r.push(63&o|128)):r.push(239,191,189)}return new Uint8Array(r).buffer}(t)),this.data=new Uint8Array(t)}W.getBitsLength=function(t){return 8*t},W.prototype.getLength=function(){return this.data.length},W.prototype.getBitsLength=function(){return W.getBitsLength(this.data.length)},W.prototype.write=function(t){for(var r=0,e=this.data.length;r=33088&&e<=40956)e-=33088;else{if(!(e>=57408&&e<=60351))throw new Error("Invalid SJIS character: "+this.data[r]+"\nMake sure your charset is UTF-8");e-=49472}e=192*(e>>>8&255)+(255&e),t.put(e,13)}};var rt=tt,et=h((function(t){var r={single_source_shortest_paths:function(t,e,n){var o={},a={};a[e]=0;var i,u,s,f,h,c,g,d=r.PriorityQueue.make();for(d.push(e,0);!d.empty();)for(s in u=(i=d.pop()).value,f=i.cost,h=t[u]||{})h.hasOwnProperty(s)&&(c=f+h[s],g=a[s],(void 0===a[s]||g>c)&&(a[s]=c,d.push(s,c),o[s]=u));if(void 0!==n&&void 0===a[n]){var l=["Could not find a path from ",e," to ",n,"."].join("");throw new Error(l)}return o},extract_shortest_path_from_predecessor_list:function(t,r){for(var e=[],n=r;n;)e.push(n),n=t[n];return e.reverse(),e},find_path:function(t,e,n){var o=r.single_source_shortest_paths(t,e,n);return r.extract_shortest_path_from_predecessor_list(o,n)},PriorityQueue:{make:function(t){var e,n=r.PriorityQueue,o={};for(e in t=t||{},n)n.hasOwnProperty(e)&&(o[e]=n[e]);return o.queue=[],o.sorter=t.sorter||n.default_sorter,o},default_sorter:function(t,r){return t.cost-r.cost},push:function(t,r){var e={value:t,cost:r};this.queue.push(e),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return 0===this.queue.length}}};t.exports=r})),nt=h((function(t,r){function e(t){return unescape(encodeURIComponent(t)).length}function n(t,r,e){for(var n,o=[];null!==(n=t.exec(e));)o.push({data:n[0],index:n.index,mode:r,length:n[0].length});return o}function o(t){var r,e,o=n(J.NUMERIC,K.NUMERIC,t),a=n(J.ALPHANUMERIC,K.ALPHANUMERIC,t);return s()?(r=n(J.BYTE,K.BYTE,t),e=n(J.KANJI,K.KANJI,t)):(r=n(J.BYTE_KANJI,K.BYTE,t),e=[]),o.concat(a,r,e).sort((function(t,r){return t.index-r.index})).map((function(t){return{data:t.data,mode:t.mode,length:t.length}}))}function a(t,r){switch(r){case K.NUMERIC:return j.getBitsLength(t);case K.ALPHANUMERIC:return Z.getBitsLength(t);case K.KANJI:return rt.getBitsLength(t);case K.BYTE:return G.getBitsLength(t)}}function i(t,r){var e,n=K.getBestModeForData(t);if((e=K.from(r,n))!==K.BYTE&&e.bit=0?t[t.length-1]:null;return e&&e.mode===r.mode?(t[t.length-1].data+=r.data,t):(t.push(r),t)}),[])}(s))},r.rawSplit=function(t){return r.fromArray(o(t))}}));function ot(t,r,e){var n,o,a=t.size,i=V(r,e);for(n=0;n<15;n++)o=1==(i>>n&1),n<6?t.set(n,8,o,!0):n<8?t.set(n+1,8,o,!0):t.set(a-15+n,8,o,!0),n<8?t.set(8,a-n-1,o,!0):n<9?t.set(8,15-n-1+1,o,!0):t.set(8,15-n-1,o,!0);t.set(a-8,8,1,!0)}function at(t,r,e){var n=new d;e.forEach((function(r){n.put(r.mode.bit,4),n.put(r.getLength(),K.getCharCountIndicator(r.mode,t)),r.write(n)}));var o=8*(a(t)-M(t,r));for(n.getLengthInBits()+4<=o&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(0);for(var i=(o-n.getLengthInBits())/8,u=0;u=0&&u<=6&&(0===s||6===s)||s>=0&&s<=6&&(0===u||6===u)||u>=2&&u<=4&&s>=2&&s<=4?t.set(a+u,i+s,!0,!0):t.set(a+u,i+s,!1,!0))}(c,r),function(t){for(var r=t.size,e=8;e=7&&function(t,r){for(var e,n,o,a=t.size,i=O.getEncodedBits(r),u=0;u<18;u++)e=Math.floor(u/3),n=u%3+a-8-3,o=1==(i>>u&1),t.set(e,n,o,!0),t.set(n,e,o,!0)}(c,r),function(t,r){for(var e=t.size,n=-1,o=e-1,a=7,i=0,u=e-1;u>0;u-=2)for(6===u&&u--;;){for(var s=0;s<2;s++)if(!t.isReserved(o,u-s)){var f=!1;i>>a&1)),t.set(o,u-s,f),-1===--a&&(i++,a=7)}if((o+=n)<0||e<=o){o-=n,n=-n;break}}}(c,f),isNaN(n)&&(n=E.getBestMask(c,ot.bind(null,c,e))),E.applyMask(n,c),ot(c,e,n),{modules:c,version:r,errorCorrectionLevel:e,maskPattern:n,segments:a}}nt.fromArray,nt.fromString,nt.rawSplit;var ut=function(t,r){if(void 0===t||""===t)throw new Error("No input text");var e,n,o=c.M;return void 0!==r&&(o=c.from(r.errorCorrectionLevel,c.M),e=O.from(r.version),n=E.from(r.maskPattern),r.toSJISFunc&&u(r.toSJISFunc)),it(t,e,o,n)},st=h((function(t,r){function e(t){if("number"==typeof t&&(t=t.toString()),"string"!=typeof t)throw new Error("Color should be defined as hex string");var r=t.slice().replace("#","").split("");if(r.length<3||5===r.length||r.length>8)throw new Error("Invalid hex color: "+t);3!==r.length&&4!==r.length||(r=Array.prototype.concat.apply([],r.map((function(t){return[t,t]})))),6===r.length&&r.push("F","F");var e=parseInt(r.join(""),16);return{r:e>>24&255,g:e>>16&255,b:e>>8&255,a:255&e,hex:"#"+r.slice(0,6).join("")}}r.getOptions=function(t){t||(t={}),t.color||(t.color={});var r=void 0===t.margin||null===t.margin||t.margin<0?4:t.margin,n=t.width&&t.width>=21?t.width:void 0,o=t.scale||4;return{width:n,scale:n?4:o,margin:r,color:{dark:e(t.color.dark||"#000000ff"),light:e(t.color.light||"#ffffffff")},type:t.type,rendererOpts:t.rendererOpts||{}}},r.getScale=function(t,r){return r.width&&r.width>=t+2*r.margin?r.width/(t+2*r.margin):r.scale},r.getImageWidth=function(t,e){var n=r.getScale(t,e);return Math.floor((t+2*e.margin)*n)},r.qrToImageData=function(t,e,n){for(var o=e.modules.size,a=e.modules.data,i=r.getScale(o,n),u=Math.floor((o+2*n.margin)*i),s=n.margin*i,f=[n.color.light,n.color.dark],h=0;h=s&&c>=s&&h':"",s="0&&s>0&&t[u-1]||(n+=a?ct("M",s+e,.5+f+e):ct("m",o,0),o=0,a=!1),s+1',f='viewBox="0 0 '+i+" "+i+'"',h=''+u+s+"\n";return"function"==typeof e&&e(null,h),h};function dt(t,r,n,o,a){var i=[].slice.call(arguments,1),u=i.length,s="function"==typeof i[u-1];if(!s&&!e())throw new Error("Callback required as last argument");if(!s){if(u<1)throw new Error("Too few arguments provided");return 1===u?(n=r,r=o=void 0):2!==u||r.getContext||(o=n,n=r,r=void 0),new Promise((function(e,a){try{var i=ut(n,o);e(t(i,r,o))}catch(t){a(t)}}))}if(u<2)throw new Error("Too few arguments provided");2===u?(a=n,n=r,r=o=void 0):3===u&&(r.getContext&&void 0===a?(a=o,o=void 0):(a=o,o=n,n=r,r=void 0));try{var f=ut(n,o);a(null,t(f,r,o))}catch(t){a(t)}}var lt=ut,vt=dt.bind(null,ft.render),pt=dt.bind(null,ft.renderToDataURL),wt=dt.bind(null,(function(t,r,e){return gt(t,e)})),mt={create:lt,toCanvas:vt,toDataURL:pt,toString:wt};return t.create=lt,t.default=mt,t.toCanvas=vt,t.toDataURL=pt,t.toString=wt,Object.defineProperty(t,"__esModule",{value:!0}),t}({}); diff --git a/static/assets/renderer.js b/static/assets/renderer.js deleted file mode 100644 index e8d5cd83..00000000 --- a/static/assets/renderer.js +++ /dev/null @@ -1,469 +0,0 @@ -const SettingList = (items, title, isCollapsible = false, direction = "column") => { - return ` - - - ${items.join("")} - - - `; -}; - -const SettingItem = (title, subtitle, action, id, visible = true) => { - return ` -
- ${title} - ${subtitle ? `${subtitle}` : ""} -
- ${action ? `
${action}
` : ""} -
`; -}; - -const SettingButton = (text, id, type = "secondary") => { - return `${text}`; -}; - -const SettingSwitch = (configKey, isActive = false, extraData) => { - return ` `data-${key}="${extraData[key]}"`) : ""} - > - `; -}; - -const SettingOption = (text, value, isSelected = false) => { - return `${text}`; -}; - -const SelectTemplate = document.createElement("template"); -SelectTemplate.innerHTML = ` -
-
- - - - -
- -
`; -window.customElements.define( - "ob-setting-select", - class extends HTMLElement { - _button; - _text; - _context; - constructor() { - super(); - this.attachShadow({ mode: "open" }); - this.shadowRoot?.append(SelectTemplate.content.cloneNode(true)); - this._button = this.shadowRoot.querySelector('div[part="button"]'); - this._text = this.shadowRoot.querySelector('input[part="current-text"]'); - this._context = this.shadowRoot.querySelector('ul[part="option-list"]'); - const buttonClick = () => { - const isHidden = this._context.classList.toggle("hidden"); - window[`${isHidden ? "remove" : "add"}EventListener`]("pointerdown", windowPointerDown); - }; - const windowPointerDown = ({ target }) => { - if (!this.contains(target)) buttonClick(); - }; - this._button.addEventListener("click", buttonClick); - this._context.addEventListener("click", (event) => { - const { target } = event; - if (target.tagName !== "SETTING-OPTION") return; - buttonClick(); - if (target.hasAttribute("is-selected")) return; - this.querySelectorAll("setting-option[is-selected]").forEach((dom) => dom.toggleAttribute("is-selected")); - target.toggleAttribute("is-selected"); - this._text.value = target.textContent; - this.dispatchEvent( - new CustomEvent("selected", { - bubbles: true, - composed: true, - detail: { - name: target.textContent, - value: target.dataset.value - } - }) - ); - }); - this._text.value = this.querySelector("setting-option[is-selected]")?.textContent; - } - } -); -const SettingSelect = (items, configKey, configValue) => { - return ` - ${items.map((e, i) => { - return SettingOption(e.text, e.value, configValue ? configValue === e.value : i === 0); - }).join("")} -`; -}; - -class WebUiApiOB11ConfigWrapper { - retCredential = ""; - async Init(Credential) { - this.retCredential = Credential; - } - async GetOB11Config() { - const ConfigResponse = await fetch("../api/OB11Config/GetConfig", { - method: "POST", - headers: { - Authorization: "Bearer " + this.retCredential, - "Content-Type": "application/json" - } - }); - if (ConfigResponse.status == 200) { - const ConfigResponseJson = await ConfigResponse.json(); - if (ConfigResponseJson.code == 0) { - return ConfigResponseJson?.data; - } - } - return {}; - } - async SetOB11Config(config) { - const ConfigResponse = await fetch("../api/OB11Config/SetConfig", { - method: "POST", - headers: { - Authorization: "Bearer " + this.retCredential, - "Content-Type": "application/json" - }, - body: JSON.stringify({ config: JSON.stringify(config) }) - }); - if (ConfigResponse.status == 200) { - const ConfigResponseJson = await ConfigResponse.json(); - if (ConfigResponseJson.code == 0) { - return true; - } - } - return false; - } -} -const OB11ConfigWrapper = new WebUiApiOB11ConfigWrapper(); - -async function onSettingWindowCreated(view) { - const isEmpty = (value) => value === void 0 || value === void 0 || value === ""; - await OB11ConfigWrapper.Init(localStorage.getItem("auth")); - const ob11Config = await OB11ConfigWrapper.GetOB11Config(); - const setOB11Config = (key, value) => { - const configKey = key.split("."); - if (configKey.length === 2) { - ob11Config[configKey[1]] = value; - } else if (configKey.length === 3) { - ob11Config[configKey[1]][configKey[2]] = value; - } - }; - const parser = new DOMParser(); - const doc = parser.parseFromString( - [ - "
", - ` -
-
`, - SettingList([ - SettingItem( - 'Napcat', - void 0, - SettingButton("V3.4.3", "napcat-update-button", "secondary") - ) - ]), - SettingList([ - SettingItem( - "启用 HTTP 服务", - void 0, - SettingSwitch("ob11.http.enable", ob11Config.http.enable, { - "control-display-id": "config-ob11-http-port" - }) - ), - SettingItem( - "HTTP 服务监听端口", - void 0, - `
`, - "config-ob11-http-port", - ob11Config.http.enable - ), - SettingItem( - "启用 HTTP 心跳", - void 0, - SettingSwitch("ob11.http.enableHeart", ob11Config.http.enableHeart, { - "control-display-id": "config-ob11-HTTP.enableHeart" - }) - ), - SettingItem( - "启用 HTTP 事件上报", - void 0, - SettingSwitch("ob11.http.enablePost", ob11Config.http.enablePost, { - "control-display-id": "config-ob11-http-postUrls" - }) - ), - `
- -
- HTTP 事件上报密钥 -
-
- -
-
- -
- HTTP 事件上报地址 -
- 添加 -
-
-
`, - SettingItem( - "启用正向 WebSocket 服务", - void 0, - SettingSwitch("ob11.ws.enable", ob11Config.ws.enable, { - "control-display-id": "config-ob11-ws-port" - }) - ), - SettingItem( - "正向 WebSocket 服务监听端口", - void 0, - `
`, - "config-ob11-ws-port", - ob11Config.ws.enable - ), - SettingItem( - "启用反向 WebSocket 服务", - void 0, - SettingSwitch("ob11.reverseWs.enable", ob11Config.reverseWs.enable, { - "control-display-id": "config-ob11-reverseWs-urls" - }) - ), - `
- -
- 反向 WebSocket 监听地址 -
- 添加 -
-
-
`, - SettingItem( - " WebSocket 服务心跳间隔", - "控制每隔多久发送一个心跳包,单位为毫秒", - `
` - ), - SettingItem( - "Access token", - void 0, - `
` - ), - SettingItem( - "新消息上报格式", - `如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 OneBot v11 文档`, - SettingSelect( - [ - { text: "消息段", value: "array" }, - { text: "CQ码", value: "string" } - ], - "ob11.messagePostFormat", - ob11Config.messagePostFormat - ) - ), - SettingItem( - "音乐卡片签名地址", - void 0, - `
`, - "ob11.musicSignUrl" - ), - SettingItem( - "启用本地进群时间与发言时间记录", - void 0, - SettingSwitch("ob11.GroupLocalTime.Record", ob11Config.GroupLocalTime.Record, { - "control-display-id": "config-ob11-GroupLocalTime-RecordList" - }) - ), - `
- -
- 群列表 -
- 添加 -
-
-
`, - SettingItem( - "", - void 0, - SettingButton("保存", "config-ob11-save", "primary") - ) - ]), - SettingList([ - SettingItem( - "上报 Bot 自身发送的消息", - "上报 event 为 message_sent", - SettingSwitch("ob11.reportSelfMessage", ob11Config.reportSelfMessage) - ) - ]), - SettingList([ - SettingItem( - "GitHub 仓库", - "https://github.com/NapNeko/NapCatQQ", - SettingButton("点个星星", "open-github") - ), - SettingItem("NapCat 文档", "", SettingButton("看看文档", "open-docs")) - ]), - SettingItem( - "Telegram 群", - "https://t.me/+nLZEnpne-pQ1OWFl", - SettingButton("进去逛逛", "open-telegram") - ), - SettingItem( - "QQ 群", - "518662028", - SettingButton("我要进去", "open-qq-group") - ), - "
" - ].join(""), - "text/html" - ); - doc.querySelector("#open-github")?.addEventListener("click", () => { - window.open("https://github.com/NapNeko/NapCatQQ", "_blank"); - }); - doc.querySelector("#open-docs")?.addEventListener("click", () => { - window.open("https://napneko.github.io/", "_blank"); - }); - doc.querySelector("#open-telegram")?.addEventListener("click", () => { - window.open("https://t.me/+nLZEnpne-pQ1OWFl", "_blank"); - }); - doc.querySelector("#open-qq-group")?.addEventListener("click", () => { - window.open("https://qm.qq.com/q/VfjAq5HIMS", "_blank"); - }); - const buildHostListItem = (type, host, index, inputAttrs = {}) => { - const dom = { - container: document.createElement("setting-item"), - input: document.createElement("input"), - inputContainer: document.createElement("div"), - deleteBtn: document.createElement("setting-button") - }; - dom.container.classList.add("setting-host-list-item"); - dom.container.dataset.direction = "row"; - Object.assign(dom.input, inputAttrs); - dom.input.classList.add("q-input__inner"); - dom.input.type = "url"; - dom.input.value = host; - dom.input.addEventListener("input", () => { - ob11Config[type.split("-")[0]][type.split("-")[1]][index] = dom.input.value; - }); - dom.inputContainer.classList.add("q-input"); - dom.inputContainer.appendChild(dom.input); - dom.deleteBtn.innerHTML = "删除"; - dom.deleteBtn.dataset.type = "secondary"; - dom.deleteBtn.addEventListener("click", () => { - ob11Config[type.split("-")[0]][type.split("-")[1]].splice(index, 1); - initReverseHost(type); - }); - dom.container.appendChild(dom.inputContainer); - dom.container.appendChild(dom.deleteBtn); - return dom.container; - }; - const buildHostList = (hosts, type, inputAttr = {}) => { - const result = []; - hosts?.forEach((host, index) => { - result.push(buildHostListItem(type, host, index, inputAttr)); - }); - return result; - }; - const addReverseHost = (type, doc2 = document, inputAttr = {}) => { - type = type.replace(/\./g, "-"); - const hostContainerDom = doc2.body.querySelector( - `#config-ob11-${type}-list` - ); - hostContainerDom?.appendChild( - buildHostListItem( - type, - "", - ob11Config[type.split("-")[0]][type.split("-")[1]].length, - inputAttr - ) - ); - ob11Config[type.split("-")[0]][type.split("-")[1]].push(""); - }; - const initReverseHost = (type, doc2 = document) => { - type = type.replace(/\./g, "-"); - const hostContainerDom = doc2.body?.querySelector( - `#config-ob11-${type}-list` - ); - if (hostContainerDom) { - [...hostContainerDom.childNodes].forEach((dom) => dom.remove()); - buildHostList( - ob11Config[type.split("-")[0]][type.split("-")[1]], - type - ).forEach((dom) => { - hostContainerDom?.appendChild(dom); - }); - } - }; - initReverseHost("http.postUrls", doc); - initReverseHost("reverseWs.urls", doc); - initReverseHost("GroupLocalTime.RecordList", doc); - doc.querySelector("#config-ob11-http-postUrls-add")?.addEventListener( - "click", - () => addReverseHost("http.postUrls", document, { - placeholder: "如:http://127.0.0.1:5140/onebot" - }) - ); - doc.querySelector("#config-ob11-reverseWs-urls-add")?.addEventListener( - "click", - () => addReverseHost("reverseWs.urls", document, { - placeholder: "如:ws://127.0.0.1:5140/onebot" - }) - ); - doc.querySelector("#config-ob11-GroupLocalTime-RecordList-add")?.addEventListener( - "click", - () => addReverseHost("GroupLocalTime.RecordList", document, { - placeholder: "此处填写群号 -1为全部" - }) - ); - doc.querySelector("#config-ffmpeg-select")?.addEventListener("click", () => { - }); - doc.querySelector("#config-open-log-path")?.addEventListener("click", () => { - }); - doc.querySelectorAll("setting-switch[data-config-key]").forEach((dom) => { - dom.addEventListener("click", () => { - const active = dom.getAttribute("is-active") == void 0; - setOB11Config(dom.dataset.configKey, active); - if (active) dom.setAttribute("is-active", ""); - else dom.removeAttribute("is-active"); - if (!isEmpty(dom.dataset.controlDisplayId)) { - const displayDom = document.querySelector( - //@ts-expect-error 等待修复 - `#${dom.dataset.controlDisplayId}` - ); - if (active) displayDom?.removeAttribute("is-hidden"); - else displayDom?.setAttribute("is-hidden", ""); - } - }); - }); - doc.querySelectorAll( - "setting-item .q-input input.q-input__inner[data-config-key]" - ).forEach((dom) => { - dom.addEventListener("input", () => { - const Type = dom.getAttribute("type"); - const configKey = dom.dataset.configKey; - const configValue = Type === "number" ? parseInt(dom.value) >= 1 ? parseInt(dom.value) : 1 : dom.value; - setOB11Config(configKey, configValue); - }); - }); - doc.querySelectorAll("ob-setting-select[data-config-key]").forEach((dom) => { - dom?.addEventListener("selected", (e) => { - const configKey = dom.dataset.configKey; - const configValue = e.detail.value; - setOB11Config(configKey, configValue); - }); - }); - doc.querySelector("#config-ob11-save")?.addEventListener("click", () => { - OB11ConfigWrapper.SetOB11Config(ob11Config); - alert("保存成功"); - }); - doc.body.childNodes.forEach((node) => { - view.appendChild(node); - }); -} - -export { onSettingWindowCreated }; diff --git a/static/assets/style.css b/static/assets/style.css deleted file mode 100644 index c52bec45..00000000 --- a/static/assets/style.css +++ /dev/null @@ -1,201 +0,0 @@ -body, html { - background: var(--bg_bottom_standard); - color: var(--text_primary); - font-family: "Color Emoji", system-ui, "PingFang SC", PingFangSC-Regular, "Microsoft YaHei", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", Arial, Helvetica, sans-serif, "Apple Braille", "Color Emoji Fix"; - min-height: 100vh; - scroll-behavior: smooth; - width: 100%; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - - --z_index_popover: 999; - --nt_mix_tokens: nt_brand_standard_2_overlay_hover_brand_2_mix,nt_brand_standard_2_overlay_pressed_brand_2_mix,nt_feedback_error_2_overlay_hover_brand_2_mix,nt_feedback_error_2_overlay_pressed_brand_2_mix,nt_icon_white_2_overlay_hover_2_mix,nt_icon_white_2_overlay_pressed_2_mix,nt_bubble_host_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_pressed_brand_2_mix,nt_bg_white_2_overlay_hover_2_mix,nt_bg_white_2_overlay_pressed_2_mix,nt_fg_white_2_overlay_hover_2_mix,nt_fg_white_2_overlay_pressed_2_mix,nt_icon_red_2_overlay_hover_2_mix,nt_icon_red_2_overlay_pressed_2_mix,nt_fg_grey_standard_2_overlay_hover_2_mix,nt_fg_grey_standard_2_overlay_pressed_2_mix,nt_bubble_guest_2_overlay_pressed_2_mix,nt_icon_primary_2_20_2_alpha,nt_bg_grey_standard_2_95_2_alpha,nt_tag_red_2_20_2_alpha,nt_tag_red_2_25_2_alpha,nt_tag_blue_2_20_2_alpha,nt_tag_blue_2_25_2_alpha,nt_tag_blue_2_10_2_alpha,nt_tag_purple_2_20_2_alpha,nt_brand_standard_2_20_2_alpha,nt_tag_sage_green_2_20_2_alpha,nt_feedback_error_2_20_2_alpha,nt_text_white_2_60_2_alpha,nt_bg_white_2_70_2_alpha,nt_bg_white_2_90_2_alpha,nt_bg_white_2_97_2_alpha,nt_bg_white_2_40_2_alpha,nt_bg_white_2_30_2_alpha,nt_text_white_2_80_2_alpha,nt_brand_standard_2_50_2_alpha,nt_bg_nav_secondary_2_60_2_alpha,nt_bg_nav_2_60_2_alpha,nt_feedback_error_2_10_2_alpha,nt_brand_standard_2_10_2_alpha,nt_on_brand_primary_2_40_2_alpha,nt_text_primary_2_72_2_alpha,nt_text_white_2_72_2_alpha; - } - - a, address, article, aside, b, blockquote, body, div, em, fieldset, footer, form, h1, h2, h3, h4, h5, h6, header, html, i, iframe, img, label, legend, li, main, nav, ol, p, s, section, span, table, tbody, td, tfoot, th, thead, tr, ul { - box-sizing: border-box; - font-size: 100%; - font-style: inherit; - font-weight: inherit; - border: 0px; - margin: 0px; - padding: 0px; - } - - #app { - position: relative; - display: block; - padding: 20px; - min-height: 100vh; - background: transparent; - } - - .fake-bar { - position: fixed; - display: none; - top: 0; - left: 0; - } - - /* ======== Input ======== */ - .q-input { - align-items: center; - border-radius: 4px; - box-sizing: border-box; - color: var(--text_secondary); - display: inline-flex; - position: relative; - width: 100%; - border: 1px solid transparent; - } - - .q-input input, .q-input textarea { - appearance: none; - background-color: transparent; - box-sizing: border-box; - color: var(--text_primary); - flex-grow: 1; - flex-shrink: 1; - flex-basis: 0%; - font-size: 14px; - height: fit-content; - outline-color: initial; - outline-style: none; - outline-width: initial; - resize: none; - width: 100%; - border: none; - } - - /* ======== Switch ======== */ - .q-switch { - background-color: var(--fill_standard_primary); - border-radius: 14px; - box-sizing: border-box; - display: inline-flex; - position: relative; - transition-behavior: normal; - transition-duration: 0.2s; - transition-timing-function: cubic-bezier(0.38, 0, 0.24, 1); - transition-delay: 0s; - transition-property: all; - width: 28px; - padding: 3px; - } - - .q-switch__handle { - border-radius: 5px; - box-shadow: rgba(0, 0, 0, 0.09) 0px 2px 4px; - box-sizing: border-box; - display: inline-block; - height: 10px; - position: relative; - transition-behavior: normal; - transition-duration: 0.2s; - transition-timing-function: cubic-bezier(0.38, 0, 0.24, 1); - transition-delay: 0s; - transition-property: all; - width: 10px; - z-index: 2; - background: var(--icon_white); - } - - .q-switch:not(.is-disabled):hover { - background: var(--fill_standard_secondary); - } - - .q-switch:not(.is-disabled):active { - background: var(--nt_bg_white_2_overlay_pressed_brand_2_mix); - } - .q-switch:not(.is-disabled):active .q-switch__handle { - width: 12px; - } - - .q-switch.is-active { - background-color: var(--brand_standard); - } - - .q-switch.is-active .q-switch__handle { - transform: translateX(12px); - } - - .q-switch.is-active:not(.is-disabled):hover { - background: var(--nt_brand_standard_2_overlay_hover_brand_2_mix); - } - - .q-switch.is-active:not(.is-disabled):active { - background: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix); - } - .q-switch.is-active:not(.is-disabled):active .q-switch__handle { - transform: translateX(10px); - } - - /* ======== Button ======== */ - .q-button { - align-items: center; - background-color: var(--brand_standard); - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; - display: inline-flex; - font-size: 14px; - justify-content: center; - line-height: 18px; - outline-color: initial; - outline-style: none; - outline-width: initial; - position: relative; - vertical-align: text-bottom; - border: 1px solid var(--fg_grey_standard); - padding: 5px 11px; - } - - .q-button--small { - font-size: 12px; - line-height: 14px; - min-width: 62px; - padding: 4px 7px; - } - - .q-button--primary { - background-color: var(--brand_standard); - border-color: var(--brand_standard); - color: var(--on_brand_primary); - } - - .q-button--secondary { - background-color: transparent; - border-color: var(--fg_grey_standard); - color: var(--text_primary); - } - - .q-button:not([disabled]):hover { - background-color: var(--overlay_hover); - } - - .q-button:not([disabled]):active { - background-color: var(--overlay_pressed); - } - - .q-button--primary:hover { - background-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix); - border-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix); - } - - .q-button--primary:active { - background-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix); - border-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix); - } - - .q-button[disabled] { - opacity: 0.3; - cursor: not-allowed; - } - - .q-button--secondary[disabled] { - background-color: transparent; - } \ No newline at end of file diff --git a/static/assets/webcomponents.css b/static/assets/webcomponents.css deleted file mode 100644 index ff7363c1..00000000 --- a/static/assets/webcomponents.css +++ /dev/null @@ -1,350 +0,0 @@ -*[is-disabled] { - opacity: 0.3; - -webkit-pointer-events: none; - -moz-pointer-events: none; - -ms-pointer-events: none; - -o-pointer-events: none; - pointer-events: none; - cursor: not-allowed; - } - - setting-section::before { - content: attr(data-title); - display: block; - margin: 0px 0px 8px 16px; - color: var(--text_primary); - font-weight: var(--font-bold); - font-size: min(var(--font_size_3), 18px); - line-height: min(var(--line_height_3), 24px); - } - - setting-panel { - display: block; - margin-bottom: 20px; - } - - setting-section:last-child setting-panel { - margin-bottom: 0; - } - - setting-list, - setting-list[data-direction="column"] { - display: flex; - background-color: var(--fill_light_primary); - border-radius: 8px; - flex-direction: column; - align-items: stretch; - justify-content: space-between; - } - - setting-list[data-direction="row"] { - padding: 16px 0; - flex-direction: row; - justify-content: space-around; - } - setting-list[data-direction="row"], - setting-list[data-direction="row"] * { - text-align: center; - } - - setting-item { - display: flex; - padding: 12px 16px; - background-color: var(--fill_light_primary); - font-size: min(var(--font_size_3),18px); - line-height: min(var(--line_height_3),24px); - border-radius: 8px; - align-items: center; - justify-content: space-between; - } - - setting-item > *:first-child { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - flex-wrap: nowrap; - } - - setting-list[data-direction="row"] setting-item { - padding: 0; - margin: 0 10px; - flex: 1; - } - - setting-list[data-direction="row"] setting-item > *:first-child { - align-items: center; - flex: 1; - } - - setting-list[data-direction="row"] setting-item > *:first-child > * { - flex: 1; - } - - setting-list setting-divider, - setting-list[data-direction="column"] setting-divider { - display: block; - position: relative; - width: unset; - height: 1px; - margin: 0 16px; - background-color: var(--border_standard); - } - - setting-list[data-direction="row"] setting-divider { - width: 1px; - height: unset; - margin: 0; - } - - setting-text[data-type="secondary"] { - margin-top: 4px; - color: var(--text_secondary); - font-size: min(var(--font_size_2),16px); - line-height: min(var(--line_height_2),22px); - } - - setting-switch { - --transition-timing: cubic-bezier(0.38, 0, 0.24, 1); - - display: block; - position: relative; - width: 28px; - height: 16px; - background: var(--fill_standard_primary); - border-radius: 14px; - transition: background var(--transition-timing) .2s; - } - setting-switch::after { - content: ''; - display: block; - position: absolute; - top: 0px; - left: 0px; - margin: 3px; - width: 10px; - height: 10px; - background: var(--icon_white); - box-shadow: rgba(0, 0, 0, 0.09) 0px 2px 4px; - border-radius: 5px; - transition: width var(--transition-timing) .2s, - left var(--transition-timing) .2s; - } - - setting-switch[is-active] { - background: var(--brand_standard); - } - setting-switch[is-active]::after { - left: calc(100% - 16px); - } - - setting-switch:hover { - background: var(--fill_standard_secondary); - } - setting-switch[is-active]:hover { - background: var(--nt_brand_standard_2_overlay_hover_brand_2_mix); - } - - setting-switch:active { - background: var(--nt_bg_white_2_overlay_pressed_brand_2_mix); - } - setting-switch[is-active]:active { - background: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix); - } - - setting-switch:active::after { - width: 12px; - } - setting-switch[is-active]:active::after { - left: calc(100% - 18px); - } - - setting-button, - setting-button[data-type="secondary"] { - position: relative; - display: inline-flex; - padding: 5px 11px; - min-width: 62px; - background-color: transparent; - color: var(--text_primary); - border-radius: 4px; - font-size: 12px; - line-height: 12px; - justify-content: center; - outline-color: initial; - outline-style: none; - outline-width: initial; - vertical-align: text-bottom; - border: 1px solid var(--fg_grey_standard); - align-items: center; - box-sizing: border-box; - } - - setting-button[data-type="primary"] { - background-color: var(--brand_standard); - color: var(--on_brand_primary); - border-color: var(--brand_standard); - } - - setting-button:hover, - setting-button[data-type="secondary"]:hover { - background-color: var(--overlay_hover); - } - - setting-button:active, - setting-button[data-type="secondary"]:active { - background-color: var(--overlay_pressed); - } - - setting-button[data-type="primary"]:hover { - background-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix); - border-color: var(--nt_brand_standard_2_overlay_hover_brand_2_mix); - } - - setting-button[data-type="primary"]:active { - background-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix); - border-color: var(--nt_brand_standard_2_overlay_pressed_brand_2_mix); - } - - setting-select, - setting-select::part(parent), - setting-select::part(button) { - display: block; - position: relative; - height: 24px; - font-size: 12px; - line-height: 24px; - box-sizing: border-box; - } - - setting-select::part(button) { - display: flex; - padding: 0px 8px; - background-color: transparent; - border-radius: 4px; - border: 1px solid var(--border_dark); - z-index: 5; - cursor: default; - align-items: center; - flex-direction: row; - flex-wrap: nowrap; - } - - setting-select::part(current-text) { - display: block; - margin-right: 8px; - padding: 0px; - background: none; - background-color: transparent; - font-size: 12px; - color: var(--text_primary); - text-overflow: ellipsis; - border-radius: 0px; - border: none; - outline: none; - overflow: hidden; - appearance: none; - box-sizing: border-box; - cursor: default; - flex: 1; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - -webkit-pointer-events: none; - -moz-pointer-events: none; - -ms-pointer-events: none; - -o-pointer-events: none; - pointer-events: none; - } - - setting-select::part(button-arrow) { - position: relative; - display: block; - width: 16px; - height: 16px; - color: var(--icon_primary); - } - - setting-select::part(option-list) { - display: flex; - position: absolute; - top: 100%; - padding: 4px; - margin: 5px 0px; - width: 100%; - max-height: var(--q-contextmenu-max-height); - background-color: var(--blur_middle_standard); - background-clip: padding-box; - backdrop-filter: blur(8px); - font-size: 12px; - box-shadow: var(--shadow_bg_middle_secondary); - border: 1px solid var(--border_secondary); - border-radius: 4px; - box-sizing: border-box; - app-region: no-drag; - overflow-x: hidden; - overflow-y: auto; - list-style: none; - z-index: 999; - flex-direction: column; - align-items: stretch; - flex-wrap: nowrap; - justify-content: flex-start; - gap: 4px; - } - - setting-option, - setting-option::part(parent) { - display: block; - position: relative; - box-sizing: border-box; - } - - setting-option::part(parent) { - display: flex; - padding: 0px 8px; - color: var(--text_primary); - font-size: 12px; - line-height: 24px; - border-radius: 4px; - flex-direction: row; - align-items: center; - flex-wrap: nowrap; - justify-content: flex-start; - } - - setting-option:hover::part(parent) { - background-color: var(--overlay_hover); - } - - setting-option:active::part(parent) { - background-color: var(--overlay_pressed); - } - - setting-option[is-selected]::part(parent) { - background-color: var(--overlay_active); - } - - setting-option::part(text) { - margin-right: 8px; - overflow: hidden; - text-wrap: nowrap; - text-overflow: ellipsis; - flex: 1; - } - - setting-option::part(check-icon) { - display: none; - position: relative; - right: -4px; - width: 1em; - height: 1em; - color: var(--icon_primary); - flex-shrink: 0; - } - - setting-option[is-selected]::part(check-icon) { - display: block; - } \ No newline at end of file diff --git a/static/config.html b/static/config.html deleted file mode 100644 index 32463a80..00000000 --- a/static/config.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - NapCat-WebUi - - - - - - - \ No newline at end of file diff --git a/static/index.html b/static/index.html deleted file mode 100644 index 7f45934e..00000000 --- a/static/index.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - WebUi - Index - - - - - - - \ No newline at end of file diff --git a/static/login.html b/static/login.html deleted file mode 100644 index 58feb93b..00000000 --- a/static/login.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - WebUi - Login - - - - - - - - - diff --git a/vite.config.ts b/vite.config.ts index d52133c2..d83f3deb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,19 +4,15 @@ import { resolve } from 'path'; import nodeResolve from '@rollup/plugin-node-resolve'; import { builtinModules } from 'module'; //依赖排除 -const external = ['silk-wasm', 'ws', 'express', 'fluent-ffmpeg', 'log4js', 'qrcode-terminal']; -const nodeModules = [...builtinModules, builtinModules.map(m => `node:${m}`)].flat(); +const external = ['silk-wasm', 'ws', 'express', 'qrcode-terminal', 'fluent-ffmpeg', 'piscina']; +const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); function genCpModule(module: string) { return { src: `./node_modules/${module}`, dest: `dist/node_modules/${module}`, flatten: false }; } let startScripts: string[] | undefined = undefined; if (process.env.NAPCAT_BUILDSYS == 'linux') { - if (process.env.NAPCAT_BUILDARCH == 'x64') { - } startScripts = []; } else if (process.env.NAPCAT_BUILDSYS == 'win32') { - if (process.env.NAPCAT_BUILDARCH == 'x64') { - } startScripts = ['./script/KillQQ.bat']; } else { startScripts = ['./script/KillQQ.bat']; @@ -26,8 +22,8 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [ targets: [ { src: './manifest.json', dest: 'dist' }, { src: './src/core/external/napcat.json', dest: 'dist/config/' }, - { src: './static/', dest: 'dist/static/', flatten: false }, - { src: './src/onebot/config/onebot11.json', dest: 'dist/config/' }, + { src: './src/native/packet', dest: 'dist/moehoo', flatten: false }, + { src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false }, { src: './src/framework/liteloader.cjs', dest: 'dist' }, { src: './src/framework/napcat.cjs', dest: 'dist' }, { src: './src/framework/preload.cjs', dest: 'dist' }, @@ -41,15 +37,14 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [ const ShellBaseConfigPlugin: PluginOption[] = [ cp({ targets: [ - { src: './src/native/external', dest: 'dist/native', flatten: false }, - { src: './static/', dest: 'dist/static/', flatten: false }, + { src: './src/native/packet', dest: 'dist/moehoo', flatten: false }, + { src: './napcat.webui/dist/', dest: 'dist/static/', flatten: false }, { src: './src/core/external/napcat.json', dest: 'dist/config/' }, - { src: './src/onebot/config/onebot11.json', dest: 'dist/config/' }, { src: './package.json', dest: 'dist' }, { src: './launcher/', dest: 'dist', flatten: true }, - ...(startScripts.map((startScript) => { + ...startScripts.map((startScript) => { return { src: startScript, dest: 'dist' }; - })), + }), ], }), nodeResolve(), @@ -69,9 +64,12 @@ const ShellBaseConfig = () => defineConfig({ target: 'esnext', minify: false, lib: { - entry: 'src/shell/napcat.ts', + entry: { + 'napcat': 'src/shell/napcat.ts', + 'audio-worker': 'src/common/audio-worker.ts', + }, formats: ['es'], - fileName: () => 'napcat.mjs', + fileName: (_, entryName) => `${entryName}.mjs`, }, rollupOptions: { external: [...nodeModules, ...external], @@ -93,9 +91,12 @@ const FrameworkBaseConfig = () => defineConfig({ target: 'esnext', minify: false, lib: { - entry: 'src/framework/napcat.ts', + entry: { + 'napcat': 'src/framework/napcat.ts', + 'audio-worker': 'src/common/audio-worker.ts', + }, formats: ['es'], - fileName: () => 'napcat.mjs', + fileName: (_, entryName) => `${entryName}.mjs`, }, rollupOptions: { external: [...nodeModules, ...external],