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); }