mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
feat & fix: add video msg pack & upload, fix some bugs in uploading c2c elements
This commit is contained in:
@@ -12,7 +12,7 @@ import { LogWrapper } from "@/common/log";
|
|||||||
import { SendLongMsgResp } from "@/core/packet/proto/message/action";
|
import { SendLongMsgResp } from "@/core/packet/proto/message/action";
|
||||||
import { PacketMsg } from "@/core/packet/msg/message";
|
import { PacketMsg } from "@/core/packet/msg/message";
|
||||||
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
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 {
|
interface OffsetType {
|
||||||
@@ -118,7 +118,13 @@ export class NTQQPacketApi {
|
|||||||
if (e instanceof PacketMsgPicElement) {
|
if (e instanceof PacketMsgPicElement) {
|
||||||
reqList.push(this.packetSession?.highwaySession.uploadImage({
|
reqList.push(this.packetSession?.highwaySession.uploadImage({
|
||||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
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));
|
}, e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,9 +8,10 @@ import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
|
|||||||
import { PacketHighwayClient } from "@/core/packet/highway/client";
|
import { PacketHighwayClient } from "@/core/packet/highway/client";
|
||||||
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
||||||
import { OidbSvcTrpcTcpBaseRsp } from "@/core/packet/proto/oidb/OidbBase";
|
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 { NTV2RichMediaHighwayExt } from "@/core/packet/proto/highway/highway";
|
||||||
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
|
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
|
||||||
|
import { calculateSha1StreamBytes } from "@/core/packet/utils/crypto/hash";
|
||||||
|
|
||||||
export const BlockSize = 1024 * 1024;
|
export const BlockSize = 1024 * 1024;
|
||||||
|
|
||||||
@@ -94,6 +95,17 @@ export class PacketHighwaySession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadVideo(peer: Peer, video: PacketMsgVideoElement): Promise<void> {
|
||||||
|
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<void> {
|
private async uploadGroupImageReq(groupUin: number, img: PacketMsgPicElement): Promise<void> {
|
||||||
const preReq = await this.packer.packUploadGroupImgReq(groupUin, img);
|
const preReq = await this.packer.packUploadGroupImgReq(groupUin, img);
|
||||||
const preRespRaw = await this.packetClient.sendPacket('OidbSvcTrpcTcp.0x11c4_100', preReq, true);
|
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 preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||||
const ukey = preRespData.upload.uKey;
|
const ukey = preRespData.upload.uKey;
|
||||||
if (ukey && 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 index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||||
@@ -121,13 +133,13 @@ export class PacketHighwaySession {
|
|||||||
});
|
});
|
||||||
await this.packetHighwayClient.upload(
|
await this.packetHighwayClient.upload(
|
||||||
1004,
|
1004,
|
||||||
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
fs.createReadStream(img.path, {highWaterMark: BlockSize}),
|
||||||
img.size,
|
img.size,
|
||||||
md5,
|
md5,
|
||||||
extend
|
extend
|
||||||
);
|
);
|
||||||
} else {
|
} 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.msgInfo = preRespData.upload.msgInfo;
|
||||||
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
|
// 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 preRespData = new NapProtoMsg(NTV2RichMediaResp).decode(preResp.body);
|
||||||
const ukey = preRespData.upload.uKey;
|
const ukey = preRespData.upload.uKey;
|
||||||
if (ukey && 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 index = preRespData.upload.msgInfo.msgInfoBody[0].index;
|
||||||
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
||||||
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
||||||
@@ -160,12 +172,145 @@ export class PacketHighwaySession {
|
|||||||
});
|
});
|
||||||
await this.packetHighwayClient.upload(
|
await this.packetHighwayClient.upload(
|
||||||
1003,
|
1003,
|
||||||
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
fs.createReadStream(img.path, {highWaterMark: BlockSize}),
|
||||||
img.size,
|
img.size,
|
||||||
md5,
|
md5,
|
||||||
extend
|
extend
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
this.logger.logDebug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
|
||||||
}
|
}
|
||||||
img.msgInfo = preRespData.upload.msgInfo;
|
img.msgInfo = preRespData.upload.msgInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async uploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toPreview(): string {
|
toPreview(): string {
|
||||||
return '[nya~]';
|
return '[暂不支持该消息类型喵~]';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,19 +84,15 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
|
|||||||
text: {
|
text: {
|
||||||
str: this.text,
|
str: this.text,
|
||||||
pbReserve: new NapProtoMsg(MentionExtra).encode({
|
pbReserve: new NapProtoMsg(MentionExtra).encode({
|
||||||
type: this.atAll ? 1 : 2,
|
type: this.atAll ? 1 : 2,
|
||||||
uin: 0,
|
uin: 0,
|
||||||
field5: 0,
|
field5: 0,
|
||||||
uid: this.targetUid,
|
uid: this.targetUid,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
toPreview(): string {
|
|
||||||
return `@${this.targetUid} ${this.text}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||||
@@ -189,7 +185,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toPreview(): string {
|
toPreview(): string {
|
||||||
return "[回复]";
|
return "[回复消息]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,8 +281,41 @@ export class PacketMsgMarkFaceElement extends IPacketMsgElement<SendMarketFaceEl
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
|
export class PacketMsgVideoElement extends IPacketMsgElement<SendVideoElement> {
|
||||||
|
fileSize?: string;
|
||||||
|
filePath?: string;
|
||||||
|
thumbSize?: number;
|
||||||
|
thumbPath?: string;
|
||||||
|
fileMd5?: string;
|
||||||
|
thumbMd5?: string;
|
||||||
|
thumbWidth?: number;
|
||||||
|
thumbHeight?: number;
|
||||||
|
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
|
||||||
|
|
||||||
constructor(element: SendVideoElement) {
|
constructor(element: SendVideoElement) {
|
||||||
super(element);
|
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<typeof Elem>[] {
|
||||||
|
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 "[视频]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ import { NTV2RichMediaReq } from "@/core/packet/proto/oidb/common/Ntv2.RichMedia
|
|||||||
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
|
import { HttpConn0x6ff_501 } from "@/core/packet/proto/action/action";
|
||||||
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
|
import { LongMsgResult, SendLongMsgReq } from "@/core/packet/proto/message/action";
|
||||||
import { PacketMsgBuilder } from "@/core/packet/msg/builder";
|
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 { LogWrapper } from "@/common/log";
|
||||||
import { PacketMsg } from "@/core/packet/msg/message";
|
import { PacketMsg } from "@/core/packet/msg/message";
|
||||||
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
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));
|
return this.toHexStr(this.packOidbPacket(0x11c5, 100, req, true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async packUploadGroupVideoReq(groupUin: number, video: PacketMsgVideoElement): Promise<PacketHexStr> {
|
||||||
|
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<PacketHexStr> {
|
||||||
|
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 {
|
packGroupFileDownloadReq(groupUin: number, fileUUID: string): PacketHexStr {
|
||||||
return this.toHexStr(
|
return this.toHexStr(
|
||||||
this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({
|
this.packOidbPacket(0x6D6, 2, new NapProtoMsg(OidbSvcTrpcTcp0x6D6).encode({
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import * as stream from 'stream';
|
import * as stream from 'stream';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import {CalculateStreamBytesTransform} from "@/core/packet/utils/crypto/sha1StreamBytesTransform";
|
||||||
|
|
||||||
function sha1Stream(readable: stream.Readable) {
|
function sha1Stream(readable: stream.Readable) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -14,3 +15,21 @@ export function calculateSha1(filePath: string): Promise<Buffer> {
|
|||||||
const readable = fs.createReadStream(filePath);
|
const readable = fs.createReadStream(filePath);
|
||||||
return sha1Stream(readable);
|
return sha1Stream(readable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateSha1StreamBytes(filePath: string): Promise<Buffer[]> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
19
src/core/packet/utils/crypto/sha1Stream.test.ts
Normal file
19
src/core/packet/utils/crypto/sha1Stream.test.ts
Normal file
@@ -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();
|
118
src/core/packet/utils/crypto/sha1Stream.ts
Normal file
118
src/core/packet/utils/crypto/sha1Stream.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
53
src/core/packet/utils/crypto/sha1StreamBytesTransform.ts
Normal file
53
src/core/packet/utils/crypto/sha1StreamBytesTransform.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user