import * as crypto from 'crypto'; import { PacketContext } from '@/snowcat-core/packet/context/packetContext'; import * as trans from '@/snowcat-core/packet/transformer'; import { PacketMsg } from '@/snowcat-core/packet/message/message'; import { PacketMsgFileElement, PacketMsgPicElement, PacketMsgPttElement, PacketMsgReplyElement, PacketMsgVideoElement, } from '@/snowcat-core/packet/message/element'; import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/snowcat-core/index'; import { MiniAppRawData, MiniAppReqParams } from '@/snowcat-core/packet/entities/miniApp'; import { AIVoiceChatType } from '@/snowcat-core/packet/entities/aiChat'; import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'snowcat-protobuf'; import { IndexNode, LongMsgResult, MsgInfo, PushMsgBody } from '@/snowcat-core/packet/transformer/proto'; import { OidbPacket } from '@/snowcat-core/packet/transformer/base'; import { ImageOcrResult } from '@/snowcat-core/packet/entities/ocrResult'; import { gunzipSync } from 'zlib'; import { PacketMsgConverter } from '@/snowcat-core/packet/message/converter'; import { UploadForwardMsgParams } from '@/snowcat-core/packet/transformer/message/UploadForwardMsgV2'; import { assertUploadResults } from '@/snowcat-core/packet/context/uploadResult'; export class PacketOperationContext { private readonly context: PacketContext; constructor (context: PacketContext) { this.context = context; } async sendPacket (pkt: OidbPacket, rsp?: T): Promise { return await this.context.client.sendOidbPacket(pkt, rsp); } async SendPoke (is_group: boolean, peer: number, target?: number) { const req = trans.SendPoke.build(is_group, peer, target ?? peer); await this.context.client.sendOidbPacket(req); } async SetGroupTodo (groupUin: number, msgSeq: string) { const req = trans.SetGroupTodo.build(groupUin, msgSeq); await this.context.client.sendOidbPacket(req, true); } async CompleteGroupTodo (groupUin: number, msgSeq: string) { const req = trans.CompleteGroupTodo.build(groupUin, msgSeq); await this.context.client.sendOidbPacket(req, true); } async CancelGroupTodo (groupUin: number, msgSeq: string) { const req = trans.CancelGroupTodo.build(groupUin, msgSeq); await this.context.client.sendOidbPacket(req, true); } async FetchRkey (timeout: number = 10000) { const req = trans.FetchRkey.build(); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.FetchRkey.parse(resp); return res.data.rkeyList; } async GroupSign (groupUin: number) { const req = trans.GroupSign.build(this.context.napcore.basicInfo.uin, groupUin); await this.context.client.sendOidbPacket(req); } async GetStrangerStatus (uin: number) { let status = 0; try { const req = trans.GetStrangerInfo.build(uin); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.GetStrangerInfo.parse(resp); const extBigInt = BigInt(res.data.status.value); if (extBigInt <= 10n) { return { status: Number(extBigInt) * 10, ext_status: 0 }; } status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); return { status: 10, ext_status: status }; } catch { return undefined; } } async SetGroupSpecialTitle (groupUin: number, uid: string, title: string) { const req = trans.SetSpecialTitle.build(groupUin, uid, title); await this.context.client.sendOidbPacket(req); } async UploadResources (msg: PacketMsg[], groupUin: number = 0) { const chatType = groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C; const peerUid = groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid; const reqList = msg.flatMap((m) => m.msg .map((e) => { if (e instanceof PacketMsgPicElement) { return this.context.highway.uploadImage({ chatType, peerUid }, e); } else if (e instanceof PacketMsgVideoElement) { return this.context.highway.uploadVideo({ chatType, peerUid }, e); } else if (e instanceof PacketMsgPttElement) { return this.context.highway.uploadPtt({ chatType, peerUid }, e); } else if (e instanceof PacketMsgFileElement) { return this.context.highway.uploadFile({ chatType, peerUid }, e); } return null; }) .filter(Boolean) ); const res = await Promise.allSettled(reqList); assertUploadResults(res, this.context.logger); } async UploadImage (img: PacketMsgPicElement) { await this.context.highway.uploadImage( { chatType: ChatType.KCHATTYPEC2C, peerUid: this.context.napcore.basicInfo.uid, }, img ); const index = img.msgInfo?.msgInfoBody?.at(0)?.index; if (!index) { throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined'); } return await this.GetImageUrl(this.context.napcore.basicInfo.uid, index); } async GetImageUrl (selfUid: string, node: NapProtoEncodeStructType) { const req = trans.DownloadImage.build(selfUid, node); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.DownloadImage.parse(resp); return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } async GetPttUrl (selfUid: string, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadPtt.build(selfUid, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadPtt.parse(resp); return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } async GetVideoUrl (selfUid: string, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadVideo.build(selfUid, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadVideo.parse(resp); return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } async GetGroupImageUrl (groupUin: number, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadGroupImage.build(groupUin, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadImage.parse(resp); return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } async GetGroupPttUrl (groupUin: number, node: NapProtoEncodeStructType, timeout?: number) { const req = trans.DownloadGroupPtt.build(groupUin, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadImage.parse(resp); return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } async GetGroupVideoUrl (groupUin: number, node: NapProtoEncodeStructType, timeout: number = 20000) { const req = trans.DownloadGroupVideo.build(groupUin, node); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadImage.parse(resp); return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } async ImageOCR (imgUrl: string) { const req = trans.ImageOCR.build(imgUrl); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.ImageOCR.parse(resp); return { texts: res.ocrRspBody.textDetections.map((item) => { return { text: item.detectedText, confidence: item.confidence, coordinates: item.polygon.coordinates.map((c) => { return { x: c.x, y: c.y, }; }), }; }), language: res.ocrRspBody.language, } as ImageOcrResult; } private async SendPreprocess (msg: PacketMsg[], groupUin: number = 0) { const ps = msg.map((m) => { return m.msg.map(async (e) => { if (e instanceof PacketMsgReplyElement && !e.targetElems) { this.context.logger.debug('Cannot find reply element\'s targetElems, prepare to fetch it...'); if (!e.targetPeer?.peerUid) { this.context.logger.error('targetPeer is undefined!'); } let targetMsg: NapProtoEncodeStructType[] | undefined; if (e.isGroupReply) { targetMsg = await this.FetchGroupMessage(+(e.targetPeer?.peerUid ?? 0), e.targetMessageSeq, e.targetMessageSeq); } else { targetMsg = await this.FetchC2CMessage(await this.context.napcore.basicInfo.uin2uid(e.targetUin), e.targetMessageSeq, e.targetMessageSeq); } e.targetElems = targetMsg.at(0)?.body?.richText?.elems; e.targetSourceMsg = targetMsg.at(0); } }); }).flat(); await Promise.all(ps); await this.UploadResources(msg, groupUin); } async FetchGroupMessage (groupUin: number, startSeq: number, endSeq: number): Promise[]> { const req = trans.FetchGroupMessage.build(groupUin, startSeq, endSeq); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.FetchGroupMessage.parse(resp); return res.body.messages; } async FetchC2CMessage (targetUid: string, startSeq: number, endSeq: number): Promise[]> { const req = trans.FetchC2CMessage.build(targetUid, startSeq, endSeq); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.FetchC2CMessage.parse(resp); return res.messages; } async UploadForwardMsg (msg: PacketMsg[], groupUin: number = 0) { await this.SendPreprocess(msg, groupUin); const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.UploadForwardMsg.parse(resp); return res.result.resId; } async UploadForwardMsgV2 (msg: UploadForwardMsgParams[], groupUin: number = 0) { // 遍历上传资源(跳过已有原始数据的项) await Promise.all(msg.filter(item => item.actionMsg).map(async (item) => { await this.SendPreprocess(item.actionMsg!, groupUin); })); const req = trans.UploadForwardMsgV2.build(this.context.napcore.basicInfo.uid, msg, groupUin); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.UploadForwardMsg.parse(resp); return res.result.resId; } async MoveGroupFile ( groupUin: number, fileUUID: string, currentParentDirectory: string, targetParentDirectory: string ) { const req = trans.MoveGroupFile.build(groupUin, fileUUID, currentParentDirectory, targetParentDirectory); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.MoveGroupFile.parse(resp); return res.move.retCode; } async RenameGroupFile (groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string) { const req = trans.RenameGroupFile.build(groupUin, fileUUID, currentParentDirectory, newName); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.RenameGroupFile.parse(resp); return res.rename.retCode; } async GetGroupFileUrl (groupUin: number, fileUUID: string, timeout?: number) { const req = trans.DownloadGroupFile.build(groupUin, fileUUID); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadGroupFile.parse(resp); return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`; } async GetPrivateFileUrl (self_id: string, fileUUID: string, md5: string, timeout?: number) { const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5); const resp = await this.context.client.sendOidbPacket(req, true, timeout); const res = trans.DownloadPrivateFile.parse(resp); return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`; } async GetMiniAppAdaptShareInfo (param: MiniAppReqParams) { const req = trans.GetMiniAppAdaptShareInfo.build(param); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.GetMiniAppAdaptShareInfo.parse(resp); return JSON.parse(res.content.jsonContent) as MiniAppRawData; } async FetchAiVoiceList (groupUin: number, chatType: AIVoiceChatType) { const req = trans.FetchAiVoiceList.build(groupUin, chatType); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.FetchAiVoiceList.parse(resp); if (!res.content) return null; return res.content.map((item) => { return { category: item.category, voices: item.voices, }; }); } async GetAiVoice ( groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType ): Promise> { let reqTime = 0; const reqMaxTime = 30; const sessionId = crypto.randomBytes(4).readUInt32BE(0); while (true) { if (reqTime >= reqMaxTime) { throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`); } reqTime++; const req = trans.GetAiVoice.build(groupUin, voiceId, text, sessionId, chatType); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.GetAiVoice.parse(resp); if (!res.msgInfo) continue; return res.msgInfo; } } async FetchForwardMsgRaw (resId: string) { const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, resId); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.DownloadForwardMsg.parse(resp); const inflate = gunzipSync(res.result.payload); const result = new NapProtoMsg(LongMsgResult).decode(inflate); return result.action; } async FetchForwardMsg (res_id: string): Promise { const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, res_id); const resp = await this.context.client.sendOidbPacket(req, true); const res = trans.DownloadForwardMsg.parse(resp); const inflate = gunzipSync(res.result.payload); const result = new NapProtoMsg(LongMsgResult).decode(inflate); const main = result.action.find((r) => r.actionCommand === 'MultiMsg'); if (!main?.actionData.msgBody) { throw new Error('msgBody is empty'); } this.context.logger.debug('rawChains ', inflate.toString('hex')); const messagesPromises = main.actionData.msgBody.map(async (msg) => { if (!msg?.body?.richText?.elems) { throw new Error('msg.body.richText.elems is empty'); } const rawChains = new PacketMsgConverter().packetMsgToRaw(msg?.body?.richText?.elems); const elements = await Promise.all( rawChains.map(async ([element, rawElem]) => { if (element.picElement && rawElem?.commonElem?.pbElem) { const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem); const index = extra?.msgInfoBody[0]?.index; if (msg?.responseHead.grp !== undefined) { const groupUin = msg?.responseHead.grp?.groupUin ?? 0; element.picElement = { ...element.picElement, originImageUrl: await this.GetGroupImageUrl(groupUin, index!), }; } else { element.picElement = { ...element.picElement, originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!), }; } return element; } if (element.videoElement && rawElem?.commonElem?.pbElem) { const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem); const index = extra?.msgInfoBody[0]?.index; if (!index) return element; if (msg?.responseHead.grp !== undefined) { element.videoElement = { ...element.videoElement, filePath: await this.GetGroupVideoUrl(msg.responseHead.grp.groupUin ?? 0, index), }; } else { element.videoElement = { ...element.videoElement, filePath: await this.GetVideoUrl(this.context.napcore.basicInfo.uid, index), }; } return element; } return element; }) ); const isGroup = msg?.responseHead.grp !== undefined; const peerUin = isGroup ? String(msg?.responseHead.grp?.groupUin ?? 0) : String(msg?.responseHead.fromUin ?? 0); const peerUid = isGroup ? String(msg?.responseHead.grp?.groupUin ?? 0) : String(msg?.responseHead.fromUid ?? ''); const sendNickName = isGroup ? (msg?.responseHead.grp?.memberName ?? '') : (msg?.responseHead.forward?.friendName ?? ''); return { chatType: isGroup ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, elements, guildId: '', isOnlineMsg: false, msgId: '7467703692092974645', // TODO: no necessary msgRandom: '0', msgSeq: String(msg.contentHead.sequence ?? 0), msgTime: String(msg.contentHead.timeStamp ?? 0), msgType: NTMsgType.KMSGTYPEMIX, parentMsgIdList: [], parentMsgPeer: { chatType: isGroup ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C, peerUid, }, peerName: '', peerUid, peerUin, recallTime: '0', records: [], sendNickName, sendRemarkName: sendNickName, senderUid: msg?.responseHead.fromUid ?? '', senderUin: String(msg?.responseHead.fromUin ?? 0), sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN, subMsgType: 1, }; }); return await Promise.all(messagesPromises); } }