656 lines
25 KiB
TypeScript
656 lines
25 KiB
TypeScript
import { PacketHighwayClient } from '@/napcat-core/packet/highway/client';
|
|
import { PacketLogger } from '@/napcat-core/packet/context/loggerContext';
|
|
import FetchSessionKey from '@/napcat-core/packet/transformer/highway/FetchSessionKey';
|
|
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from '@/napcat-core/packet/highway/utils';
|
|
import {
|
|
PacketMsgFileElement,
|
|
PacketMsgPicElement,
|
|
PacketMsgPttElement,
|
|
PacketMsgVideoElement,
|
|
} from '@/napcat-core/packet/message/element';
|
|
import { ChatType, Peer } from '@/napcat-core/index';
|
|
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from '@/napcat-core/packet/utils/crypto/hash';
|
|
import UploadGroupImage from '@/napcat-core/packet/transformer/highway/UploadGroupImage';
|
|
import { NapProtoMsg } from 'napcat-protobuf';
|
|
import * as proto from '@/napcat-core/packet/transformer/proto';
|
|
import * as trans from '@/napcat-core/packet/transformer';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { NapCoreContext } from '@/napcat-core/packet/context/napCoreContext';
|
|
import { PacketClientContext } from '@/napcat-core/packet/context/clientContext';
|
|
import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg';
|
|
import { defaultVideoThumbB64 } from '@/napcat-core/helper/ffmpeg/video';
|
|
import { calculateFileMD5 } from 'napcat-common/src/file';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { imageSizeFallBack } from 'napcat-image-size/src/index';
|
|
|
|
export const BlockSize = 1024 * 1024;
|
|
const VIDEO_THUMBNAIL_MAX_BYTES = 1024 * 1024;
|
|
|
|
interface HighwayServerAddr {
|
|
ip: string;
|
|
port: number;
|
|
}
|
|
|
|
export interface PacketHighwaySig {
|
|
uin: string;
|
|
uid: string;
|
|
sigSession: Uint8Array | null;
|
|
sessionKey: Uint8Array | null;
|
|
serverAddr: HighwayServerAddr[];
|
|
}
|
|
|
|
export class PacketHighwayContext {
|
|
private readonly napcore: NapCoreContext;
|
|
private readonly client: PacketClientContext;
|
|
protected sig: PacketHighwaySig;
|
|
protected logger: PacketLogger;
|
|
protected hwClient: PacketHighwayClient;
|
|
private cachedPrepareReq: Promise<void> | null = null;
|
|
|
|
constructor (napcore: NapCoreContext, logger: PacketLogger, client: PacketClientContext) {
|
|
this.napcore = napcore;
|
|
this.client = client;
|
|
this.sig = {
|
|
uin: String(this.napcore.basicInfo.uin),
|
|
uid: this.napcore.basicInfo.uid,
|
|
sigSession: null,
|
|
sessionKey: null,
|
|
serverAddr: [],
|
|
};
|
|
this.logger = logger;
|
|
this.hwClient = new PacketHighwayClient(this.sig, this.logger);
|
|
}
|
|
|
|
private async checkAvailable () {
|
|
if (this.sig.sigSession === null || this.sig.sessionKey === null) {
|
|
if (this.cachedPrepareReq === null) {
|
|
this.cachedPrepareReq = this.prepareUpload().finally(() => {
|
|
this.cachedPrepareReq = null;
|
|
});
|
|
}
|
|
await this.cachedPrepareReq;
|
|
}
|
|
}
|
|
|
|
private async prepareUpload (): Promise<void> {
|
|
this.logger.debug('[Highway] on prepareUpload!');
|
|
const packet = FetchSessionKey.build();
|
|
const req = await this.client.sendOidbPacket(packet, true);
|
|
const rsp = FetchSessionKey.parse(req);
|
|
this.sig.sigSession = rsp.httpConn.sigSession;
|
|
this.sig.sessionKey = rsp.httpConn.sessionKey;
|
|
for (const info of rsp.httpConn.serverInfos) {
|
|
if (info.serviceType !== 1) continue;
|
|
for (const addr of info.serverAddrs) {
|
|
this.logger.debug(`[Highway PrepareUpload] server addr add: ${int32ip2str(addr.ip)}:${addr.port}`);
|
|
this.sig.serverAddr.push({
|
|
ip: int32ip2str(addr.ip),
|
|
port: addr.port,
|
|
});
|
|
this.hwClient.changeServer(int32ip2str(addr.ip), addr.port);
|
|
}
|
|
}
|
|
if (this.sig.serverAddr.length === 0) {
|
|
this.logger.warn('[Highway PrepareUpload] server addr is empty!');
|
|
}
|
|
}
|
|
|
|
async uploadImage (peer: Peer, img: PacketMsgPicElement): Promise<void> {
|
|
await this.checkAvailable();
|
|
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
|
await this.uploadGroupImage(+peer.peerUid, img);
|
|
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
|
await this.uploadC2CImage(peer.peerUid, img);
|
|
} else {
|
|
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
|
}
|
|
}
|
|
|
|
async uploadVideo (peer: Peer, video: PacketMsgVideoElement): Promise<void> {
|
|
await this.checkAvailable();
|
|
if (+(video.fileSize ?? 0) > 1024 * 1024 * 100) {
|
|
throw new Error(`[Highway] 视频文件过大: ${(+(video.fileSize ?? 0) / (1024 * 1024)).toFixed(2)} MB > 100 MB,请使用文件上传!`);
|
|
}
|
|
|
|
const cleanupThumbPaths: string[] = [];
|
|
try {
|
|
let thumbExists = false;
|
|
if (video.thumbPath) {
|
|
try {
|
|
await fs.promises.access(video.thumbPath, fs.constants.F_OK);
|
|
thumbExists = true;
|
|
} catch {
|
|
thumbExists = false;
|
|
}
|
|
}
|
|
|
|
let shouldGenerateThumb = !video.thumbPath || !thumbExists;
|
|
if (!shouldGenerateThumb) {
|
|
try {
|
|
await this.updateVideoThumbInfo(video);
|
|
if ((video.thumbSize ?? 0) > VIDEO_THUMBNAIL_MAX_BYTES) {
|
|
this.logger.warn(`[Highway] Video thumbnail is too large: ${video.thumbSize} bytes, regenerating`);
|
|
shouldGenerateThumb = true;
|
|
}
|
|
} catch (e) {
|
|
this.logger.warn(`[Highway] Failed to inspect video thumbnail, regenerating. Reason: ${e instanceof Error ? e.message : e}`);
|
|
shouldGenerateThumb = true;
|
|
}
|
|
}
|
|
|
|
if (shouldGenerateThumb) {
|
|
cleanupThumbPaths.push(await this.ensureVideoThumb(video));
|
|
}
|
|
video.businessType = peer.chatType === ChatType.KCHATTYPEGROUP ? 21 : 11;
|
|
|
|
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
|
await this.uploadGroupVideo(+peer.peerUid, video);
|
|
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
|
await this.uploadC2CVideo(peer.peerUid, video);
|
|
} else {
|
|
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
|
}
|
|
} finally {
|
|
await Promise.allSettled(cleanupThumbPaths.map(async (thumbToClean) => {
|
|
try {
|
|
await fs.promises.unlink(thumbToClean);
|
|
this.logger.debug(`[Highway] Cleaned up temp thumbnail: ${thumbToClean}`);
|
|
} catch (e) {
|
|
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
this.logger.warn(`[Highway] Failed to clean up temp thumbnail: ${thumbToClean}, reason: ${e instanceof Error ? e.message : e}`);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 使用当前 FFmpeg 适配器生成符合 QQ 大小限制的视频缩略图
|
|
* @returns 生成的临时缩略图路径,用于后续清理
|
|
*/
|
|
private async ensureVideoThumb (video: PacketMsgVideoElement): Promise<string> {
|
|
if (!video.filePath) {
|
|
throw new Error('video.filePath is empty, cannot generate thumbnail');
|
|
}
|
|
|
|
// 生成缩略图路径
|
|
const videoDir = path.dirname(video.filePath);
|
|
const videoBasename = path.basename(video.filePath, path.extname(video.filePath));
|
|
const thumbPath = path.join(videoDir, `${videoBasename}_thumb_${randomUUID()}.jpg`);
|
|
|
|
this.logger.debug(`[Highway] Video thumb missing, generating at: ${thumbPath}`);
|
|
video.thumbPath = thumbPath;
|
|
|
|
try {
|
|
// 尝试使用 FFmpeg 提取视频缩略图
|
|
await FFmpegService.extractThumbnail(video.filePath, thumbPath);
|
|
try {
|
|
await fs.promises.access(thumbPath, fs.constants.F_OK);
|
|
this.logger.debug('[Highway] Video thumbnail generated successfully using FFmpeg');
|
|
} catch {
|
|
throw new Error('FFmpeg failed to generate thumbnail');
|
|
}
|
|
await this.updateVideoThumbInfo(video);
|
|
if ((video.thumbSize ?? 0) > VIDEO_THUMBNAIL_MAX_BYTES) {
|
|
throw new Error(`generated thumbnail is too large: ${video.thumbSize} bytes`);
|
|
}
|
|
} catch (e) {
|
|
// FFmpeg 失败时(包括未初始化的情况)使用默认缩略图
|
|
this.logger.warn(`[Highway] Failed to extract thumbnail, using default. Reason: ${e instanceof Error ? e.message : e}`);
|
|
await fs.promises.writeFile(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
|
await this.updateVideoThumbInfo(video);
|
|
}
|
|
|
|
return thumbPath;
|
|
}
|
|
|
|
private async updateVideoThumbInfo (video: PacketMsgVideoElement): Promise<void> {
|
|
if (!video.thumbPath) {
|
|
throw new Error('video.thumbPath is empty, cannot inspect thumbnail');
|
|
}
|
|
const [thumbStat, thumbMd5, dimensions] = await Promise.all([
|
|
fs.promises.stat(video.thumbPath),
|
|
calculateFileMD5(video.thumbPath),
|
|
imageSizeFallBack(video.thumbPath),
|
|
]);
|
|
video.thumbSize = thumbStat.size;
|
|
video.thumbMd5 = thumbMd5;
|
|
video.thumbWidth = dimensions.width;
|
|
video.thumbHeight = dimensions.height;
|
|
this.logger.debug(`[Highway] Video thumbnail ready: ${dimensions.width}x${dimensions.height}, ${thumbStat.size} bytes`);
|
|
}
|
|
|
|
async uploadPtt (peer: Peer, ptt: PacketMsgPttElement): Promise<void> {
|
|
await this.checkAvailable();
|
|
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
|
await this.uploadGroupPtt(+peer.peerUid, ptt);
|
|
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
|
await this.uploadC2CPtt(peer.peerUid, ptt);
|
|
} else {
|
|
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
|
}
|
|
}
|
|
|
|
async uploadFile (peer: Peer, file: PacketMsgFileElement): Promise<void> {
|
|
await this.checkAvailable();
|
|
if (peer.chatType === ChatType.KCHATTYPEGROUP) {
|
|
await this.uploadGroupFile(+peer.peerUid, file);
|
|
} else if (peer.chatType === ChatType.KCHATTYPEC2C) {
|
|
await this.uploadC2CFile(peer.peerUid, file);
|
|
} else {
|
|
throw new Error(`[Highway] unsupported chatType: ${peer.chatType}`);
|
|
}
|
|
}
|
|
|
|
private async uploadGroupImage (groupUin: number, img: PacketMsgPicElement): Promise<void> {
|
|
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
|
const req = UploadGroupImage.build(groupUin, img);
|
|
const resp = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = UploadGroupImage.parse(resp);
|
|
const ukey = preRespData.upload.uKey;
|
|
if (ukey && ukey !== '') {
|
|
this.logger.debug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
|
|
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
|
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
|
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: index.fileUuid,
|
|
uKey: ukey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: [sha1],
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1004,
|
|
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
|
img.size,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadGroupImageReq get upload invalid ukey ${ukey}, don't need upload!`);
|
|
}
|
|
img.msgInfo = preRespData.upload.msgInfo;
|
|
// img.groupPicExt = new NapProtoMsg(CustomFace).decode(preRespData.tcpUpload.compatQMsg)
|
|
}
|
|
|
|
private async uploadC2CImage (peerUid: string, img: PacketMsgPicElement): Promise<void> {
|
|
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
|
const req = trans.UploadPrivateImage.build(peerUid, img);
|
|
const resp = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = trans.UploadPrivateImage.parse(resp);
|
|
const ukey = preRespData.upload.uKey;
|
|
if (ukey && ukey !== '') {
|
|
this.logger.debug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
|
|
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
|
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
|
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: index.fileUuid,
|
|
uKey: ukey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: [sha1],
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1003,
|
|
fs.createReadStream(img.path, { highWaterMark: BlockSize }),
|
|
img.size,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadC2CImageReq get upload invalid ukey ${ukey}, don't need upload!`);
|
|
}
|
|
img.msgInfo = preRespData.upload.msgInfo;
|
|
}
|
|
|
|
private async uploadGroupVideo (groupUin: number, video: PacketMsgVideoElement): Promise<void> {
|
|
if (!video.filePath || !video.thumbPath) throw new Error('video.filePath or video.thumbPath is empty');
|
|
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
|
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
|
const req = trans.UploadGroupVideo.build(groupUin, video);
|
|
const resp = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = trans.UploadGroupVideo.parse(resp);
|
|
const videoIndex = preRespData.upload.msgInfo.msgInfoBody[0]?.index;
|
|
const thumbIndex = preRespData.upload.msgInfo.msgInfoBody[1]?.index;
|
|
if (!videoIndex || !thumbIndex) {
|
|
throw new Error('[Highway] uploadGroupVideo response is missing video or thumbnail MsgInfo');
|
|
}
|
|
const ukey = preRespData.upload.uKey;
|
|
if (ukey && ukey !== '') {
|
|
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
|
|
const md5 = Buffer.from(videoIndex.info.fileHash, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: videoIndex.fileUuid,
|
|
uKey: ukey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: await calculateSha1StreamBytes(video.filePath),
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1005,
|
|
fs.createReadStream(video.filePath, { highWaterMark: BlockSize }),
|
|
+video.fileSize!,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
|
}
|
|
const subFile = preRespData.upload.subFileInfos[0];
|
|
if (subFile?.uKey && subFile.uKey !== '') {
|
|
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
|
const md5 = Buffer.from(thumbIndex.info.fileHash, 'hex');
|
|
const sha1 = Buffer.from(thumbIndex.info.fileSha1, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: thumbIndex.fileUuid,
|
|
uKey: subFile.uKey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: [sha1],
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1006,
|
|
fs.createReadStream(video.thumbPath, { highWaterMark: BlockSize }),
|
|
+video.thumbSize!,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile?.uKey ?? ''}, don't need upload!`);
|
|
}
|
|
video.msgInfo = preRespData.upload.msgInfo;
|
|
}
|
|
|
|
private async uploadC2CVideo (peerUid: string, video: PacketMsgVideoElement): Promise<void> {
|
|
if (!video.filePath || !video.thumbPath) throw new Error('video.filePath or video.thumbPath is empty');
|
|
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
|
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
|
const req = trans.UploadPrivateVideo.build(peerUid, video);
|
|
const resp = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = trans.UploadPrivateVideo.parse(resp);
|
|
const videoIndex = preRespData.upload.msgInfo.msgInfoBody[0]?.index;
|
|
const thumbIndex = preRespData.upload.msgInfo.msgInfoBody[1]?.index;
|
|
if (!videoIndex || !thumbIndex) {
|
|
throw new Error('[Highway] uploadC2CVideo response is missing video or thumbnail MsgInfo');
|
|
}
|
|
const ukey = preRespData.upload.uKey;
|
|
if (ukey && ukey !== '') {
|
|
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
|
|
const md5 = Buffer.from(videoIndex.info.fileHash, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: videoIndex.fileUuid,
|
|
uKey: ukey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: await calculateSha1StreamBytes(video.filePath),
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1001,
|
|
fs.createReadStream(video.filePath, { highWaterMark: BlockSize }),
|
|
+video.fileSize!,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
|
|
}
|
|
const subFile = preRespData.upload.subFileInfos[0];
|
|
if (subFile?.uKey && subFile.uKey !== '') {
|
|
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
|
|
const md5 = Buffer.from(thumbIndex.info.fileHash, 'hex');
|
|
const sha1 = Buffer.from(thumbIndex.info.fileSha1, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: thumbIndex.fileUuid,
|
|
uKey: subFile.uKey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: [sha1],
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1002,
|
|
fs.createReadStream(video.thumbPath, { highWaterMark: BlockSize }),
|
|
+video.thumbSize!,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile?.uKey ?? ''}, don't need upload!`);
|
|
}
|
|
video.msgInfo = preRespData.upload.msgInfo;
|
|
}
|
|
|
|
private async uploadGroupPtt (groupUin: number, ptt: PacketMsgPttElement): Promise<void> {
|
|
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
|
const req = trans.UploadGroupPtt.build(groupUin, ptt);
|
|
const resp = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = trans.UploadGroupPtt.parse(resp);
|
|
const ukey = preRespData.upload.uKey;
|
|
if (ukey && ukey !== '') {
|
|
this.logger.debug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
|
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
|
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
|
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: index.fileUuid,
|
|
uKey: ukey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: [sha1],
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1008,
|
|
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
|
|
ptt.fileSize,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadGroupPttReq get upload invalid ukey ${ukey}, don't need upload!`);
|
|
}
|
|
ptt.msgInfo = preRespData.upload.msgInfo;
|
|
}
|
|
|
|
private async uploadC2CPtt (peerUid: string, ptt: PacketMsgPttElement): Promise<void> {
|
|
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
|
const req = trans.UploadPrivatePtt.build(peerUid, ptt);
|
|
const resp = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = trans.UploadPrivatePtt.parse(resp);
|
|
const ukey = preRespData.upload.uKey;
|
|
if (ukey && ukey !== '') {
|
|
this.logger.debug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
|
|
const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
|
|
const md5 = Buffer.from(index.info.fileHash, 'hex');
|
|
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
|
|
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
|
|
fileUuid: index.fileUuid,
|
|
uKey: ukey,
|
|
network: {
|
|
ipv4S: oidbIpv4s2HighwayIpv4s(preRespData.upload.ipv4S),
|
|
},
|
|
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
|
|
blockSize: BlockSize,
|
|
hash: {
|
|
fileSha1: [sha1],
|
|
},
|
|
});
|
|
await this.hwClient.upload(
|
|
1007,
|
|
fs.createReadStream(ptt.filePath, { highWaterMark: BlockSize }),
|
|
ptt.fileSize,
|
|
md5,
|
|
extend
|
|
);
|
|
} else {
|
|
this.logger.debug(`[Highway] uploadC2CPttReq get upload invalid ukey ${ukey}, don't need upload!`);
|
|
}
|
|
ptt.msgInfo = preRespData.upload.msgInfo;
|
|
}
|
|
|
|
private async uploadGroupFile (groupUin: number, file: PacketMsgFileElement): Promise<void> {
|
|
file.isGroupFile = true;
|
|
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
|
file.fileSha1 = await calculateSha1(file.filePath);
|
|
const req = trans.UploadGroupFile.build(groupUin, file);
|
|
const resp = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = trans.UploadGroupFile.parse(resp);
|
|
if (!preRespData?.upload?.boolFileExist) {
|
|
this.logger.debug('[Highway] uploadGroupFileReq file not exist, need upload!');
|
|
const ext = new NapProtoMsg(proto.FileUploadExt).encode({
|
|
unknown1: 100,
|
|
unknown2: 1,
|
|
entry: {
|
|
busiBuff: {
|
|
senderUin: BigInt(this.sig.uin),
|
|
receiverUin: BigInt(groupUin),
|
|
groupCode: BigInt(groupUin),
|
|
},
|
|
fileEntry: {
|
|
fileSize: BigInt(file.fileSize),
|
|
md5: file.fileMd5,
|
|
md5S2: file.fileMd5,
|
|
checkKey: preRespData.upload.checkKey,
|
|
fileId: preRespData.upload.fileId,
|
|
uploadKey: preRespData.upload.fileKey,
|
|
},
|
|
clientInfo: {
|
|
clientType: 3,
|
|
appId: '100',
|
|
terminalType: 3,
|
|
clientVer: '1.1.1',
|
|
unknown: 4,
|
|
},
|
|
fileNameInfo: {
|
|
fileName: file.fileName,
|
|
},
|
|
host: {
|
|
hosts: [
|
|
{
|
|
url: {
|
|
host: preRespData.upload.uploadIp,
|
|
unknown: 1,
|
|
},
|
|
port: preRespData.upload.uploadPort,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
unknown200: 0,
|
|
});
|
|
await this.hwClient.upload(
|
|
71,
|
|
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
|
|
file.fileSize,
|
|
file.fileMd5,
|
|
ext
|
|
);
|
|
} else {
|
|
this.logger.debug('[Highway] uploadGroupFileReq file exist, don\'t need upload!');
|
|
}
|
|
file.fileUuid = preRespData.upload.fileId;
|
|
}
|
|
|
|
private async uploadC2CFile (peerUid: string, file: PacketMsgFileElement): Promise<void> {
|
|
file.isGroupFile = false;
|
|
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
|
file.fileSha1 = await calculateSha1(file.filePath);
|
|
const req = await trans.UploadPrivateFile.build(this.sig.uid, peerUid, file);
|
|
const res = await this.client.sendOidbPacket(req, true);
|
|
const preRespData = trans.UploadPrivateFile.parse(res);
|
|
if (!preRespData.upload?.boolFileExist) {
|
|
this.logger.debug('[Highway] uploadC2CFileReq file not exist, need upload!');
|
|
const ext = new NapProtoMsg(proto.FileUploadExt).encode({
|
|
unknown1: 100,
|
|
unknown2: 1,
|
|
entry: {
|
|
busiBuff: {
|
|
senderUin: BigInt(this.sig.uin),
|
|
},
|
|
fileEntry: {
|
|
fileSize: BigInt(file.fileSize),
|
|
md5: file.fileMd5,
|
|
md5S2: file.fileMd5,
|
|
checkKey: file.fileSha1,
|
|
fileId: preRespData.upload?.uuid,
|
|
uploadKey: preRespData.upload?.mediaPlatformUploadKey,
|
|
},
|
|
clientInfo: {
|
|
clientType: 3,
|
|
appId: '100',
|
|
terminalType: 3,
|
|
clientVer: '1.1.1',
|
|
unknown: 4,
|
|
},
|
|
fileNameInfo: {
|
|
fileName: file.fileName,
|
|
},
|
|
host: {
|
|
hosts: [
|
|
{
|
|
url: {
|
|
host: preRespData.upload?.uploadIp,
|
|
unknown: 1,
|
|
},
|
|
port: preRespData.upload?.uploadPort,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
unknown200: 1,
|
|
unknown3: 0,
|
|
});
|
|
await this.hwClient.upload(
|
|
95,
|
|
fs.createReadStream(file.filePath, { highWaterMark: BlockSize }),
|
|
file.fileSize,
|
|
file.fileMd5,
|
|
ext
|
|
);
|
|
}
|
|
file.fileUuid = preRespData.upload?.uuid;
|
|
file.fileHash = preRespData.upload?.fileAddon;
|
|
const fileExistReq = trans.DownloadOfflineFile.build(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
|
|
const fileExistRes = await this.client.sendOidbPacket(fileExistReq, true);
|
|
file._e37_800_rsp = trans.DownloadOfflineFile.parse(fileExistRes);
|
|
file._private_send_uid = this.sig.uid;
|
|
file._private_recv_uid = peerUid;
|
|
}
|
|
}
|