From 98c65c4923efbccd9974b4dab1d9b1dbf2c24d5e Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Tue, 12 Nov 2024 04:02:19 +0800 Subject: [PATCH 1/8] refactor: packet x1 --- package.json | 3 +- src/common/forward-msg-builder.ts | 2 +- src/core/apis/file.ts | 2 +- src/core/apis/packet.ts | 209 +---- .../external/proto/EmojiLikeToOthers.proto | 31 - src/core/external/proto/GreyTipWrapper.proto | 9 - src/core/external/proto/ProfileLikeTip.proto | 18 - src/core/external/proto/SysMessage.proto | 36 - src/core/helper/adaptDecoder.ts | 61 ++ .../client/{client.ts => baseClient.ts} | 70 +- src/core/packet/client/nativeClient.ts | 41 +- src/core/packet/client/wsClient.ts | 155 ++-- src/core/packet/clientSession.ts | 27 + src/core/packet/context/clientContext.ts | 82 ++ src/core/packet/context/loggerContext.ts | 35 + src/core/packet/context/napCoreContext.ts | 36 + src/core/packet/context/operationContext.ts | 165 ++++ src/core/packet/context/packetContext.ts | 25 + src/core/packet/highway/client.ts | 15 +- .../highway/{session.ts => highwayContext.ts} | 249 +++--- src/core/packet/highway/uploader.ts | 215 ----- .../highway/uploader/highwayHttpUploader.ts | 75 ++ .../highway/uploader/highwayTcpUploader.ts | 85 ++ .../highway/uploader/highwayUploader.ts | 63 ++ src/core/packet/highway/utils.ts | 8 +- src/core/packet/message/builder.ts | 17 +- src/core/packet/message/converter.ts | 74 +- src/core/packet/message/element.ts | 13 +- src/core/packet/packer.ts | 803 ------------------ src/core/packet/proto/old/Message.ts | 49 -- src/core/packet/proto/old/ProfileLike.ts | 59 -- src/core/packet/service/base.ts | 0 src/core/packet/session.ts | 73 -- .../transformer/action/FetchAiVoiceList.ts | 26 + .../packet/transformer/action/GetAiVoice.ts | 31 + .../action/GetMiniAppAdaptShareInfo.ts | 53 ++ .../transformer/action/GetStrangerInfo.ts | 25 + .../packet/transformer/action/GroupSign.ts | 29 + .../packet/transformer/action/SendPoke.ts | 26 + .../transformer/action/SetSpecialTitle.ts | 30 + src/core/packet/transformer/action/index.ts | 7 + src/core/packet/transformer/base.ts | 25 + .../transformer/highway/DownloadGroupFile.ts | 33 + .../transformer/highway/DownloadGroupPtt.ts | 49 ++ .../highway/DownloadOfflineFile.ts | 35 + .../highway/DownloadPrivateFile.ts | 36 + .../transformer/highway/FetchSessionKey.ts | 37 + .../transformer/highway/UploadGroupFile.ts | 38 + .../transformer/highway/UploadGroupImage.ts | 87 ++ .../transformer/highway/UploadGroupPtt.ts | 84 ++ .../transformer/highway/UploadGroupVideo.ts | 104 +++ .../transformer/highway/UploadPrivateFile.ts | 41 + .../transformer/highway/UploadPrivateImage.ts | 87 ++ .../transformer/highway/UploadPrivatePtt.ts | 81 ++ .../transformer/highway/UploadPrivateVideo.ts | 105 +++ src/core/packet/transformer/highway/index.ts | 13 + src/core/packet/transformer/index.ts | 4 + .../transformer/message/UploadForwardMsg.ts | 51 ++ src/core/packet/transformer/message/index.ts | 1 + src/core/packet/transformer/oidb/oidbBase.ts | 32 + .../{ => transformer}/proto/action/action.ts | 2 +- .../proto/action/miniAppAdaptShareInfo.ts | 3 +- .../proto/highway/highway.ts | 5 +- src/core/packet/transformer/proto/index.ts | 31 + .../{ => transformer}/proto/message/action.ts | 5 +- .../{ => transformer}/proto/message/c2c.ts | 3 +- .../proto/message/component.ts | 5 +- .../proto/message/element.ts | 3 +- .../{ => transformer}/proto/message/group.ts | 3 +- .../proto/message/message.ts | 16 +- .../{ => transformer}/proto/message/notify.ts | 3 +- .../proto/message/routing.ts | 3 +- .../proto/oidb/Oidb.0XE37_800.ts | 5 +- .../proto/oidb/Oidb.0XFE1_2.ts | 3 +- .../proto/oidb/Oidb.0x6D6.ts | 3 +- .../proto/oidb/Oidb.0x8FC_2.ts | 3 +- .../proto/oidb/Oidb.0x9067_202.ts | 3 +- .../proto/oidb/Oidb.0x929.ts | 6 +- .../proto/oidb/Oidb.0xE37_1200.ts | 3 +- .../proto/oidb/Oidb.0xE37_1700.ts | 3 +- .../proto/oidb/Oidb.0xEB7.ts | 3 +- .../proto/oidb/Oidb.0xED3_1.ts | 3 +- .../{ => transformer}/proto/oidb/OidbBase.ts | 3 +- .../proto/oidb/common/Ntv2.RichMediaReq.ts | 3 +- .../proto/oidb/common/Ntv2.RichMediaResp.ts | 6 +- .../packet/transformer/system/FetchRkey.ts | 40 + src/core/packet/transformer/system/index.ts | 1 + .../{ => utils}/helper/miniAppHelper.ts | 2 +- src/onebot/action/extends/GetAiCharacters.ts | 2 +- src/onebot/action/extends/GetMiniAppArk.ts | 4 +- src/onebot/action/extends/GetRkey.ts | 2 +- src/onebot/action/extends/GetUserStatus.ts | 2 +- src/onebot/action/extends/SetGroupSign.ts | 4 +- src/onebot/action/extends/SetSpecialTittle.ts | 2 +- src/onebot/action/file/GetGroupFileUrl.ts | 2 +- src/onebot/action/group/GetAiRecord.ts | 4 +- src/onebot/action/group/GroupPoke.ts | 2 +- src/onebot/action/group/SendGroupAiRecord.ts | 6 +- src/onebot/action/msg/MarkMsgAsRead.ts | 4 +- src/onebot/action/msg/SendMsg.ts | 10 +- src/onebot/action/packet/GetPacketStatus.ts | 1 + src/onebot/action/user/FriendPoke.ts | 2 +- src/onebot/api/msg.ts | 18 +- src/onebot/api/user.ts | 4 +- src/onebot/index.ts | 32 +- 105 files changed, 2286 insertions(+), 1962 deletions(-) delete mode 100644 src/core/external/proto/EmojiLikeToOthers.proto delete mode 100644 src/core/external/proto/GreyTipWrapper.proto delete mode 100644 src/core/external/proto/ProfileLikeTip.proto delete mode 100644 src/core/external/proto/SysMessage.proto create mode 100644 src/core/helper/adaptDecoder.ts rename src/core/packet/client/{client.ts => baseClient.ts} (56%) create mode 100644 src/core/packet/clientSession.ts create mode 100644 src/core/packet/context/clientContext.ts create mode 100644 src/core/packet/context/loggerContext.ts create mode 100644 src/core/packet/context/napCoreContext.ts create mode 100644 src/core/packet/context/operationContext.ts create mode 100644 src/core/packet/context/packetContext.ts rename src/core/packet/highway/{session.ts => highwayContext.ts} (61%) delete mode 100644 src/core/packet/highway/uploader.ts create mode 100644 src/core/packet/highway/uploader/highwayHttpUploader.ts create mode 100644 src/core/packet/highway/uploader/highwayTcpUploader.ts create mode 100644 src/core/packet/highway/uploader/highwayUploader.ts delete mode 100644 src/core/packet/packer.ts delete mode 100644 src/core/packet/proto/old/Message.ts delete mode 100644 src/core/packet/proto/old/ProfileLike.ts create mode 100644 src/core/packet/service/base.ts delete mode 100644 src/core/packet/session.ts create mode 100644 src/core/packet/transformer/action/FetchAiVoiceList.ts create mode 100644 src/core/packet/transformer/action/GetAiVoice.ts create mode 100644 src/core/packet/transformer/action/GetMiniAppAdaptShareInfo.ts create mode 100644 src/core/packet/transformer/action/GetStrangerInfo.ts create mode 100644 src/core/packet/transformer/action/GroupSign.ts create mode 100644 src/core/packet/transformer/action/SendPoke.ts create mode 100644 src/core/packet/transformer/action/SetSpecialTitle.ts create mode 100644 src/core/packet/transformer/action/index.ts create mode 100644 src/core/packet/transformer/base.ts create mode 100644 src/core/packet/transformer/highway/DownloadGroupFile.ts create mode 100644 src/core/packet/transformer/highway/DownloadGroupPtt.ts create mode 100644 src/core/packet/transformer/highway/DownloadOfflineFile.ts create mode 100644 src/core/packet/transformer/highway/DownloadPrivateFile.ts create mode 100644 src/core/packet/transformer/highway/FetchSessionKey.ts create mode 100644 src/core/packet/transformer/highway/UploadGroupFile.ts create mode 100644 src/core/packet/transformer/highway/UploadGroupImage.ts create mode 100644 src/core/packet/transformer/highway/UploadGroupPtt.ts create mode 100644 src/core/packet/transformer/highway/UploadGroupVideo.ts create mode 100644 src/core/packet/transformer/highway/UploadPrivateFile.ts create mode 100644 src/core/packet/transformer/highway/UploadPrivateImage.ts create mode 100644 src/core/packet/transformer/highway/UploadPrivatePtt.ts create mode 100644 src/core/packet/transformer/highway/UploadPrivateVideo.ts create mode 100644 src/core/packet/transformer/highway/index.ts create mode 100644 src/core/packet/transformer/index.ts create mode 100644 src/core/packet/transformer/message/UploadForwardMsg.ts create mode 100644 src/core/packet/transformer/message/index.ts create mode 100644 src/core/packet/transformer/oidb/oidbBase.ts rename src/core/packet/{ => transformer}/proto/action/action.ts (98%) rename src/core/packet/{ => transformer}/proto/action/miniAppAdaptShareInfo.ts (94%) rename src/core/packet/{ => transformer}/proto/highway/highway.ts (96%) create mode 100644 src/core/packet/transformer/proto/index.ts rename src/core/packet/{ => transformer}/proto/message/action.ts (95%) rename src/core/packet/{ => transformer}/proto/message/c2c.ts (76%) rename src/core/packet/{ => transformer}/proto/message/component.ts (97%) rename src/core/packet/{ => transformer}/proto/message/element.ts (99%) rename src/core/packet/{ => transformer}/proto/message/group.ts (82%) rename src/core/packet/{ => transformer}/proto/message/message.ts (87%) rename src/core/packet/{ => transformer}/proto/message/notify.ts (87%) rename src/core/packet/{ => transformer}/proto/message/routing.ts (91%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0XE37_800.ts (95%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0XFE1_2.ts (85%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0x6D6.ts (97%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0x8FC_2.ts (81%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0x9067_202.ts (88%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0x929.ts (87%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0xE37_1200.ts (96%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0xE37_1700.ts (89%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0xEB7.ts (72%) rename src/core/packet/{ => transformer}/proto/oidb/Oidb.0xED3_1.ts (69%) rename src/core/packet/{ => transformer}/proto/oidb/OidbBase.ts (79%) rename src/core/packet/{ => transformer}/proto/oidb/common/Ntv2.RichMediaReq.ts (98%) rename src/core/packet/{ => transformer}/proto/oidb/common/Ntv2.RichMediaResp.ts (95%) create mode 100644 src/core/packet/transformer/system/FetchRkey.ts create mode 100644 src/core/packet/transformer/system/index.ts rename src/core/packet/{ => utils}/helper/miniAppHelper.ts (98%) diff --git a/package.json b/package.json index 2fcae1c7..7d6dc72b 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,7 @@ "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.14.0", "@log4js-node/log4js-api": "^1.0.2", - "@napneko/nap-proto-core": "^0.0.2", - "@protobuf-ts/runtime": "^2.9.4", + "@napneko/nap-proto-core": "^0.0.4", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", "@types/cors": "^2.8.17", diff --git a/src/common/forward-msg-builder.ts b/src/common/forward-msg-builder.ts index 17c164e9..55ecd65f 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 diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 9f9ae2ea..a3a34c77 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -433,7 +433,7 @@ export class NTQQFileApi { const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000; const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000; if (rkey_expired_private || rkey_expired_group) { - this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket(); + this.packetRkey = await this.core.apis.PacketApi.pkt.operation.FetchRkey(); } if (this.packetRkey && this.packetRkey.length > 0) { rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6); diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 848d7d91..fd3a94ba 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -1,33 +1,10 @@ -import * as crypto from 'crypto'; import * as os from 'os'; -import { ChatType, InstanceContext, NapCatCore } from '..'; import offset from '@/core/external/offset.json'; -import { PacketSession } from "@/core/packet/session"; -import { OidbPacket, PacketHexStr } from "@/core/packet/packer"; -import { NapProtoMsg, NapProtoEncodeStructType, NapProtoDecodeStructType } from "@napneko/nap-proto-core"; -import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202'; -import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase'; -import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2'; +import { InstanceContext, NapCatCore } from "@/core"; import { LogWrapper } from "@/common/log"; -import { SendLongMsgResp } from "@/core/packet/proto/message/action"; -import { PacketMsg } from "@/core/packet/message/message"; -import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; -import { - PacketMsgFileElement, - PacketMsgPicElement, - PacketMsgPttElement, - PacketMsgVideoElement -} from "@/core/packet/message/element"; -import { MiniAppReqParams, MiniAppRawData } from "@/core/packet/entities/miniApp"; -import { MiniAppAdaptShareInfoResp } from "@/core/packet/proto/action/miniAppAdaptShareInfo"; -import { AIVoiceChatType, AIVoiceItemList } from "@/core/packet/entities/aiChat"; -import { OidbSvcTrpcTcp0X929B_0Resp, OidbSvcTrpcTcp0X929D_0Resp } from "@/core/packet/proto/oidb/Oidb.0x929"; -import { IndexNode, MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; -import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; -import { RecvPacketData } from "@/core/packet/client/client"; +import { PacketClientSession } from "@/core/packet/clientSession"; import { napCatVersion } from "@/common/version"; - interface OffsetType { [key: string]: { recv: string; @@ -42,27 +19,27 @@ export class NTQQPacketApi { core: NapCatCore; logger: LogWrapper; qqVersion: string | undefined; - packetSession: PacketSession | undefined; + pkt: PacketClientSession; constructor(context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; this.logger = core.context.logger; - this.packetSession = undefined; + this.pkt = new PacketClientSession(core); this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion()) .then() .catch(this.core.context.logger.logError.bind(this.core.context.logger)); } get available(): boolean { - return this.packetSession?.client.available ?? false; + return this.pkt?.available; } - async InitSendPacket(qqversion: string) { - this.qqVersion = qqversion; - const table = typedOffset[qqversion + '-' + os.arch()]; + async InitSendPacket(qqVer: string) { + this.qqVersion = qqVer; + const table = typedOffset[qqVer + '-' + os.arch()]; if (!table) { - this.logger.logError(`[Core] [Packet] PacketBackend 不支持当前QQ版本架构:${qqversion}-${os.arch()}, + this.logger.logError(`[Core] [Packet] PacketBackend 不支持当前QQ版本架构:${qqVer}-${os.arch()}, 请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本!`); return false; } @@ -70,173 +47,7 @@ export class NTQQPacketApi { this.logger.logWarn('[Core] [Packet] 已禁用PacketBackend,NapCat.Packet将不会加载!'); return false; } - this.packetSession = new PacketSession(this.core); - const cb = () => { - if (this.packetSession && this.packetSession.client) { - this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger)); - } - }; - await this.packetSession.client.connect(cb); + 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 sendGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType) { - const data = this.packetSession?.packer.packGroupPttFileDownloadReq(groupUin, node); - const ret = await this.sendOidbPacket(data!, true); - const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; - const resp = new NapProtoMsg(NTV2RichMediaResp).decode(body); - const info = resp.download.info; - return `https://${info.domain}${info.urlPath}${resp.download.rKeyParam}`; - } - - async sendMiniAppShareInfoReq(param: MiniAppReqParams) { - const data = this.packetSession?.packer.packMiniAppAdaptShareInfo(param); - const ret = await this.sendPacket("LightAppSvc.mini_app_share.AdaptShareInfo", data!, true); - const body = new NapProtoMsg(MiniAppAdaptShareInfoResp).decode(Buffer.from(ret.hex_data, 'hex')); - return JSON.parse(body.content.jsonContent) as MiniAppRawData; - } - - async sendFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType) : Promise { - const data = this.packetSession?.packer.packFetchAiVoiceListReq(groupUin, chatType); - const ret = await this.sendOidbPacket(data!, true); - const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body; - const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929D_0Resp).decode(body); - if (!resp.content) return null; - return resp.content.map((item) => { - return { - category: item.category, - voices: item.voices - }; - }); - } - - async sendAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType): Promise> { - let reqTime = 0; - const reqMaxTime = 30; - const sessionId = crypto.randomBytes(4).readUInt32BE(0); - while (true) { - if (reqTime >= reqMaxTime) { - throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`); - } - reqTime++; - const data = this.packetSession?.packer.packAiVoiceChatReq(groupUin, voiceId, text, chatType, sessionId); - const ret = await this.sendOidbPacket(data!, true); - const body = new NapProtoMsg(OidbSvcTrpcTcpBase).decode(Buffer.from(ret.hex_data, 'hex')); - if (body.errorCode) { - throw new Error(`sendAiVoiceChatReq retCode: ${body.errorCode} error: ${body.errorMsg}`); - } - const resp = new NapProtoMsg(OidbSvcTrpcTcp0X929B_0Resp).decode(body.body); - if (!resp.msgInfo) continue; - return resp.msgInfo; - } - } } 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..8b9eda02 --- /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"; + +export const LikeDetail = { + txt: ProtoField(1, ScalarType.STRING), + uin: ProtoField(3, ScalarType.INT64), + nickname: ProtoField(5, ScalarType.STRING) +}; + +export const LikeMsg = { + times: ProtoField(1, ScalarType.INT32), + time: ProtoField(2, ScalarType.INT32), + detail: ProtoField(3, () => LikeDetail) +}; + +export const ProfileLikeSubTip = { + msg: ProtoField(14, () => LikeMsg) +}; + +export const ProfileLikeTip = { + msgType: ProtoField(1, ScalarType.INT32), + subType: ProtoField(2, ScalarType.INT32), + content: ProtoField(203, () => ProfileLikeSubTip) +}; + +export const SysMessageHeader = { + PeerNumber: ProtoField(1, ScalarType.UINT32), + PeerString: ProtoField(2, ScalarType.STRING), + Uin: ProtoField(5, ScalarType.UINT32), + Uid: ProtoField(6, ScalarType.STRING, true) +}; + +export 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) +}; + +export const SysMessageBodyWrapper = { + wrappedBody: ProtoField(2, ScalarType.BYTES) +}; + +export 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/packet/client/client.ts b/src/core/packet/client/baseClient.ts similarity index 56% rename from src/core/packet/client/client.ts rename to src/core/packet/client/baseClient.ts index 0bbe7dc8..d7ee669d 100644 --- a/src/core/packet/client/client.ts +++ b/src/core/packet/client/baseClient.ts @@ -1,9 +1,7 @@ import { LRUCache } from "@/common/lru-cache"; -import { NapCatCore } from "@/core"; -import { LogWrapper } from "@/common/log"; import crypto, { createHash } from "crypto"; -import { OidbPacket, PacketHexStr } from "@/core/packet/packer"; -import { NapCatConfig } from "@/core/helper/config"; +import { PacketContext } from "@/core/packet/context/packetContext"; +import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base"; export interface RecvPacket { type: string, // 仅recv @@ -17,38 +15,28 @@ export interface RecvPacketData { hex_data: string } -export abstract class PacketClient { - readonly napCatCore: NapCatCore; - protected readonly logger: LogWrapper; +function randText(len: number): string { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < len; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +export abstract class IPacketClient { + protected readonly context: PacketContext; protected readonly cb = new LRUCache Promise>(500); // trace_id-type callback - protected isAvailable: boolean = false; - protected config: NapCatConfig; + available: boolean = false; - protected constructor(core: NapCatCore) { - this.napCatCore = core; - this.logger = core.context.logger; - this.config = core.configLoader.configData; + protected constructor(context: PacketContext) { + this.context = context; } - private randText(len: number): string { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < len; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; - } - - get available(): boolean { - return this.isAvailable; - } - - abstract check(core: NapCatCore): boolean; + abstract check(): boolean; abstract init(pid: number, recv: string, send: string): Promise; - abstract connect(cb: () => void): Promise; - abstract sendCommandImpl(cmd: string, data: string, trace_id: string): void; private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise): Promise { @@ -58,9 +46,14 @@ export abstract class PacketClient { private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => { }): Promise { 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) { @@ -68,30 +61,23 @@ export abstract class PacketClient { resolve(json); } }); + if (rsp) { this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => { clearTimeout(timeoutHandle); resolve(json); }); } + this.sendCommandImpl(cmd, data, trace_id); }); } async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise { - return new Promise((resolve, reject) => { - if (!this.available) { - this.logger.logError('NapCat.Packet 未初始化!'); - return undefined; - } - - const md5 = crypto.createHash('md5').update(data).digest('hex'); - const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2); - - this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => { - //console.log('sendPacket:', cmd, data, trace_id); - await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id); - }).then((res) => resolve(res)).catch((e: Error) => reject(e)); + const md5 = crypto.createHash('md5').update(data).digest('hex'); + const trace_id = (randText(4) + md5 + data).slice(0, data.length / 2); + return this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => { + await this.context.napcore.sendSsoCmdReqByContend(cmd, trace_id); }); } diff --git a/src/core/packet/client/nativeClient.ts b/src/core/packet/client/nativeClient.ts index cf2a6c43..a5398ff3 100644 --- a/src/core/packet/client/nativeClient.ts +++ b/src/core/packet/client/nativeClient.ts @@ -1,37 +1,36 @@ -import crypto, { createHash } from "crypto"; -import { NapCatCore } from "@/core"; +import { createHash } from "crypto"; import path, { dirname } from "path"; import { fileURLToPath } from "url"; import fs from "fs"; -import { PacketClient } from "@/core/packet/client/client"; +import { IPacketClient } from "@/core/packet/client/baseClient"; import { constants } from "node:os"; import { LRUCache } from "@/common/lru-cache"; -//0 send 1recv +import { PacketContext } from "@/core/packet/context/packetContext"; + +// 0 send 1 recv export interface NativePacketExportType { InitHook?: (recv: string, send: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean; SendPacket?: (cmd: string, data: string, trace_id: string) => void; } -export class NativePacketClient extends PacketClient { + +export class NativePacketClient extends IPacketClient { private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64']; private MoeHooExport: { exports: NativePacketExportType } = { exports: {} }; - private sendEvent = new LRUCache(500);//seq->trace_id - constructor(core: NapCatCore) { - super(core); - } + private sendEvent = new LRUCache(500); // seq->trace_id - get available(): boolean { - return this.isAvailable; + constructor(context: PacketContext) { + super(context); } check(): boolean { const platform = process.platform + '.' + process.arch; if (!this.supportedPlatforms.includes(platform)) { - this.logger.logWarn(`[Core] [Packet:Native] 不支持的平台: ${platform}`); + this.context.logger.warn(`不支持的平台: ${platform}`); return false; } const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node'); if (!fs.existsSync(moehoo_path)) { - this.logger.logWarn(`[Core] [Packet:Native] 缺失运行时文件: ${moehoo_path}`); + this.context.logger.warn(`[Core] [Packet:Native] 缺失运行时文件: ${moehoo_path}`); return false; } return true; @@ -54,27 +53,13 @@ export class NativePacketClient extends PacketClient { // console.log('callback:', callback, trace_id); callback?.({ seq, cmd, hex_data }); } - - // const callback = this.cb.get(createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex') + (type === 0 ? 'send' : 'recv')); - // if (callback) { - // callback({ seq, cmd, hex_data }); - // } else { - // this.logger.logError(`Callback not found for hex_data: ${hex_data}`); - // } - //console.log('type:', type, 'cmd:', cmd, 'trace_id:', trace_id); }); - this.isAvailable = true; + this.available = true; } sendCommandImpl(cmd: string, data: string, trace_id: string): void { const trace_id_md5 = createHash('md5').update(trace_id).digest('hex'); - //console.log('sendCommandImpl:', cmd, data, trace_id_md5); this.MoeHooExport.exports.SendPacket?.(cmd, data, trace_id_md5); this.cb.get(trace_id_md5 + 'send')?.({ seq: 0, cmd, hex_data: '' }); } - - connect(cb: () => void): Promise { - cb(); - return Promise.resolve(); - } } diff --git a/src/core/packet/client/wsClient.ts b/src/core/packet/client/wsClient.ts index 0111018a..69368c8d 100644 --- a/src/core/packet/client/wsClient.ts +++ b/src/core/packet/client/wsClient.ts @@ -1,98 +1,100 @@ import { Data, WebSocket } from "ws"; -import { NapCatCore } from "@/core"; -import { PacketClient, RecvPacket } from "@/core/packet/client/client"; +import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient"; +import { PacketContext } from "@/core/packet/context/packetContext"; -export class wsPacketClient extends PacketClient { - private websocket: WebSocket | undefined; +export class wsPacketClient extends IPacketClient { + private websocket: WebSocket | null = null; private reconnectAttempts: number = 0; private readonly maxReconnectAttempts: number = 60; // 现在暂时不可配置 - private readonly clientUrl: string | null = null; - private clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`; + private readonly clientUrl: string; + private readonly clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`; - constructor(core: NapCatCore) { - super(core); - this.clientUrl = this.config.packetServer ? this.clientUrlWrap( this.config.packetServer) : null; + private isInitialized: boolean = false; + private initPayload: { pid: number, recv: string, send: string } | null = null; + + constructor(context: PacketContext) { + super(context); + this.clientUrl = this.context.napcore.config.packetServer + ? this.clientUrlWrap(this.context.napcore.config.packetServer) + : this.clientUrlWrap('127.0.0.1:8083'); } check(): boolean { - if (!this.clientUrl) { - this.logger.logWarn(`[Core] [Packet:Server] 未配置服务器地址`); + if (!this.context.napcore.config.packetServer) { + this.context.logger.warn(`wsPacketClient 未配置服务器地址`); return false; } return true; } - connect(cb: () => void): 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)*/); + 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.context.logger.warn(`WebSocket 未连接,无法发送命令: ${cmd}`); + } + } + + private async connectWithRetry(): Promise { + while (this.reconnectAttempts < this.maxReconnectAttempts) { + try { + await this.connect(); + return; + } catch (error) { + this.reconnectAttempts++; + this.context.logger.warn(`第 ${this.reconnectAttempts}/${this.maxReconnectAttempts} 次尝试重连失败!`); + await this.delay(5000); + } + } + this.context.logger.error(`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.isAvailable = true; + this.available = true; this.reconnectAttempts = 0; - this.logger.log.bind(this.logger)(`[Core] [Packet:Server] 已连接到 ${this.clientUrl}`); - cb(); + this.context.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`); + if (!this.isInitialized && this.initPayload) { + this.websocket!.send(JSON.stringify({ + action: 'init', + ...this.initPayload + })); + this.isInitialized = true; + } resolve(); }; - - this.websocket.onerror = (error) => { - //this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`); - reject(new Error(`${error.message}`)); - }; - - this.websocket.onmessage = (event) => { - // const message = JSON.parse(event.data.toString()); - // console.log("Received message:", message); - this.handleMessage(event.data).then().catch(); - }; - this.websocket.onclose = () => { - this.isAvailable = false; - //this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`); - this.attemptReconnect(cb); + this.available = false; + this.context.logger.warn(`WebSocket 连接关闭,尝试重连...`); + reject(new Error('WebSocket 连接关闭')); + }; + this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => { + this.context.logger.error(`处理消息时出错: ${err}`); + }); + this.websocket.onerror = (error) => { + this.available = false; + this.context.logger.error(`WebSocket 出错: ${error.message}`); + this.websocket?.close(); + reject(error); }; }); } - private attemptReconnect(cb: any): void { - try { - if (this.reconnectAttempts < this.maxReconnectAttempts) { - this.reconnectAttempts++; - setTimeout(() => { - this.connect(cb).catch((error) => { - this.logger.logError.bind(this.logger)(`[Core] [Packet:Server] 尝试重连失败:${error.message}`); - }); - }, 5000 * this.reconnectAttempts); - } else { - this.logger.logError.bind(this.logger)(`[Core] [Packet:Server] ${this.clientUrl} 已达到最大重连次数!`); - } - } catch (error: any) { - this.logger.logError.bind(this.logger)(`[Core] [Packet:Server] 重连时出错: ${error.message}`); - } - } - - async init(pid: number, recv: string, send: string): Promise { - if (!this.isAvailable || !this.websocket) { - throw new Error("WebSocket is not connected"); - } - const initMessage = { - action: 'init', - pid: pid, - recv: recv, - send: send - }; - this.websocket.send(JSON.stringify(initMessage)); - } - - sendCommandImpl(cmd: string, data: string, trace_id: string) : void { - const commandMessage = { - action: 'send', - cmd: cmd, - data: data, - trace_id: trace_id - }; - this.websocket!.send(JSON.stringify(commandMessage)); + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); } private async handleMessage(message: Data): Promise { @@ -100,13 +102,10 @@ export class wsPacketClient extends PacketClient { const json: RecvPacket = JSON.parse(message.toString()); const trace_id_md5 = json.trace_id_md5; const action = json?.type ?? 'init'; - const event = this.cb.get(trace_id_md5 + action); - if (event) { - await event(json.data); - } - //console.log("Received message:", json); + const event = this.cb.get(`${trace_id_md5}${action}`); + if (event) await event(json.data); } catch (error) { - this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`); + this.context.logger.error(`解析ws消息时出错: ${(error as Error).message}`); } } } diff --git a/src/core/packet/clientSession.ts b/src/core/packet/clientSession.ts new file mode 100644 index 00000000..15dd77ed --- /dev/null +++ b/src/core/packet/clientSession.ts @@ -0,0 +1,27 @@ +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 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..6d6c372d --- /dev/null +++ b/src/core/packet/context/clientContext.ts @@ -0,0 +1,82 @@ +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"; + +type clientPriority = { + [key: number]: (context: PacketContext) => IPacketClient; +} + +const clientPriority: clientPriority = { + 10: (context: PacketContext) => new NativePacketClient(context), + 1: (context: PacketContext) => new wsPacketClient(context), +}; + +export class PacketClientContext { + private readonly _client: IPacketClient; + private readonly context: PacketContext; + + constructor(context: PacketContext) { + this.context = context; + this._client = this.newClient(); + } + + get available(): boolean { + return this._client.available; + } + + async init(pid: number, recv: string, send: string): Promise { + await this._client.init(pid, recv, send); + } + + async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { + console.log("REQ", pkt.cmd, pkt.data); + const raw = await this._client.sendOidbPacket(pkt, rsp); + console.log("RES", raw.cmd, raw.hex_data); + return Buffer.from(raw.hex_data, "hex"); + } + + 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); + break; + case "frida": + this.context.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端"); + client = new wsPacketClient(this.context); + break; + case "auto": + case undefined: + client = this.judgeClient(); + break; + default: + this.context.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`); + client = null; + } + if (!(client && client.check())) { + throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!"); + } + return client; + } + + private judgeClient(): IPacketClient { + const sortedClients = Object.entries(clientPriority) + .map(([priority, clientFactory]) => { + const client = clientFactory(this.context); + 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..8188a1e8 --- /dev/null +++ b/src/core/packet/context/operationContext.ts @@ -0,0 +1,165 @@ +import * as crypto from 'crypto'; +import { PacketContext } from "@/core/packet/context/packetContext"; +import * as trans from "@/core/packet/transformer"; +import { PacketMsg } from "@/core/packet/message/message"; +import { + PacketMsgFileElement, + PacketMsgPicElement, + PacketMsgPttElement, + PacketMsgVideoElement +} from "@/core/packet/message/element"; +import { ChatType } from "@/core"; +import { MiniAppRawData, MiniAppReqParams } from "@/core/packet/entities/miniApp"; +import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; +import { NapProtoDecodeStructType, NapProtoEncodeStructType } from "@napneko/nap-proto-core"; +import { IndexNode, MsgInfo } from "@/core/packet/transformer/proto"; + +export class PacketOperationContext { + private context: PacketContext; + constructor(context: PacketContext) { + this.context = context; + } + + async GroupPoke(groupUin: number, uin: number) { + const req = trans.SendPoke.build(groupUin, uin); + await this.context.client.sendOidbPacket(req); + } + + async FriendPoke(uin: number) { + const req = trans.SendPoke.build(uin); + await this.context.client.sendOidbPacket(req); + } + + async FetchRkey() { + const req = trans.FetchRkey.build(); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.FetchRkey.parse(resp); + return res.data.rkeyList; + } + + async GroupSign(groupUin: number) { + const req = trans.GroupSign.build(this.context.napcore.basicInfo.uin, groupUin); + await this.context.client.sendOidbPacket(req); + } + + async GetStrangerStatus(uin: number) { + let status = 0; + try { + const req = trans.GetStrangerInfo.build(uin); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.GetStrangerInfo.parse(resp); + const extBigInt = BigInt(res.data.status.value); + if (extBigInt <= 10n) { + return { status: Number(extBigInt) * 10, ext_status: 0 }; + } + status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); + return { status: 10, ext_status: status }; + } catch (e) { + return undefined; + } + } + + async SetGroupSpecialTitle(groupUin: number, uid: string, tittle: string) { + const req = trans.SetSpecialTitle.build(groupUin, uid, tittle); + await this.context.client.sendOidbPacket(req); + } + + async UploadResources(msg: PacketMsg[], groupUin: number = 0) { + const reqList = []; + for (const m of msg) { + for (const e of m.msg) { + if (e instanceof PacketMsgPicElement) { + reqList.push(this.context.highway.uploadImage({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid + }, e)); + } + if (e instanceof PacketMsgVideoElement) { + reqList.push(this.context.highway.uploadVideo({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid + }, e)); + } + if (e instanceof PacketMsgPttElement) { + reqList.push(this.context.highway.uploadPtt({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid + }, e)); + } + if (e instanceof PacketMsgFileElement) { + reqList.push(this.context.highway.uploadFile({ + chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, + peerUid: groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid + }, e)); + } + } + } + const res = await Promise.allSettled(reqList); + this.context.logger.info(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}个`); + res.forEach((result, index) => { + if (result.status === 'rejected') { + this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`); + } + }); + } + + async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) { + await this.UploadResources(msg, groupUin); + const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.UploadForwardMsg.parse(resp); + return res.result.resId; + } + + async GetGroupFileUrl(groupUin: number, fileUUID: string) { + const req = trans.DownloadGroupFile.build(groupUin, fileUUID); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.DownloadGroupFile.parse(resp); + return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`; + } + + // TODO: why type hint is not working here? + 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/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 61% rename from src/core/packet/highway/session.ts rename to src/core/packet/highway/highwayContext.ts index 8cc534d3..77e7b8ce 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 { PacketPacker } from "@/core/packet/packer"; -import { NapProtoMsg } from "@napneko/nap-proto-core"; -import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action"; import { PacketHighwayClient } from "@/core/packet/highway/client"; -import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; -import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase"; +import { PacketContext } from "@/core/packet/context/packetContext"; +import { PacketLogger } from "@/core/packet/context/loggerContext"; +import FetchSessionKey from "@/core/packet/transformer/highway/FetchSessionKey"; +import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils"; import { PacketMsgFileElement, PacketMsgPicElement, PacketMsgPttElement, PacketMsgVideoElement } from "@/core/packet/message/element"; -import { FileUploadExt, NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; -import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils"; +import { ChatType, Peer } from "@/core"; import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash"; -import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; -import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; -import { PacketClient } from "@/core/packet/client/client"; +import UploadGroupImage from "@/core/packet/transformer/highway/UploadGroupImage"; +import { NapProtoMsg } from "@napneko/nap-proto-core"; +import * as proto from "@/core/packet/transformer/proto"; +import * as trans from "@/core/packet/transformer"; +import fs from "fs"; export const BlockSize = 1024 * 1024; @@ -35,34 +32,28 @@ export interface PacketHighwaySig { serverAddr: HighwayServerAddr[] } -export class PacketHighwaySession { - protected packetClient: PacketClient; - protected packetHighwayClient: PacketHighwayClient; +export class PacketHighwayContext { + private context: PacketContext; protected sig: PacketHighwaySig; - protected logger: LogWrapper; - protected packer: PacketPacker; + protected logger: PacketLogger; + protected hwClient: PacketHighwayClient; private cachedPrepareReq: Promise | null = null; - constructor(logger: LogWrapper, client: PacketClient, packer: PacketPacker) { - this.packetClient = client; - this.logger = logger; + constructor(context: PacketContext) { + this.context = context; this.sig = { - uin: this.packetClient.napCatCore.selfInfo.uin, - uid: this.packetClient.napCatCore.selfInfo.uid, + uin: String(context.napcore.basicInfo.uin), + uid: context.napcore.basicInfo.uid, sigSession: null, sessionKey: null, serverAddr: [], }; - this.packer = packer; - this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger); + this.logger = context.logger; + this.hwClient = new PacketHighwayClient(this.sig, context.logger); } private async checkAvailable() { - if (!this.packetClient.available) { - throw new Error('packetBackend不可用,请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置!'); - } if (this.sig.sigSession === null || this.sig.sessionKey === null) { - this.logger.logWarn('[Highway] sigSession or sessionKey not available!'); if (this.cachedPrepareReq === null) { this.cachedPrepareReq = this.prepareUpload().finally(() => { this.cachedPrepareReq = null; @@ -73,17 +64,16 @@ export class PacketHighwaySession { } private async prepareUpload(): Promise { - 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: { @@ -245,7 +226,7 @@ export class PacketHighwaySession { fileSha1: await calculateSha1StreamBytes(video.filePath!) } }); - await this.packetHighwayClient.upload( + await this.hwClient.upload( 1005, fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }), +video.fileSize!, @@ -253,15 +234,15 @@ export class PacketHighwaySession { extend ); } else { - this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`); } const subFile = preRespData.upload.subFileInfos[0]; if (subFile.uKey && subFile.uKey != "") { - this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); + this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); const index = preRespData.upload.msgInfo.msgInfoBody[1].index; const md5 = Buffer.from(index.info.fileHash, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex'); - const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ fileUuid: index.fileUuid, uKey: subFile.uKey, network: { @@ -273,7 +254,7 @@ export class PacketHighwaySession { fileSha1: [sha1] } }); - await this.packetHighwayClient.upload( + await this.hwClient.upload( 1006, fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }), +video.thumbSize!, @@ -281,27 +262,24 @@ export class PacketHighwaySession { extend ); } else { - this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); } video.msgInfo = preRespData.upload.msgInfo; } - private async uploadC2CVideoReq(peerUid: string, video: PacketMsgVideoElement): Promise { + 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: { @@ -313,7 +291,7 @@ export class PacketHighwaySession { fileSha1: await calculateSha1StreamBytes(video.filePath!) } }); - await this.packetHighwayClient.upload( + await this.hwClient.upload( 1001, fs.createReadStream(video.filePath!, { highWaterMark: BlockSize }), +video.fileSize!, @@ -321,15 +299,15 @@ export class PacketHighwaySession { extend ); } else { - this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`); } const subFile = preRespData.upload.subFileInfos[0]; if (subFile.uKey && subFile.uKey != "") { - this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); + this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); const index = preRespData.upload.msgInfo.msgInfoBody[1].index; const md5 = Buffer.from(index.info.fileHash, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex'); - const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ fileUuid: index.fileUuid, uKey: subFile.uKey, network: { @@ -341,7 +319,7 @@ export class PacketHighwaySession { fileSha1: [sha1] } }); - await this.packetHighwayClient.upload( + await this.hwClient.upload( 1002, fs.createReadStream(video.thumbPath!, { highWaterMark: BlockSize }), +video.thumbSize!, @@ -349,26 +327,23 @@ export class PacketHighwaySession { extend ); } else { - this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); } video.msgInfo = preRespData.upload.msgInfo; } - private async uploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { + 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 34822df4..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 "@napneko/nap-proto-core"; -import { ReqDataHighwayHead, RespDataHighwayHead } from "@/core/packet/proto/highway/highway"; -import { BlockSize } from "@/core/packet/highway/session"; -import { PacketHighwayTrans } from "@/core/packet/highway/client"; -import { Frame } from "@/core/packet/highway/frame"; - -abstract class HighwayUploader { - readonly trans: PacketHighwayTrans; - readonly logger: LogWrapper; - - constructor(trans: PacketHighwayTrans, logger: LogWrapper) { - this.trans = trans; - this.logger = logger; - } - - private encryptTransExt(key: Uint8Array) { - if (!this.trans.encrypt) return; - this.trans.ext = tea.encrypt(Buffer.from(this.trans.ext), Buffer.from(key)); - } - - protected timeout(): Promise { - 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..a3411b90 --- /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) => { + reject(error); + }); + } catch (error) { + reject(error); + } + }); + } +} 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 383155d3..2a31af87 100644 --- a/src/core/packet/highway/utils.ts +++ b/src/core/packet/highway/utils.ts @@ -1,13 +1,13 @@ import { NapProtoEncodeStructType } from "@napneko/nap-proto-core"; -import { IPv4 } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp"; -import { NTHighwayIPv4 } from "@/core/packet/proto/highway/highway"; +import * as proto from "@/core/packet/transformer/proto"; + export const int32ip2str = (ip: number) => { ip = ip & 0xffffffff; return [ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, ((ip & 0xff000000) >> 24) & 0xff].join('.'); }; -export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType[]): NapProtoEncodeStructType[] =>{ +export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType[]): NapProtoEncodeStructType[] =>{ return ipv4s.map((ip) => { return { domain: { @@ -15,6 +15,6 @@ export const oidbIpv4s2HighwayIpv4s = (ipv4s: NapProtoEncodeStructType; + } as NapProtoEncodeStructType; }); }; diff --git a/src/core/packet/message/builder.ts b/src/core/packet/message/builder.ts index 12fa374b..8da0aaa4 100644 --- a/src/core/packet/message/builder.ts +++ b/src/core/packet/message/builder.ts @@ -1,21 +1,20 @@ import * as crypto from "crypto"; -import { PushMsgBody } from "@/core/packet/proto/message/message"; +import { PushMsgBody } from "@/core/packet/transformer/proto"; import { NapProtoEncodeStructType } from "@napneko/nap-proto-core"; -import { LogWrapper } from "@/common/log"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message"; import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/element"; import { SendTextElement } from "@/core"; export class PacketMsgBuilder { - private logger: LogWrapper; - - constructor(logger: LogWrapper) { - this.logger = logger; - } + // private logger: LogWrapper; + // + // constructor(logger: LogWrapper) { + // this.logger = logger; + // } protected static failBackText = new PacketMsgTextElement( { - textElement: { content: "[该消息类型暂不支持查看]" }! + textElement: { content: "[该消息类型暂不支持查看]" } } as SendTextElement ); @@ -27,7 +26,7 @@ export class PacketMsgBuilder { }, undefined); const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); if (!msgContent && !msgElement.length) { - this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement!`); + // 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..749c855e 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,50 +76,12 @@ export type rawMsgWithSendMsg = { msg: PacketSendMsgElement[] } +// TODO: make it become adapter? export class PacketMsgConverter { - private logger: LogWrapper; - - constructor(logger: LogWrapper) { - this.logger = logger; - } - private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters { return SupportedElementTypes.includes(type); } - rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg { - return { - senderUid: msg.senderUid ?? '', - senderUin: msg.senderUin, - senderName: msg.senderName, - groupId: msg.groupId, - time: msg.time, - msg: msg.msg.map((element) => { - if (!this.isValidElementType(element.elementType)) return null; - return this.rawToPacketMsgConverters[element.elementType](element as MessageElement); - }).filter((e) => e !== null) - }; - } - - rawMsgToPacketMsg(msg: RawMessage, ctxPeer: Peer): PacketMsg { - return { - seq: +msg.msgSeq, - groupId: ctxPeer.chatType === ChatType.KCHATTYPEGROUP ? +msg.peerUid : undefined, - senderUid: msg.senderUid, - senderUin: +msg.senderUin, - senderName: msg.sendMemberName && msg.sendMemberName !== '' - ? msg.sendMemberName - : msg.sendNickName && msg.sendNickName !== '' - ? msg.sendNickName - : "QQ用户", - time: +msg.msgTime, - msg: msg.elements.map((element) => { - if (!this.isValidElementType(element.elementType)) return null; - return this.rawToPacketMsgConverters[element.elementType](element); - }).filter((e) => e !== null) - }; - } - private rawToPacketMsgConverters: ElementToPacketMsgConverters = { [ElementType.TEXT]: (element) => { if (element.textElement?.atType) { @@ -160,4 +121,37 @@ export class PacketMsgConverter { return new PacketMultiMsgElement(element as SendStructLongMsgElement); } }; + + rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg { + return { + senderUid: msg.senderUid ?? '', + senderUin: msg.senderUin, + senderName: msg.senderName, + groupId: msg.groupId, + time: msg.time, + msg: msg.msg.map((element) => { + if (!this.isValidElementType(element.elementType)) return null; + return this.rawToPacketMsgConverters[element.elementType](element as MessageElement); + }).filter((e) => e !== null) + }; + } + + rawMsgToPacketMsg(msg: RawMessage, ctxPeer: Peer): PacketMsg { + return { + seq: +msg.msgSeq, + groupId: ctxPeer.chatType === ChatType.KCHATTYPEGROUP ? +msg.peerUid : undefined, + senderUid: msg.senderUid, + senderUin: +msg.senderUin, + senderName: msg.sendMemberName && msg.sendMemberName !== '' + ? msg.sendMemberName + : msg.sendNickName && msg.sendNickName !== '' + ? msg.sendNickName + : "QQ用户", + time: +msg.msgTime, + msg: msg.elements.map((element) => { + if (!this.isValidElementType(element.elementType)) return null; + return this.rawToPacketMsgConverters[element.elementType](element); + }).filter((e) => e !== null) + }; + } } diff --git a/src/core/packet/message/element.ts b/src/core/packet/message/element.ts index 9deb598d..8142f1cb 100644 --- a/src/core/packet/message/element.ts +++ b/src/core/packet/message/element.ts @@ -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 diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts deleted file mode 100644 index 039cf11e..00000000 --- a/src/core/packet/packer.ts +++ /dev/null @@ -1,803 +0,0 @@ -import * as zlib from "node:zlib"; -import * as crypto from "node:crypto"; -import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash"; -import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core"; -import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase"; -import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202"; -import { OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body } from "@/core/packet/proto/oidb/Oidb.0x8FC_2"; -import { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.0XFE1_2"; -import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.0xED3_1"; -import { IndexNode, NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; -import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action"; -import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action"; -import { PacketMsgBuilder } from "@/core/packet/message/builder"; -import { - PacketMsgFileElement, - PacketMsgPicElement, - PacketMsgPttElement, - PacketMsgVideoElement -} from "@/core/packet/message/element"; -import { LogWrapper } from "@/common/log"; -import { PacketMsg } from "@/core/packet/message/message"; -import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6"; -import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; -import { PacketMsgConverter } from "@/core/packet/message/converter"; -import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700"; -import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; -import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7"; -import { MiniAppReqParams } from "@/core/packet/entities/miniApp"; -import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo"; -import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; -import { OidbSvcTrpcTcp0X929B_0, OidbSvcTrpcTcp0X929D_0 } from "@/core/packet/proto/oidb/Oidb.0x929"; -import { PacketClient } from "@/core/packet/client/client"; - -export type PacketHexStr = string & { readonly hexNya: unique symbol }; - -export interface OidbPacket { - cmd: string; - data: PacketHexStr -} - -export class PacketPacker { - readonly logger: LogWrapper; - readonly client: PacketClient; - readonly packetBuilder: PacketMsgBuilder; - readonly packetConverter: PacketMsgConverter; - - constructor(logger: LogWrapper, client: PacketClient) { - this.logger = logger; - this.client = client; - this.packetBuilder = new PacketMsgBuilder(logger); - this.packetConverter = new PacketMsgConverter(logger); - } - - private packetPacket(byteArray: Uint8Array): PacketHexStr { - return Buffer.from(byteArray).toString('hex') as PacketHexStr; - } - - packOidbPacket(cmd: number, subCmd: number, body: Uint8Array, isUid: boolean = true, isLafter: boolean = false): OidbPacket { - const data = new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ - command: cmd, - subCommand: subCmd, - body: body, - isReserved: isUid ? 1 : 0 - }); - return { - cmd: `OidbSvcTrpcTcp.0x${cmd.toString(16).toUpperCase()}_${subCmd}`, - data: this.packetPacket(data) - }; - } - - packPokePacket(peer: number, group?: number): OidbPacket { - const oidb_0xed3 = new NapProtoMsg(OidbSvcTrpcTcp0XED3_1).encode({ - uin: peer, - groupUin: group, - friendUin: group ?? peer, - ext: 0 - }); - return this.packOidbPacket(0xed3, 1, oidb_0xed3); - } - - packRkeyPacket(): OidbPacket { - const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({ - reqHead: { - common: { - requestId: 1, - command: 202 - }, - scene: { - requestType: 2, - businessType: 1, - sceneType: 0 - }, - client: { - agentType: 2 - } - }, - downloadRKeyReq: { - key: [10, 20, 2] - }, - }); - return this.packOidbPacket(0x9067, 202, oidb_0x9067_202); - } - - packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket { - const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({ - targetUid: uid, - specialTitle: tittle, - expiredTime: -1, - uinName: tittle - }); - const oidb_0x8FC_2 = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2).encode({ - groupUin: +groupCode, - body: oidb_0x8FC_2_body - }); - return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false); - } - - packStatusPacket(uin: number): OidbPacket { - const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({ - uin: uin, - key: [{ key: 27372 }] - }); - return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2); - } - - async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise { - 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]) - }) - ); - } - - packGroupPttFileDownloadReq(groupUin: number, node: NapProtoEncodeStructType): OidbPacket { - return this.packOidbPacket(0x126E, 200, new NapProtoMsg(NTV2RichMediaReq).encode({ - reqHead: { - common: { - requestId: 4, - command: 200 - }, - scene: { - requestType: 1, - businessType: 3, - sceneType: 2, - group: { - groupUin: groupUin - } - }, - client: { - agentType: 2 - } - }, - download: { - node: node, - download: { - video: { - busiType: 0, - sceneType: 0, - } - } - } - }), true, false); - } - - packGroupSignReq(uin: string, groupCode: string): OidbPacket { - return this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode( - { - body: { - uin: uin, - groupUin: groupCode, - version: "9.0.90" - } - } - ), false, false); - } - - packMiniAppAdaptShareInfo(req: MiniAppReqParams): PacketHexStr { - return this.packetPacket( - new NapProtoMsg(MiniAppAdaptShareInfoReq).encode( - { - appId: req.sdkId, - body: { - extInfo: { - field2: Buffer.alloc(0) - }, - appid: req.appId, - title: req.title, - desc: req.desc, - time: BigInt(Date.now()), - scene: req.scene, - templateType: req.templateType, - businessType: req.businessType, - picUrl: req.picUrl, - vidUrl: "", - jumpUrl: req.jumpUrl, - iconUrl: req.iconUrl, - verType: req.verType, - shareType: req.shareType, - versionId: req.versionId, - withShareTicket: req.withShareTicket, - webURL: "", - appidRich: Buffer.alloc(0), - template: { - templateId: "", - templateData: "" - }, - field20: "" - } - } - ) - ); - } - - packFetchAiVoiceListReq(groupUin: number, chatType: AIVoiceChatType): OidbPacket { - return this.packOidbPacket(0x929D, 0, - new NapProtoMsg(OidbSvcTrpcTcp0X929D_0).encode({ - groupUin: groupUin, - chatType: chatType - }) - ); - } - - packAiVoiceChatReq(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType, sessionId: number): OidbPacket { - return this.packOidbPacket(0x929B, 0, - new NapProtoMsg(OidbSvcTrpcTcp0X929B_0).encode({ - groupUin: groupUin, - voiceId: voiceId, - text: text, - chatType: chatType, - session: { - sessionId: sessionId - } - }) - ); - } -} 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/service/base.ts b/src/core/packet/service/base.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/core/packet/session.ts b/src/core/packet/session.ts deleted file mode 100644 index 7886ee8d..00000000 --- a/src/core/packet/session.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { PacketHighwaySession } from "@/core/packet/highway/session"; -import { LogWrapper } from "@/common/log"; -import { PacketPacker } from "@/core/packet/packer"; -import { PacketClient } from "@/core/packet/client/client"; -import { NativePacketClient } from "@/core/packet/client/nativeClient"; -import { wsPacketClient } from "@/core/packet/client/wsClient"; -import { NapCatCore } from "@/core"; - -type clientPriority = { - [key: number]: (core: NapCatCore) => PacketClient; -} - -const clientPriority: clientPriority = { - 10: (core: NapCatCore) => new NativePacketClient(core), - 1: (core: NapCatCore) => new wsPacketClient(core), -}; - -export class PacketSession { - readonly logger: LogWrapper; - readonly client: PacketClient ; - readonly packer: PacketPacker; - readonly highwaySession: PacketHighwaySession; - - constructor(core: NapCatCore) { - this.logger = core.context.logger; - this.client = this.newClient(core); - this.packer = new PacketPacker(this.logger, this.client); - this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer); - } - - private newClient(core: NapCatCore): PacketClient { - const prefer = core.configLoader.configData.packetBackend; - let client: PacketClient | null; - switch (prefer) { - case "native": - this.logger.log("[Core] [Packet] 使用指定的 NativePacketClient 作为后端"); - client = new NativePacketClient(core); - break; - case "frida": - this.logger.log("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端"); - client = new wsPacketClient(core); - break; - case "auto": - case undefined: - client = this.judgeClient(core); - break; - default: - this.logger.logError(`[Core] [Packet] 未知的PacketBackend ${prefer},请检查配置文件!`); - client = null; - } - if (!(client && client.check(core))) { - throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!"); - } - return client; - } - - private judgeClient(core: NapCatCore): PacketClient { - const sortedClients = Object.entries(clientPriority) - .map(([priority, clientFactory]) => { - const client = clientFactory(core); - const score = +priority * +client.check(core); - return { client, score }; - }) - .filter(({ score }) => score > 0) - .sort((a, b) => b.score - a.score); - const selectedClient = sortedClients[0]?.client; - if (!selectedClient) { - throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!"); - } - this.logger.log(`[Core] [Packet] 自动选择 ${selectedClient.constructor.name} 作为后端`); - return selectedClient; - } -} 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..e9ee00b5 --- /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 98% rename from src/core/packet/proto/action/action.ts rename to src/core/packet/transformer/proto/action/action.ts index 78452bbc..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 "@napneko/nap-proto-core"; -import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/proto/message/message"; +import { ContentHead, MessageBody, MessageControl, RoutingHead } from "@/core/packet/transformer/proto"; export const FaceRoamRequest = { comm: ProtoField(1, () => PlatInfo, true), 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 0c137253..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 "@napneko/nap-proto-core"; +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 1e96074d..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 "@napneko/nap-proto-core"; -import { MsgInfo, MsgInfoBody } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { MsgInfoBody } from "@/core/packet/transformer/proto"; export const DataHighwayHead = { version: ProtoField(1, ScalarType.UINT32), 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 df26d42f..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 "@napneko/nap-proto-core"; -import { PushMsgBody } from "@/core/packet/proto/message/message"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { PushMsgBody } from "@/core/packet/transformer/proto"; export const LongMsgResult = { action: ProtoField(2, () => LongMsgAction) diff --git a/src/core/packet/proto/message/c2c.ts b/src/core/packet/transformer/proto/message/c2c.ts similarity index 76% rename from src/core/packet/proto/message/c2c.ts rename to src/core/packet/transformer/proto/message/c2c.ts index 6db5b646..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 "@napneko/nap-proto-core"; +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 04d0468d..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 "@napneko/nap-proto-core"; -import { Elem } from "@/core/packet/proto/message/element"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { Elem } from "@/core/packet/transformer/proto"; export const Attr = { codePage: ProtoField(1, ScalarType.INT32), 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 ed6a237f..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 "@napneko/nap-proto-core"; +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 82% rename from src/core/packet/proto/message/group.ts rename to src/core/packet/transformer/proto/message/group.ts index 2c8ef617..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 "@napneko/nap-proto-core"; +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 87% rename from src/core/packet/proto/message/message.ts rename to src/core/packet/transformer/proto/message/message.ts index 11a43665..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 "@napneko/nap-proto-core"; -import { ForwardHead, Grp, GrpTmp, ResponseForward, ResponseGrp, Trans0X211, WPATmp } from "@/core/packet/proto/message/routing"; -import { RichText } from "@/core/packet/proto/message/component"; -import { C2C } from "@/core/packet/proto/message/c2c"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { + C2C, + ForwardHead, + Grp, + GrpTmp, + ResponseForward, + ResponseGrp, RichText, + Trans0X211, + WPATmp +} from "@/core/packet/transformer/proto"; export const ContentHead = { type: ProtoField(1, ScalarType.UINT32), diff --git a/src/core/packet/proto/message/notify.ts b/src/core/packet/transformer/proto/message/notify.ts similarity index 87% rename from src/core/packet/proto/message/notify.ts rename to src/core/packet/transformer/proto/message/notify.ts index efedfd45..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 "@napneko/nap-proto-core"; +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 91% rename from src/core/packet/proto/message/routing.ts rename to src/core/packet/transformer/proto/message/routing.ts index b8d539b0..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 "@napneko/nap-proto-core"; +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 a8418e76..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 "@napneko/nap-proto-core"; -import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { OidbSvcTrpcTcp0XE37_800_1200Metadata } from "@/core/packet/transformer/proto"; export const OidbSvcTrpcTcp0XE37_800 = { subCommand: ProtoField(1, ScalarType.UINT32), 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 85% rename from src/core/packet/proto/oidb/Oidb.0XFE1_2.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0XFE1_2.ts index 13d7f428..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 "@napneko/nap-proto-core"; +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 2824035a..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 "@napneko/nap-proto-core"; +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 81% rename from src/core/packet/proto/oidb/Oidb.0x8FC_2.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts index c39cdbbe..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 "@napneko/nap-proto-core"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; //设置群头衔 OidbSvcTrpcTcp.0x8fc_2 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 88% rename from src/core/packet/proto/oidb/Oidb.0x9067_202.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0x9067_202.ts index c72fceef..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 "@napneko/nap-proto-core"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; import { MultiMediaReqHead } from "./common/Ntv2.RichMediaReq"; //Req diff --git a/src/core/packet/proto/oidb/Oidb.0x929.ts b/src/core/packet/transformer/proto/oidb/Oidb.0x929.ts similarity index 87% rename from src/core/packet/proto/oidb/Oidb.0x929.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0x929.ts index b401d13d..7bcd587f 100644 --- a/src/core/packet/proto/oidb/Oidb.0x929.ts +++ b/src/core/packet/transformer/proto/oidb/Oidb.0x929.ts @@ -1,6 +1,6 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "@napneko/nap-proto-core"; -import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; +import { MsgInfo } from "@/core/packet/transformer/proto"; + export const OidbSvcTrpcTcp0X929D_0 = { groupUin: ProtoField(1, ScalarType.UINT32), 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 969b36f2..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 "@napneko/nap-proto-core"; +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 89% rename from src/core/packet/proto/oidb/Oidb.0xE37_1700.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0xE37_1700.ts index 4c800f4e..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 "@napneko/nap-proto-core"; +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 72% rename from src/core/packet/proto/oidb/Oidb.0xEB7.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0xEB7.ts index 947a2268..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 "@napneko/nap-proto-core"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcp0XEB7_Body = { uin: ProtoField(1, ScalarType.STRING), 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 69% rename from src/core/packet/proto/oidb/Oidb.0xED3_1.ts rename to src/core/packet/transformer/proto/oidb/Oidb.0xED3_1.ts index c8d6af21..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 "@napneko/nap-proto-core"; +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 79% rename from src/core/packet/proto/oidb/OidbBase.ts rename to src/core/packet/transformer/proto/oidb/OidbBase.ts index fe1d1fab..fb5c94eb 100644 --- a/src/core/packet/proto/oidb/OidbBase.ts +++ b/src/core/packet/transformer/proto/oidb/OidbBase.ts @@ -1,5 +1,4 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "@napneko/nap-proto-core"; +import { ProtoField, ScalarType } from "@napneko/nap-proto-core"; export const OidbSvcTrpcTcpBase = { command: ProtoField(1, ScalarType.UINT32), 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 e279de3d..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 "@napneko/nap-proto-core"; +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 95% rename from src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts rename to src/core/packet/transformer/proto/oidb/common/Ntv2.RichMediaResp.ts index fe7ac659..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 "@napneko/nap-proto-core"; -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/helper/miniAppHelper.ts b/src/core/packet/utils/helper/miniAppHelper.ts similarity index 98% rename from src/core/packet/helper/miniAppHelper.ts rename to src/core/packet/utils/helper/miniAppHelper.ts index a65f4e3c..33a458eb 100644 --- a/src/core/packet/helper/miniAppHelper.ts +++ b/src/core/packet/utils/helper/miniAppHelper.ts @@ -4,7 +4,7 @@ import { MiniAppRawData, MiniAppReqCustomParams, MiniAppReqTemplateParams -} from "@/core/packet/entities/miniApp"; +} from "@/core/packet/client/entities/miniApp"; type MiniAppTemplateNameList = "bili" | "weibo"; diff --git a/src/onebot/action/extends/GetAiCharacters.ts b/src/onebot/action/extends/GetAiCharacters.ts index 95617f90..bc6e61e7 100644 --- a/src/onebot/action/extends/GetAiCharacters.ts +++ b/src/onebot/action/extends/GetAiCharacters.ts @@ -28,7 +28,7 @@ export class GetAiCharacters extends GetPacketStatusDepends ({ type: item.category, characters: item.voices.map((voice) => ({ diff --git a/src/onebot/action/extends/GetMiniAppArk.ts b/src/onebot/action/extends/GetMiniAppArk.ts index 6baafb80..26870a5b 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', @@ -77,7 +77,7 @@ export class GetMiniAppArk 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 { payloadSchema = SchemaData; async _handle(payload: Payload) { - return await this.core.apis.PacketApi.sendGroupSignPacket(payload.group_id.toString()); + return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id); } } export class SendGroupSign extends SetGroupSign { actionName = ActionName.SendGroupSign; -} \ No newline at end of file +} 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 { payloadSchema = SchemaData; async _handle(payload: Payload) { - const rawRsp = await this.core.apis.PacketApi.sendAiVoiceChatReq(+payload.group_id, payload.character, payload.text, AIVoiceChatType.Sound); - return await this.core.apis.PacketApi.sendGroupPttFileDownloadReq(+payload.group_id, rawRsp.msgInfoBody[0].index); + 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/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 index 6b227b50..a766c881 100644 --- a/src/onebot/action/group/SendGroupAiRecord.ts +++ b/src/onebot/action/group/SendGroupAiRecord.ts @@ -1,9 +1,9 @@ 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"; import { uri2local } from "@/common/file"; import { ChatType, Peer } from "@/core"; +import { AIVoiceChatType } from "@/core/packet/entities/aiChat"; const SchemaData = { type: 'object', @@ -24,8 +24,8 @@ export class SendGroupAiRecord extends GetPacketStatusDepends; class MarkMsgAsRead extends BaseAction { async getPeer(payload: PlayloadType): Promise { if (payload.message_id) { - let s_peer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)?.Peer; + const s_peer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)?.Peer; if (s_peer) { return s_peer; } - let l_peer = MessageUnique.getPeerByMsgId(payload.message_id.toString())?.Peer; + const l_peer = MessageUnique.getPeerByMsgId(payload.message_id.toString())?.Peer; if (l_peer) { return l_peer; } diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts index b46ac58d..dacc8350 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; @@ -192,7 +192,7 @@ export class SendMsg extends BaseAction { 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!); } else if (node.data.id) { @@ -205,7 +205,7 @@ 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!); } else { @@ -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: { diff --git a/src/onebot/action/packet/GetPacketStatus.ts b/src/onebot/action/packet/GetPacketStatus.ts index 2c611135..20f005c6 100644 --- a/src/onebot/action/packet/GetPacketStatus.ts +++ b/src/onebot/action/packet/GetPacketStatus.ts @@ -7,6 +7,7 @@ export abstract class GetPacketStatusDepends extends BaseAction protected async check(payload: PT): Promise{ if (!this.core.apis.PacketApi.available) { + // TODO: add error stack? return { valid: false, message: "packetBackend不可用,请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置!", 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/api/msg.ts b/src/onebot/api/msg.ts index bce77da1..f83aec82 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]: ( @@ -506,13 +501,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) => { @@ -892,6 +886,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 +895,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/index.ts b/src/onebot/index.ts index ecc17613..d84d299c 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -46,7 +46,6 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal 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'; //OneBot实现类 export class NapCatOneBot11Adapter { @@ -85,21 +84,22 @@ export class NapCatOneBot11Adapter { 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]); - } + // TODO: refactor! + // 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); } From 4520a20bd4a9467112930852da9411ab4f883cde Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Tue, 12 Nov 2024 04:08:51 +0800 Subject: [PATCH 2/8] chore: remove debug output --- src/core/packet/context/clientContext.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/packet/context/clientContext.ts b/src/core/packet/context/clientContext.ts index 6d6c372d..51dfefde 100644 --- a/src/core/packet/context/clientContext.ts +++ b/src/core/packet/context/clientContext.ts @@ -31,9 +31,7 @@ export class PacketClientContext { } async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { - console.log("REQ", pkt.cmd, pkt.data); const raw = await this._client.sendOidbPacket(pkt, rsp); - console.log("RES", raw.cmd, raw.hex_data); return Buffer.from(raw.hex_data, "hex"); } From 740d80e8510c300658ec8d2e3c902a2e3db863d6 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Tue, 12 Nov 2024 04:14:08 +0800 Subject: [PATCH 3/8] chore: minor adjust packet module --- src/core/packet/message/builder.ts | 7 ------- src/core/packet/service/base.ts | 0 2 files changed, 7 deletions(-) delete mode 100644 src/core/packet/service/base.ts diff --git a/src/core/packet/message/builder.ts b/src/core/packet/message/builder.ts index 8da0aaa4..ddbe3825 100644 --- a/src/core/packet/message/builder.ts +++ b/src/core/packet/message/builder.ts @@ -6,12 +6,6 @@ import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/message/e import { SendTextElement } from "@/core"; export class PacketMsgBuilder { - // private logger: LogWrapper; - // - // constructor(logger: LogWrapper) { - // this.logger = logger; - // } - protected static failBackText = new PacketMsgTextElement( { textElement: { content: "[该消息类型暂不支持查看]" } @@ -26,7 +20,6 @@ export class PacketMsgBuilder { }, undefined); const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); if (!msgContent && !msgElement.length) { - // this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement!`); msgElement.push(PacketMsgBuilder.failBackText.buildElement()); } return { diff --git a/src/core/packet/service/base.ts b/src/core/packet/service/base.ts deleted file mode 100644 index e69de29b..00000000 From 783a534768882e08cb2050602305fcc3a6b53daf Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 13 Nov 2024 14:16:42 +0800 Subject: [PATCH 4/8] fix: NativePacketClient --- src/core/packet/client/nativeClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/packet/client/nativeClient.ts b/src/core/packet/client/nativeClient.ts index a5398ff3..d5c3537f 100644 --- a/src/core/packet/client/nativeClient.ts +++ b/src/core/packet/client/nativeClient.ts @@ -9,12 +9,12 @@ import { PacketContext } from "@/core/packet/context/packetContext"; // 0 send 1 recv export interface NativePacketExportType { - InitHook?: (recv: string, send: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean; + InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean; SendPacket?: (cmd: string, data: string, trace_id: string) => void; } export class NativePacketClient extends IPacketClient { - private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64']; + private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64']; private MoeHooExport: { exports: NativePacketExportType } = { exports: {} }; private sendEvent = new LRUCache(500); // seq->trace_id From 49ec6181b0b37484ec041e9f25521f23536ce255 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 13 Nov 2024 15:35:07 +0800 Subject: [PATCH 5/8] fix: macos --- src/core/packet/context/operationContext.ts | 1 - src/native/packet/MoeHoo.darwin.arm64.node | Bin 1177416 -> 1177560 bytes src/native/packet/MoeHoo.darwin.x64.node | Bin 0 -> 120192 bytes 3 files changed, 1 deletion(-) create mode 100644 src/native/packet/MoeHoo.darwin.x64.node diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts index 8188a1e8..5e9afd72 100644 --- a/src/core/packet/context/operationContext.ts +++ b/src/core/packet/context/operationContext.ts @@ -118,7 +118,6 @@ export class PacketOperationContext { return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`; } - // TODO: why type hint is not working here? async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType) { const req = trans.DownloadGroupPtt.build(groupUin, node); const resp = await this.context.client.sendOidbPacket(req, true); diff --git a/src/native/packet/MoeHoo.darwin.arm64.node b/src/native/packet/MoeHoo.darwin.arm64.node index 9315302a60cf02be76715cd32c31e962689cec6f..829749c485892240ea4401b9ae82b889f35a1da5 100644 GIT binary patch delta 87981 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Ð9>bC$B%nIpl4egS;v_@vL@_OSWe>cVEx)S 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 R7Y<#k{)xC-IS@|pGiFbq!pHys delta 87492 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 P7Wk}eGB}K$^1RgF+-w;d diff --git a/src/native/packet/MoeHoo.darwin.x64.node b/src/native/packet/MoeHoo.darwin.x64.node new file mode 100644 index 0000000000000000000000000000000000000000..66cfff98de7cdd434926d0190d88971a7255774e GIT binary patch literal 120192 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 IVGGCq09mnWTL1t6 literal 0 HcmV?d00001 From 75866b435e03f33ea78bd4bb5d6a511471536c3d Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 13 Nov 2024 16:52:03 +0800 Subject: [PATCH 6/8] feat: errorStack --- src/common/log.ts | 5 +- src/core/apis/packet.ts | 26 +++++--- src/core/packet/client/baseClient.ts | 6 +- src/core/packet/client/nativeClient.ts | 9 +-- src/core/packet/client/wsClient.ts | 13 ++-- src/core/packet/clientSession.ts | 4 ++ src/core/packet/context/clientContext.ts | 61 ++++++++++++++++--- src/core/packet/utils/helper/miniAppHelper.ts | 2 +- src/onebot/action/packet/GetPacketStatus.ts | 3 +- src/onebot/index.ts | 6 +- src/shell/napcat.ts | 2 +- 11 files changed, 102 insertions(+), 35 deletions(-) diff --git a/src/common/log.ts b/src/common/log.ts index 50e04173..346baaed 100644 --- a/src/common/log.ts +++ b/src/common/log.ts @@ -148,6 +148,7 @@ export class LogWrapper { } 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, '')); } } @@ -226,7 +227,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string { ? rawMessageToText(recordMsgOrNull, recursiveLevel + 1) : `未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})` - }]`; + }]`; } if (element.picElement) { @@ -277,4 +278,4 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string { } return tokens.join(' '); -} \ No newline at end of file +} diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index fd3a94ba..c8c65021 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -19,34 +19,46 @@ export class NTQQPacketApi { core: NapCatCore; logger: LogWrapper; qqVersion: string | undefined; - pkt: PacketClientSession; + pkt!: PacketClientSession; + errStack: string[] = []; constructor(context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; this.logger = core.context.logger; - this.pkt = new PacketClientSession(core); this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion()) .then() - .catch(this.core.context.logger.logError.bind(this.core.context.logger)); + .catch((err) => { + this.logger.logError.bind(this.core.context.logger); + this.errStack.push(err); + }); } get available(): boolean { - return this.pkt?.available; + return this.pkt?.available ?? false; + } + + get clientLogStack() { + return this.pkt?.clientLogStack + '\n' + this.errStack.join('\n'); } async InitSendPacket(qqVer: string) { this.qqVersion = qqVer; const table = typedOffset[qqVer + '-' + os.arch()]; if (!table) { - this.logger.logError(`[Core] [Packet] PacketBackend 不支持当前QQ版本架构:${qqVer}-${os.arch()}, - 请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本!`); + const err = `[Core] [Packet] PacketBackend 不支持当前QQ版本架构:${qqVer}-${os.arch()}, + 请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本!`; + this.logger.logError(err); + this.errStack.push(err); return false; } if (this.core.configLoader.configData.packetBackend === 'disable') { - this.logger.logWarn('[Core] [Packet] 已禁用PacketBackend,NapCat.Packet将不会加载!'); + const err = '[Core] [Packet] 已禁用PacketBackend,NapCat.Packet将不会加载!'; + this.logger.logError(err); + this.errStack.push(err); return false; } + this.pkt = new PacketClientSession(this.core); await this.pkt.init(process.pid, table.recv, table.send); return true; } diff --git a/src/core/packet/client/baseClient.ts b/src/core/packet/client/baseClient.ts index d7ee669d..5516550a 100644 --- a/src/core/packet/client/baseClient.ts +++ b/src/core/packet/client/baseClient.ts @@ -2,6 +2,7 @@ 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 @@ -24,13 +25,16 @@ function randText(len: number): string { 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) { + protected constructor(context: PacketContext, logStack: LogStack) { this.context = context; + this.logStack = logStack; } abstract check(): boolean; diff --git a/src/core/packet/client/nativeClient.ts b/src/core/packet/client/nativeClient.ts index d5c3537f..abd1758b 100644 --- a/src/core/packet/client/nativeClient.ts +++ b/src/core/packet/client/nativeClient.ts @@ -6,6 +6,7 @@ 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 { @@ -18,19 +19,19 @@ export class NativePacketClient extends IPacketClient { private MoeHooExport: { exports: NativePacketExportType } = { exports: {} }; private sendEvent = new LRUCache(500); // seq->trace_id - constructor(context: PacketContext) { - super(context); + constructor(context: PacketContext, logStack: LogStack) { + super(context, logStack); } check(): boolean { const platform = process.platform + '.' + process.arch; if (!this.supportedPlatforms.includes(platform)) { - this.context.logger.warn(`不支持的平台: ${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.context.logger.warn(`[Core] [Packet:Native] 缺失运行时文件: ${moehoo_path}`); + this.logStack.pushLogWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`); return false; } return true; diff --git a/src/core/packet/client/wsClient.ts b/src/core/packet/client/wsClient.ts index 69368c8d..a4ffb0b3 100644 --- a/src/core/packet/client/wsClient.ts +++ b/src/core/packet/client/wsClient.ts @@ -1,6 +1,7 @@ import { Data, WebSocket } 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; @@ -12,8 +13,8 @@ export class wsPacketClient extends IPacketClient { private isInitialized: boolean = false; private initPayload: { pid: number, recv: string, send: string } | null = null; - constructor(context: PacketContext) { - super(context); + 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'); @@ -21,7 +22,7 @@ export class wsPacketClient extends IPacketClient { check(): boolean { if (!this.context.napcore.config.packetServer) { - this.context.logger.warn(`wsPacketClient 未配置服务器地址`); + this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`); return false; } return true; @@ -41,7 +42,7 @@ export class wsPacketClient extends IPacketClient { trace_id })); } else { - this.context.logger.warn(`WebSocket 未连接,无法发送命令: ${cmd}`); + this.logStack.pushLogWarn(`WebSocket 未连接,无法发送命令: ${cmd}`); } } @@ -52,11 +53,11 @@ export class wsPacketClient extends IPacketClient { return; } catch (error) { this.reconnectAttempts++; - this.context.logger.warn(`第 ${this.reconnectAttempts}/${this.maxReconnectAttempts} 次尝试重连失败!`); + this.logStack.pushLogWarn(`第 ${this.reconnectAttempts}/${this.maxReconnectAttempts} 次尝试重连失败!`); await this.delay(5000); } } - this.context.logger.error(`wsPacketClient 在 ${this.clientUrl} 达到最大重连次数 (${this.maxReconnectAttempts})!`); + this.logStack.pushLogError(`wsPacketClient 在 ${this.clientUrl} 达到最大重连次数 (${this.maxReconnectAttempts})!`); throw new Error(`无法连接到 WebSocket 服务器:${this.clientUrl}`); } diff --git a/src/core/packet/clientSession.ts b/src/core/packet/clientSession.ts index 15dd77ed..3faea314 100644 --- a/src/core/packet/clientSession.ts +++ b/src/core/packet/clientSession.ts @@ -12,6 +12,10 @@ export class PacketClientSession { return this.context.client.init(pid, recv, send); } + get clientLogStack() { + return this.context.client.clientLogStack; + } + get available() { return this.context.client.available; } diff --git a/src/core/packet/context/clientContext.ts b/src/core/packet/context/clientContext.ts index 51dfefde..fc9c1c8b 100644 --- a/src/core/packet/context/clientContext.ts +++ b/src/core/packet/context/clientContext.ts @@ -3,22 +3,61 @@ 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) => IPacketClient; + [key: number]: (context: PacketContext, logStack: LogStack) => IPacketClient; } const clientPriority: clientPriority = { - 10: (context: PacketContext) => new NativePacketClient(context), - 1: (context: PacketContext) => new wsPacketClient(context), + 10: (context: PacketContext, logStack: LogStack) => new NativePacketClient(context, logStack), + 1: (context: PacketContext, logStack: LogStack) => new wsPacketClient(context, logStack), }; +export class LogStack { + private stack: string[] = []; + private logger: PacketLogger; + + constructor(logger: PacketLogger) { + this.logger = logger; + } + + push(msg: string) { + this.stack.push(msg); + } + + pushLogInfo(msg: string) { + this.logger.info(msg); + this.stack.push(`${new Date().toISOString()} [INFO] ${msg}`); + } + + pushLogWarn(msg: string) { + this.logger.warn(msg); + this.stack.push(`${new Date().toISOString()} [WARN] ${msg}`); + } + + pushLogError(msg: string) { + this.logger.error(msg); + this.stack.push(`${new Date().toISOString()} [ERROR] ${msg}`); + } + + clear() { + this.stack = []; + } + + content() { + return this.stack.join('\n'); + } +} + export class PacketClientContext { - private readonly _client: IPacketClient; 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(); } @@ -26,13 +65,17 @@ export class PacketClientContext { 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 = false): Promise { + async sendOidbPacket(pkt: OidbPacket, rsp?: T): Promise { const raw = await this._client.sendOidbPacket(pkt, rsp); - return Buffer.from(raw.hex_data, "hex"); + return (rsp ? Buffer.from(raw.hex_data, "hex") : undefined) as T extends true ? Buffer : void; } private newClient(): IPacketClient { @@ -41,11 +84,11 @@ export class PacketClientContext { switch (prefer) { case "native": this.context.logger.info("使用指定的 NativePacketClient 作为后端"); - client = new NativePacketClient(this.context); + client = new NativePacketClient(this.context, this.logStack); break; case "frida": this.context.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端"); - client = new wsPacketClient(this.context); + client = new wsPacketClient(this.context, this.logStack); break; case "auto": case undefined: @@ -64,7 +107,7 @@ export class PacketClientContext { private judgeClient(): IPacketClient { const sortedClients = Object.entries(clientPriority) .map(([priority, clientFactory]) => { - const client = clientFactory(this.context); + const client = clientFactory(this.context, this.logStack); const score = +priority * +client.check(); return { client, score }; }) diff --git a/src/core/packet/utils/helper/miniAppHelper.ts b/src/core/packet/utils/helper/miniAppHelper.ts index 33a458eb..a65f4e3c 100644 --- a/src/core/packet/utils/helper/miniAppHelper.ts +++ b/src/core/packet/utils/helper/miniAppHelper.ts @@ -4,7 +4,7 @@ import { MiniAppRawData, MiniAppReqCustomParams, MiniAppReqTemplateParams -} from "@/core/packet/client/entities/miniApp"; +} from "@/core/packet/entities/miniApp"; type MiniAppTemplateNameList = "bili" | "weibo"; diff --git a/src/onebot/action/packet/GetPacketStatus.ts b/src/onebot/action/packet/GetPacketStatus.ts index 20f005c6..3e013245 100644 --- a/src/onebot/action/packet/GetPacketStatus.ts +++ b/src/onebot/action/packet/GetPacketStatus.ts @@ -10,7 +10,8 @@ export abstract class GetPacketStatusDepends extends BaseAction // TODO: add error stack? return { valid: false, - message: "packetBackend不可用,请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置!", + message: "packetBackend不可用,请参照文档 https://napneko.github.io/config/advanced 和启动日志检查packetBackend状态或进行配置!" + + "错误堆栈信息:" + this.core.apis.PacketApi.clientLogStack, }; } return await super.check(payload); diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 7f304a59..495619f6 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -83,7 +83,7 @@ export class NapCatOneBot11Adapter { async registerNative(core: NapCatCore, context: InstanceContext) { try { this.nativeCore = new Native(context.pathWrapper.binaryPath); - if (!this.nativeCore.inited) throw new Error('Native Not Init'); + // if (!this.nativeCore.inited) throw new Error('Native Not Init'); this.nativeCore.registerRecallCallback(async (hex: string) => { try { // TODO: refactor! @@ -346,10 +346,10 @@ export class NapCatOneBot11Adapter { } }; msgListener.onKickedOffLine = async (kick) => { - let event = new BotOfflineEvent(this.core, kick.tipsTitle, kick.tipsDesc); + 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), ); diff --git a/src/shell/napcat.ts b/src/shell/napcat.ts index f0d80eac..ff5f59d3 100644 --- a/src/shell/napcat.ts +++ b/src/shell/napcat.ts @@ -232,7 +232,7 @@ export async function NCoreInitShell() { logger.log(`可用于快速登录的 QQ:\n${historyLoginList .map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`) .join('\n') - }`); + }`); } loginService.getQRCodePicture(); } From f7f7e09cab2bb0325bc58bc55cd1f6d3da733638 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 13 Nov 2024 17:44:12 +0800 Subject: [PATCH 7/8] feat: sysMessage oldProto adapter --- src/core/helper/adaptDecoder.ts | 16 ++++---- src/core/helper/adaptSysMessageDecoder.ts | 49 +++++++++++++++++++++++ src/onebot/index.ts | 35 ++++++++-------- 3 files changed, 74 insertions(+), 26 deletions(-) create mode 100644 src/core/helper/adaptSysMessageDecoder.ts diff --git a/src/core/helper/adaptDecoder.ts b/src/core/helper/adaptDecoder.ts index 8b9eda02..f3910363 100644 --- a/src/core/helper/adaptDecoder.ts +++ b/src/core/helper/adaptDecoder.ts @@ -1,36 +1,36 @@ // TODO: further refactor in NapCat.Packet v2 import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core"; -export const LikeDetail = { +const LikeDetail = { txt: ProtoField(1, ScalarType.STRING), uin: ProtoField(3, ScalarType.INT64), nickname: ProtoField(5, ScalarType.STRING) }; -export const LikeMsg = { +const LikeMsg = { times: ProtoField(1, ScalarType.INT32), time: ProtoField(2, ScalarType.INT32), detail: ProtoField(3, () => LikeDetail) }; -export const ProfileLikeSubTip = { +const ProfileLikeSubTip = { msg: ProtoField(14, () => LikeMsg) }; -export const ProfileLikeTip = { +const ProfileLikeTip = { msgType: ProtoField(1, ScalarType.INT32), subType: ProtoField(2, ScalarType.INT32), content: ProtoField(203, () => ProfileLikeSubTip) }; -export const SysMessageHeader = { +const SysMessageHeader = { PeerNumber: ProtoField(1, ScalarType.UINT32), PeerString: ProtoField(2, ScalarType.STRING), Uin: ProtoField(5, ScalarType.UINT32), Uid: ProtoField(6, ScalarType.STRING, true) }; -export const SysMessageMsgSpec = { +const SysMessageMsgSpec = { msgType: ProtoField(1, ScalarType.UINT32), subType: ProtoField(2, ScalarType.UINT32), subSubType: ProtoField(3, ScalarType.UINT32), @@ -40,11 +40,11 @@ export const SysMessageMsgSpec = { other: ProtoField(13, ScalarType.UINT32) }; -export const SysMessageBodyWrapper = { +const SysMessageBodyWrapper = { wrappedBody: ProtoField(2, ScalarType.BYTES) }; -export const SysMessage = { +const SysMessage = { header: ProtoField(1, () => SysMessageHeader, false, true), msgSpec: ProtoField(2, () => SysMessageMsgSpec, false, true), bodyWrapper: ProtoField(3, () => SysMessageBodyWrapper) 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/onebot/index.ts b/src/onebot/index.ts index 495619f6..6b8986cc 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -46,7 +46,7 @@ import { OB11GroupRecallNoticeEvent } from '@/onebot/event/notice/OB11GroupRecal 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 { decodeMessage, decodeRecallGroup } from "@/core/helper/adaptSysMessageDecoder"; import { BotOfflineEvent } from './event/notice/BotOfflineEvent'; //OneBot实现类 @@ -83,25 +83,24 @@ export class NapCatOneBot11Adapter { async registerNative(core: NapCatCore, context: InstanceContext) { try { this.nativeCore = new Native(context.pathWrapper.binaryPath); - // if (!this.nativeCore.inited) throw new Error('Native Not Init'); + if (!this.nativeCore.inited) throw new Error('Native Not Init'); this.nativeCore.registerRecallCallback(async (hex: string) => { try { - // TODO: refactor! - // 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]); - // } + 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 && data?.msgHead?.noifyData?.innerData) { + 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); } From 2ea025047f59c557f63522dd3c606158d22d9377 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 13 Nov 2024 17:48:42 +0800 Subject: [PATCH 8/8] feat: comment out logic that is not currently needed --- src/onebot/index.ts | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 6b8986cc..712a1e01 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -75,7 +75,7 @@ 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.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)); @@ -84,27 +84,27 @@ export class NapCatOneBot11Adapter { 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 && data?.msgHead?.noifyData?.innerData) { - 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); - } - }); + // 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 && data?.msgHead?.noifyData?.innerData) { + // 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;