import * as crypto from 'node:crypto'; import * as zlib from 'node:zlib'; import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'napcat-protobuf'; import { CustomFace, Elem, FileExtra, GroupFileExtra, MarkdownData, MentionExtra, MsgInfo, NotOnlineImage, OidbSvcTrpcTcp0XE37_800Response, PushMsgBody, QBigFaceExtra, QSmallFaceExtra, } from '@/napcat-core/packet/transformer/proto'; import { ElementType, FaceType, MessageElement, NTMsgAtType, NTVideoType, PicType, Peer, } from '@/napcat-core/types/msg'; import { SendArkElement, SendFaceElement, SendFileElement, SendMarkdownElement, SendMarketFaceElement, SendPicElement, SendPttElement, SendReplyElement, SendMultiForwardMsgElement, SendTextElement, SendVideoElement, } from '@/napcat-core/types/element'; import { ForwardMsgBuilder } from '@/napcat-core/helper/forward-msg-builder'; import { PacketMsg, PacketSendMsgElement } from '@/napcat-core/packet/message/message'; export type ParseElementFnR = [MessageElement, NapProtoDecodeStructType | null] | undefined; type ParseElementFn = (elem: NapProtoDecodeStructType) => ParseElementFnR; // raw <-> packet // TODO: SendStructLongMsgElement export abstract class IPacketMsgElement { protected constructor (_rawElement: T) { } get valid (): boolean { return true; } buildContent (): Uint8Array | undefined { return undefined; } buildElement (): NapProtoEncodeStructType[] { return []; } static parseElement: ParseElementFn; toPreview (): string { return '[暂不支持该消息类型喵~]'; } } export class PacketMsgTextElement extends IPacketMsgElement { text: string; constructor (element: SendTextElement) { super(element); this.text = element.textElement.content; } override buildElement (): NapProtoEncodeStructType[] { return [{ text: { str: this.text, }, }]; } static override parseElement = (elem: NapProtoDecodeStructType): ParseElementFnR => { if (elem.text?.str && (elem.text?.attr6Buf === undefined || elem.text?.attr6Buf?.length === 0)) { return [{ textElement: { content: elem.text?.str, atType: NTMsgAtType.ATTYPEUNKNOWN, atUid: '', atTinyId: '', atNtUid: '', }, elementType: ElementType.UNKNOWN, elementId: '', }, null]; } return undefined; }; override toPreview (): string { return this.text; } } export class PacketMsgAtElement extends PacketMsgTextElement { targetUid: string; atAll: boolean; constructor (element: SendTextElement) { super(element); this.targetUid = element.textElement.atNtUid; this.atAll = element.textElement.atType === NTMsgAtType.ATTYPEALL; } override buildElement (): NapProtoEncodeStructType[] { return [{ text: { str: this.text, pbReserve: new NapProtoMsg(MentionExtra).encode({ type: this.atAll ? 1 : 2, uin: 0, field5: 0, uid: this.targetUid, } ), }, }]; } static override parseElement = (elem: NapProtoDecodeStructType): ParseElementFnR => { if (elem.text?.str && (elem.text?.attr6Buf?.length ?? 100) >= 11) { return [{ textElement: { content: elem.text?.str, atType: NTMsgAtType.ATTYPEONE, atUid: String(Buffer.from(elem.text!.attr6Buf!).readUInt32BE(7)), // FIXME: hack atTinyId: '', atNtUid: '', }, elementType: ElementType.UNKNOWN, elementId: '', }, null]; } return undefined; }; } export class PacketMsgReplyElement extends IPacketMsgElement { time: number; targetMessageId: bigint; targetMessageSeq: number; targetMessageClientSeq: number; targetUin: number; targetUid: string; targetElems?: NapProtoEncodeStructType[]; targetSourceMsg?: NapProtoEncodeStructType; targetPeer?: Peer; constructor (element: SendReplyElement) { super(element); this.time = +(element.replyElement.replyMsgTime ?? Math.floor(Date.now() / 1000)); this.targetMessageId = BigInt(element.replyElement.replayMsgId ?? 0); this.targetMessageSeq = +(element.replyElement.replayMsgSeq ?? 0); this.targetMessageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0); this.targetUin = +(element.replyElement.senderUin ?? 0); this.targetUid = element.replyElement.senderUidStr ?? ''; this.targetPeer = element.replyElement._replyMsgPeer; } get isGroupReply (): boolean { return this.targetMessageClientSeq === 0; } override buildElement (): NapProtoEncodeStructType[] { return [{ srcMsg: { origSeqs: [this.isGroupReply ? this.targetMessageSeq : this.targetMessageClientSeq], senderUin: BigInt(this.targetUin), time: this.time, elems: this.targetElems ?? [], sourceMsg: new NapProtoMsg(PushMsgBody).encode(this.targetSourceMsg ?? {}), toUin: BigInt(0), }, }]; } static override parseElement = (elem: NapProtoDecodeStructType): ParseElementFnR => { if (elem.srcMsg && elem.srcMsg.pbReserve) { const reserve = elem.srcMsg.pbReserve; return [{ replyElement: { replayMsgSeq: String(reserve.friendSeq ?? elem.srcMsg?.origSeqs?.[0] ?? 0), replayMsgId: String(reserve.messageId ?? 0), senderUin: String(elem?.srcMsg ?? 0), }, elementType: ElementType.UNKNOWN, elementId: '', }, null]; } return undefined; }; override toPreview (): string { return '[回复消息]'; } } export class PacketMsgFaceElement extends IPacketMsgElement { faceId: number; faceText: string; isLargeFace: boolean; resultId?: string; constructor (element: SendFaceElement) { super(element); this.faceId = element.faceElement.faceIndex; this.faceText = element.faceElement.faceText || '[表情]'; this.resultId = element.faceElement.resultId; this.isLargeFace = element.faceElement.faceType === FaceType.AniSticke; } override buildElement (): NapProtoEncodeStructType[] { if (this.isLargeFace) { return [{ commonElem: { serviceType: 37, pbElem: new NapProtoMsg(QBigFaceExtra).encode({ aniStickerPackId: '1', aniStickerId: '8', faceId: this.faceId, sourceType: 1, resultId: this.resultId, preview: '', randomType: 1, }), businessType: 1, }, }, { // QQ consumes the element after an animated face as compatibility text. // Without it, the next user-supplied message segment is discarded. text: { str: this.faceText, }, }]; } else if (this.faceId < 260) { return [{ face: { index: this.faceId, }, }]; } else { return [{ commonElem: { serviceType: 33, pbElem: new NapProtoMsg(QSmallFaceExtra).encode({ faceId: this.faceId, preview: '', preview2: '', }), businessType: 1, }, }]; } } static override parseElement = (elem: NapProtoDecodeStructType): ParseElementFnR => { if (elem.face?.index) { return [{ faceElement: { faceIndex: elem.face.index, faceType: FaceType.Normal, }, elementType: ElementType.UNKNOWN, elementId: '', }, null]; } if (elem?.commonElem?.serviceType === 37 && elem?.commonElem?.pbElem) { const qface = new NapProtoMsg(QBigFaceExtra).decode(elem?.commonElem?.pbElem); if (qface?.faceId) { return [{ faceElement: { faceIndex: qface.faceId, faceType: FaceType.Normal, }, elementType: ElementType.UNKNOWN, elementId: '', }, null]; } } if (elem?.commonElem?.serviceType === 33 && elem?.commonElem?.pbElem) { const qface = new NapProtoMsg(QSmallFaceExtra).decode(elem?.commonElem?.pbElem); if (qface?.faceId) { return [{ faceElement: { faceIndex: qface.faceId, faceType: FaceType.Normal, }, elementType: ElementType.UNKNOWN, elementId: '', }, null]; } } return undefined; }; override toPreview (): string { return '[表情]'; } } export class PacketMsgMarkFaceElement extends IPacketMsgElement { emojiName: string; emojiId: string; emojiPackageId: number; emojiKey: string; constructor (element: SendMarketFaceElement) { super(element); this.emojiName = element.marketFaceElement.faceName; this.emojiId = element.marketFaceElement.emojiId; this.emojiPackageId = element.marketFaceElement.emojiPackageId; this.emojiKey = element.marketFaceElement.key; } override buildElement (): NapProtoEncodeStructType[] { return [{ marketFace: { faceName: this.emojiName, itemType: 6, faceInfo: 1, faceId: Buffer.from(this.emojiId, 'hex'), tabId: this.emojiPackageId, subType: 3, key: this.emojiKey, imageWidth: 300, imageHeight: 300, pbReserve: { field8: 1, }, }, }]; } override toPreview (): string { return `${this.emojiName}`; } } export class PacketMsgPicElement extends IPacketMsgElement { path: string; name: string; size: number; md5: string; width: number; height: number; picType: PicType; picSubType: number; summary: string; sha1: string | null = null; msgInfo: NapProtoEncodeStructType | null = null; groupPicExt: NapProtoEncodeStructType | null = null; c2cPicExt: NapProtoEncodeStructType | null = null; constructor (element: SendPicElement) { super(element); this.path = element.picElement.sourcePath; this.name = element.picElement.fileName; this.size = +element.picElement.fileSize; this.md5 = element.picElement.md5HexStr ?? ''; this.width = element.picElement.picWidth; this.height = element.picElement.picHeight; this.picType = element.picElement.picType; this.picSubType = element.picElement.picSubType ?? 0; this.summary = element.picElement.summary === '' ? ( element.picElement.picSubType === 0 ? '[图片]' : '[动画表情]' ) : element.picElement.summary; } override get valid (): boolean { return !!this.msgInfo; } override buildElement (): NapProtoEncodeStructType[] { if (!this.msgInfo) return []; return [{ commonElem: { serviceType: 48, pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), businessType: 10, }, }]; } static override parseElement = (elem: NapProtoDecodeStructType): ParseElementFnR => { if (elem?.commonElem?.serviceType === 48 && [10, 20].includes(elem.commonElem.businessType ?? 0) && elem.commonElem.pbElem) { const extra = new NapProtoMsg(MsgInfo).decode(elem.commonElem.pbElem); const msgInfoBody = extra.msgInfoBody[0]; const index = msgInfoBody?.index; return [{ picElement: { fileSize: index?.info.fileSize ?? 0, picWidth: index?.info?.width ?? 0, picHeight: index?.info?.height ?? 0, fileName: index?.info?.fileHash ?? '', sourcePath: '', original: false, picType: PicType.NEWPIC_APNG, fileUuid: '', fileSubId: '', thumbFileSize: 0, summary: '[图片]', thumbPath: new Map(), }, elementType: ElementType.PIC, elementId: '', }, elem]; } if (elem?.notOnlineImage) { const img = elem?.notOnlineImage; // url in originImageUrl const preImg: MessageElement = { picElement: { fileSize: img.fileLen ?? 0, picWidth: img.picWidth ?? 0, picHeight: img.picHeight ?? 0, fileName: Buffer.from(img.picMd5!).toString('hex') ?? '', sourcePath: '', original: false, picType: PicType.NEWPIC_APNG, fileUuid: '', fileSubId: '', thumbFileSize: 0, summary: '[图片]', thumbPath: new Map(), }, elementType: ElementType.UNKNOWN, elementId: '', }; if (img.origUrl?.includes('&fileid=')) { preImg.picElement!.originImageUrl = `https://multimedia.nt.qq.com.cn${img.origUrl}`; } else { preImg.picElement!.originImageUrl = `https://gchat.qpic.cn${img.origUrl}`; } return [preImg, elem]; } return undefined; }; override toPreview (): string { return this.summary; } } export class PacketMsgVideoElement extends IPacketMsgElement { fileName: string; fileSize?: string; filePath?: string; fileTime: number; fileFormat: NTVideoType; fileWidth?: number; fileHeight?: number; thumbSize?: number; thumbPath?: string; fileMd5?: string; fileSha1?: string; thumbMd5?: string; thumbSha1?: string; thumbWidth?: number; thumbHeight?: number; businessType: 11 | 21 = 21; msgInfo: NapProtoEncodeStructType | null = null; constructor (element: SendVideoElement) { super(element); this.fileName = element.videoElement.fileName; this.fileSize = element.videoElement.fileSize; this.filePath = element.videoElement.filePath; this.fileTime = Math.max(0, Math.round(element.videoElement.fileTime ?? 0)); this.fileFormat = element.videoElement.fileFormat ?? NTVideoType.VIDEO_FORMAT_MP4; this.fileWidth = element.videoElement.fileWidth; this.fileHeight = element.videoElement.fileHeight; this.thumbSize = element.videoElement.thumbSize; this.thumbPath = element.videoElement.thumbPath?.get(0) as string | undefined; this.fileMd5 = element.videoElement.videoMd5; this.thumbMd5 = element.videoElement.thumbMd5; this.thumbWidth = element.videoElement.thumbWidth; this.thumbHeight = element.videoElement.thumbHeight; } override get valid (): boolean { return !!this.msgInfo; } override buildElement (): NapProtoEncodeStructType[] { if (!this.msgInfo) return []; return [{ commonElem: { serviceType: 48, pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), businessType: this.businessType, }, }]; } static override parseElement = (elem: NapProtoDecodeStructType): ParseElementFnR => { if (elem?.commonElem?.serviceType !== 48 || ![11, 21].includes(elem.commonElem.businessType ?? 0) || !elem.commonElem.pbElem) { return undefined; } const extra = new NapProtoMsg(MsgInfo).decode(elem.commonElem.pbElem); const videoIndex = extra.msgInfoBody[0]?.index; const thumbIndex = extra.msgInfoBody[1]?.index; if (!videoIndex) return undefined; return [{ videoElement: { filePath: '', fileName: videoIndex.info.fileName || videoIndex.info.fileHash, videoMd5: videoIndex.info.fileHash, thumbMd5: thumbIndex?.info.fileHash, fileTime: videoIndex.info.time, fileFormat: (videoIndex.info.type.videoFormat || NTVideoType.VIDEO_FORMAT_MP4) as NTVideoType, fileSize: String(videoIndex.info.fileSize), fileWidth: videoIndex.info.width, fileHeight: videoIndex.info.height, thumbWidth: thumbIndex?.info.width, thumbHeight: thumbIndex?.info.height, thumbSize: thumbIndex?.info.fileSize, thumbPath: new Map(), fileUuid: videoIndex.fileUuid, busiType: 0, subBusiType: 0, }, elementType: ElementType.VIDEO, elementId: '', }, elem]; }; override toPreview (): string { return '[视频]'; } } export class PacketMsgPttElement extends IPacketMsgElement { filePath: string; fileSize: number; fileMd5: string; fileSha1?: string; fileDuration: number; msgInfo: NapProtoEncodeStructType | null = null; constructor (element: SendPttElement) { super(element); this.filePath = element.pttElement.filePath; this.fileSize = +element.pttElement.fileSize; // TODO: cc this.fileMd5 = element.pttElement.md5HexStr; this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc } override get valid (): boolean { return false; } override buildElement (): NapProtoEncodeStructType[] { // return []; if (!this.msgInfo) return []; return [{ commonElem: { serviceType: 48, pbElem: new NapProtoMsg(MsgInfo).encode(this.msgInfo), businessType: 22, }, }]; } override toPreview (): string { return '[语音]'; } } export class PacketMsgFileElement extends IPacketMsgElement { fileName: string; filePath: string; fileSize: number; fileSha1?: Uint8Array; fileMd5?: Uint8Array; fileUuid?: string; fileHash?: string; isGroupFile?: boolean; _private_send_uid?: string; _private_recv_uid?: string; _e37_800_rsp?: NapProtoEncodeStructType; constructor (element: SendFileElement) { super(element); this.fileName = element.fileElement.fileName; this.filePath = element.fileElement.filePath; this.fileSize = +element.fileElement.fileSize; } override get valid (): boolean { return this.isGroupFile || Boolean(this._e37_800_rsp); } override buildContent (): Uint8Array | undefined { if (this.isGroupFile || !this._e37_800_rsp) return undefined; return new NapProtoMsg(FileExtra).encode({ file: { fileType: 0, fileUuid: this.fileUuid, fileMd5: this.fileMd5, fileName: this.fileName, fileSize: BigInt(this.fileSize), subcmd: 1, dangerEvel: 0, expireTime: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, fileHash: this.fileHash, }, field6: { field2: { field1: this._e37_800_rsp?.body?.field30?.field110, fileUuid: this.fileUuid, fileName: this.fileName, field6: this._e37_800_rsp?.body?.field30?.field3, field7: this._e37_800_rsp?.body?.field30?.field101, field8: this._e37_800_rsp?.body?.field30?.field100, timestamp1: this._e37_800_rsp?.body?.field30?.timestamp1, fileHash: this.fileHash, selfUid: this._private_send_uid, destUid: this._private_recv_uid, }, }, }); } override buildElement (): NapProtoEncodeStructType[] { if (!this.isGroupFile) return []; const lb = Buffer.alloc(2); const transElemVal = new NapProtoMsg(GroupFileExtra).encode({ field1: 6, fileName: this.fileName, inner: { info: { busId: 102, fileId: this.fileUuid, fileSize: BigInt(this.fileSize), fileName: this.fileName, fileSha: this.fileSha1, extInfoString: '', fileMd5: this.fileMd5, }, }, }); lb.writeUInt16BE(transElemVal.length); return [{ transElem: { elemType: 24, elemValue: Buffer.concat([Buffer.from([0x01]), lb, transElemVal]), // TLV }, }]; } override toPreview (): string { return `[文件]${this.fileName}`; } } export class PacketMsgLightAppElement extends IPacketMsgElement { payload: string; constructor (element: SendArkElement) { super(element); this.payload = element.arkElement.bytesData; } override buildElement (): NapProtoEncodeStructType[] { return [{ lightAppElem: { data: Buffer.concat([ Buffer.from([0x01]), zlib.deflateSync(Buffer.from(this.payload, 'utf-8')), ]), }, }]; } override toPreview (): string { return '[卡片消息]'; } } export class PacketMsgMarkDownElement extends IPacketMsgElement { content: string; constructor (element: SendMarkdownElement) { super(element); this.content = element.markdownElement.content; } override buildElement (): NapProtoEncodeStructType[] { return [{ commonElem: { serviceType: 45, pbElem: new NapProtoMsg(MarkdownData).encode({ content: this.content, }), businessType: 1, }, }]; } override toPreview (): string { return `[Markdown消息 ${this.content}]`; } } export class PacketMultiMsgElement extends IPacketMsgElement { resid: string; message: PacketMsg[]; uuid: string; constructor (rawElement: SendMultiForwardMsgElement, message?: PacketMsg[]) { super(rawElement); this.resid = rawElement.multiForwardMsgElement.resId; this.message = message ?? []; this.uuid = rawElement.multiForwardMsgElement.fileName || crypto.randomUUID(); } override buildElement (): NapProtoEncodeStructType[] { return [{ lightAppElem: { data: Buffer.concat([ Buffer.from([0x01]), zlib.deflateSync(Buffer.from(JSON.stringify(ForwardMsgBuilder.fromPacketMsg(this.resid, this.message, undefined, undefined, undefined, undefined, this.uuid)), 'utf-8')), ]), }, }]; } override toPreview (): string { return '[聊天记录]'; } }