From e9332e7646b4fcd0ef4376ba1cf9bb75511eaa3c Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 23 Oct 2024 16:12:31 +0800 Subject: [PATCH] feat: add ptt msg pack & upload --- src/core/apis/packet.ts | 10 ++- src/core/packet/highway/session.ts | 89 ++++++++++++++++++- src/core/packet/msg/element.ts | 39 ++++++-- src/core/packet/packer.ts | 137 ++++++++++++++++++++++++++++- 4 files changed, 261 insertions(+), 14 deletions(-) diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index b1bdeed8..114b0d59 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -12,7 +12,7 @@ import { LogWrapper } from "@/common/log"; import { SendLongMsgResp } from "@/core/packet/proto/message/action"; import { PacketMsg } from "@/core/packet/msg/message"; import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6"; -import { PacketMsgPicElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, PacketMsgPttElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; interface OffsetType { @@ -111,7 +111,7 @@ export class NTQQPacketApi { await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true); } - private async uploadResources(msg: PacketMsg[], groupUin: number = 0) { + async uploadResources(msg: PacketMsg[], groupUin: number = 0) { const reqList = []; for (const m of msg) { for (const e of m.msg) { @@ -127,6 +127,12 @@ export class NTQQPacketApi { 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)); + } } } return Promise.all(reqList); diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index b13f30a7..cb4d5bd4 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -8,7 +8,7 @@ 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 { PacketMsgPicElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, PacketMsgPttElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; import { NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils"; import { calculateSha1StreamBytes } from "@/core/packet/utils/crypto/hash"; @@ -106,6 +106,17 @@ export class PacketHighwaySession { } } + async uploadPtt(peer: Peer, ptt: PacketMsgPttElement): Promise { + await this.checkAvailable(); + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupPttReq(Number(peer.peerUid), ptt); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CPttReq(peer.peerUid, ptt); + } else { + throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`); + } + } + private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise { const preReq = await this.packer.packUploadGroupImgReq(groupUin, img); const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true); @@ -313,4 +324,80 @@ export class PacketHighwaySession { } video.msgInfo = preRespData.upload.msgInfo; } + + private async uploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { + const preReq = await this.packer.packUploadGroupPttReq(groupUin, ptt); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x126e_100', preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[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({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }) + await this.packetHighwayClient.upload( + 1008, + fs.createReadStream(ptt.filePath, {highWaterMark: BlockSize}), + ptt.fileSize, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupPttReq get upload invalid ukey ${ukey}, don't need upload!`); + } + ptt.msgInfo = preRespData.upload.msgInfo; + } + + private async uploadC2CPttReq(peerUid: string, ptt: PacketMsgPttElement): Promise { + const preReq = await this.packer.packUploadC2CPttReq(peerUid, ptt); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x126d_100', preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.logDebug(`[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({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }) + await this.packetHighwayClient.upload( + 1007, + fs.createReadStream(ptt.filePath, {highWaterMark: BlockSize}), + ptt.fileSize, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CPttReq get upload invalid ukey ${ukey}, don't need upload!`); + } + ptt.msgInfo = preRespData.upload.msgInfo; + } } diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index 9f7cc562..e9693e48 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -25,9 +25,9 @@ import { SendTextElement, SendVideoElement } from "@/core"; -import { MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; -import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; -import { ForwardMsgBuilder } from "@/common/forward-msg-builder"; +import {MsgInfo} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +import {PacketMsg, PacketSendMsgElement} from "@/core/packet/msg/message"; +import {ForwardMsgBuilder} from "@/common/forward-msg-builder"; // raw <-> packet // TODO: SendStructLongMsgElement @@ -319,14 +319,39 @@ export class PacketMsgVideoElement extends IPacketMsgElement { } } -export class PacketMsgFileElement extends IPacketMsgElement { - constructor(element: SendFileElement) { +export class PacketMsgPttElement extends IPacketMsgElement { + filePath: string; + fileSize: number; + fileMd5: string; + fileDuration: number; + msgInfo: NapProtoEncodeStructType | null = null; + + constructor(element: SendPttElement) { super(element); + this.filePath = element.pttElement.filePath; + this.fileSize = +element.pttElement.fileSize; // TODO: cc + this.fileMd5 = element.pttElement.md5HexStr; + this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc + } + + buildElement(): NapProtoEncodeStructType[] { + assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); + return [{ + commonElem: { + serviceType: 48, + pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + businessType: 22, + } + }]; + } + + toPreview(): string { + return "[语音]"; } } -export class PacketMsgPttElement extends IPacketMsgElement { - constructor(element: SendPttElement) { +export class PacketMsgFileElement extends IPacketMsgElement { + constructor(element: SendFileElement) { super(element); } } diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index 087b7fd0..13e40846 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -11,7 +11,7 @@ import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMedia import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action"; import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action"; import { PacketMsgBuilder } from "@/core/packet/msg/builder"; -import { PacketMsgPicElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, PacketMsgPttElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; import { LogWrapper } from "@/common/log"; import { PacketMsg } from "@/core/packet/msg/message"; import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6"; @@ -97,7 +97,7 @@ export class PacketPacker { packStatusPacket(uin: number): PacketHexStr { const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({ uin: uin, - key: [{ key: 27372 }] + key: [{key: 27372}] }); return this.toHexStr(this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2)); } @@ -357,7 +357,7 @@ export class PacketPacker { clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), compatQMsgSceneType: 2, extBizInfo: { - pic : { + pic: { bizType: 0, textSummary: "Nya~", }, @@ -446,7 +446,7 @@ export class PacketPacker { clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), compatQMsgSceneType: 2, extBizInfo: { - pic : { + pic: { bizType: 0, textSummary: "Nya~", }, @@ -466,6 +466,135 @@ export class PacketPacker { return this.toHexStr(this.packOidbPacket(0x11E9, 100, req, true, false)); } + async packUploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { + const pttSha1 = await calculateSha1(ptt.filePath); + 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: this.toHexStr(pttSha1), + 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.toHexStr(this.packOidbPacket(0x126E, 100, req, true, false)); + } + + async packUploadC2CPttReq(peerUin: string, ptt: PacketMsgPttElement): Promise { + const pttSha1 = await calculateSha1(ptt.filePath); + 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: this.toHexStr(pttSha1), + 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.toHexStr(this.packOidbPacket(0x126D, 100, req, true, false)); + } + packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { return this.toHexStr( this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({