From 4082b651c5a5fa7aba03788da6ddb4113d91c791 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 23 Oct 2024 06:14:48 +0800 Subject: [PATCH 01/11] feat & fix: add video msg pack & upload, fix some bugs in uploading c2c elements --- src/core/apis/packet.ts | 10 +- src/core/packet/highway/session.ts | 157 ++++++++++++++- src/core/packet/msg/element.ts | 51 +++-- src/core/packet/packer.ts | 179 +++++++++++++++++- src/core/packet/utils/crypto/hash.ts | 19 ++ .../packet/utils/crypto/sha1Stream.test.ts | 19 ++ src/core/packet/utils/crypto/sha1Stream.ts | 118 ++++++++++++ .../utils/crypto/sha1StreamBytesTransform.ts | 53 ++++++ 8 files changed, 586 insertions(+), 20 deletions(-) create mode 100644 src/core/packet/utils/crypto/sha1Stream.test.ts create mode 100644 src/core/packet/utils/crypto/sha1Stream.ts create mode 100644 src/core/packet/utils/crypto/sha1StreamBytesTransform.ts diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index fb930e95..b1bdeed8 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 } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; interface OffsetType { @@ -118,7 +118,13 @@ export class NTQQPacketApi { if (e instanceof PacketMsgPicElement) { reqList.push(this.packetSession?.highwaySession.uploadImage({ chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, - peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid + 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)); } } diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index 54384840..b13f30a7 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -8,9 +8,10 @@ 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 } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, 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"; export const BlockSize = 1024 * 1024; @@ -94,6 +95,17 @@ export class PacketHighwaySession { } } + async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise { + await this.checkAvailable(); + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupVideoReq(Number(peer.peerUid), video); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CVideoReq(peer.peerUid, video); + } 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); @@ -103,7 +115,7 @@ export class PacketHighwaySession { const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const ukey = preRespData.upload.uKey; if (ukey && ukey != "") { - this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); + this.logger.logDebug(`[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'); @@ -121,13 +133,13 @@ export class PacketHighwaySession { }); await this.packetHighwayClient.upload( 1004, - fs.createReadStream(img.path, { highWaterMark: BlockSize }), + fs.createReadStream(img.path, {highWaterMark: BlockSize}), img.size, md5, extend ); } else { - this.logger.logDebug(`[Highway] get upload invalid ukey ${ukey}, don't need upload!`); + this.logger.logDebug(`[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) @@ -142,7 +154,7 @@ export class PacketHighwaySession { const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); const ukey = preRespData.upload.uKey; if (ukey && ukey != "") { - this.logger.logDebug(`[Highway] get upload ukey: ${ukey}, need upload!`); + this.logger.logDebug(`[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'); @@ -160,12 +172,145 @@ export class PacketHighwaySession { }); await this.packetHighwayClient.upload( 1003, - fs.createReadStream(img.path, { highWaterMark: BlockSize }), + fs.createReadStream(img.path, {highWaterMark: BlockSize}), img.size, md5, extend ); + } else { + this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`); } img.msgInfo = preRespData.upload.msgInfo; } + + private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { + const preReq = await this.packer.packUploadGroupVideoReq(groupUin, video); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11ea_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] 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({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: await calculateSha1StreamBytes(video.filePath!) + } + }) + await this.packetHighwayClient.upload( + 1005, + fs.createReadStream(video.filePath!, {highWaterMark: BlockSize}), + +video.fileSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + } + 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!`); + 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({ + fileUuid: index.fileUuid, + uKey: subFile.uKey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }); + await this.packetHighwayClient.upload( + 1006, + fs.createReadStream(video.thumbPath!, {highWaterMark: BlockSize}), + +video.thumbSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + } + video.msgInfo = preRespData.upload.msgInfo; + } + + private async uploadC2CVideoReq(peerUid: string, video: PacketMsgVideoElement): Promise { + const preReq = await this.packer.packUploadC2CVideoReq(peerUid, video); + const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11e9_100', preReq, true); + console.log(preRespRaw); + 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] 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({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: await calculateSha1StreamBytes(video.filePath!) + } + }) + await this.packetHighwayClient.upload( + 1001, + fs.createReadStream(video.filePath!, {highWaterMark: BlockSize}), + +video.fileSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`); + } + 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!`); + 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({ + fileUuid: index.fileUuid, + uKey: subFile.uKey, + network: { + ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }); + await this.packetHighwayClient.upload( + 1002, + fs.createReadStream(video.thumbPath!, {highWaterMark: BlockSize}), + +video.thumbSize!, + md5, + extend + ); + } else { + this.logger.logDebug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); + } + video.msgInfo = preRespData.upload.msgInfo; + } } diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index 81d1401f..9f7cc562 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -44,7 +44,7 @@ export abstract class IPacketMsgElement { } toPreview(): string { - return '[nya~]'; + return '[暂不支持该消息类型喵~]'; } } @@ -84,19 +84,15 @@ export class PacketMsgAtElement extends PacketMsgTextElement { text: { str: this.text, pbReserve: new NapProtoMsg(MentionExtra).encode({ - type: this.atAll ? 1 : 2, - uin: 0, - field5: 0, - uid: this.targetUid, - } + type: this.atAll ? 1 : 2, + uin: 0, + field5: 0, + uid: this.targetUid, + } ) } }]; } - - toPreview(): string { - return `@${this.targetUid} ${this.text}`; - } } export class PacketMsgPicElement extends IPacketMsgElement { @@ -189,7 +185,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement { } toPreview(): string { - return "[回复]"; + return "[回复消息]"; } } @@ -285,8 +281,41 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement { + fileSize?: string; + filePath?: string; + thumbSize?: number; + thumbPath?: string; + fileMd5?: string; + thumbMd5?: string; + thumbWidth?: number; + thumbHeight?: number; + msgInfo: NapProtoEncodeStructType | null = null; + constructor(element: SendVideoElement) { super(element); + this.fileSize = element.videoElement.fileSize; + this.filePath = element.videoElement.filePath; + this.thumbSize = element.videoElement.thumbSize; + this.thumbPath = element.videoElement.thumbPath?.get(0); + this.fileMd5 = element.videoElement.videoMd5 + this.thumbMd5 = element.videoElement.thumbMd5; + this.thumbWidth = element.videoElement.thumbWidth; + this.thumbHeight = element.videoElement.thumbHeight; + } + + buildElement(): NapProtoEncodeStructType[] { + assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); + return [{ + commonElem: { + serviceType: 48, + pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + businessType: 21, + } + }]; + } + + toPreview(): string { + return "[视频]"; } } diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index cb53db5c..087b7fd0 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 } from "@/core/packet/msg/element"; +import { PacketMsgPicElement, 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"; @@ -289,6 +289,183 @@ export class PacketPacker { return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false)); } + async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { + if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); + if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); + const videoSha1 = await calculateSha1(video.filePath ?? ""); + const videoThumbSha1 = await calculateSha1(video.thumbPath ?? ""); + 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: this.toHexStr(videoSha1), + 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: this.toHexStr(videoThumbSha1), + 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.toHexStr(this.packOidbPacket(0x11EA, 100, req, true, false)); + } + + async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise { + if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); + if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); + const videoSha1 = await calculateSha1(video.filePath ?? ""); + const videoThumbSha1 = await calculateSha1(video.thumbPath ?? ""); + 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: this.toHexStr(videoSha1), + 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: this.toHexStr(videoThumbSha1), + 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.toHexStr(this.packOidbPacket(0x11E9, 100, req, true, false)); + } + packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { return this.toHexStr( this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ diff --git a/src/core/packet/utils/crypto/hash.ts b/src/core/packet/utils/crypto/hash.ts index 53901e6c..51eaf4ec 100644 --- a/src/core/packet/utils/crypto/hash.ts +++ b/src/core/packet/utils/crypto/hash.ts @@ -2,6 +2,7 @@ import * as crypto from 'crypto'; import * as stream from 'stream'; import * as fs from 'fs'; +import {CalculateStreamBytesTransform} from "@/core/packet/utils/crypto/sha1StreamBytesTransform"; function sha1Stream(readable: stream.Readable) { return new Promise((resolve, reject) => { @@ -14,3 +15,21 @@ export function calculateSha1(filePath: string): Promise { const readable = fs.createReadStream(filePath); return sha1Stream(readable); } + +export function calculateSha1StreamBytes(filePath: string): Promise { + return new Promise((resolve, reject) => { + const readable = fs.createReadStream(filePath); + const calculateStreamBytes = new CalculateStreamBytesTransform(); + const byteArrayList: Buffer[] = []; + calculateStreamBytes.on('data', (chunk: Buffer) => { + byteArrayList.push(chunk); + }); + calculateStreamBytes.on('end', () => { + resolve(byteArrayList); + }); + calculateStreamBytes.on('error', (err) => { + reject(err); + }); + readable.pipe(calculateStreamBytes); + }); +} diff --git a/src/core/packet/utils/crypto/sha1Stream.test.ts b/src/core/packet/utils/crypto/sha1Stream.test.ts new file mode 100644 index 00000000..b8c9ef7f --- /dev/null +++ b/src/core/packet/utils/crypto/sha1Stream.test.ts @@ -0,0 +1,19 @@ +import crypto from 'crypto'; +import assert from 'assert'; +import { Sha1Stream } from './sha1Stream'; + +function testSha1Stream() { + for (let i = 0; i < 100000; i++) { + const randomLength = Math.floor(Math.random() * 1024); + const randomData = crypto.randomBytes(randomLength); + const sha1Stream = new Sha1Stream(); + sha1Stream.update(randomData); + const hash = sha1Stream.final(); + const expectedDigest = crypto.createHash('sha1').update(randomData).digest(); + assert.strictEqual(hash.toString('hex'), expectedDigest.toString('hex')); + console.log(`Test ${i + 1}: Passed`); + } + console.log('All tests passed successfully.'); +} + +testSha1Stream(); diff --git a/src/core/packet/utils/crypto/sha1Stream.ts b/src/core/packet/utils/crypto/sha1Stream.ts new file mode 100644 index 00000000..f1c344df --- /dev/null +++ b/src/core/packet/utils/crypto/sha1Stream.ts @@ -0,0 +1,118 @@ +export class Sha1Stream { + readonly Sha1BlockSize = 64; + readonly Sha1DigestSize = 20; + private readonly _padding = Buffer.concat([Buffer.from([0x80]), Buffer.alloc(63)]); + private readonly _state = new Uint32Array(5); + private readonly _count = new Uint32Array(2); + private readonly _buffer = Buffer.allocUnsafe(this.Sha1BlockSize); + private readonly _w = new Uint32Array(80); + + constructor() { + this.reset(); + } + + private reset(): void { + this._state[0] = 0x67452301; + this._state[1] = 0xEFCDAB89; + this._state[2] = 0x98BADCFE; + this._state[3] = 0x10325476; + this._state[4] = 0xC3D2E1F0; + this._count[0] = 0; + this._count[1] = 0; + this._buffer.fill(0); + } + + private rotateLeft(v: number, o: number): number { + return ((v << o) | (v >>> (32 - o))) >>> 0; + } + + private transform(chunk: Buffer, offset: number): void { + const w = this._w; + const view = new DataView(chunk.buffer, chunk.byteOffset + offset, 64); + + for (let i = 0; i < 16; i++) { + w[i] = view.getUint32(i * 4, false); + } + + for (let i = 16; i < 80; i++) { + w[i] = this.rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1) >>> 0; + } + + let a = this._state[0]; + let b = this._state[1]; + let c = this._state[2]; + let d = this._state[3]; + let e = this._state[4]; + + for (let i = 0; i < 80; i++) { + const [f, k] = (i < 20) ? [(b & c) | ((~b) & d), 0x5A827999] : + (i < 40) ? [b ^ c ^ d, 0x6ED9EBA1] : + (i < 60) ? [(b & c) | (b & d) | (c & d), 0x8F1BBCDC] : + [b ^ c ^ d, 0xCA62C1D6]; + const temp = (this.rotateLeft(a, 5) + f + k + e + w[i]) >>> 0; + e = d; + d = c; + c = this.rotateLeft(b, 30) >>> 0; + b = a; + a = temp; + } + + this._state[0] = (this._state[0] + a) >>> 0; + this._state[1] = (this._state[1] + b) >>> 0; + this._state[2] = (this._state[2] + c) >>> 0; + this._state[3] = (this._state[3] + d) >>> 0; + this._state[4] = (this._state[4] + e) >>> 0; + } + + public update(data: Buffer, len?: number): void { + let index = ((this._count[0] >>> 3) & 0x3F) >>> 0; + const dataLen = len ?? data.length; + this._count[0] = (this._count[0] + (dataLen << 3)) >>> 0; + + if (this._count[0] < (dataLen << 3)) this._count[1] = (this._count[1] + 1) >>> 0; + + this._count[1] = (this._count[1] + (dataLen >>> 29)) >>> 0; + + let partLen = (this.Sha1BlockSize - index) >>> 0; + let i = 0; + + if (dataLen >= partLen) { + data.copy(this._buffer, index, 0, partLen); + this.transform(this._buffer, 0); + for (i = partLen; (i + this.Sha1BlockSize) <= dataLen; i = (i + this.Sha1BlockSize) >>> 0) { + this.transform(data, i); + } + index = 0; + } + + data.copy(this._buffer, index, i, dataLen); + } + + public hash(bigEndian: boolean = true): Buffer { + const digest = Buffer.allocUnsafe(this.Sha1DigestSize); + if (bigEndian) { + for (let i = 0; i < 5; i++) digest.writeUInt32BE(this._state[i], i * 4); + } else { + for (let i = 0; i < 5; i++) digest.writeUInt32LE(this._state[i], i * 4); + } + return digest; + } + + public final(): Buffer { + const digest = Buffer.allocUnsafe(this.Sha1DigestSize); + const bits = Buffer.allocUnsafe(8) + bits.writeUInt32BE(this._count[1], 0); + bits.writeUInt32BE(this._count[0], 4); + + let index = ((this._count[0] >>> 3) & 0x3F) >>> 0; + const padLen = ((index < 56) ? (56 - index) : (120 - index)) >>> 0; + this.update(this._padding, padLen); + this.update(bits); + + for (let i = 0; i < 5; i++) { + digest.writeUInt32BE(this._state[i], i * 4); + } + + return digest; + } +} diff --git a/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts new file mode 100644 index 00000000..38fd66eb --- /dev/null +++ b/src/core/packet/utils/crypto/sha1StreamBytesTransform.ts @@ -0,0 +1,53 @@ +import * as stream from "node:stream"; +import {Sha1Stream} from "@/core/packet/utils/crypto/sha1Stream"; + +export class CalculateStreamBytesTransform extends stream.Transform { + private readonly blockSize = 1024 * 1024; + private sha1: Sha1Stream; + private buffer: Buffer; + private bytesRead: number; + private readonly byteArrayList: Buffer[]; + + constructor() { + super(); + this.sha1 = new Sha1Stream(); + this.buffer = Buffer.alloc(0); + this.bytesRead = 0; + this.byteArrayList = []; + } + + _transform(chunk: Buffer, _: BufferEncoding, callback: stream.TransformCallback): void { + try { + this.buffer = Buffer.concat([this.buffer, chunk]); + let offset = 0; + while (this.buffer.length - offset >= this.sha1.Sha1BlockSize) { + const block = this.buffer.subarray(offset, offset + this.sha1.Sha1BlockSize); + this.sha1.update(block); + offset += this.sha1.Sha1BlockSize; + this.bytesRead += this.sha1.Sha1BlockSize; + if (this.bytesRead % this.blockSize === 0) { + const digest = this.sha1.hash(false); + this.byteArrayList.push(Buffer.from(digest)); + } + } + this.buffer = this.buffer.subarray(offset); + callback(null); + } catch (err) { + callback(err as Error); + } + } + + _flush(callback: stream.TransformCallback): void { + try { + if (this.buffer.length > 0) this.sha1.update(this.buffer); + const finalDigest = this.sha1.final(); + this.byteArrayList.push(Buffer.from(finalDigest)); + for (const digest of this.byteArrayList) { + this.push(digest); + } + callback(null); + } catch (err) { + callback(err as Error); + } + } +} From e9332e7646b4fcd0ef4376ba1cf9bb75511eaa3c Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 23 Oct 2024 16:12:31 +0800 Subject: [PATCH 02/11] 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({ From 5cca8457e7518ba5a2c7ae4e496da28d063bf7a2 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 23 Oct 2024 16:33:05 +0800 Subject: [PATCH 03/11] =?UTF-8?q?chore:=20=E6=9C=89=E7=AC=A8=E8=9B=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/apis/packet.ts | 3 ++- src/core/packet/highway/uploader.ts | 2 ++ src/core/packet/packer.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 114b0d59..7e65f930 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -111,6 +111,7 @@ export class NTQQPacketApi { await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true); } + // TODO: can simplify this async uploadResources(msg: PacketMsg[], groupUin: number = 0) { const reqList = []; for (const m of msg) { @@ -135,7 +136,7 @@ export class NTQQPacketApi { } } } - return Promise.all(reqList); + return Promise.all(reqList); // TODO: use promise.allSettled } async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) { diff --git a/src/core/packet/highway/uploader.ts b/src/core/packet/highway/uploader.ts index 4b4a864f..d19dd2f7 100644 --- a/src/core/packet/highway/uploader.ts +++ b/src/core/packet/highway/uploader.ts @@ -94,6 +94,7 @@ export class HighwayTcpUploader extends HighwayUploader { const handleRspHeader = (header: Buffer) => { const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header); if (rsp.errorCode !== 0) { + // TODO: immediately reject promise if error code is not 0 this.logger.logWarn(`[Highway] tcpUpload failed (code: ${rsp.errorCode})`); } const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2); @@ -159,6 +160,7 @@ export class HighwayHttpUploader extends HighwayUploader { 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) { + // TODO: immediately throw error if error code is not 0 this.logger.logError(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`); } } diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index 13e40846..b3f8dd74 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -34,6 +34,7 @@ export class PacketPacker { this.packetConverter = new PacketMsgConverter(logger); } + // TODO: 一步到位多好ww private toHexStr(byteArray: Uint8Array): PacketHexStr { return Buffer.from(byteArray).toString('hex') as PacketHexStr; } From 9f8f938c4724fb069b67913a624e80bceeee366c Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Thu, 24 Oct 2024 04:53:41 +0800 Subject: [PATCH 04/11] feat: build & upload file --- src/core/apis/packet.ts | 13 +- src/core/packet/highway/session.ts | 164 +++++++++++++++++- src/core/packet/msg/builder.ts | 9 +- src/core/packet/msg/element.ts | 83 ++++++++- src/core/packet/packer.ts | 70 +++++++- src/core/packet/proto/message/component.ts | 23 ++- src/core/packet/proto/oidb/Oidb.0XE37_800.ts | 62 +++++++ src/core/packet/proto/oidb/Oidb.0xE37_1200.ts | 4 +- src/core/packet/proto/oidb/Oidb.0xE37_1700.ts | 23 +++ src/core/packet/utils/crypto/hash.ts | 12 ++ 10 files changed, 449 insertions(+), 14 deletions(-) create mode 100644 src/core/packet/proto/oidb/Oidb.0XE37_800.ts create mode 100644 src/core/packet/proto/oidb/Oidb.0xE37_1700.ts diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 7e65f930..d381c225 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -12,7 +12,12 @@ 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, PacketMsgPttElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; +import { + PacketMsgFileElement, + PacketMsgPicElement, + PacketMsgPttElement, + PacketMsgVideoElement +} from "@/core/packet/msg/element"; interface OffsetType { @@ -134,6 +139,12 @@ export class NTQQPacketApi { 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)); + } } } return Promise.all(reqList); // TODO: use promise.allSettled diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index cb4d5bd4..b5b2b178 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -8,10 +8,17 @@ 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, PacketMsgPttElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; -import { NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; +import { + PacketMsgFileElement, + PacketMsgPicElement, + PacketMsgPttElement, + PacketMsgVideoElement +} from "@/core/packet/msg/element"; +import { FileUploadExt, NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway"; import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils"; -import { calculateSha1StreamBytes } from "@/core/packet/utils/crypto/hash"; +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"; export const BlockSize = 1024 * 1024; @@ -22,6 +29,7 @@ interface HighwayServerAddr { export interface PacketHighwaySig { uin: string; + uid: string; sigSession: Uint8Array | null sessionKey: Uint8Array | null serverAddr: HighwayServerAddr[] @@ -40,6 +48,7 @@ export class PacketHighwaySession { this.logger = logger; this.sig = { uin: this.packetClient.napCatCore.selfInfo.uin, + uid: this.packetClient.napCatCore.selfInfo.uid, sigSession: null, sessionKey: null, serverAddr: [], @@ -117,6 +126,17 @@ export class PacketHighwaySession { } } + async uploadFile(peer: Peer, file: PacketMsgFileElement): Promise { + await this.checkAvailable(); + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupFileReq(Number(peer.peerUid), file); + } else if (peer.chatType === ChatType.KCHATTYPEC2C) { + await this.uploadC2CFileReq(peer.peerUid, file); + } 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); @@ -400,4 +420,142 @@ export class PacketHighwaySession { } ptt.msgInfo = preRespData.upload.msgInfo; } + + private async uploadGroupFileReq(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.sendPacket('OidbSvcTrpcTcp.0x6d6_0', preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(preResp.body); + if (!preRespData?.upload?.boolFileExist) { + this.logger.logDebug(`[Highway] uploadGroupFileReq file not exist, need upload!`); + const ext = new NapProtoMsg(FileUploadExt).encode({ + unknown1: 100, + unknown2: 1, + entry: { + busiBuff: { + senderUin: BigInt(this.sig.uin), + receiverUin: BigInt(groupUin), + groupCode: BigInt(groupUin), + }, + fileEntry: { + fileSize: BigInt(file.fileSize), + md5: file.fileMd5, + md5S2: file.fileMd5, + checkKey: preRespData.upload.checkKey, + fileId: preRespData.upload.fileId, + uploadKey: preRespData.upload.fileKey, + }, + clientInfo: { + clientType: 3, + appId: "100", + terminalType: 3, + clientVer: "1.1.1", + unknown: 4 + }, + fileNameInfo: { + fileName: file.fileName + }, + host: { + hosts: [ + { + url: { + host: preRespData.upload.uploadIp, + unknown: 1, + }, + port: preRespData.upload.uploadPort, + } + ] + } + }, + unknown200: 0, + }) + await this.packetHighwayClient.upload( + 71, + fs.createReadStream(file.filePath, {highWaterMark: BlockSize}), + file.fileSize, + file.fileMd5, + ext + ); + } else { + this.logger.logDebug(`[Highway] uploadGroupFileReq file exist, don't need upload!`); + } + file.fileUuid = preRespData.upload.fileId; + } + + private async uploadC2CFileReq(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.sendPacket('OidbSvcTrpcTcp.0xe37_1700', preReq, true); + const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + console.log("OidbSvcTrpcTcp.0xe37_1700", preRespRaw); + const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0XE37Response).decode(preResp.body); + if (!preRespData.upload?.boolFileExist) { + this.logger.logDebug(`[Highway] uploadC2CFileReq file not exist, need upload!`); + const ext = new NapProtoMsg(FileUploadExt).encode({ + unknown1: 100, + unknown2: 1, + entry: { + busiBuff: { + senderUin: BigInt(this.sig.uin), + }, + fileEntry: { + fileSize: BigInt(file.fileSize), + md5: file.fileMd5, + md5S2: file.fileMd5, + checkKey: file.fileSha1, + fileId: preRespData.upload?.uuid, + uploadKey: preRespData.upload?.mediaPlatformUploadKey, + }, + clientInfo: { + clientType: 3, + appId: "100", + terminalType: 3, + clientVer: "1.1.1", + unknown: 4 + }, + fileNameInfo: { + fileName: file.fileName + }, + host: { + hosts: [ + { + url: { + host: preRespData.upload?.uploadIp, + unknown: 1, + }, + port: preRespData.upload?.uploadPort, + } + ] + } + }, + unknown200: 1, + unknown3: 0 + }) + await this.packetHighwayClient.upload( + 95, + fs.createReadStream(file.filePath, {highWaterMark: BlockSize}), + file.fileSize, + file.fileMd5, + ext + ); + } + 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.sendPacket('OidbSvcTrpcTcp.0xe37_800', FetchExistFileReq, true); + console.log("OidbSvcTrpcTcp.0xe37_800", resp); + 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); + file._private_send_uid = this.sig.uid; + file._private_recv_uid = peerUid; + } } diff --git a/src/core/packet/msg/builder.ts b/src/core/packet/msg/builder.ts index 97736ae2..ea6a1d3c 100644 --- a/src/core/packet/msg/builder.ts +++ b/src/core/packet/msg/builder.ts @@ -2,7 +2,8 @@ import * as crypto from "crypto"; import { PushMsgBody } from "@/core/packet/proto/message/message"; import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; import { LogWrapper } from "@/common/log"; -import { PacketMsg } from "@/core/packet/msg/message"; +import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; +import { IPacketMsgElement } from "@/core/packet/msg/element"; export class PacketMsgBuilder { private logger: LogWrapper; @@ -14,6 +15,9 @@ export class PacketMsgBuilder { buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType[] { return element.map((node): NapProtoEncodeStructType => { const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`; + const msgContent = node.msg.reduceRight((acc: undefined | Uint8Array, msg: IPacketMsgElement) => { + return acc !== undefined ? acc : msg.buildContent(); + }, undefined); const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); return { responseHead: { @@ -50,7 +54,8 @@ export class PacketMsgBuilder { body: { richText: { elems: msgElement - } + }, + msgContent: msgContent, } }; }); diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index e9693e48..e96a0c30 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -28,6 +28,8 @@ import { 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 { FileExtra, GroupFileExtra } from "@/core/packet/proto/message/component"; +import { OidbSvcTrpcTcp0XE37_800Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; // raw <-> packet // TODO: SendStructLongMsgElement @@ -39,8 +41,8 @@ export abstract class IPacketMsgElement { return undefined; } - buildElement(): NapProtoEncodeStructType[] | undefined { - return undefined; + buildElement(): NapProtoEncodeStructType[] { + return []; } toPreview(): string { @@ -351,8 +353,85 @@ export class PacketMsgPttElement extends IPacketMsgElement { } export class PacketMsgFileElement extends IPacketMsgElement { + fileName: string; + filePath: string; + fileSize: number; + fileSha1?: Uint8Array; + fileMd5?: Uint8Array; + fileUuid?: string; + fileHash?: string; + isGroupFile?: boolean; + _private_send_uid?: string; + _private_recv_uid?: string; + _e37_800_rsp?: NapProtoEncodeStructType + constructor(element: SendFileElement) { super(element); + this.fileName = element.fileElement.fileName; + this.filePath = element.fileElement.filePath; + this.fileSize = +element.fileElement.fileSize; + } + + buildContent(): Uint8Array | undefined { + if (this.isGroupFile) return undefined; + return new NapProtoMsg(FileExtra).encode({ + file: { + fileType: 0, + fileUuid: this.fileUuid, + fileMd5: this.fileMd5, + fileName: this.fileName, + fileSize: BigInt(this.fileSize), + subcmd: 1, + dangerEvel: 0, + expireTime: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, + fileHash: this.fileHash, + }, + field6: { + field2: { + field1: this._e37_800_rsp?.body?.field30?.field110, + fileUuid: this.fileUuid, + fileName: this.fileName, + field6: this._e37_800_rsp?.body?.field30?.field3, + field7: this._e37_800_rsp?.body?.field30?.field101, + field8: this._e37_800_rsp?.body?.field30?.field100, + timestamp1: this._e37_800_rsp?.body?.field30?.timestamp1, + fileHash: this.fileHash, + selfUid: this._private_send_uid, + destUid: this._private_recv_uid, + } + } + }) + } + + buildElement(): NapProtoEncodeStructType[] { + if (!this.isGroupFile) return []; + const lb = Buffer.alloc(2); + const transElemVal = new NapProtoMsg(GroupFileExtra).encode({ + field1: 6, + fileName: this.fileName, + inner: { + info: { + busId: 102, + fileId: this.fileUuid, + fileSize: BigInt(this.fileSize), + fileName: this.fileName, + fileSha: this.fileSha1, + extInfoString: "", + fileMd5: this.fileMd5, + } + } + }) + lb.writeUInt16BE(transElemVal.length); + return [{ + transElem: { + elemType: 24, + elemValue: Buffer.concat([Buffer.from([0x01]), lb, transElemVal]) // TLV + } + }]; + } + + toPreview(): string { + return `[文件]${this.fileName}`; } } diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index b3f8dd74..8b7d3277 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -1,6 +1,6 @@ import * as zlib from "node:zlib"; import * as crypto from "node:crypto"; -import { calculateSha1 } from "@/core/packet/utils/crypto/hash"; +import { calculateSha1, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash"; import { NapProtoMsg } from "@/core/packet/proto/NapProto"; import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase"; import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202"; @@ -11,13 +11,20 @@ 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, PacketMsgPttElement, PacketMsgVideoElement } from "@/core/packet/msg/element"; +import { + PacketMsgFileElement, + 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"; import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; import { PacketMsgConverter } from "@/core/packet/msg/converter"; import { PacketClient } from "@/core/packet/client"; +import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700"; +import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; export type PacketHexStr = string & { readonly hexNya: unique symbol }; @@ -596,6 +603,65 @@ export class PacketPacker { return this.toHexStr(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: await calculateSha1(file.filePath), + fileSha3: Buffer.alloc(0), + field15: true + } + }); + return this.toHexStr(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.toHexStr(this.packOidbPacket(0xE37, 1700, body, false, false)); + } + + packOfflineFileDownloadReq(fileUUID: string, fileHash: string, senderUid: string, receiverUid: string): PacketHexStr { + const req = 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); + return this.toHexStr(req); + } + packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { return this.toHexStr( this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ diff --git a/src/core/packet/proto/message/component.ts b/src/core/packet/proto/message/component.ts index 984b5f3a..c83ed1e9 100644 --- a/src/core/packet/proto/message/component.ts +++ b/src/core/packet/proto/message/component.ts @@ -113,8 +113,26 @@ export const Permission = { export const FileExtra = { file: ProtoField(1, () => NotOnlineFile), + field6: ProtoField(6, () => PrivateFileExtra), }; +export const PrivateFileExtra = { + field2: ProtoField(2, () => PrivateFileExtraField2), +} + +export const PrivateFileExtraField2 = { + field1: ProtoField(1, ScalarType.UINT32), + fileUuid: ProtoField(4, ScalarType.STRING), + fileName: ProtoField(5, ScalarType.STRING), + field6: ProtoField(6, ScalarType.UINT32), + field7: ProtoField(7, ScalarType.BYTES), + field8: ProtoField(8, ScalarType.BYTES), + timestamp1: ProtoField(9, ScalarType.UINT32), + fileHash: ProtoField(14, ScalarType.STRING), + selfUid: ProtoField(15, ScalarType.STRING), + destUid: ProtoField(16, ScalarType.STRING), +} + export const GroupFileExtra = { field1: ProtoField(1, ScalarType.UINT32), fileName: ProtoField(2, ScalarType.STRING), @@ -132,8 +150,9 @@ export const GroupFileExtraInfo = { fileSize: ProtoField(3, ScalarType.UINT64), fileName: ProtoField(4, ScalarType.STRING), field5: ProtoField(5, ScalarType.UINT32), - field7: ProtoField(7, ScalarType.STRING), - fileMd5: ProtoField(8, ScalarType.STRING), + fileSha: ProtoField(6, ScalarType.BYTES), + extInfoString: ProtoField(7, ScalarType.STRING), + fileMd5: ProtoField(8, ScalarType.BYTES), }; export const ImageExtraUrl = { diff --git a/src/core/packet/proto/oidb/Oidb.0XE37_800.ts b/src/core/packet/proto/oidb/Oidb.0XE37_800.ts new file mode 100644 index 00000000..f2e6fe5b --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0XE37_800.ts @@ -0,0 +1,62 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import {OidbSvcTrpcTcp0XE37_800_1200Metadata} from "@/core/packet/proto/oidb/Oidb.0xE37_1200"; + +export const OidbSvcTrpcTcp0XE37_800 = { + subCommand: ProtoField(1, ScalarType.UINT32), + field2: ProtoField(2, ScalarType.INT32), + body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800Body, true), + field101: ProtoField(101, ScalarType.INT32), + field102: ProtoField(102, ScalarType.INT32), + field200: ProtoField(200, ScalarType.INT32) +}; + +export const OidbSvcTrpcTcp0XE37_800Body = { + senderUid: ProtoField(10, ScalarType.STRING, true), + receiverUid: ProtoField(20, ScalarType.STRING, true), + fileUuid: ProtoField(30, ScalarType.STRING, true), + fileHash: ProtoField(40, ScalarType.STRING, true) +}; + +export const OidbSvcTrpcTcp0XE37Response = { + command: ProtoField(1, ScalarType.UINT32), + seq: ProtoField(2, ScalarType.INT32), + upload: ProtoField(19, () => ApplyUploadRespV3, true), + businessId: ProtoField(101, ScalarType.INT32), + clientType: ProtoField(102, ScalarType.INT32), + flagSupportMediaPlatform: ProtoField(200, ScalarType.INT32) +}; + +export const ApplyUploadRespV3 = { + retCode: ProtoField(10, ScalarType.INT32), + retMsg: ProtoField(20, ScalarType.STRING, true), + totalSpace: ProtoField(30, ScalarType.INT64), + usedSpace: ProtoField(40, ScalarType.INT64), + uploadedSize: ProtoField(50, ScalarType.INT64), + uploadIp: ProtoField(60, ScalarType.STRING, true), + uploadDomain: ProtoField(70, ScalarType.STRING, true), + uploadPort: ProtoField(80, ScalarType.UINT32), + uuid: ProtoField(90, ScalarType.STRING, true), + uploadKey: ProtoField(100, ScalarType.BYTES, true), + boolFileExist: ProtoField(110, ScalarType.BOOL), + packSize: ProtoField(120, ScalarType.INT32), + uploadIpList: ProtoField(130, ScalarType.STRING, false, true), // repeated + uploadHttpsPort: ProtoField(140, ScalarType.INT32), + uploadHttpsDomain: ProtoField(150, ScalarType.STRING, true), + uploadDns: ProtoField(160, ScalarType.STRING, true), + uploadLanip: ProtoField(170, ScalarType.STRING, true), + fileAddon: ProtoField(200, ScalarType.STRING, true), + mediaPlatformUploadKey: ProtoField(220, ScalarType.BYTES, true) +}; + +export const OidbSvcTrpcTcp0XE37_800Response = { + command: ProtoField(1, ScalarType.UINT32, true), + subCommand: ProtoField(2, ScalarType.UINT32, true), + body: ProtoField(10, () => OidbSvcTrpcTcp0XE37_800ResponseBody, true), + field50: ProtoField(50, ScalarType.UINT32, true), +}; + +export const OidbSvcTrpcTcp0XE37_800ResponseBody = { + field10: ProtoField(10, ScalarType.UINT32, true), + field30: ProtoField(30, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true), +} diff --git a/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts b/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts index 80ecbe2c..ba2b0f16 100644 --- a/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts +++ b/src/core/packet/proto/oidb/Oidb.0xE37_1200.ts @@ -30,7 +30,7 @@ export const OidbSvcTrpcTcp0XE37_1200ResponseBody = { field10: ProtoField(10, ScalarType.UINT32, true), state: ProtoField(20, ScalarType.STRING, true), result: ProtoField(30, () => OidbSvcTrpcTcp0XE37_1200Result, true), - metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_1200Metadata, true), + metadata: ProtoField(40, () => OidbSvcTrpcTcp0XE37_800_1200Metadata, true), }; export const OidbSvcTrpcTcp0XE37_1200Result = { @@ -43,7 +43,7 @@ export const OidbSvcTrpcTcp0XE37_1200Result = { extra: ProtoField(120, ScalarType.BYTES, true), }; -export const OidbSvcTrpcTcp0XE37_1200Metadata = { +export const OidbSvcTrpcTcp0XE37_800_1200Metadata = { uin: ProtoField(1, ScalarType.UINT32, true), field2: ProtoField(2, ScalarType.UINT32, true), field3: ProtoField(3, ScalarType.UINT32, true), diff --git a/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts b/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts new file mode 100644 index 00000000..8d5c3f73 --- /dev/null +++ b/src/core/packet/proto/oidb/Oidb.0xE37_1700.ts @@ -0,0 +1,23 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; + +export const OidbSvcTrpcTcp0XE37_1700 = { + command: ProtoField(1, ScalarType.UINT32, true), + seq: ProtoField(2, ScalarType.INT32, true), + upload: ProtoField(19, () => ApplyUploadReqV3, true), + businessId: ProtoField(101, ScalarType.INT32, true), + clientType: ProtoField(102, ScalarType.INT32, true), + flagSupportMediaPlatform: ProtoField(200, ScalarType.INT32, true), +} + +export const ApplyUploadReqV3 = { + senderUid: ProtoField(10, ScalarType.STRING, true), + receiverUid: ProtoField(20, ScalarType.STRING, true), + fileSize: ProtoField(30, ScalarType.UINT32, true), + fileName: ProtoField(40, ScalarType.STRING, true), + md510MCheckSum: ProtoField(50, ScalarType.BYTES, true), + sha1CheckSum: ProtoField(60, ScalarType.BYTES, true), + localPath: ProtoField(70, ScalarType.STRING, true), + md5CheckSum: ProtoField(110, ScalarType.BYTES, true), + sha3CheckSum: ProtoField(120, ScalarType.BYTES, true), +} diff --git a/src/core/packet/utils/crypto/hash.ts b/src/core/packet/utils/crypto/hash.ts index 51eaf4ec..9576e2c0 100644 --- a/src/core/packet/utils/crypto/hash.ts +++ b/src/core/packet/utils/crypto/hash.ts @@ -11,11 +11,23 @@ function sha1Stream(readable: stream.Readable) { }) as Promise; } +function md5Stream(readable: stream.Readable) { + return new Promise((resolve, reject) => { + readable.on('error', reject); + readable.pipe(crypto.createHash('md5').on('error', reject).on('data', resolve)); + }) as Promise; +} + export function calculateSha1(filePath: string): Promise { const readable = fs.createReadStream(filePath); return sha1Stream(readable); } +export function computeMd5AndLengthWithLimit(filePath: string, limit?: number): Promise { + const readStream = fs.createReadStream(filePath, limit ? { start: 0, end: limit - 1 } : {}); + return md5Stream(readStream); +} + export function calculateSha1StreamBytes(filePath: string): Promise { return new Promise((resolve, reject) => { const readable = fs.createReadStream(filePath); From 801a97d85b3ce90f424446d08014d1bd9c9c09c6 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Thu, 24 Oct 2024 04:58:53 +0800 Subject: [PATCH 05/11] chore: remove useless log --- src/core/packet/highway/session.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index b5b2b178..e06768d7 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -282,7 +282,6 @@ export class PacketHighwaySession { private async uploadC2CVideoReq(peerUid: string, video: PacketMsgVideoElement): Promise { const preReq = await this.packer.packUploadC2CVideoReq(peerUid, video); const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11e9_100', preReq, true); - console.log(preRespRaw); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -496,7 +495,6 @@ export class PacketHighwaySession { const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); - console.log("OidbSvcTrpcTcp.0xe37_1700", preRespRaw); const preRespData = new NapProtoMsg(OidbSvcTrpcTcp0XE37Response).decode(preResp.body); if (!preRespData.upload?.boolFileExist) { this.logger.logDebug(`[Highway] uploadC2CFileReq file not exist, need upload!`); @@ -552,7 +550,6 @@ export class PacketHighwaySession { file.fileHash = preRespData.upload?.fileAddon; const FetchExistFileReq = this.packer.packOfflineFileDownloadReq(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid); const resp = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0xe37_800', FetchExistFileReq, true); - console.log("OidbSvcTrpcTcp.0xe37_800", resp); 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); file._private_send_uid = this.sig.uid; From d3405edd42f0a2a91326c61093f4bf1ace923d36 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 25 Oct 2024 05:11:10 +0800 Subject: [PATCH 06/11] refactor: packet highway & etc, kill some todo --- src/core/apis/packet.ts | 2 +- src/core/packet/highway/client.ts | 2 +- src/core/packet/highway/session.ts | 9 ++- src/core/packet/highway/uploader.ts | 89 +++++++++++++++++------------ src/core/packet/msg/element.ts | 10 ++-- src/core/packet/packer.ts | 4 +- 6 files changed, 66 insertions(+), 50 deletions(-) diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index b51bbff2..99982318 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -150,7 +150,7 @@ export class NTQQPacketApi { } } } - return Promise.all(reqList); // TODO: use promise.allSettled + return Promise.allSettled(reqList); } async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) { diff --git a/src/core/packet/highway/client.ts b/src/core/packet/highway/client.ts index dcf4ca5c..4c788568 100644 --- a/src/core/packet/highway/client.ts +++ b/src/core/packet/highway/client.ts @@ -36,7 +36,7 @@ export class PacketHighwayClient { this.port = port; } - private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 3600): PacketHighwayTrans { + private buildDataUpTrans(cmd: number, data: ReadStream, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array, timeout: number = 1200): PacketHighwayTrans { return { uin: this.sig.uin, cmd: cmd, diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index e06768d7..d66df215 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -59,7 +59,6 @@ export class PacketHighwaySession { private async checkAvailable() { if (!this.packetClient.available) { - this.logger.logError('[Highway] packetServer not available!'); throw new Error('packetServer不可用,请参照文档 https://napneko.github.io/config/advanced 检查packetServer状态或进行配置'); } if (this.sig.sigSession === null || this.sig.sessionKey === null) { @@ -96,7 +95,7 @@ export class PacketHighwaySession { async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise { await this.checkAvailable(); if (peer.chatType === ChatType.KCHATTYPEGROUP) { - await this.uploadGroupImageReq(Number(peer.peerUid), img); + await this.uploadGroupImageReq(+peer.peerUid, img); } else if (peer.chatType === ChatType.KCHATTYPEC2C) { await this.uploadC2CImageReq(peer.peerUid, img); } else { @@ -107,7 +106,7 @@ export class PacketHighwaySession { async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise { await this.checkAvailable(); if (peer.chatType === ChatType.KCHATTYPEGROUP) { - await this.uploadGroupVideoReq(Number(peer.peerUid), video); + await this.uploadGroupVideoReq(+peer.peerUid, video); } else if (peer.chatType === ChatType.KCHATTYPEC2C) { await this.uploadC2CVideoReq(peer.peerUid, video); } else { @@ -118,7 +117,7 @@ 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); + await this.uploadGroupPttReq(+peer.peerUid, ptt); } else if (peer.chatType === ChatType.KCHATTYPEC2C) { await this.uploadC2CPttReq(peer.peerUid, ptt); } else { @@ -129,7 +128,7 @@ export class PacketHighwaySession { async uploadFile(peer: Peer, file: PacketMsgFileElement): Promise { await this.checkAvailable(); if (peer.chatType === ChatType.KCHATTYPEGROUP) { - await this.uploadGroupFileReq(Number(peer.peerUid), file); + await this.uploadGroupFileReq(+peer.peerUid, file); } else if (peer.chatType === ChatType.KCHATTYPEC2C) { await this.uploadC2CFileReq(peer.peerUid, file); } else { diff --git a/src/core/packet/highway/uploader.ts b/src/core/packet/highway/uploader.ts index d19dd2f7..ae3c3fbb 100644 --- a/src/core/packet/highway/uploader.ts +++ b/src/core/packet/highway/uploader.ts @@ -19,11 +19,20 @@ abstract class HighwayUploader { this.logger = logger; } - encryptTransExt(key: Uint8Array) { + 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] tcpUpload 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: { @@ -86,16 +95,18 @@ class HighwayTcpUploaderTransform extends stream.Transform { export class HighwayTcpUploader extends HighwayUploader { async upload(): Promise { - const highwayTransForm = new HighwayTcpUploaderTransform(this); - const upload = new Promise((resolve, _) => { + 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 }); + this.trans.data.pipe(highwayTransForm).pipe(socket, {end: false}); }); const handleRspHeader = (header: Buffer) => { const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header); if (rsp.errorCode !== 0) { - // TODO: immediately reject promise if error code is not 0 - this.logger.logWarn(`[Highway] tcpUpload failed (code: ${rsp.errorCode})`); + 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')}`); @@ -106,49 +117,58 @@ export class HighwayTcpUploader extends HighwayUploader { } }; socket.on('data', (chunk: Buffer) => { - try { - const [head, _] = Frame.unpack(chunk); - handleRspHeader(head); - } catch (e) { - this.logger.logError(`[Highway] tcpUpload parse response error: ${e}`); + 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) => { - this.logger.logError('[Highway] tcpUpload socket.on error:', err); + socket.end(); + reject(new Error(`[Highway] tcpUpload socket.on error: ${err}`)); }); this.trans.data.on('error', (err) => { - this.logger.logError('[Highway] tcpUpload readable error:', err); socket.end(); + reject(new Error(`[Highway] tcpUpload readable error: ${err}`)); }); }); - const timeout = new Promise((_, reject) => { - setTimeout(() => { - reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`)); - }, (this.trans.timeout ?? Infinity) * 1000 - ); + const timeout = this.timeout().then(() => { + controller.abort(); + throw new Error('Highway TCP Upload timed out'); }); await Promise.race([upload, timeout]); } } -// TODO: timeout impl export class HighwayHttpUploader extends HighwayUploader { async upload(): Promise { - let offset = 0; - for await (const chunk of this.trans.data) { - const block = chunk as Buffer; - try { - await this.uploadBlock(block, offset); - } catch (err) { - this.logger.logError(`[Highway] httpUpload Error uploading block at offset ${offset}: ${err}`); - throw err; + 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; } - offset += block.length; - } + })(); + const timeout = this.timeout().then(() => { + controller.abort(); + throw new Error('Highway HTTP Upload timed out'); + }); + await Promise.race([upload, timeout]); } private async uploadBlock(block: Buffer, offset: number): Promise { @@ -159,10 +179,7 @@ export class HighwayHttpUploader extends HighwayUploader { 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) { - // TODO: immediately throw error if error code is not 0 - this.logger.logError(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`); - } + if (headData.errorCode !== 0) throw new Error(`[Highway] httpUploadBlock failed (code=${headData.errorCode})`); } private async httpPostHighwayContent(frame: Buffer, serverURL: string): Promise { @@ -178,12 +195,12 @@ export class HighwayHttpUploader extends HighwayUploader { }, }; const req = http.request(serverURL, options, (res) => { - let data = Buffer.alloc(0); + const data: Buffer[] = []; res.on('data', (chunk) => { - data = Buffer.concat([data, chunk]); + data.push(chunk); }); res.on('end', () => { - resolve(data); + resolve(Buffer.concat(data)); }); }); req.write(frame); diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index e96a0c30..e036c7f8 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -114,7 +114,7 @@ export class PacketMsgPicElement extends IPacketMsgElement { super(element); this.path = element.picElement.sourcePath; this.name = element.picElement.fileName; - this.size = Number(element.picElement.fileSize); + this.size = +element.picElement.fileSize; this.md5 = element.picElement.md5HexStr ?? ''; this.width = element.picElement.picWidth; this.height = element.picElement.picHeight; @@ -149,11 +149,11 @@ export class PacketMsgReplyElement extends IPacketMsgElement { constructor(element: SendReplyElement) { super(element); this.messageId = BigInt(element.replyElement.replayMsgId ?? 0); - this.messageSeq = Number(element.replyElement.replayMsgSeq ?? 0); - this.messageClientSeq = Number(element.replyElement.replyMsgClientSeq ?? 0); - this.targetUin = Number(element.replyElement.senderUin ?? 0); + this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0); + this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0); + this.targetUin = +(element.replyElement.senderUin ?? 0); this.targetUid = element.replyElement.senderUidStr ?? ''; - this.time = Number(element.replyElement.replyMsgTime ?? 0); + this.time = +(element.replyElement.replyMsgTime ?? 0); this.elems = []; // TODO: in replyElement.sourceMsgTextElems } diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index 3875ab55..6f99efa9 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -186,7 +186,7 @@ export class PacketPacker { uploadInfo: [ { fileInfo: { - fileSize: Number(img.size), + fileSize: +img.size, fileHash: img.md5, fileSha1: this.toHexStr(await calculateSha1(img.path)), fileName: img.name, @@ -254,7 +254,7 @@ export class PacketPacker { uploadInfo: [ { fileInfo: { - fileSize: Number(img.size), + fileSize: +img.size, fileHash: img.md5, fileSha1: this.toHexStr(await calculateSha1(img.path)), fileName: img.name, From aa67a2b71c10bfd3c57801a7586ebe3d7acec522 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 25 Oct 2024 05:17:01 +0800 Subject: [PATCH 07/11] =?UTF-8?q?chore:=20cv=E5=A4=9A=E4=BA=86=EF=BC=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/packet/highway/uploader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/packet/highway/uploader.ts b/src/core/packet/highway/uploader.ts index ae3c3fbb..43acaa1e 100644 --- a/src/core/packet/highway/uploader.ts +++ b/src/core/packet/highway/uploader.ts @@ -27,7 +27,7 @@ abstract class HighwayUploader { protected timeout(): Promise { return new Promise((_, reject) => { setTimeout(() => { - reject(new Error(`[Highway] tcpUpload timeout after ${this.trans.timeout}s`)); + reject(new Error(`[Highway] timeout after ${this.trans.timeout}s`)); }, (this.trans.timeout ?? Infinity) * 1000 ); }) @@ -137,9 +137,9 @@ export class HighwayTcpUploader extends HighwayUploader { reject(new Error(`[Highway] tcpUpload readable error: ${err}`)); }); }); - const timeout = this.timeout().then(() => { + const timeout = this.timeout().catch((err) => { controller.abort(); - throw new Error('Highway TCP Upload timed out'); + throw new Error(err.message); }); await Promise.race([upload, timeout]); } @@ -164,9 +164,9 @@ export class HighwayHttpUploader extends HighwayUploader { offset += block.length; } })(); - const timeout = this.timeout().then(() => { + const timeout = this.timeout().catch((err) => { controller.abort(); - throw new Error('Highway HTTP Upload timed out'); + throw new Error(err.message); }); await Promise.race([upload, timeout]); } From aa9d96718c785ae9ccedd859455f7db0376748a2 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 25 Oct 2024 05:54:46 +0800 Subject: [PATCH 08/11] refactor: outer calculation --- src/core/packet/highway/session.ts | 10 ++++++++++ src/core/packet/msg/element.ts | 3 +++ src/core/packet/packer.ts | 26 +++++++++----------------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index d66df215..b8cf7f33 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -137,6 +137,7 @@ export class PacketHighwaySession { } private async uploadGroupImageReq(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.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( @@ -176,6 +177,7 @@ export class PacketHighwaySession { } private async uploadC2CImageReq(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.sendPacket('OidbSvcTrpcTcp.0x11c5_100', preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( @@ -214,6 +216,9 @@ export class PacketHighwaySession { } private async uploadGroupVideoReq(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.sendPacket('OidbSvcTrpcTcp.0x11ea_100', preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( @@ -279,6 +284,9 @@ export class PacketHighwaySession { } private async uploadC2CVideoReq(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.sendPacket('OidbSvcTrpcTcp.0x11e9_100', preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( @@ -344,6 +352,7 @@ export class PacketHighwaySession { } private async uploadGroupPttReq(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.sendPacket('OidbSvcTrpcTcp.0x126e_100', preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( @@ -382,6 +391,7 @@ export class PacketHighwaySession { } private async uploadC2CPttReq(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.sendPacket('OidbSvcTrpcTcp.0x126d_100', preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index e036c7f8..1fa9e064 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -288,7 +288,9 @@ export class PacketMsgVideoElement extends IPacketMsgElement { thumbSize?: number; thumbPath?: string; fileMd5?: string; + fileSha1?: string; thumbMd5?: string; + thumbSha1?: string; thumbWidth?: number; thumbHeight?: number; msgInfo: NapProtoEncodeStructType | null = null; @@ -325,6 +327,7 @@ export class PacketMsgPttElement extends IPacketMsgElement { filePath: string; fileSize: number; fileMd5: string; + fileSha1?: string; fileDuration: number; msgInfo: NapProtoEncodeStructType | null = null; diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index 6f99efa9..67a1b86d 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -188,7 +188,7 @@ export class PacketPacker { fileInfo: { fileSize: +img.size, fileHash: img.md5, - fileSha1: this.toHexStr(await calculateSha1(img.path)), + fileSha1: img.sha1!, fileName: img.name, type: { type: 1, @@ -256,7 +256,7 @@ export class PacketPacker { fileInfo: { fileSize: +img.size, fileHash: img.md5, - fileSha1: this.toHexStr(await calculateSha1(img.path)), + fileSha1: img.sha1!, fileName: img.name, type: { type: 1, @@ -299,10 +299,7 @@ export class PacketPacker { } async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { - if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); - const videoSha1 = await calculateSha1(video.filePath ?? ""); - const videoThumbSha1 = await calculateSha1(video.thumbPath ?? ""); const req = new NapProtoMsg(NTV2RichMediaReq).encode({ reqHead: { common: { @@ -327,7 +324,7 @@ export class PacketPacker { fileInfo: { fileSize: +video.fileSize, fileHash: video.fileMd5, - fileSha1: this.toHexStr(videoSha1), + fileSha1: video.fileSha1, fileName: "nya.mp4", type: { type: 2, @@ -345,7 +342,7 @@ export class PacketPacker { fileInfo: { fileSize: +video.thumbSize, fileHash: video.thumbMd5, - fileSha1: this.toHexStr(videoThumbSha1), + fileSha1: video.thumbSha1, fileName: "nya.jpg", type: { type: 1, @@ -387,10 +384,7 @@ export class PacketPacker { } async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise { - if (!video.filePath || !video.thumbPath) throw new Error("video.filePath or video.thumbPath is empty"); if (!video.fileSize || !video.thumbSize) throw new Error("video.fileSize or video.thumbSize is empty"); - const videoSha1 = await calculateSha1(video.filePath ?? ""); - const videoThumbSha1 = await calculateSha1(video.thumbPath ?? ""); const req = new NapProtoMsg(NTV2RichMediaReq).encode({ reqHead: { common: { @@ -416,7 +410,7 @@ export class PacketPacker { fileInfo: { fileSize: +video.fileSize, fileHash: video.fileMd5, - fileSha1: this.toHexStr(videoSha1), + fileSha1: video.fileSha1, fileName: "nya.mp4", type: { type: 2, @@ -434,7 +428,7 @@ export class PacketPacker { fileInfo: { fileSize: +video.thumbSize, fileHash: video.thumbMd5, - fileSha1: this.toHexStr(videoThumbSha1), + fileSha1: video.thumbSha1, fileName: "nya.jpg", type: { type: 1, @@ -476,7 +470,6 @@ export class PacketPacker { } async packUploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { - const pttSha1 = await calculateSha1(ptt.filePath); const req = new NapProtoMsg(NTV2RichMediaReq).encode({ reqHead: { common: { @@ -501,7 +494,7 @@ export class PacketPacker { fileInfo: { fileSize: ptt.fileSize, fileHash: ptt.fileMd5, - fileSha1: this.toHexStr(pttSha1), + fileSha1: ptt.fileSha1, fileName: `${ptt.fileMd5}.amr`, type: { type: 3, @@ -542,7 +535,6 @@ export class PacketPacker { } async packUploadC2CPttReq(peerUin: string, ptt: PacketMsgPttElement): Promise { - const pttSha1 = await calculateSha1(ptt.filePath); const req = new NapProtoMsg(NTV2RichMediaReq).encode({ reqHead: { common: { @@ -568,7 +560,7 @@ export class PacketPacker { fileInfo: { fileSize: ptt.fileSize, fileHash: ptt.fileMd5, - fileSha1: this.toHexStr(pttSha1), + fileSha1: ptt.fileSha1, fileName: `${ptt.fileMd5}.amr`, type: { type: 3, @@ -616,7 +608,7 @@ export class PacketPacker { localDirectory: `/${file.fileName}`, fileSize: BigInt(file.fileSize), fileMd5: file.fileMd5, - fileSha1: await calculateSha1(file.filePath), + fileSha1: file.fileSha1, fileSha3: Buffer.alloc(0), field15: true } From 9ed2a2fd196ac39698f8c7ac9b21b9005cb5171a Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 25 Oct 2024 06:48:01 +0800 Subject: [PATCH 09/11] refactor: simplify oidb packet pack & send --- src/core/apis/packet.ts | 18 ++- src/core/packet/client.ts | 7 +- src/core/packet/highway/session.ts | 18 +-- src/core/packet/packer.ts | 219 +++++++++++++++-------------- 4 files changed, 137 insertions(+), 125 deletions(-) diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 99982318..43092d52 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -3,7 +3,7 @@ import { ChatType, InstanceContext, NapCatCore } from '..'; import offset from '@/core/external/offset.json'; import { PacketClient, RecvPacketData } from '@/core/packet/client'; import { PacketSession } from "@/core/packet/session"; -import { PacketHexStr } from "@/core/packet/packer"; +import {OidbPacket, PacketHexStr} from "@/core/packet/packer"; import { NapProtoMsg } from '@/core/packet/proto/NapProto'; import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202'; import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase'; @@ -78,14 +78,18 @@ export class NTQQPacketApi { 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.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false); + await this.sendOidbPacket(data!, false); } async sendRkeyPacket() { const packet = this.packetSession?.packer.packRkeyPacket(); - const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', packet!, true); + 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); @@ -93,13 +97,13 @@ export class NTQQPacketApi { } async sendGroupSignPacket(groupCode: string) { const packet = this.packetSession?.packer.packGroupSignReq(this.core.selfInfo.uin, groupCode); - await this.sendPacket('OidbSvcTrpcTcp.0xeb7', packet!, true); + 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.sendPacket('OidbSvcTrpcTcp.0xfe1_2', packet!, true); + 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 @@ -116,7 +120,7 @@ export class NTQQPacketApi { async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) { const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle); - await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true); + await this.sendOidbPacket(data!, true); } // TODO: can simplify this @@ -164,7 +168,7 @@ export class NTQQPacketApi { async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) { const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID); - const ret = await this.sendPacket('OidbSvcTrpcTcp.0x6d6_2', data!, true); + 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) { diff --git a/src/core/packet/client.ts b/src/core/packet/client.ts index 441c3465..f4e0a27c 100644 --- a/src/core/packet/client.ts +++ b/src/core/packet/client.ts @@ -3,8 +3,7 @@ import { LRUCache } from "@/common/lru-cache"; import WebSocket, { Data } from "ws"; import crypto, { createHash } from "crypto"; import { NapCatCore } from "@/core"; -import { PacketHexStr } from "@/core/packet/packer"; -import { sleep } from "@/common/helper"; +import { OidbPacket, PacketHexStr } from "@/core/packet/packer"; export interface RecvPacket { type: string, // 仅recv @@ -177,4 +176,8 @@ export class PacketClient { }).then((res) => resolve(res)).catch((e: Error) => reject(e)); }); } + + async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise { + return this.sendPacket(pkt.cmd, pkt.data, rsp); + } } diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts index b8cf7f33..256b6846 100644 --- a/src/core/packet/highway/session.ts +++ b/src/core/packet/highway/session.ts @@ -139,7 +139,7 @@ export class PacketHighwaySession { private async uploadGroupImageReq(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.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -179,7 +179,7 @@ export class PacketHighwaySession { private async uploadC2CImageReq(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.sendPacket('OidbSvcTrpcTcp.0x11c5_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -220,7 +220,7 @@ export class PacketHighwaySession { 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.sendPacket('OidbSvcTrpcTcp.0x11ea_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -288,7 +288,7 @@ export class PacketHighwaySession { 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.sendPacket('OidbSvcTrpcTcp.0x11e9_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -354,7 +354,7 @@ export class PacketHighwaySession { private async uploadGroupPttReq(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.sendPacket('OidbSvcTrpcTcp.0x126e_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -393,7 +393,7 @@ export class PacketHighwaySession { private async uploadC2CPttReq(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.sendPacket('OidbSvcTrpcTcp.0x126d_100', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -434,7 +434,7 @@ export class PacketHighwaySession { 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.sendPacket('OidbSvcTrpcTcp.0x6d6_0', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket(preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -500,7 +500,7 @@ export class PacketHighwaySession { 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.sendPacket('OidbSvcTrpcTcp.0xe37_1700', preReq, true); + const preRespRaw = await this.packetClient.sendOidbPacket( preReq, true); const preResp = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode( Buffer.from(preRespRaw.hex_data, 'hex') ); @@ -558,7 +558,7 @@ 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.sendPacket('OidbSvcTrpcTcp.0xe37_800', FetchExistFileReq, true); + 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); file._private_send_uid = this.sig.uid; diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index 67a1b86d..95a37a53 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -1,6 +1,6 @@ import * as zlib from "node:zlib"; import * as crypto from "node:crypto"; -import { calculateSha1, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash"; +import { computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash"; import { NapProtoMsg } from "@/core/packet/proto/NapProto"; import { OidbSvcTrpcTcpBase } from "@/core/packet/proto/oidb/OidbBase"; import { OidbSvcTrpcTcp0X9067_202 } from "@/core/packet/proto/oidb/Oidb.0x9067_202"; @@ -29,6 +29,11 @@ import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7"; export type PacketHexStr = string & { readonly hexNya: unique symbol }; +export interface OidbPacket { + cmd: string; + data: PacketHexStr +} + export class PacketPacker { readonly logger: LogWrapper; readonly client: PacketClient; @@ -42,31 +47,34 @@ export class PacketPacker { this.packetConverter = new PacketMsgConverter(logger); } - // TODO: 一步到位多好ww - private toHexStr(byteArray: Uint8Array): PacketHexStr { + 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): Uint8Array { - return new NapProtoMsg(OidbSvcTrpcTcpBase).encode({ + 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): PacketHexStr { + 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.toHexStr(this.packOidbPacket(0xed3, 1, oidb_0xed3)); + return this.packOidbPacket(0xed3, 1, oidb_0xed3); } - packRkeyPacket(): PacketHexStr { + packRkeyPacket(): OidbPacket { const oidb_0x9067_202 = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202).encode({ reqHead: { common: { @@ -86,10 +94,10 @@ export class PacketPacker { key: [10, 20, 2] }, }); - return this.toHexStr(this.packOidbPacket(0x9067, 202, oidb_0x9067_202)); + return this.packOidbPacket(0x9067, 202, oidb_0x9067_202); } - packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): PacketHexStr { + packSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string): OidbPacket { const oidb_0x8FC_2_body = new NapProtoMsg(OidbSvcTrpcTcp0X8FC_2_Body).encode({ targetUid: uid, specialTitle: tittle, @@ -100,15 +108,15 @@ export class PacketPacker { groupUin: +groupCode, body: oidb_0x8FC_2_body }); - return this.toHexStr(this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false)); + return this.packOidbPacket(0x8FC, 2, oidb_0x8FC_2, false, false); } - packStatusPacket(uin: number): PacketHexStr { + packStatusPacket(uin: number): OidbPacket { const oidb_0xfe1_2 = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2).encode({ uin: uin, key: [{key: 27372}] }); - return this.toHexStr(this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2)); + return this.packOidbPacket(0xfe1, 2, oidb_0xfe1_2); } async packUploadForwardMsg(selfUid: string, msg: PacketMsg[], groupUin: number = 0): Promise { @@ -140,12 +148,12 @@ export class PacketPacker { } ); // this.logger.logDebug("packUploadForwardMsg REQ!!!", req); - return this.toHexStr(req); + return this.packetPacket(req); } // highway part packHttp0x6ff_501(): PacketHexStr { - return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({ + return this.packetPacket(new NapProtoMsg(HttpConn0x6ff_501).encode({ httpConn: { field1: 0, field2: 0, @@ -162,7 +170,7 @@ export class PacketPacker { })); } - async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise { + async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement): Promise { const req = new NapProtoMsg(NTV2RichMediaReq).encode( { reqHead: { @@ -227,78 +235,78 @@ export class PacketPacker { } } ); - return this.toHexStr(this.packOidbPacket(0x11c4, 100, req, true, false)); + return this.packOidbPacket(0x11c4, 100, req, true, false); } - async packUploadC2CImgReq(peerUin: string, img: PacketMsgPicElement): Promise { + 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 + reqHead: { + common: { + requestId: 1, + command: 100 }, - }, - 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 + scene: { + requestType: 2, + businessType: 1, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: peerUin }, - 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), + client: { + agentType: 2, } }, - clientSeq: 0, - noNeedCompatMsg: false, + 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.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false)); + return this.packOidbPacket(0x11c5, 100, req, true, false); } - async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise { + 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: { @@ -380,10 +388,10 @@ export class PacketPacker { noNeedCompatMsg: false } }); - return this.toHexStr(this.packOidbPacket(0x11EA, 100, req, true, false)); + return this.packOidbPacket(0x11EA, 100, req, true, false); } - async packUploadC2CVideoReq(peerUin: string, video: PacketMsgVideoElement): Promise { + 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: { @@ -466,10 +474,10 @@ export class PacketPacker { noNeedCompatMsg: false } }); - return this.toHexStr(this.packOidbPacket(0x11E9, 100, req, true, false)); + return this.packOidbPacket(0x11E9, 100, req, true, false); } - async packUploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { + async packUploadGroupPttReq(groupUin: number, ptt: PacketMsgPttElement): Promise { const req = new NapProtoMsg(NTV2RichMediaReq).encode({ reqHead: { common: { @@ -531,10 +539,10 @@ export class PacketPacker { noNeedCompatMsg: false } }) - return this.toHexStr(this.packOidbPacket(0x126E, 100, req, true, false)); + return this.packOidbPacket(0x126E, 100, req, true, false); } - async packUploadC2CPttReq(peerUin: string, ptt: PacketMsgPttElement): Promise { + async packUploadC2CPttReq(peerUin: string, ptt: PacketMsgPttElement): Promise { const req = new NapProtoMsg(NTV2RichMediaReq).encode({ reqHead: { common: { @@ -593,10 +601,10 @@ export class PacketPacker { noNeedCompatMsg: false } }) - return this.toHexStr(this.packOidbPacket(0x126D, 100, req, true, false)); + return this.packOidbPacket(0x126D, 100, req, true, false); } - async packUploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise { + async packUploadGroupFileReq(groupUin: number, file: PacketMsgFileElement): Promise { const body = new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ file: { groupUin: groupUin, @@ -613,10 +621,10 @@ export class PacketPacker { field15: true } }); - return this.toHexStr(this.packOidbPacket(0x6D6, 0, body, true, false)); + return this.packOidbPacket(0x6D6, 0, body, true, false); } - async packUploadC2CFileReq(selfUid: string, peerUid: string, file: PacketMsgFileElement): Promise { + async packUploadC2CFileReq(selfUid: string, peerUid: string, file: PacketMsgFileElement): Promise { const body = new NapProtoMsg(OidbSvcTrpcTcp0XE37_1700).encode({ command: 1700, seq: 0, @@ -625,7 +633,7 @@ export class PacketPacker { receiverUid: peerUid, fileSize: file.fileSize, fileName: file.fileName, - md510MCheckSum: await computeMd5AndLengthWithLimit(file.filePath, 10*1024*1024), + md510MCheckSum: await computeMd5AndLengthWithLimit(file.filePath, 10 * 1024 * 1024), sha1CheckSum: file.fileSha1, localPath: "/", md5CheckSum: file.fileMd5, @@ -635,11 +643,11 @@ export class PacketPacker { clientType: 1, flagSupportMediaPlatform: 1 }) - return this.toHexStr(this.packOidbPacket(0xE37, 1700, body, false, false)); + return this.packOidbPacket(0xE37, 1700, body, false, false); } - packOfflineFileDownloadReq(fileUUID: string, fileHash: string, senderUid: string, receiverUid: string): PacketHexStr { - const req = this.packOidbPacket(0xE37, 800, new NapProtoMsg(OidbSvcTrpcTcp0XE37_800).encode({ + 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: { @@ -652,24 +660,22 @@ export class PacketPacker { field102: 1, field200: 1, }), false, false); - return this.toHexStr(req); } - packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr { - return this.toHexStr( - this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({ + 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) + }), true, false ); } packC2CFileDownloadReq(selfUid: string, fileUUID: string, fileHash: string): PacketHexStr { - return this.toHexStr( + return this.packetPacket( new NapProtoMsg(OidbSvcTrpcTcp0XE37_1200).encode({ subCommand: 1200, field2: 1, @@ -687,17 +693,16 @@ export class PacketPacker { }) ); } - packGroupSignReq(uin: string, groupCode: string): PacketHexStr { - return this.toHexStr( - this.packOidbPacket(0XEB7, 1, new NapProtoMsg(OidbSvcTrpcTcp0XEB7).encode( - { - body: { - uin: uin, - groupUin: groupCode, - version: "9.0.90" - } + + 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) - ); + } + ), false, false); } } From a27c2a69c40c5240a0fc29550570f7b72eec2e47 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 25 Oct 2024 07:27:35 +0800 Subject: [PATCH 10/11] feat: maybe more stable fake forwardMsg --- src/core/packet/msg/builder.ts | 13 ++++- src/core/packet/msg/element.ts | 93 +++++++++++++++++----------------- 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/src/core/packet/msg/builder.ts b/src/core/packet/msg/builder.ts index ea6a1d3c..eef14eec 100644 --- a/src/core/packet/msg/builder.ts +++ b/src/core/packet/msg/builder.ts @@ -3,7 +3,8 @@ import { PushMsgBody } from "@/core/packet/proto/message/message"; import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; import { LogWrapper } from "@/common/log"; import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; -import { IPacketMsgElement } from "@/core/packet/msg/element"; +import { IPacketMsgElement, PacketMsgTextElement } from "@/core/packet/msg/element"; +import { SendTextElement } from "@/core"; export class PacketMsgBuilder { private logger: LogWrapper; @@ -12,6 +13,12 @@ export class PacketMsgBuilder { this.logger = logger; } + protected static failBackText = new PacketMsgTextElement( + { + textElement: {content: "[该消息类型暂不支持查看]"}! + } as SendTextElement + ) + buildFakeMsg(selfUid: string, element: PacketMsg[]): NapProtoEncodeStructType[] { return element.map((node): NapProtoEncodeStructType => { const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderUin}&spec=640&img_type=jpg`; @@ -19,6 +26,10 @@ export class PacketMsgBuilder { return acc !== undefined ? acc : msg.buildContent(); }, undefined); const msgElement = node.msg.flatMap(msg => msg.buildElement() ?? []); + if (!msgContent && !msgElement.length) { + this.logger.logWarn(`[PacketMsgBuilder] buildFakeMsg: 空的msgContent和msgElement!`); + msgElement.push(PacketMsgBuilder.failBackText.buildElement()); + } return { responseHead: { fromUid: "", diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index 1fa9e064..80205ec0 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -1,4 +1,3 @@ -import assert from "node:assert"; import * as zlib from "node:zlib"; import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto"; import { @@ -25,9 +24,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"; import { FileExtra, GroupFileExtra } from "@/core/packet/proto/message/component"; import { OidbSvcTrpcTcp0XE37_800Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800"; @@ -97,46 +96,6 @@ export class PacketMsgAtElement extends PacketMsgTextElement { } } -export class PacketMsgPicElement extends IPacketMsgElement { - path: string; - name: string; - size: number; - md5: string; - width: number; - height: number; - picType: PicType; - sha1: string | null = null; - msgInfo: NapProtoEncodeStructType | null = null; - groupPicExt: NapProtoEncodeStructType | null = null; - c2cPicExt: NapProtoEncodeStructType | null = null; - - constructor(element: SendPicElement) { - super(element); - this.path = element.picElement.sourcePath; - this.name = element.picElement.fileName; - this.size = +element.picElement.fileSize; - this.md5 = element.picElement.md5HexStr ?? ''; - this.width = element.picElement.picWidth; - this.height = element.picElement.picHeight; - this.picType = element.picElement.picType; - } - - buildElement(): NapProtoEncodeStructType[] { - assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); - return [{ - commonElem: { - serviceType: 48, - pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), - businessType: 10, - } - }]; - } - - toPreview(): string { - return "[图片]"; - } -} - export class PacketMsgReplyElement extends IPacketMsgElement { messageId: bigint; messageSeq: number; @@ -282,6 +241,46 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement { + path: string; + name: string; + size: number; + md5: string; + width: number; + height: number; + picType: PicType; + sha1: string | null = null; + msgInfo: NapProtoEncodeStructType | null = null; + groupPicExt: NapProtoEncodeStructType | null = null; + c2cPicExt: NapProtoEncodeStructType | null = null; + + constructor(element: SendPicElement) { + super(element); + this.path = element.picElement.sourcePath; + this.name = element.picElement.fileName; + this.size = +element.picElement.fileSize; + this.md5 = element.picElement.md5HexStr ?? ''; + this.width = element.picElement.picWidth; + this.height = element.picElement.picHeight; + this.picType = element.picElement.picType; + } + + buildElement(): NapProtoEncodeStructType[] { + if (!this.msgInfo) return []; + return [{ + commonElem: { + serviceType: 48, + pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + businessType: 10, + } + }]; + } + + toPreview(): string { + return "[图片]"; + } +} + export class PacketMsgVideoElement extends IPacketMsgElement { fileSize?: string; filePath?: string; @@ -308,7 +307,7 @@ export class PacketMsgVideoElement extends IPacketMsgElement { } buildElement(): NapProtoEncodeStructType[] { - assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); + if (!this.msgInfo) return []; return [{ commonElem: { serviceType: 48, @@ -340,7 +339,7 @@ export class PacketMsgPttElement extends IPacketMsgElement { } buildElement(): NapProtoEncodeStructType[] { - assert(this.msgInfo !== null, 'msgInfo is null, expected not null'); + if (!this.msgInfo) return []; return [{ commonElem: { serviceType: 48, @@ -376,7 +375,7 @@ export class PacketMsgFileElement extends IPacketMsgElement { } buildContent(): Uint8Array | undefined { - if (this.isGroupFile) return undefined; + if (this.isGroupFile || !this._e37_800_rsp) return undefined; return new NapProtoMsg(FileExtra).encode({ file: { fileType: 0, From da0fad743d81471b5da53d610d39f3d110ec66ce Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Fri, 25 Oct 2024 08:09:17 +0800 Subject: [PATCH 11/11] feat: maybe more stable fake forwardMsg --- src/common/forward-msg-builder.ts | 2 +- src/core/packet/msg/element.ts | 37 ++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/common/forward-msg-builder.ts b/src/common/forward-msg-builder.ts index bcd521b0..2771156b 100644 --- a/src/common/forward-msg-builder.ts +++ b/src/common/forward-msg-builder.ts @@ -99,7 +99,7 @@ export class ForwardMsgBuilder { senderName: msg.senderName, isGroupMsg: msg.groupId !== undefined, msg: msg.msg.map(m => ({ - preview: m.toPreview(), + preview: m.valid? m.toPreview() : "[该消息类型暂不支持查看]", })) }))); } diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index 80205ec0..fc602734 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -36,6 +36,10 @@ export abstract class IPacketMsgElement { protected constructor(rawElement: T) { } + get valid(): boolean { + return true; + } + buildContent(): Uint8Array | undefined { return undefined; } @@ -265,6 +269,10 @@ export class PacketMsgPicElement extends IPacketMsgElement { this.picType = element.picElement.picType; } + get valid(): boolean { + return !!this.msgInfo; + } + buildElement(): NapProtoEncodeStructType[] { if (!this.msgInfo) return []; return [{ @@ -306,6 +314,10 @@ export class PacketMsgVideoElement extends IPacketMsgElement { this.thumbHeight = element.videoElement.thumbHeight; } + get valid(): boolean { + return !!this.msgInfo; + } + buildElement(): NapProtoEncodeStructType[] { if (!this.msgInfo) return []; return [{ @@ -338,15 +350,20 @@ export class PacketMsgPttElement extends IPacketMsgElement { this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc } + get valid(): boolean { + return !!this.msgInfo; + } + buildElement(): NapProtoEncodeStructType[] { - if (!this.msgInfo) return []; - return [{ - commonElem: { - serviceType: 48, - pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), - businessType: 22, - } - }]; + return [] + // if (!this.msgInfo) return []; + // return [{ + // commonElem: { + // serviceType: 48, + // pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), + // businessType: 22, + // } + // }]; } toPreview(): string { @@ -374,6 +391,10 @@ export class PacketMsgFileElement extends IPacketMsgElement { this.fileSize = +element.fileElement.fileSize; } + get valid(): boolean { + return this.isGroupFile || Boolean(this._e37_800_rsp); + } + buildContent(): Uint8Array | undefined { if (this.isGroupFile || !this._e37_800_rsp) return undefined; return new NapProtoMsg(FileExtra).encode({