From d30d467a21124195a7a43995278cd418074c7e67 Mon Sep 17 00:00:00 2001 From: pk5ls20 Date: Wed, 16 Oct 2024 11:58:47 +0800 Subject: [PATCH] feat: broken highway --- src/core/apis/packet.ts | 5 +- src/core/packet/client.ts | 2 +- src/core/packet/highway/client.ts | 279 ++++++++++++++++++ src/core/packet/highway/session.ts | 143 +++++++++ src/core/packet/msg/builder.ts | 19 +- src/core/packet/msg/element.ts | 59 +++- src/core/packet/packer.ts | 129 ++++++-- src/core/packet/proto/action/action.ts | 114 +++++++ src/core/packet/proto/highway/highway.ts | 155 ++++++++++ src/core/packet/proto/oidb/Oidb.0x9067_202.ts | 4 +- .../proto/oidb/common/Ntv2.RichMedia.ts | 83 ------ .../proto/oidb/common/Ntv2.RichMediaReq.ts | 214 ++++++++++++++ .../proto/oidb/common/Ntv2.RichMediaResp.ts | 114 +++++++ src/core/packet/session.ts | 6 +- src/core/packet/utils/crypto/hash.ts | 16 + src/core/packet/utils/crypto/tea.ts | 86 ++++++ src/onebot/action/extends/UploadForwardMsg.ts | 68 +++-- 17 files changed, 1352 insertions(+), 144 deletions(-) create mode 100644 src/core/packet/highway/client.ts create mode 100644 src/core/packet/highway/session.ts create mode 100644 src/core/packet/proto/action/action.ts create mode 100644 src/core/packet/proto/highway/highway.ts delete mode 100644 src/core/packet/proto/oidb/common/Ntv2.RichMedia.ts create mode 100644 src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts create mode 100644 src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts create mode 100644 src/core/packet/utils/crypto/hash.ts create mode 100644 src/core/packet/utils/crypto/tea.ts diff --git a/src/core/apis/packet.ts b/src/core/apis/packet.ts index 9cf877e4..62a67775 100644 --- a/src/core/apis/packet.ts +++ b/src/core/apis/packet.ts @@ -9,6 +9,7 @@ import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase'; import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.fe1_2'; import { PacketForwardNode } from "@/core/packet/msg/entity/forward"; +import {LogWrapper} from "@/common/log"; interface OffsetType { [key: string]: { @@ -22,6 +23,7 @@ const typedOffset: OffsetType = offset; export class NTQQPacketApi { context: InstanceContext; core: NapCatCore; + logger: LogWrapper serverUrl: string | undefined; qqVersion: string | undefined; packetPacker: PacketPacker; @@ -30,7 +32,8 @@ export class NTQQPacketApi { constructor(context: InstanceContext, core: NapCatCore) { this.context = context; this.core = core; - this.packetPacker = new PacketPacker(); + this.logger = core.context.logger; + this.packetPacker = new PacketPacker(this.logger); this.packetSession = undefined; const config = this.core.configLoader.configData; if (config && config.packetServer && config.packetServer.length > 0) { diff --git a/src/core/packet/client.ts b/src/core/packet/client.ts index 18d61da5..9436e6ee 100644 --- a/src/core/packet/client.ts +++ b/src/core/packet/client.ts @@ -24,7 +24,7 @@ export class PacketClient { private readonly maxReconnectAttempts: number = 5;//现在暂时不可配置 private readonly cb = new LRUCache Promise>(500); // trace_id-type callback private readonly clientUrl: string = ''; - private readonly napCatCore: NapCatCore; + readonly napCatCore: NapCatCore; private readonly logger: LogWrapper; constructor(url: string, core: NapCatCore) { diff --git a/src/core/packet/highway/client.ts b/src/core/packet/highway/client.ts new file mode 100644 index 00000000..257b9e96 --- /dev/null +++ b/src/core/packet/highway/client.ts @@ -0,0 +1,279 @@ +import * as net from "node:net"; +import * as stream from 'node:stream'; +import * as crypto from 'node:crypto'; +import * as tea from '@/core/packet/utils/crypto/tea'; +import {BlockSize, PacketHighwaySig} from "@/core/packet/highway/session"; +import {NapProtoMsg} from "@/core/packet/proto/NapProto"; +import {ReqDataHighwayHead, RespDataHighwayHead} from "@/core/packet/proto/highway/highway"; +import {LogWrapper} from "@/common/log"; +import {createHash} from "crypto"; +import {toHexString} from "image-size/dist/types/utils"; + +interface PacketHighwayTrans { + uin: string; + cmd: number; + data: stream.Readable; + sum: Uint8Array; + size: number; + ticket: Uint8Array; + loginSig?: Uint8Array; + ext: Uint8Array; + encrypt: boolean; + timeout?: number; +} + +class PacketHighwayTransform extends stream.Transform { + private seq: number = 0; + private readonly trans: PacketHighwayTrans; + private offset: number = 0; + + constructor(trans: PacketHighwayTrans) { + super(); + this.trans = trans; + } + + private nextSeq() { + console.log(`[Highway] nextSeq: ${this.seq}`); + this.seq += 2; + return this.seq; + } + + private encryptTrans(trans: PacketHighwayTrans, key: Uint8Array) { + if (!trans.encrypt) return; + trans.ext = tea.encrypt(Buffer.from(trans.ext), Buffer.from(key)); + } + + buildHead(trans: PacketHighwayTrans, offset: number, length: number, md5Hash: Uint8Array): Uint8Array { + return new NapProtoMsg(ReqDataHighwayHead).encode({ + msgBaseHead: { + version: 1, + uin: trans.uin, // TODO: + command: "PicUp.DataUp", + seq: this.nextSeq(), + retryTimes: 0, + appId: 537234773, + dataFlag: 16, + commandId: trans.cmd, + }, + msgSegHead: { + filesize: BigInt(trans.size), + dataOffset: BigInt(offset), + dataLength: length, + serviceTicket: trans.ticket, + md5: md5Hash, + fileMd5: trans.sum, + }, + bytesReqExtendInfo: trans.ext, + timestamp: BigInt(Date.now()), + msgLoginSigHead: { + uint32LoginSigType: 8, + appId: 1600001615, + } + }) + } + + _transform(data: Buffer, encoding: BufferEncoding, callback: stream.TransformCallback) { + let offset = 0; // Offset within the current chunk + console.log(`[Highway] CALLED!!! _transform data.length = ${data.length}`); + while (offset < data.length) { + console.log(`[Highway] _transform offset = ${offset}, data.length = ${data.length}`); + const chunkSize = data.length > BlockSize ? BlockSize : data.length; + console.log(`[Highway] _transform calced chunkSize = ${chunkSize}`); + const chunk = data.slice(offset, offset + chunkSize); + const chunkMd5 = createHash('md5').update(chunk).digest(); + const head = this.buildHead(this.trans, this.offset, chunk.length, chunkMd5); + console.log(`[Highway] _transform: ${this.offset} | ${data.length} | ${chunkMd5.toString('hex')}`); + this.offset += chunk.length; + offset += chunk.length; + const headerBuffer = Buffer.allocUnsafe(9); + headerBuffer.writeUInt8(40); + headerBuffer.writeUInt32BE(head.length, 1); + headerBuffer.writeUInt32BE(chunk.length, 5); + this.push(headerBuffer); + this.push(head); + this.push(chunk); + this.push(Buffer.from([41])); + } + callback(null); + } +} + +export class PacketHighwayClient { + sig: PacketHighwaySig; + ip: string = 'htdata3.qq.com'; + port: number = 80; + logger: LogWrapper; + + constructor(sig: PacketHighwaySig, logger: LogWrapper) { + this.sig = sig; + this.logger = logger; + } + + changeServer(server: string, port: number) { + this.ip = server; + this.port = port; + } + + framePack(head: Buffer, body: Buffer): Buffer[] { + const buffers: Buffer[] = []; + const buffer0 = Buffer.alloc(9); + buffer0[0] = 0x28; + buffer0.writeUInt32BE(head.length, 1); + buffer0.writeUInt32BE(body.length, 5); + buffers.push(buffer0); + buffers.push(head); + buffers.push(body); + buffers.push(Buffer.from([0x29])); + return buffers; + } + + frameUnpack(frame: Buffer): [Buffer, Buffer] { + const headLen = frame.readUInt32BE(1); + const bodyLen = frame.readUInt32BE(5); + return [frame.slice(9, 9 + headLen), frame.slice(9 + headLen, 9 + headLen + bodyLen)]; + } + + async postHighwayContent(frame: Buffer[], serverURL: string, end: boolean): Promise { + try { + const combinedBuffer = Buffer.concat(frame); + const response: Response = await fetch(serverURL, { + method: 'POST', + headers: new Headers({ + 'Connection': end ? 'close' : 'keep-alive', + 'Accept-Encoding': 'identity', + 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)', + }), + body: combinedBuffer, + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status} - ${response.statusText}`); + } + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); + } catch (error) { + throw error; + } + } + + private async httpUploadBlock(trans: PacketHighwayTrans, offset: number, block: Buffer): Promise { + const highwayTransForm = new PacketHighwayTransform(trans); + const isEnd = offset + block.length === trans.size; + const md5 = crypto.createHash('md5').update(block).digest(); + const payload = highwayTransForm.buildHead(trans, offset, block.length, md5); + this.logger.log(`[Highway] httpUploadBlock: payload = ${toHexString(payload)}`); + const frame = this.framePack(Buffer.from(payload), block); + const addr = this.sig.serverAddr[0]; + this.logger.log(`[Highway] httpUploadBlock: ${offset} | ${block.length} | ${toHexString(md5)}`); + const resp = await this.postHighwayContent(frame, `http://${addr.ip}:${addr.port}/cgi-bin/httpconn?htcmd=0x6FF0087&uin=3767830885`, isEnd); + const [head, body] = this.frameUnpack(resp); + const headData = new NapProtoMsg(RespDataHighwayHead).decode(head); + this.logger.log(`[Highway] ${headData.errorCode} | ${headData.msgSegHead?.retCode} | ${headData.bytesRspExtendInfo} | ${head.toString('hex')} | ${body.toString('hex')}`); + if (headData.errorCode !== 0) { + throw new Error(`[Highway] upload failed (code: ${headData.errorCode})`); + } + } + + async httpUpload(cmd: number, data: stream.Readable, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array): Promise { + const trans: PacketHighwayTrans = { + uin: this.sig.uin, + cmd: cmd, + data: data, + sum: md5, + size: fileSize, + ticket: this.sig.sigSession!, + ext: extendInfo, + encrypt: false, + timeout: 360, // TODO: + }; + let offset = 0; + console.log(`[Highway] httpUpload trans=${JSON.stringify(trans)}`); + for await (const chunk of data) { + let buffer = chunk as Buffer; + try { + await this.httpUploadBlock(trans, offset, buffer); + } catch (err) { + console.error(`Error uploading block at offset ${offset}: ${err}`); + throw err; + } + offset += buffer.length; + } + } + + async tcpUpload(cmd: number, data: stream.Readable, fileSize: number, md5: Uint8Array, extendInfo: Uint8Array): Promise { + const trans: PacketHighwayTrans = { + uin: this.sig.uin, + cmd: cmd, + data: data, + sum: md5, + size: fileSize, + ticket: this.sig.sigSession!, + ext: extendInfo, + encrypt: false, + timeout: 360, // TODO: + }; + const highwayTransForm = new PacketHighwayTransform(trans); + return new Promise((resolve, reject) => { + const socket = net.connect(this.port, this.ip, () => { + trans.data.pipe(highwayTransForm).pipe(socket, {end: false}); + }) + const handleRspHeader = (header: Buffer) => { + console.log(`[Highway] handleRspHeader: ${header.toString('hex')}`); + const rsp = new NapProtoMsg(RespDataHighwayHead).decode(header); + if (rsp.errorCode !== 0) { + this.logger.logWarn(`highway upload failed (code: ${rsp.errorCode})`); + trans.data.unpipe(highwayTransForm).destroy(); + highwayTransForm.unpipe(socket).destroy(); + socket.end(); + reject(new Error(`highway upload failed (code: ${rsp.errorCode})`)); + } else { + const percent = ((Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength)) / Number(rsp.msgSegHead?.filesize)).toFixed(2); + this.logger.log(`[Highway] ${rsp.errorCode} | ${percent} | ${Buffer.from(header).toString('hex')}`); + if (rsp.msgSegHead?.flag === 1) { + this.logger.log('[Highway] tcpUpload finished.'); + socket.end(); + resolve(); + } + // if (Number(rsp.msgSegHead?.dataOffset) + Number(rsp.msgSegHead?.dataLength) > Number(rsp.msgSegHead?.filesize)) { + // this.logger.log('[Highway] tcpUpload finished.'); + // socket.end(); + // resolve(); + // } + } + }; + let buf = Buffer.alloc(0); + socket.on('data', (chunk: Buffer) => { + try { + buf = buf.length ? Buffer.concat([buf, chunk]) : chunk; + while (buf.length >= 5) { + const len = buf.readInt32BE(1); + if (buf.length >= len + 10) { + handleRspHeader(buf.slice(9, len + 9)); + buf = buf.slice(len + 10); + } else { + break; + } + } + } catch (e) { + this.logger.logError(`[Highway] upload error: ${e}`); + } + }) + socket.on('close', () => { + this.logger.log('[Highway] socket closed.'); + resolve(); + }) + socket.on('error', (err) => { + this.logger.logError('[Highway] socket.on tcpUpload error:', err); + }) + trans.data.on('error', (err) => { + this.logger.logError('[Highway] readable tcpUpload error:', err); + socket.end(); + }) + if (trans.timeout) { + setTimeout(() => { + this.logger.logError('[Highway] tcpUpload timeout!'); + socket.end(); + }, trans.timeout * 1000); + } + }) + } +} diff --git a/src/core/packet/highway/session.ts b/src/core/packet/highway/session.ts new file mode 100644 index 00000000..9e311707 --- /dev/null +++ b/src/core/packet/highway/session.ts @@ -0,0 +1,143 @@ +import * as fs from "node:fs"; +import {LogWrapper} from "@/common/log"; +import {PacketClient} from "@/core/packet/client"; +import {PacketPacker} from "@/core/packet/packer"; +import {NapProtoEncodeStructType, NapProtoMsg} from "@/core/packet/proto/NapProto"; +import {HttpConn0x6ff_501Response} from "@/core/packet/proto/action/action"; +import {PacketHighwayClient} from "@/core/packet/highway/client"; +import {ChatType, Peer} from "@/core"; +import {IPv4, 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 {NTHighwayIPv4, NTV2RichMediaHighwayExt} from "@/core/packet/proto/highway/highway"; + +export const BlockSize = 1024 * 1024; + +interface HighwayServerAddr { + ip: string + port: number +} + +export interface PacketHighwaySig { + uin: string; + sigSession: Uint8Array | null + sessionKey: Uint8Array | null + serverAddr: HighwayServerAddr[] +} + +export class PacketHighwaySession { + protected packetClient: PacketClient; + protected logger: LogWrapper; + protected packer: PacketPacker; + protected sig: PacketHighwaySig; + protected packetHighwayClient: PacketHighwayClient; + + constructor(logger: LogWrapper, client: PacketClient) { + this.packetClient = client; + this.logger = logger; + this.packer = new PacketPacker(logger); + this.sig = { + uin: this.packetClient.napCatCore.selfInfo.uin, + sigSession: null, + sessionKey: null, + serverAddr: [], + } + this.packetHighwayClient = new PacketHighwayClient(this.sig, this.logger); + } + + get available(): boolean { + return this.packetClient.available && this.sig.sigSession !== null && + this.sig.sessionKey !== null && this.sig.serverAddr.length > 0; + } + + private int32ip2str(ip: number) { + ip = ip & 0xffffffff; + return [ip & 0xff, (ip & 0xff00) >> 8, (ip & 0xff0000) >> 16, ((ip & 0xff000000) >> 24) & 0xff].join('.'); + } + + private oidbIpv4s2HighwayIpv4s(ipv4s: NapProtoEncodeStructType[]): NapProtoEncodeStructType[] { + return ipv4s.map((ipv4) => { + return { + domain: { + isEnable: true, + ip: this.int32ip2str(ipv4.outIP!), + } + } as NapProtoEncodeStructType + }) + } + + // TODO: add signal to auto prepare when ready + // TODO: refactor + async prepareUpload(): Promise { + this.logger.log('[Highway] prepare tcpUpload!'); + const packet = this.packer.packHttp0x6ff_501(); + const req = await this.packetClient.sendPacket('HttpConn.0x6ff_501', packet, true); + const u8RspData = Buffer.from(req.hex_data, 'hex'); + const rsp = new NapProtoMsg(HttpConn0x6ff_501Response).decode(u8RspData); + this.sig.sigSession = rsp.httpConn.sigSession + this.sig.sessionKey = rsp.httpConn.sessionKey + // this.logger.log(`[Highway] sigSession ${Buffer.from(this.sigSession).toString('hex')}, + // sessionKey ${Buffer.from(this.sessionKey).toString('hex')}`) + for (const info of rsp.httpConn.serverInfos) { + if (info.serviceType !== 1) continue; + for (const addr of info.serverAddrs) { + this.logger.log(`[Highway PrepareUpload] server addr add: ${this.int32ip2str(addr.ip)}:${addr.port}`); + this.sig.serverAddr.push({ + ip: this.int32ip2str(addr.ip), + port: addr.port + }) + } + } + } + + private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise { + if (!this.available) { + this.logger.logError('[Highway] not ready to Upload image!'); + return; + } + 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( + Buffer.from(preRespRaw.hex_data, 'hex') + ); + const preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body); + const ukey = preRespData.upload.uKey; + if (ukey && ukey != "") { + this.logger.log(`[Highway] get upload ukey: ${ukey}, need upload!`); + this.logger.log(preRespData.upload.msgInfo) + const index = preRespData.upload.msgInfo.msgInfoBody[0].index; + const sha1 = Buffer.from(index.info.fileSha1, 'hex'); + const md5 = Buffer.from(index.info.fileHash, 'hex'); + const extend = new NapProtoMsg(NTV2RichMediaHighwayExt).encode({ + fileUuid: index.fileUuid, + uKey: ukey, + network: { + ipv4S: this.oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S) + }, + msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, + blockSize: BlockSize, + hash: { + fileSha1: [sha1] + } + }) + console.log('extend', Buffer.from(extend).toString('hex')) + await this.packetHighwayClient.httpUpload(1004, fs.createReadStream(img.path, { highWaterMark: BlockSize }), img.size, md5, extend); + } else { + this.logger.logError(`[Highway] get upload invalid ukey ${ukey}, don't need upload!`); + } + img.msgInfo = preRespData.upload.msgInfo; + // img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg) + } + + async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise { + await this.prepareUpload(); + if (!this.available) { + this.logger.logError('[Highway] not ready to tcpUpload image!'); + return; + } + if (peer.chatType === ChatType.KCHATTYPEGROUP) { + await this.uploadGroupImageReq(Number(peer.peerUid), img); + } + // const uploadReq + } +} diff --git a/src/core/packet/msg/builder.ts b/src/core/packet/msg/builder.ts index 0d9681cd..aa52b6a2 100644 --- a/src/core/packet/msg/builder.ts +++ b/src/core/packet/msg/builder.ts @@ -1,12 +1,21 @@ -import { PushMsgBody } from "@/core/packet/proto/message/message"; -import { NapProtoEncodeStructType } from "@/core/packet/proto/NapProto"; import * as crypto from "crypto"; -import { PacketForwardNode } from "@/core/packet/msg/entity/forward"; +import {PushMsgBody} from "@/core/packet/proto/message/message"; +import {NapProtoEncodeStructType} from "@/core/packet/proto/NapProto"; +import {PacketForwardNode} from "@/core/packet/msg/entity/forward"; +import {LogWrapper} from "@/common/log"; export class PacketMsgBuilder { + private logger: LogWrapper; + + constructor(logger: LogWrapper) { + this.logger = logger; + } + buildFakeMsg(selfUid: string, element: PacketForwardNode[]): NapProtoEncodeStructType[] { return element.map((node): NapProtoEncodeStructType => { const avatar = `https://q.qlogo.cn/headimg_dl?dst_uin=${node.senderId}&spec=640&img_type=jpg`; + const msgElement = node.msg.map((msg) => msg.buildElement() ?? []); + // this.logger.logDebug(`NOW MSG ELEMENT: ${JSON.stringify(msgElement)}`); return { responseHead: { fromUid: "", @@ -41,9 +50,7 @@ export class PacketMsgBuilder { }, body: { richText: { - elems: node.msg.map( - (msg) => msg.buildElement() ?? [] - ) + elems: msgElement } } }; diff --git a/src/core/packet/msg/element.ts b/src/core/packet/msg/element.ts index b021b515..d8bcbdba 100644 --- a/src/core/packet/msg/element.ts +++ b/src/core/packet/msg/element.ts @@ -1,7 +1,9 @@ -import { NapProtoEncodeStructType, NapProtoMsg } from "@/core/packet/proto/NapProto"; -import { Elem, MentionExtra } from "@/core/packet/proto/message/element"; +import assert from "node:assert"; +import {NapProtoEncodeStructType, NapProtoMsg} from "@/core/packet/proto/NapProto"; +import {CustomFace, Elem, MentionExtra, NotOnlineImage} from "@/core/packet/proto/message/element"; import { AtType, + PicType, SendArkElement, SendFaceElement, SendFileElement, @@ -12,6 +14,7 @@ import { SendTextElement, SendVideoElement } from "@/core"; +import {MsgInfo} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; // raw <-> packet // TODO: check ob11 -> raw impl! @@ -34,7 +37,6 @@ export class PacketMsgTextElement extends IPacketMsgElement { constructor(element: SendTextElement) { super(element); - console.log(JSON.stringify(element)); this.text = element.textElement.content; } @@ -59,11 +61,11 @@ export class PacketMsgAtElement extends PacketMsgTextElement { buildElement(): NapProtoEncodeStructType { const res = 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, + } ); return { text: { @@ -74,17 +76,48 @@ 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 = Number(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, + } + } as NapProtoEncodeStructType + } +} + export class PacketMsgPttElement extends IPacketMsgElement { constructor(element: SendPttElement) { super(element); } } -export class PacketMsgPicElement extends IPacketMsgElement { - constructor(element: SendPicElement) { - super(element); - } -} export class PacketMsgReplyElement extends IPacketMsgElement { constructor(element: SendReplyElement) { diff --git a/src/core/packet/packer.ts b/src/core/packet/packer.ts index 9893d6d6..4808474b 100644 --- a/src/core/packet/packer.ts +++ b/src/core/packet/packer.ts @@ -1,21 +1,29 @@ import * as zlib from "node:zlib"; -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"; -import { OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body } from "@/core/packet/proto/oidb/Oidb.0x8FC_2"; -import { OidbSvcTrpcTcp0XFE1_2 } from "@/core/packet/proto/oidb/Oidb.fe1_2"; -import { OidbSvcTrpcTcp0XED3_1 } from "@/core/packet/proto/oidb/Oidb.ed3_1"; -import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action"; -import { PacketMsgBuilder } from "@/core/packet/msg/builder"; -import { PacketForwardNode } from "@/core/packet/msg/entity/forward"; +import * as crypto from "node:crypto"; +import {calculateSha1} 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"; +import {OidbSvcTrpcTcp0X8FC_2, OidbSvcTrpcTcp0X8FC_2_Body} from "@/core/packet/proto/oidb/Oidb.0x8FC_2"; +import {OidbSvcTrpcTcp0XFE1_2} from "@/core/packet/proto/oidb/Oidb.fe1_2"; +import {OidbSvcTrpcTcp0XED3_1} from "@/core/packet/proto/oidb/Oidb.ed3_1"; +import {NTV2RichMediaReq} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; +import {HttpConn0x6ff_501} from "@/core/packet/proto/action/action"; +import {LongMsgResult, SendLongMsgReq} from "@/core/packet/proto/message/action"; +import {PacketMsgBuilder} from "@/core/packet/msg/builder"; +import {PacketForwardNode} from "@/core/packet/msg/entity/forward"; +import {PacketMsgPicElement} from "@/core/packet/msg/element"; +import {LogWrapper} from "@/common/log"; export type PacketHexStr = string & { readonly hexNya: unique symbol }; export class PacketPacker { + private readonly logger: LogWrapper; private readonly packetBuilder: PacketMsgBuilder; - constructor() { - this.packetBuilder = new PacketMsgBuilder(); + constructor(logger: LogWrapper) { + this.logger = logger; + this.packetBuilder = new PacketMsgBuilder(logger); } private toHexStr(byteArray: Uint8Array): PacketHexStr { @@ -81,13 +89,13 @@ 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)); } - packUploadForwardMsg(selfUid: string, msg: PacketForwardNode[], groupUin: number = 0) : PacketHexStr { - // console.log("packUploadForwardMsg START!!!", selfUid, msg, groupUin); + packUploadForwardMsg(selfUid: string, msg: PacketForwardNode[], groupUin: number = 0): PacketHexStr { + // this.logger.logDebug("packUploadForwardMsg START!!!", selfUid, msg, groupUin); const msgBody = this.packetBuilder.buildFakeMsg(selfUid, msg); const longMsgResultData = new NapProtoMsg(LongMsgResult).encode( { @@ -99,9 +107,9 @@ export class PacketPacker { } } ); - // console.log("packUploadForwardMsg LONGMSGRESULT!!!", this.toHexStr(longMsgResultData)); + this.logger.logDebug("packUploadForwardMsg LONGMSGRESULT!!!", this.toHexStr(longMsgResultData)); const payload = zlib.gzipSync(Buffer.from(longMsgResultData)); - // console.log("packUploadForwardMsg PAYLOAD!!!", payload); + // this.logger.logDebug("packUploadForwardMsg PAYLOAD!!!", payload); const req = new NapProtoMsg(SendLongMsgReq).encode( { info: { @@ -117,7 +125,94 @@ export class PacketPacker { } } ); - // console.log("packUploadForwardMsg REQ!!!", req); + // this.logger.logDebug("packUploadForwardMsg REQ!!!", req); return this.toHexStr(req); } + + // highway part + packHttp0x6ff_501() { + return this.toHexStr(new NapProtoMsg(HttpConn0x6ff_501).encode({ + httpConn: { + field1: 0, + field2: 0, + field3: 16, + field4: 1, + field6: 3, + serviceTypes: [1, 5, 10, 21], + // tgt: "", // TODO: do we really need tgt? seems not + field9: 2, + field10: 9, + field11: 8, + ver: "1.0.1" + } + })); + } + + async packUploadGroupImgReq(groupUin: number, img: PacketMsgPicElement) { + const req = new NapProtoMsg(NTV2RichMediaReq).encode( + { + reqHead: { + common: { + requestId: 1, + command: 100 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 2, + group: { + groupUin: groupUin + }, + }, + client: { + agentType: 2 + } + }, + upload: { + uploadInfo: [ + { + fileInfo: { + fileSize: Number(img.size), + fileHash: img.md5, + fileSha1: this.toHexStr(await calculateSha1(img.path)), + fileName: img.name, + type: { + type: 1, + picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa + videoFormat: 0, + voiceFormat: 0, + }, + width: img.width, + height: img.height, + time: 0, + original: 1 + }, + subFileType: 0, + } + ], + tryFastUploadCompleted: true, + srvSendMsg: false, + clientRandomId: crypto.randomBytes(8).readBigUInt64BE() & BigInt('0x7FFFFFFFFFFFFFFF'), + compatQMsgSceneType: 2, + extBizInfo: { + pic: { + bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'), + textSummary: "Nya~", // TODO: + }, + video: { + bytesPbReserve: Buffer.alloc(0), + }, + ptt: { + bytesPbReserve: Buffer.alloc(0), + bytesReserve: Buffer.alloc(0), + bytesGeneralFlags: Buffer.alloc(0), + } + }, + clientSeq: 0, + noNeedCompatMsg: false, + } + } + ) + return this.toHexStr(this.packOidbPacket(0x11c4, 100, req, true, false)); + } } diff --git a/src/core/packet/proto/action/action.ts b/src/core/packet/proto/action/action.ts new file mode 100644 index 00000000..3d4c9e44 --- /dev/null +++ b/src/core/packet/proto/action/action.ts @@ -0,0 +1,114 @@ +import { ScalarType } from "@protobuf-ts/runtime"; +import { ProtoField } from "../NapProto"; +import {ContentHead, MessageBody, MessageControl, RoutingHead} from "@/core/packet/proto/message/message"; + +export const FaceRoamRequest = { + comm: ProtoField(1, () => PlatInfo, true), + selfUin: ProtoField(2, ScalarType.UINT32), + subCmd: ProtoField(3, ScalarType.UINT32), + field6: ProtoField(6, ScalarType.UINT32), +}; + +export const PlatInfo = { + imPlat: ProtoField(1, ScalarType.UINT32), + osVersion: ProtoField(2, ScalarType.STRING, true), + qVersion: ProtoField(3, ScalarType.STRING, true), +}; + +export const FaceRoamResponse = { + retCode: ProtoField(1, ScalarType.UINT32), + errMsg: ProtoField(2, ScalarType.STRING), + subCmd: ProtoField(3, ScalarType.UINT32), + userInfo: ProtoField(6, () => FaceRoamUserInfo), +}; + +export const FaceRoamUserInfo = { + fileName: ProtoField(1, ScalarType.STRING, false, true), + deleteFile: ProtoField(2, ScalarType.STRING, false, true), + bid: ProtoField(3, ScalarType.STRING), + maxRoamSize: ProtoField(4, ScalarType.UINT32), + emojiType: ProtoField(5, ScalarType.UINT32, false, true), +}; + +export const SendMessageRequest = { + state: ProtoField(1, ScalarType.INT32), + sizeCache: ProtoField(2, ScalarType.INT32), + unknownFields: ProtoField(3, ScalarType.BYTES), + routingHead: ProtoField(4, () => RoutingHead), + contentHead: ProtoField(5, () => ContentHead), + messageBody: ProtoField(6, () => MessageBody), + msgSeq: ProtoField(7, ScalarType.INT32), + msgRand: ProtoField(8, ScalarType.INT32), + syncCookie: ProtoField(9, ScalarType.BYTES), + msgVia: ProtoField(10, ScalarType.INT32), + dataStatist: ProtoField(11, ScalarType.INT32), + messageControl: ProtoField(12, () => MessageControl), + multiSendSeq: ProtoField(13, ScalarType.INT32), +}; + +export const SendMessageResponse = { + result: ProtoField(1, ScalarType.INT32), + errMsg: ProtoField(2, ScalarType.STRING, true), + timestamp1: ProtoField(3, ScalarType.UINT32), + field10: ProtoField(10, ScalarType.UINT32), + groupSequence: ProtoField(11, ScalarType.UINT32, true), + timestamp2: ProtoField(12, ScalarType.UINT32), + privateSequence: ProtoField(14, ScalarType.UINT32), +}; + +export const SetStatus = { + status: ProtoField(1, ScalarType.UINT32), + extStatus: ProtoField(2, ScalarType.UINT32), + batteryStatus: ProtoField(3, ScalarType.UINT32), + customExt: ProtoField(4, () => SetStatusCustomExt, true), +}; + +export const SetStatusCustomExt = { + faceId: ProtoField(1, ScalarType.UINT32), + text: ProtoField(2, ScalarType.STRING, true), + field3: ProtoField(3, ScalarType.UINT32), +}; + +export const SetStatusResponse = { + message: ProtoField(2, ScalarType.STRING), +}; + +export const HttpConn = { + field1: ProtoField(1, ScalarType.INT32), + field2: ProtoField(2, ScalarType.INT32), + field3: ProtoField(3, ScalarType.INT32), + field4: ProtoField(4, ScalarType.INT32), + tgt: ProtoField(5, ScalarType.STRING), + field6: ProtoField(6, ScalarType.INT32), + serviceTypes: ProtoField(7, ScalarType.INT32, false, true), + field9: ProtoField(9, ScalarType.INT32), + field10: ProtoField(10, ScalarType.INT32), + field11: ProtoField(11, ScalarType.INT32), + ver: ProtoField(15, ScalarType.STRING), +}; + +export const HttpConn0x6ff_501 = { + httpConn: ProtoField(0x501, () => HttpConn), +}; + +export const HttpConn0x6ff_501Response = { + httpConn: ProtoField(0x501, () => HttpConnResponse), +}; + +export const HttpConnResponse = { + sigSession: ProtoField(1, ScalarType.BYTES), + sessionKey: ProtoField(2, ScalarType.BYTES), + serverInfos: ProtoField(3, () => ServerInfo, false, true), +}; + +export const ServerAddr = { + type: ProtoField(1, ScalarType.UINT32), + ip: ProtoField(2, ScalarType.FIXED32), + port: ProtoField(3, ScalarType.UINT32), + area: ProtoField(4, ScalarType.UINT32), +}; + +export const ServerInfo = { + serviceType: ProtoField(1, ScalarType.UINT32), + serverAddrs: ProtoField(2, () => ServerAddr, false, true), +}; diff --git a/src/core/packet/proto/highway/highway.ts b/src/core/packet/proto/highway/highway.ts new file mode 100644 index 00000000..cd9cce5d --- /dev/null +++ b/src/core/packet/proto/highway/highway.ts @@ -0,0 +1,155 @@ +import {ScalarType} from "@protobuf-ts/runtime"; +import {ProtoField} from "../NapProto"; +import {MsgInfo, MsgInfoBody} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; + +export const DataHighwayHead = { + version: ProtoField(1, ScalarType.UINT32), + uin: ProtoField(2, ScalarType.STRING, true), + command: ProtoField(3, ScalarType.STRING, true), + seq: ProtoField(4, ScalarType.UINT32, true), + retryTimes: ProtoField(5, ScalarType.UINT32, true), + appId: ProtoField(6, ScalarType.UINT32), + dataFlag: ProtoField(7, ScalarType.UINT32), + commandId: ProtoField(8, ScalarType.UINT32), + buildVer: ProtoField(9, ScalarType.BYTES, true), +} + +export const FileUploadExt = { + unknown1: ProtoField(1, ScalarType.INT32), + unknown2: ProtoField(2, ScalarType.INT32), + unknown3: ProtoField(3, ScalarType.INT32), + entry: ProtoField(100, () => FileUploadEntry), + unknown200: ProtoField(200, ScalarType.INT32), +} + +export const FileUploadEntry = { + busiBuff: ProtoField(100, () => ExcitingBusiInfo), + fileEntry: ProtoField(200, () => ExcitingFileEntry), + clientInfo: ProtoField(300, () => ExcitingClientInfo), + fileNameInfo: ProtoField(400, () => ExcitingFileNameInfo), + host: ProtoField(500, () => ExcitingHostConfig), +} + +export const ExcitingBusiInfo = { + busId: ProtoField(1, ScalarType.INT32), + senderUin: ProtoField(100, ScalarType.UINT64), + receiverUin: ProtoField(200, ScalarType.UINT64), + groupCode: ProtoField(400, ScalarType.UINT64), +} + +export const ExcitingFileEntry = { + fileSize: ProtoField(100, ScalarType.UINT64), + md5: ProtoField(200, ScalarType.BYTES), + checkKey: ProtoField(300, ScalarType.BYTES), + md5S2: ProtoField(400, ScalarType.BYTES), + fileId: ProtoField(600, ScalarType.STRING), + uploadKey: ProtoField(700, ScalarType.BYTES), +} + +export const ExcitingClientInfo = { + clientType: ProtoField(100, ScalarType.INT32), + appId: ProtoField(200, ScalarType.STRING), + terminalType: ProtoField(300, ScalarType.INT32), + clientVer: ProtoField(400, ScalarType.STRING), + unknown: ProtoField(600, ScalarType.INT32), +} + +export const ExcitingFileNameInfo = { + fileName: ProtoField(100, ScalarType.STRING), +} + +export const ExcitingHostConfig = { + hosts: ProtoField(200, () => ExcitingHostInfo, false, true), +} + +export const ExcitingHostInfo = { + url: ProtoField(1, () => ExcitingUrlInfo), + port: ProtoField(2, ScalarType.UINT32), +} + +export const ExcitingUrlInfo = { + unknown: ProtoField(1, ScalarType.INT32), + host: ProtoField(2, ScalarType.STRING), +} + +export const LoginSigHead = { + uint32LoginSigType: ProtoField(1, ScalarType.UINT32), + bytesLoginSig: ProtoField(2, ScalarType.BYTES), + appId: ProtoField(3, ScalarType.UINT32), +} + +export const NTV2RichMediaHighwayExt = { + fileUuid: ProtoField(1, ScalarType.STRING), + uKey: ProtoField(2, ScalarType.STRING), + network: ProtoField(5, () => NTHighwayNetwork), + msgInfoBody: ProtoField(6, () => MsgInfoBody, false, true), + blockSize: ProtoField(10, ScalarType.UINT32), + hash: ProtoField(11, () => NTHighwayHash), +} + +export const NTHighwayHash = { + fileSha1: ProtoField(1, ScalarType.BYTES, false, true), +} + +export const NTHighwayNetwork = { + ipv4s: ProtoField(1, () => NTHighwayIPv4, false, true), +} + +export const NTHighwayIPv4 = { + domain: ProtoField(1, () => NTHighwayDomain), + port: ProtoField(2, ScalarType.UINT32), +} + +export const NTHighwayDomain = { + isEnable: ProtoField(1, ScalarType.BOOL), + ip: ProtoField(2, ScalarType.STRING), +} + +export const ReqDataHighwayHead = { + msgBaseHead: ProtoField(1, () => DataHighwayHead, true), + msgSegHead: ProtoField(2, () => SegHead, true), + bytesReqExtendInfo: ProtoField(3, ScalarType.BYTES, true), + timestamp: ProtoField(4, ScalarType.UINT64), + msgLoginSigHead: ProtoField(5, () => LoginSigHead, true), +} + +export const RespDataHighwayHead = { + msgBaseHead: ProtoField(1, () => DataHighwayHead, true), + msgSegHead: ProtoField(2, () => SegHead, true), + errorCode: ProtoField(3, ScalarType.UINT32), + allowRetry: ProtoField(4, ScalarType.UINT32), + cacheCost: ProtoField(5, ScalarType.UINT32), + htCost: ProtoField(6, ScalarType.UINT32), + bytesRspExtendInfo: ProtoField(7, ScalarType.BYTES, true), + timestamp: ProtoField(8, ScalarType.UINT64), + range: ProtoField(9, ScalarType.UINT64), + isReset: ProtoField(10, ScalarType.UINT32), +} + +export const SegHead = { + serviceId: ProtoField(1, ScalarType.UINT32, true), + filesize: ProtoField(2, ScalarType.UINT64), + dataOffset: ProtoField(3, ScalarType.UINT64, true), + dataLength: ProtoField(4, ScalarType.UINT32), + retCode: ProtoField(5, ScalarType.UINT32, true), + serviceTicket: ProtoField(6, ScalarType.BYTES), + flag: ProtoField(7, ScalarType.UINT32, true), + md5: ProtoField(8, ScalarType.BYTES), + fileMd5: ProtoField(9, ScalarType.BYTES), + cacheAddr: ProtoField(10, ScalarType.UINT32, true), + queryTimes: ProtoField(11, ScalarType.UINT32), + updateCacheIp: ProtoField(12, ScalarType.UINT32), + cachePort: ProtoField(13, ScalarType.UINT32, true), +} + +export const GroupAvatarExtra = { + type: ProtoField(1, ScalarType.UINT32), + groupUin: ProtoField(2, ScalarType.UINT32), + field3: ProtoField(3, () => GroupAvatarExtraField3), + field5: ProtoField(5, ScalarType.UINT32), + field6: ProtoField(6, ScalarType.UINT32), +} + +export const GroupAvatarExtraField3 = { + field1: ProtoField(1, ScalarType.UINT32), +} diff --git a/src/core/packet/proto/oidb/Oidb.0x9067_202.ts b/src/core/packet/proto/oidb/Oidb.0x9067_202.ts index 35c02ec8..ca52561a 100644 --- a/src/core/packet/proto/oidb/Oidb.0x9067_202.ts +++ b/src/core/packet/proto/oidb/Oidb.0x9067_202.ts @@ -1,6 +1,6 @@ import { ScalarType } from "@protobuf-ts/runtime"; import { ProtoField } from "../NapProto"; -import { MultiMediaReqHead } from "./common/Ntv2.RichMedia"; +import { MultiMediaReqHead } from "./common/Ntv2.RichMediaReq"; //Req export const OidbSvcTrpcTcp0X9067_202 = { @@ -23,4 +23,4 @@ export const OidbSvcTrpcTcp0X9067_202_Data = { }; export const OidbSvcTrpcTcp0X9067_202_Rsp_Body = { data: ProtoField(4, () => OidbSvcTrpcTcp0X9067_202_Data), -}; \ No newline at end of file +}; diff --git a/src/core/packet/proto/oidb/common/Ntv2.RichMedia.ts b/src/core/packet/proto/oidb/common/Ntv2.RichMedia.ts deleted file mode 100644 index d4f5f5b8..00000000 --- a/src/core/packet/proto/oidb/common/Ntv2.RichMedia.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ScalarType } from "@protobuf-ts/runtime"; -import { ProtoField } from "../../NapProto"; - -export const NTV2RichMediaReq = { - ReqHead: ProtoField(1, ScalarType.BYTES), - DownloadRKeyReq: ProtoField(4, ScalarType.BYTES), -}; -export const MultiMediaReqHead = { - Common: ProtoField(1, () => CommonHead), - Scene: ProtoField(2, () => SceneInfo), - Client: ProtoField(3, () => ClientMeta), -}; -export const CommonHead = { - RequestId: ProtoField(1, ScalarType.UINT32), - Command: ProtoField(2, ScalarType.UINT32), -}; -export const SceneInfo = { - RequestType: ProtoField(101, ScalarType.UINT32), - BusinessType: ProtoField(102, ScalarType.UINT32), - SceneType: ProtoField(200, ScalarType.UINT32), -}; -export const ClientMeta = { - AgentType: ProtoField(1, ScalarType.UINT32), -}; -export const C2CUserInfo = { - AccountType: ProtoField(1, ScalarType.UINT32), - TargetUid: ProtoField(2, ScalarType.STRING), -}; -export const GroupInfo = { - GroupUin: ProtoField(1, ScalarType.UINT32), -}; -export const DownloadReq = { - Node: ProtoField(1, ScalarType.BYTES), - Download: ProtoField(2, ScalarType.BYTES), -}; -export const FileInfo = { - FileSize: ProtoField(1, ScalarType.UINT32), - FileHash: ProtoField(2, ScalarType.STRING), - FileSha1: ProtoField(3, ScalarType.STRING), - FileName: ProtoField(4, ScalarType.STRING), - Type: ProtoField(5, ScalarType.BYTES), - Width: ProtoField(6, ScalarType.UINT32), - Height: ProtoField(7, ScalarType.UINT32), - Time: ProtoField(8, ScalarType.UINT32), - Original: ProtoField(9, ScalarType.UINT32), -}; -export const IndexNode = { - Info: ProtoField(1, ScalarType.BYTES), - FileUuid: ProtoField(2, ScalarType.STRING), - StoreId: ProtoField(3, ScalarType.UINT32), - UploadTime: ProtoField(4, ScalarType.UINT32), - Ttl: ProtoField(5, ScalarType.UINT32), - subType: ProtoField(6, ScalarType.UINT32), -}; -export const FileType = { - Type: ProtoField(1, ScalarType.UINT32), - PicFormat: ProtoField(2, ScalarType.UINT32), - VideoFormat: ProtoField(3, ScalarType.UINT32), - VoiceFormat: ProtoField(4, ScalarType.UINT32), -}; -export const DownloadExt = { - Pic: ProtoField(1, ScalarType.BYTES), - Video: ProtoField(2, ScalarType.BYTES), - Ptt: ProtoField(3, ScalarType.BYTES), -}; -export const VideoDownloadExt = { - BusiType: ProtoField(1, ScalarType.UINT32), - SceneType: ProtoField(2, ScalarType.UINT32), - SubBusiType: ProtoField(3, ScalarType.UINT32), -}; -export const PicDownloadExt = {}; -export const PttDownloadExt = {}; -export const PicUrlExtInfo = { - OriginalParameter: ProtoField(1, ScalarType.STRING), - BigParameter: ProtoField(2, ScalarType.STRING), - ThumbParameter: ProtoField(3, ScalarType.STRING), -}; -export const VideoExtInfo = { - VideoCodecFormat: ProtoField(1, ScalarType.UINT32), -}; -export const MsgInfo = { - -}; diff --git a/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts b/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts new file mode 100644 index 00000000..0d768159 --- /dev/null +++ b/src/core/packet/proto/oidb/common/Ntv2.RichMediaReq.ts @@ -0,0 +1,214 @@ +import {ScalarType} from "@protobuf-ts/runtime"; +import {ProtoField} from "../../NapProto"; + +export const NTV2RichMediaReq = { + ReqHead: ProtoField(1, () => MultiMediaReqHead), + Upload: ProtoField(2, () => UploadReq), + Download: ProtoField(3, () => DownloadReq), + DownloadRKey: ProtoField(4, () => DownloadRKeyReq), + Delete: ProtoField(5, () => DeleteReq), + UploadCompleted: ProtoField(6, () => UploadCompletedReq), + MsgInfoAuth: ProtoField(7, () => MsgInfoAuthReq), + UploadKeyRenewal: ProtoField(8, () => UploadKeyRenewalReq), + DownloadSafe: ProtoField(9, () => DownloadSafeReq), + Extension: ProtoField(99, ScalarType.BYTES, true), +}; + +export const MultiMediaReqHead = { + Common: ProtoField(1, () => CommonHead), + Scene: ProtoField(2, () => SceneInfo), + Client: ProtoField(3, () => ClientMeta), +}; + +export const CommonHead = { + RequestId: ProtoField(1, ScalarType.UINT32), + Command: ProtoField(2, ScalarType.UINT32), +}; + +export const SceneInfo = { + RequestType: ProtoField(101, ScalarType.UINT32), + BusinessType: ProtoField(102, ScalarType.UINT32), + SceneType: ProtoField(200, ScalarType.UINT32), + C2C: ProtoField(201, () => C2CUserInfo, true), + Group: ProtoField(202, () => NTGroupInfo, true), +}; + +export const C2CUserInfo = { + AccountType: ProtoField(1, ScalarType.UINT32), + TargetUid: ProtoField(2, ScalarType.STRING), +}; + +export const NTGroupInfo = { + GroupUin: ProtoField(1, ScalarType.UINT32), +}; + +export const ClientMeta = { + AgentType: ProtoField(1, ScalarType.UINT32), +}; + +export const DownloadReq = { + Node: ProtoField(1, () => IndexNode), + Download: ProtoField(2, () => DownloadExt), +}; + +export const IndexNode = { + Info: ProtoField(1, () => FileInfo), + FileUuid: ProtoField(2, ScalarType.STRING), + StoreId: ProtoField(3, ScalarType.UINT32), + UploadTime: ProtoField(4, ScalarType.UINT32), + Ttl: ProtoField(5, ScalarType.UINT32), + SubType: ProtoField(6, ScalarType.UINT32), +}; + +export const FileInfo = { + FileSize: ProtoField(1, ScalarType.UINT32), + FileHash: ProtoField(2, ScalarType.STRING), + FileSha1: ProtoField(3, ScalarType.STRING), + FileName: ProtoField(4, ScalarType.STRING), + Type: ProtoField(5, () => FileType), + Width: ProtoField(6, ScalarType.UINT32), + Height: ProtoField(7, ScalarType.UINT32), + Time: ProtoField(8, ScalarType.UINT32), + Original: ProtoField(9, ScalarType.UINT32), +}; + +export const FileType = { + Type: ProtoField(1, ScalarType.UINT32), + PicFormat: ProtoField(2, ScalarType.UINT32), + VideoFormat: ProtoField(3, ScalarType.UINT32), + VoiceFormat: ProtoField(4, ScalarType.UINT32), +}; + +export const DownloadExt = { + Pic: ProtoField(1, () => PicDownloadExt), + Video: ProtoField(2, () => VideoDownloadExt), + Ptt: ProtoField(3, () => PttDownloadExt), +}; + +export const VideoDownloadExt = { + BusiType: ProtoField(1, ScalarType.UINT32), + SceneType: ProtoField(2, ScalarType.UINT32), + SubBusiType: ProtoField(3, ScalarType.UINT32), +}; + +export const PicDownloadExt = {}; + +export const PttDownloadExt = {}; + +export const DownloadRKeyReq = { + Types: ProtoField(1, ScalarType.INT32, false, true), +}; + +export const DeleteReq = { + Index: ProtoField(1, () => IndexNode, false, true), + NeedRecallMsg: ProtoField(2, ScalarType.BOOL), + MsgSeq: ProtoField(3, ScalarType.UINT64), + MsgRandom: ProtoField(4, ScalarType.UINT64), + MsgTime: ProtoField(5, ScalarType.UINT64), +}; + +export const UploadCompletedReq = { + SrvSendMsg: ProtoField(1, ScalarType.BOOL), + ClientRandomId: ProtoField(2, ScalarType.UINT64), + MsgInfo: ProtoField(3, () => MsgInfo), + ClientSeq: ProtoField(4, ScalarType.UINT32), +}; + +export const MsgInfoAuthReq = { + Msg: ProtoField(1, ScalarType.BYTES), + AuthTime: ProtoField(2, ScalarType.UINT64), +}; + +export const DownloadSafeReq = { + Index: ProtoField(1, () => IndexNode), +}; + +export const UploadKeyRenewalReq = { + OldUKey: ProtoField(1, ScalarType.STRING), + SubType: ProtoField(2, ScalarType.UINT32), +}; + +export const MsgInfo = { + MsgInfoBody: ProtoField(1, () => MsgInfoBody, false, true), + ExtBizInfo: ProtoField(2, () => ExtBizInfo), +}; + +export const MsgInfoBody = { + Index: ProtoField(1, () => IndexNode), + Picture: ProtoField(2, () => PictureInfo), + Video: ProtoField(3, () => VideoInfo), + Audio: ProtoField(4, () => AudioInfo), + FileExist: ProtoField(5, ScalarType.BOOL), + HashSum: ProtoField(6, ScalarType.BYTES), +}; + +export const VideoInfo = {}; + +export const AudioInfo = {}; + +export const PictureInfo = { + UrlPath: ProtoField(1, ScalarType.STRING), + Ext: ProtoField(2, () => PicUrlExtInfo), + Domain: ProtoField(3, ScalarType.STRING), +}; + +export const PicUrlExtInfo = { + OriginalParameter: ProtoField(1, ScalarType.STRING), + BigParameter: ProtoField(2, ScalarType.STRING), + ThumbParameter: ProtoField(3, ScalarType.STRING), +}; + +export const VideoExtInfo = { + VideoCodecFormat: ProtoField(1, ScalarType.UINT32), +} + +export const ExtBizInfo = { + Pic: ProtoField(1, () => PicExtBizInfo), + Video: ProtoField(2, () => VideoExtBizInfo), + Ptt: ProtoField(3, () => PttExtBizInfo), + BusiType: ProtoField(10, ScalarType.UINT32), +}; + +export const PttExtBizInfo = { + SrcUin: ProtoField(1, ScalarType.UINT64), + PttScene: ProtoField(2, ScalarType.UINT32), + PttType: ProtoField(3, ScalarType.UINT32), + ChangeVoice: ProtoField(4, ScalarType.UINT32), + Waveform: ProtoField(5, ScalarType.BYTES), + AutoConvertText: ProtoField(6, ScalarType.UINT32), + BytesReserve: ProtoField(11, ScalarType.BYTES), + BytesPbReserve: ProtoField(12, ScalarType.BYTES), + BytesGeneralFlags: ProtoField(13, ScalarType.BYTES), +}; + +export const VideoExtBizInfo = { + FromScene: ProtoField(1, ScalarType.UINT32), + ToScene: ProtoField(2, ScalarType.UINT32), + BytesPbReserve: ProtoField(3, ScalarType.BYTES), +}; + +export const PicExtBizInfo = { + BizType: ProtoField(1, ScalarType.UINT32), + TextSummary: ProtoField(2, ScalarType.STRING), + BytesPbReserveC2c: ProtoField(11, ScalarType.BYTES), + BytesPbReserveTroop: ProtoField(12, ScalarType.BYTES), + FromScene: ProtoField(1001, ScalarType.UINT32), + ToScene: ProtoField(1002, ScalarType.UINT32), + OldFileId: ProtoField(1003, ScalarType.UINT32), +}; + +export const UploadReq = { + UploadInfo: ProtoField(1, () => UploadInfo, false, true), + TryFastUploadCompleted: ProtoField(2, ScalarType.BOOL), + SrvSendMsg: ProtoField(3, ScalarType.BOOL), + ClientRandomId: ProtoField(4, ScalarType.UINT64), + CompatQMsgSceneType: ProtoField(5, ScalarType.UINT32), + ExtBizInfo: ProtoField(6, () => ExtBizInfo), + ClientSeq: ProtoField(7, ScalarType.UINT32), + NoNeedCompatMsg: ProtoField(8, ScalarType.BOOL), +}; + +export const UploadInfo = { + FileInfo: ProtoField(1, () => FileInfo), + SubFileType: ProtoField(2, ScalarType.UINT32), +}; diff --git a/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts b/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts new file mode 100644 index 00000000..0b85b36e --- /dev/null +++ b/src/core/packet/proto/oidb/common/Ntv2.RichMediaResp.ts @@ -0,0 +1,114 @@ +import {ScalarType} from "@protobuf-ts/runtime"; +import {ProtoField} from "../../NapProto"; +import {CommonHead, MsgInfo, PicUrlExtInfo, VideoExtInfo} from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq"; + +export const NTV2RichMediaResp = { + respHead: ProtoField(1, () => MultiMediaRespHead), + upload: ProtoField(2, () => UploadResp), + download: ProtoField(3, () => DownloadResp), + downloadRKey: ProtoField(4, () => DownloadRKeyResp), + delete: ProtoField(5, () => DeleteResp), + uploadCompleted: ProtoField(6, () => UploadCompletedResp), + msgInfoAuth: ProtoField(7, () => MsgInfoAuthResp), + uploadKeyRenewal: ProtoField(8, () => UploadKeyRenewalResp), + downloadSafe: ProtoField(9, () => DownloadSafeResp), + extension: ProtoField(99, ScalarType.BYTES, true), +} + +export const MultiMediaRespHead = { + common: ProtoField(1, () => CommonHead), + retCode: ProtoField(2, ScalarType.UINT32), + message: ProtoField(3, ScalarType.STRING), +} + +export const DownloadResp = { + rKeyParam: ProtoField(1, ScalarType.STRING), + rKeyTtlSecond: ProtoField(2, ScalarType.UINT32), + info: ProtoField(3, () => DownloadInfo), + rKeyCreateTime: ProtoField(4, ScalarType.UINT32), +} + +export const DownloadInfo = { + domain: ProtoField(1, ScalarType.STRING), + urlPath: ProtoField(2, ScalarType.STRING), + httpsPort: ProtoField(3, ScalarType.UINT32), + ipv4s: ProtoField(4, () => IPv4, false, true), + ipv6s: ProtoField(5, () => IPv6, false, true), + picUrlExtInfo: ProtoField(6, () => PicUrlExtInfo), + videoExtInfo: ProtoField(7, () => VideoExtInfo), +} + +export const IPv4 = { + outIP: ProtoField(1, ScalarType.UINT32), + outPort: ProtoField(2, ScalarType.UINT32), + inIP: ProtoField(3, ScalarType.UINT32), + inPort: ProtoField(4, ScalarType.UINT32), + ipType: ProtoField(5, ScalarType.UINT32), +} + +export const IPv6 = { + outIP: ProtoField(1, ScalarType.BYTES), + outPort: ProtoField(2, ScalarType.UINT32), + inIP: ProtoField(3, ScalarType.BYTES), + inPort: ProtoField(4, ScalarType.UINT32), + ipType: ProtoField(5, ScalarType.UINT32), +} + +export const UploadResp = { + uKey: ProtoField(1, ScalarType.STRING, true), + uKeyTtlSecond: ProtoField(2, ScalarType.UINT32), + ipv4s: ProtoField(3, () => IPv4, false, true), + ipv6s: ProtoField(4, () => IPv6, false, true), + msgSeq: ProtoField(5, ScalarType.UINT64), + msgInfo: ProtoField(6, () => MsgInfo), + ext: ProtoField(7, () => RichMediaStorageTransInfo, false, true), + compatQMsg: ProtoField(8, ScalarType.BYTES), + subFileInfos: ProtoField(10, () => SubFileInfo, false, true), +} + +export const RichMediaStorageTransInfo = { + subType: ProtoField(1, ScalarType.UINT32), + extType: ProtoField(2, ScalarType.UINT32), + extValue: ProtoField(3, ScalarType.BYTES), +} + +export const SubFileInfo = { + subType: ProtoField(1, ScalarType.UINT32), + uKey: ProtoField(2, ScalarType.STRING), + uKeyTtlSecond: ProtoField(3, ScalarType.UINT32), + ipv4s: ProtoField(4, () => IPv4, false, true), + ipv6s: ProtoField(5, () => IPv6, false, true), +} + +export const DownloadSafeResp = { +} + +export const UploadKeyRenewalResp = { + ukey: ProtoField(1, ScalarType.STRING), + ukeyTtlSec: ProtoField(2, ScalarType.UINT64), +} + +export const MsgInfoAuthResp = { + authCode: ProtoField(1, ScalarType.UINT32), + msg: ProtoField(2, ScalarType.BYTES), + resultTime: ProtoField(3, ScalarType.UINT64), +} + +export const UploadCompletedResp = { + msgSeq: ProtoField(1, ScalarType.UINT64), +} + +export const DeleteResp = { +} + +export const DownloadRKeyResp = { + rKeys: ProtoField(1, () => RKeyInfo, false, true), +} + +export const RKeyInfo = { + rkey: ProtoField(1, ScalarType.STRING), + rkeyTtlSec: ProtoField(2, ScalarType.UINT64), + storeId: ProtoField(3, ScalarType.UINT32), + rkeyCreateTime: ProtoField(4, ScalarType.UINT32, true), + type: ProtoField(5, ScalarType.UINT32, true), +} diff --git a/src/core/packet/session.ts b/src/core/packet/session.ts index 775e1bc7..06cd5b7b 100644 --- a/src/core/packet/session.ts +++ b/src/core/packet/session.ts @@ -1,15 +1,15 @@ import { PacketClient } from "@/core/packet/client"; -import { PacketHighwayClient } from "@/core/packet/highway/highwayClient"; +import { PacketHighwaySession } from "@/core/packet/highway/session"; import { LogWrapper } from "@/common/log"; export class PacketSession { readonly logger: LogWrapper; readonly client: PacketClient; - private readonly highwayClient: PacketHighwayClient; + readonly highwaySession: PacketHighwaySession; constructor(logger: LogWrapper, client: PacketClient) { this.logger = logger; this.client = client; - this.highwayClient = new PacketHighwayClient(this.logger, this.client); + this.highwaySession = new PacketHighwaySession(this.logger, this.client); } } diff --git a/src/core/packet/utils/crypto/hash.ts b/src/core/packet/utils/crypto/hash.ts new file mode 100644 index 00000000..53901e6c --- /dev/null +++ b/src/core/packet/utils/crypto/hash.ts @@ -0,0 +1,16 @@ +// love from https://github.com/LagrangeDev/lagrangejs & https://github.com/takayama-lily/oicq +import * as crypto from 'crypto'; +import * as stream from 'stream'; +import * as fs from 'fs'; + +function sha1Stream(readable: stream.Readable) { + return new Promise((resolve, reject) => { + readable.on('error', reject); + readable.pipe(crypto.createHash('sha1').on('error', reject).on('data', resolve)); + }) as Promise; +} + +export function calculateSha1(filePath: string): Promise { + const readable = fs.createReadStream(filePath); + return sha1Stream(readable); +} diff --git a/src/core/packet/utils/crypto/tea.ts b/src/core/packet/utils/crypto/tea.ts new file mode 100644 index 00000000..0c8c1c99 --- /dev/null +++ b/src/core/packet/utils/crypto/tea.ts @@ -0,0 +1,86 @@ +// love from https://github.com/LagrangeDev/lagrangejs/blob/main/src/core/tea.ts & https://github.com/takayama-lily/oicq/blob/main/lib/core/tea.ts +const BUF7 = Buffer.alloc(7); +const deltas = [ + 0x9e3779b9, 0x3c6ef372, 0xdaa66d2b, 0x78dde6e4, 0x1715609d, 0xb54cda56, 0x5384540f, 0xf1bbcdc8, 0x8ff34781, + 0x2e2ac13a, 0xcc623af3, 0x6a99b4ac, 0x08d12e65, 0xa708a81e, 0x454021d7, 0xe3779b90, +]; + +function _toUInt32(num: number) { + return num >>> 0; +} + +function _encrypt(x: number, y: number, k0: number, k1: number, k2: number, k3: number): [number, number] { + for (let i = 0; i < 16; ++i) { + let aa = ((_toUInt32(((y << 4) >>> 0) + k0) ^ _toUInt32(y + deltas[i])) >>> 0) ^ _toUInt32(~~(y / 32) + k1); + aa >>>= 0; + x = _toUInt32(x + aa); + let bb = ((_toUInt32(((x << 4) >>> 0) + k2) ^ _toUInt32(x + deltas[i])) >>> 0) ^ _toUInt32(~~(x / 32) + k3); + bb >>>= 0; + y = _toUInt32(y + bb); + } + return [x, y]; +} + +export function encrypt(data: Buffer, key: Buffer) { + let n = (6 - data.length) >>> 0; + n = (n % 8) + 2; + const v = Buffer.concat([Buffer.from([(n - 2) | 0xf8]), Buffer.allocUnsafe(n), data, BUF7]); + const k0 = key.readUInt32BE(0); + const k1 = key.readUInt32BE(4); + const k2 = key.readUInt32BE(8); + const k3 = key.readUInt32BE(12); + let r1 = 0, r2 = 0, t1 = 0, t2 = 0; + for (let i = 0; i < v.length; i += 8) { + const a1 = v.readUInt32BE(i); + const a2 = v.readUInt32BE(i + 4); + const b1 = a1 ^ r1; + const b2 = a2 ^ r2; + const [x, y] = _encrypt(b1 >>> 0, b2 >>> 0, k0, k1, k2, k3); + r1 = x ^ t1; + r2 = y ^ t2; + t1 = b1; + t2 = b2; + v.writeInt32BE(r1, i); + v.writeInt32BE(r2, i + 4); + } + return v; +} + +function _decrypt(x: number, y: number, k0: number, k1: number, k2: number, k3: number) { + for (let i = 15; i >= 0; --i) { + const aa = ((_toUInt32(((x << 4) >>> 0) + k2) ^ _toUInt32(x + deltas[i])) >>> 0) ^ _toUInt32(~~(x / 32) + k3); + y = (y - aa) >>> 0; + const bb = ((_toUInt32(((y << 4) >>> 0) + k0) ^ _toUInt32(y + deltas[i])) >>> 0) ^ _toUInt32(~~(y / 32) + k1); + x = (x - bb) >>> 0; + } + return [x, y]; +} + +export function decrypt(encrypted: Buffer, key: Buffer) { + if (encrypted.length % 8) throw ERROR_ENCRYPTED_LENGTH; + const k0 = key.readUInt32BE(0); + const k1 = key.readUInt32BE(4); + const k2 = key.readUInt32BE(8); + const k3 = key.readUInt32BE(12); + let r1 = 0, r2 = 0, t1 = 0, t2 = 0, x = 0, y = 0; + for (let i = 0; i < encrypted.length; i += 8) { + const a1 = encrypted.readUInt32BE(i); + const a2 = encrypted.readUInt32BE(i + 4); + const b1 = a1 ^ x; + const b2 = a2 ^ y; + [x, y] = _decrypt(b1 >>> 0, b2 >>> 0, k0, k1, k2, k3); + r1 = x ^ t1; + r2 = y ^ t2; + t1 = a1; + t2 = a2; + encrypted.writeInt32BE(r1, i); + encrypted.writeInt32BE(r2, i + 4); + } + if (Buffer.compare(encrypted.subarray(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL + // if (Buffer.compare(encrypted.slice(encrypted.length - 7), BUF7) !== 0) throw ERROR_ENCRYPTED_ILLEGAL; + return encrypted.subarray((encrypted[0] & 0x07) + 3, encrypted.length - 7); + // return encrypted.slice((encrypted[0] & 0x07) + 3, encrypted.length - 7); +} + +const ERROR_ENCRYPTED_LENGTH = new Error('length of encrypted data must be a multiple of 8'); +const ERROR_ENCRYPTED_ILLEGAL = new Error('encrypted data is illegal'); diff --git a/src/onebot/action/extends/UploadForwardMsg.ts b/src/onebot/action/extends/UploadForwardMsg.ts index 239895aa..3fa1fcbc 100644 --- a/src/onebot/action/extends/UploadForwardMsg.ts +++ b/src/onebot/action/extends/UploadForwardMsg.ts @@ -1,15 +1,17 @@ import BaseAction from '../BaseAction'; -import { ActionName } from '../types'; -import { FromSchema, JSONSchema } from 'json-schema-to-ts'; - +import {ActionName} from '../types'; +import {FromSchema, JSONSchema} from 'json-schema-to-ts'; +import {ChatType, SendTextElement} from "@/core"; +import {PacketMsgPicElement, PacketMsgTextElement} from "@/core/packet/msg/element"; const SchemaData = { type: 'object', properties: { - group_id: { type: ['number', 'string'] }, + group_id: {type: ['number', 'string']}, + pic: {type: 'string'}, }, - required: ['group_id'], -}as const satisfies JSONSchema; + required: ['group_id', 'pic'], +} as const satisfies JSONSchema; type Payload = FromSchema; @@ -21,17 +23,47 @@ export class UploadForwardMsg extends BaseAction { if (!this.core.apis.PacketApi.available) { throw new Error('PacketClient is not init'); } - throw new Error('Not implemented'); - // return await this.core.apis.PacketApi.sendUploadForwardMsg([{ - // groupId: 0, - // senderId: 0, - // senderName: "NapCat", - // time: Math.floor(Date.now() / 1000), - // msg: [new PacketMsgTextElement({ - // textElement: { - // content: "Nya~" - // } - // } as SendTextElement)] - // }], 0); + // throw new Error('Not implemented'); + const peer = { + chatType: ChatType.KCHATTYPEGROUP, + peerUid: "10001", // TODO: must be a valid group id + } + const img = await this.core.apis.FileApi.createValidSendPicElement( + { + deleteAfterSentFiles: [], + peer: peer + }, + "", // TODO: + "www", + 0, + ) + const sendImg = new PacketMsgPicElement(img); + console.log(JSON.stringify(img)); + await this.core.apis.PacketApi.packetSession?.highwaySession.uploadImage( + peer, sendImg + ) + return await this.core.apis.PacketApi.sendUploadForwardMsg([ + { + groupId: 10001, + senderId: 10001, + senderName: "qwq", + time: Math.floor(Date.now() / 1000), + msg: [new PacketMsgTextElement({ + textElement: { + content: "Love from Napcat.Packet~" + } + } as SendTextElement)] + }, + { + groupId: 10001, + senderId: 10001, + senderName: "qwq", + time: Math.floor(Date.now() / 1000), + msg: [new PacketMsgTextElement({ + textElement: { + content: "Nya~" + } + } as SendTextElement), sendImg] + }], 10001); // TODO: must be a valid group id } }