diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts index 0e558058..d89e8899 100644 --- a/src/core/packet/context/operationContext.ts +++ b/src/core/packet/context/operationContext.ts @@ -6,13 +6,14 @@ import { PacketMsgFileElement, PacketMsgPicElement, PacketMsgPttElement, - PacketMsgVideoElement + PacketMsgReplyElement, + PacketMsgVideoElement, } from '@/core/packet/message/element'; import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/core'; import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp'; import { AIVoiceChatType } from '@/core/packet/entities/aiChat'; import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core'; -import { IndexNode, LongMsgResult, MsgInfo } from '@/core/packet/transformer/proto'; +import { IndexNode, LongMsgResult, MsgInfo, PushMsgBody } from '@/core/packet/transformer/proto'; import { OidbPacket } from '@/core/packet/transformer/base'; import { ImageOcrResult } from '@/core/packet/entities/ocrResult'; import { gunzipSync } from 'zlib'; @@ -76,22 +77,24 @@ export class PacketOperationContext { 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 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); - this.context.logger.info(`上传资源${res.length}个,失败${res.filter(r => r.status === 'rejected').length}个`); + this.context.logger.info(`上传资源${res.length}个,失败${res.filter((r) => r.status === 'rejected').length}个`); res.forEach((result, index) => { if (result.status === 'rejected') { this.context.logger.error(`上传第${index + 1}个资源失败:${result.reason.stack}`); @@ -100,10 +103,13 @@ export class PacketOperationContext { } async UploadImage(img: PacketMsgPicElement) { - await this.context.highway.uploadImage({ - chatType: ChatType.KCHATTYPEC2C, - peerUid: this.context.napcore.basicInfo.uid - }, img); + 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'); @@ -137,24 +143,66 @@ export class PacketOperationContext { coordinates: item.polygon.coordinates.map((c) => { return { x: c.x, - y: c.y + y: c.y, }; }), }; }), - language: res.ocrRspBody.language + language: res.ocrRspBody.language, } as ImageOcrResult; } - async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) { + 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 MoveGroupFile(groupUin: number, fileUUID: string, currentParentDirectory: string, targetParentDirectory: string) { + 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); @@ -203,12 +251,17 @@ export class PacketOperationContext { return res.content.map((item) => { return { category: item.category, - voices: item.voices + voices: item.voices, }; }); } - async GetAiVoice(groupUin: number, voiceId: string, text: string, chatType: AIVoiceChatType): Promise> { + async GetAiVoice( + groupUin: number, + voiceId: string, + text: string, + chatType: AIVoiceChatType + ): Promise> { let reqTime = 0; const reqMaxTime = 30; const sessionId = crypto.randomBytes(4).readUInt32BE(0); @@ -236,6 +289,7 @@ export class PacketOperationContext { 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) { @@ -251,12 +305,12 @@ export class PacketOperationContext { const groupUin = msg?.responseHead.grp?.groupUin ?? 0; element.picElement = { ...element.picElement, - originImageUrl: await this.GetGroupImageUrl(groupUin, index!) + originImageUrl: await this.GetGroupImageUrl(groupUin, index!), }; } else { element.picElement = { ...element.picElement, - originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!) + originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!), }; } return element; @@ -269,7 +323,7 @@ export class PacketOperationContext { elements: elements, guildId: '', isOnlineMsg: false, - msgId: '7467703692092974645', // TODO: no necessary + msgId: '7467703692092974645', // TODO: no necessary msgRandom: '0', msgSeq: String(msg.contentHead.sequence ?? 0), msgTime: String(msg.contentHead.timeStamp ?? 0), diff --git a/src/core/packet/message/builder.ts b/src/core/packet/message/builder.ts index 2503645b..8080cd48 100644 --- a/src/core/packet/message/builder.ts +++ b/src/core/packet/message/builder.ts @@ -24,12 +24,15 @@ export class PacketMsgBuilder { } return { responseHead: { - fromUid: '', fromUin: node.senderUin, - toUid: node.groupId ? undefined : selfUid, + type: 0, + sigMap: 0, + toUin: 0, + fromUid: '', forward: node.groupId ? undefined : { friendName: node.senderName, }, + toUid: node.groupId ? undefined : selfUid, grp: node.groupId ? { groupUin: node.groupId, memberName: node.senderName, @@ -40,16 +43,13 @@ export class PacketMsgBuilder { type: node.groupId ? 82 : 9, subType: node.groupId ? undefined : 4, divSeq: node.groupId ? undefined : 4, - msgId: crypto.randomBytes(4).readUInt32LE(0), + autoReply: 0, sequence: crypto.randomBytes(4).readUInt32LE(0), timeStamp: +node.time.toString().substring(0, 10), - field7: BigInt(1), - field8: 0, - field9: 0, forward: { field1: 0, field2: 0, - field3: node.groupId ? 0 : 2, + field3: node.groupId ? 1 : 2, unknownBase64: avatar, avatar: avatar } diff --git a/src/core/packet/message/element.ts b/src/core/packet/message/element.ts index df1a1d4d..7ed98fc1 100644 --- a/src/core/packet/message/element.ts +++ b/src/core/packet/message/element.ts @@ -10,6 +10,7 @@ import { MsgInfo, NotOnlineImage, OidbSvcTrpcTcp0XE37_800Response, + PushMsgBody, QBigFaceExtra, QSmallFaceExtra, } from '@/core/packet/transformer/proto'; @@ -29,7 +30,8 @@ import { SendReplyElement, SendMultiForwardMsgElement, SendTextElement, - SendVideoElement + SendVideoElement, + Peer } from '@/core'; import {ForwardMsgBuilder} from '@/common/forward-msg-builder'; import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message'; @@ -146,41 +148,40 @@ export class PacketMsgAtElement extends PacketMsgTextElement { } export class PacketMsgReplyElement extends IPacketMsgElement { - messageId: bigint; - messageSeq: number; - messageClientSeq: number; + time: number; + targetMessageId: bigint; + targetMessageSeq: number; + targetMessageClientSeq: number; targetUin: number; targetUid: string; - time: number; - elems: PacketMsg[]; + targetElems?: NapProtoEncodeStructType[]; + targetSourceMsg?: NapProtoEncodeStructType; + targetPeer?: Peer; constructor(element: SendReplyElement) { super(element); - this.messageId = BigInt(element.replyElement.replayMsgId ?? 0); - this.messageSeq = +(element.replyElement.replayMsgSeq ?? 0); - this.messageClientSeq = +(element.replyElement.replyMsgClientSeq ?? 0); + 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.time = +(element.replyElement.replyMsgTime ?? 0); - this.elems = []; // TODO: in replyElement.sourceMsgTextElems + this.targetPeer = element.replyElement._replyMsgPeer; } get isGroupReply(): boolean { - return this.messageClientSeq === 0; + return this.targetMessageClientSeq === 0; } override buildElement(): NapProtoEncodeStructType[] { return [{ srcMsg: { - origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq], + origSeqs: [this.isGroupReply ? this.targetMessageSeq : this.targetMessageClientSeq], senderUin: BigInt(this.targetUin), time: this.time, - elems: [], // TODO: in replyElement.sourceMsgTextElems - pbReserve: { - messageId: this.messageId, - }, - toUin: BigInt(this.targetUin), - type: 1, + elems: this.targetElems ?? [], + sourceMsg: new NapProtoMsg(PushMsgBody).encode(this.targetSourceMsg ?? {}), + toUin: BigInt(0), } }]; } diff --git a/src/core/packet/transformer/message/FetchC2CMessage.ts b/src/core/packet/transformer/message/FetchC2CMessage.ts new file mode 100644 index 00000000..92785163 --- /dev/null +++ b/src/core/packet/transformer/message/FetchC2CMessage.ts @@ -0,0 +1,27 @@ +import * as proto from '@/core/packet/transformer/proto'; +import { NapProtoMsg } from '@napneko/nap-proto-core'; +import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base'; + +class FetchC2CMessage extends PacketTransformer { + constructor() { + super(); + } + + build(targetUid: string, startSeq: number, endSeq: number): OidbPacket { + const req = new NapProtoMsg(proto.SsoGetC2cMsg).encode({ + friendUid: targetUid, + startSequence: startSeq, + endSequence: endSeq, + }); + return { + cmd: 'trpc.msg.register_proxy.RegisterProxy.SsoGetC2cMsg', + data: PacketHexStrBuilder(req) + }; + } + + parse(data: Buffer) { + return new NapProtoMsg(proto.SsoGetC2cMsgResponse).decode(data); + } +} + +export default new FetchC2CMessage(); diff --git a/src/core/packet/transformer/message/FetchGroupMessage.ts b/src/core/packet/transformer/message/FetchGroupMessage.ts new file mode 100644 index 00000000..ffd4c672 --- /dev/null +++ b/src/core/packet/transformer/message/FetchGroupMessage.ts @@ -0,0 +1,30 @@ +import * as proto from '@/core/packet/transformer/proto'; +import { NapProtoMsg } from '@napneko/nap-proto-core'; +import { OidbPacket, PacketHexStrBuilder, PacketTransformer } from '@/core/packet/transformer/base'; + +class FetchGroupMessage extends PacketTransformer { + constructor() { + super(); + } + + build(groupUin: number, startSeq: number, endSeq: number): OidbPacket { + const req = new NapProtoMsg(proto.SsoGetGroupMsg).encode({ + info: { + groupUin: groupUin, + startSequence: startSeq, + endSequence: endSeq + }, + direction: true + }); + return { + cmd: 'trpc.msg.register_proxy.RegisterProxy.SsoGetGroupMsg', + data: PacketHexStrBuilder(req) + }; + } + + parse(data: Buffer) { + return new NapProtoMsg(proto.SsoGetGroupMsgResponse).decode(data); + } +} + +export default new FetchGroupMessage(); diff --git a/src/core/packet/transformer/message/index.ts b/src/core/packet/transformer/message/index.ts index 16a5d338..78e757c8 100644 --- a/src/core/packet/transformer/message/index.ts +++ b/src/core/packet/transformer/message/index.ts @@ -1,2 +1,4 @@ export { default as UploadForwardMsg } from './UploadForwardMsg'; -export { default as DownloadForwardMsg } from './DownloadForwardMsg'; \ No newline at end of file +export { default as FetchGroupMessage } from './FetchGroupMessage'; +export { default as FetchC2CMessage } from './FetchC2CMessage'; +export { default as DownloadForwardMsg } from './DownloadForwardMsg'; diff --git a/src/core/packet/transformer/proto/message/message.ts b/src/core/packet/transformer/proto/message/message.ts index 24200ca1..fdae58cf 100644 --- a/src/core/packet/transformer/proto/message/message.ts +++ b/src/core/packet/transformer/proto/message/message.ts @@ -13,13 +13,15 @@ import { export const ContentHead = { type: ProtoField(1, ScalarType.UINT32), subType: ProtoField(2, ScalarType.UINT32, true), - divSeq: ProtoField(3, ScalarType.UINT32, true), - msgId: ProtoField(4, ScalarType.UINT32, true), + c2cCmd: ProtoField(3, ScalarType.UINT32, true), + ranDom: ProtoField(4, ScalarType.UINT32, true), sequence: ProtoField(5, ScalarType.UINT32, true), timeStamp: ProtoField(6, ScalarType.UINT32, true), - field7: ProtoField(7, ScalarType.UINT64, true), - field8: ProtoField(8, ScalarType.UINT32, true), - field9: ProtoField(9, ScalarType.UINT32, true), + pkgNum: ProtoField(7, ScalarType.UINT64, true), + pkgIndex: ProtoField(8, ScalarType.UINT32, true), + divSeq: ProtoField(9, ScalarType.UINT32, true), + autoReply: ProtoField(10, ScalarType.UINT32), + ntMsgSeq: ProtoField(10, ScalarType.UINT32, true), newId: ProtoField(12, ScalarType.UINT64, true), forward: ProtoField(15, () => ForwardHead, true), }; diff --git a/src/core/types/element.ts b/src/core/types/element.ts index 193f7a12..dd417b13 100644 --- a/src/core/types/element.ts +++ b/src/core/types/element.ts @@ -1,4 +1,15 @@ -import { ElementType, MessageElement, NTGrayTipElementSubTypeV2, PicSubType, PicType, TipAioOpGrayTipElement, TipGroupElement, NTVideoType, FaceType } from './msg'; +import { + ElementType, + MessageElement, + NTGrayTipElementSubTypeV2, + PicSubType, + PicType, + TipAioOpGrayTipElement, + TipGroupElement, + NTVideoType, + FaceType, + Peer +} from './msg'; type ElementFullBase = Omit; @@ -213,6 +224,9 @@ export interface ReplyElement { senderUidStr?: string; replyMsgTime?: string; replyMsgClientSeq?: string; + // HACK: Attributes that were not originally available, + // but were added due to NTQQ and NapCat's internal implementation, are used to supplement NapCat + _replyMsgPeer?: Peer; } export interface CalendarElement { diff --git a/src/core/types/msg.ts b/src/core/types/msg.ts index 083fb94b..9e542ad8 100644 --- a/src/core/types/msg.ts +++ b/src/core/types/msg.ts @@ -403,7 +403,7 @@ export interface NTGroupGrayMember { } /** * 群灰色提示邀请者和被邀请者接口 - * + * * */ export interface NTGroupGrayInviterAndInvite { invited: NTGroupGrayMember; @@ -501,6 +501,7 @@ export interface RawMessage { elements: MessageElement[];// 消息元素 sourceType: MsgSourceType;// 消息来源类型 isOnlineMsg: boolean;// 是否为在线消息 + clientSeq?: string; } /** @@ -565,4 +566,4 @@ export enum FaceType { AniSticke = 3, // 动画贴纸 Lottie = 4,// 新格式表情 Poke = 5 // 可变Poke -} \ No newline at end of file +} diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index fd81f461..ffef73b0 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -467,6 +467,8 @@ export class OneBotMsgApi { replayMsgId: replyMsg.msgId, // raw.msgId senderUin: replyMsg.senderUin, senderUinStr: replyMsg.senderUin, + replyMsgClientSeq: replyMsg.clientSeq, + _replyMsgPeer: replyMsgM.Peer }, } : undefined;