diff --git a/src/common/config-base.ts b/src/common/config-base.ts index 490a1ec1..8f39a4b0 100644 --- a/src/common/config-base.ts +++ b/src/common/config-base.ts @@ -66,7 +66,7 @@ export abstract class ConfigBase { private handleError(e: unknown, message: string): void { if (e instanceof SyntaxError) { - this.core.context.logger.logError(`[Core] [Config] 操作配置文件格式错误,请检查配置文件:`, e.message); + this.core.context.logger.logError('[Core] [Config] 操作配置文件格式错误,请检查配置文件:', e.message); } else { this.core.context.logger.logError(`[Core] [Config] ${message}:`, (e as Error).message); } diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 4909316c..c93d0f8e 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -43,7 +43,7 @@ export class NTQQFileApi { this.rkeyManager = new RkeyManager([ 'https://rkey.napneko.icu/rkeys' ], - this.context.logger + this.context.logger ); } @@ -300,18 +300,18 @@ export class NTQQFileApi { element.elementType === ElementType.FILE ) { switch (element.elementType) { - case ElementType.PIC: + case ElementType.PIC: element.picElement!.sourcePath = elementResults?.[elementIndex] ?? ''; - break; - case ElementType.VIDEO: + break; + case ElementType.VIDEO: element.videoElement!.filePath = elementResults?.[elementIndex] ?? ''; - break; - case ElementType.PTT: + break; + case ElementType.PTT: element.pttElement!.filePath = elementResults?.[elementIndex] ?? ''; - break; - case ElementType.FILE: + break; + case ElementType.FILE: element.fileElement!.filePath = elementResults?.[elementIndex] ?? ''; - break; + break; } elementIndex++; } diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts index fa354f94..e9c89cba 100644 --- a/src/core/packet/context/operationContext.ts +++ b/src/core/packet/context/operationContext.ts @@ -1,20 +1,22 @@ import * as crypto from 'crypto'; -import { PacketContext } from '@/core/packet/context/packetContext'; +import {PacketContext} from '@/core/packet/context/packetContext'; import * as trans from '@/core/packet/transformer'; -import { PacketMsg } from '@/core/packet/message/message'; +import {PacketMsg} from '@/core/packet/message/message'; import { PacketMsgFileElement, PacketMsgPicElement, PacketMsgPttElement, PacketMsgVideoElement } from '@/core/packet/message/element'; -import { ChatType } from '@/core'; -import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp'; -import { AIVoiceChatType } from '@/core/packet/entities/aiChat'; -import { NapProtoDecodeStructType, NapProtoEncodeStructType } from '@napneko/nap-proto-core'; -import { IndexNode, MsgInfo } from '@/core/packet/transformer/proto'; -import { OidbPacket } from '@/core/packet/transformer/base'; -import { ImageOcrResult } from '@/core/packet/entities/ocrResult'; +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 {OidbPacket} from '@/core/packet/transformer/base'; +import {ImageOcrResult} from '@/core/packet/entities/ocrResult'; +import {gunzipSync} from 'zlib'; +import {PacketMsgConverter} from '@/core/packet/message/converter'; export class PacketOperationContext { private readonly context: PacketContext; @@ -57,10 +59,10 @@ export class PacketOperationContext { const res = trans.GetStrangerInfo.parse(resp); const extBigInt = BigInt(res.data.status.value); if (extBigInt <= 10n) { - return { status: Number(extBigInt) * 10, ext_status: 0 }; + return {status: Number(extBigInt) * 10, ext_status: 0}; } status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); - return { status: 10, ext_status: status }; + return {status: 10, ext_status: status}; } catch { return undefined; } @@ -77,13 +79,13 @@ export class PacketOperationContext { const reqList = msg.flatMap(m => m.msg.map(e => { if (e instanceof PacketMsgPicElement) { - return this.context.highway.uploadImage({ chatType, peerUid }, e); + return this.context.highway.uploadImage({chatType, peerUid}, e); } else if (e instanceof PacketMsgVideoElement) { - return this.context.highway.uploadVideo({ chatType, peerUid }, e); + return this.context.highway.uploadVideo({chatType, peerUid}, e); } else if (e instanceof PacketMsgPttElement) { - return this.context.highway.uploadPtt({ chatType, peerUid }, e); + return this.context.highway.uploadPtt({chatType, peerUid}, e); } else if (e instanceof PacketMsgFileElement) { - return this.context.highway.uploadFile({ chatType, peerUid }, e); + return this.context.highway.uploadFile({chatType, peerUid}, e); } return null; }).filter(Boolean) @@ -116,6 +118,13 @@ export class PacketOperationContext { return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } + async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType) { + const req = trans.DownloadGroupImage.build(groupUin, 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 ImageOCR(imgUrl: string) { const req = trans.ImageOCR.build(imgUrl); const resp = await this.context.client.sendOidbPacket(req, true); @@ -195,4 +204,74 @@ export class PacketOperationContext { return res.msgInfo; } } + + 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'); + } + + 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; + } + return element; + }) + ); + return { + chatType: ChatType.KCHATTYPEGROUP, + elements: 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: ChatType.KCHATTYPEGROUP, + peerUid: String(msg?.responseHead.grp?.groupUin ?? 0), + }, + peerName: '', + peerUid: '1094950020', + peerUin: '1094950020', + recallTime: '0', + records: [], + sendNickName: msg?.responseHead.grp?.memberName ?? '', + sendRemarkName: msg?.responseHead.grp?.memberName ?? '', + senderUid: '', + senderUin: '1094950020', + sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN, + subMsgType: 1, + }; + }); + return await Promise.all(messagesPromises); + } } diff --git a/src/core/packet/message/converter.ts b/src/core/packet/message/converter.ts index f995ac3e..77588a2f 100644 --- a/src/core/packet/message/converter.ts +++ b/src/core/packet/message/converter.ts @@ -1,8 +1,8 @@ import { - Peer, ChatType, ElementType, MessageElement, + Peer, RawMessage, SendArkElement, SendFaceElement, @@ -31,7 +31,9 @@ import { PacketMsgVideoElement, PacketMultiMsgElement } from '@/core/packet/message/element'; -import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message'; +import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message'; +import {NapProtoDecodeStructType} from '@napneko/nap-proto-core'; +import {Elem} from '@/core/packet/transformer/proto'; const SupportedElementTypes = [ ElementType.TEXT, @@ -154,4 +156,16 @@ export class PacketMsgConverter { }).filter((e) => e !== null) }; } + + packetMsgToRaw(msg: NapProtoDecodeStructType[]): [MessageElement, NapProtoDecodeStructType | null][] { + const converters = [PacketMsgTextElement.parseElement, + PacketMsgAtElement.parseElement, PacketMsgReplyElement.parseElement, PacketMsgPicElement.parseElement]; + return msg.map((element) => { + for (const converter of converters) { + const result = converter(element); + if (result) return result; + } + return null; + }).filter((e) => e !== null); + } } diff --git a/src/core/packet/message/element.ts b/src/core/packet/message/element.ts index 3a1197ba..22372303 100644 --- a/src/core/packet/message/element.ts +++ b/src/core/packet/message/element.ts @@ -1,20 +1,22 @@ import * as zlib from 'node:zlib'; -import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core'; +import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core'; import { CustomFace, Elem, + FileExtra, + GroupFileExtra, MarkdownData, MentionExtra, + MsgInfo, NotOnlineImage, + OidbSvcTrpcTcp0XE37_800Response, QBigFaceExtra, QSmallFaceExtra, - MsgInfo, - OidbSvcTrpcTcp0XE37_800Response, - FileExtra, - GroupFileExtra } from '@/core/packet/transformer/proto'; import { + ElementType, FaceType, + MessageElement, NTMsgAtType, PicType, SendArkElement, @@ -29,8 +31,11 @@ import { SendTextElement, SendVideoElement } from '@/core'; -import { ForwardMsgBuilder } from '@/common/forward-msg-builder'; -import { PacketMsg, PacketSendMsgElement } from '@/core/packet/message/message'; +import {ForwardMsgBuilder} from '@/common/forward-msg-builder'; +import {PacketMsg, PacketSendMsgElement} from '@/core/packet/message/message'; + +export type ParseElementFnR = [MessageElement, NapProtoDecodeStructType | null] | undefined; +type ParseElementFn = (elem: NapProtoDecodeStructType) => ParseElementFnR; // raw <-> packet // TODO: SendStructLongMsgElement @@ -51,6 +56,8 @@ export abstract class IPacketMsgElement { return []; } + static parseElement: ParseElementFn; + toPreview(): string { return '[暂不支持该消息类型喵~]'; } @@ -72,11 +79,30 @@ export class PacketMsgTextElement extends IPacketMsgElement { }]; } + 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; @@ -101,6 +127,22 @@ export class PacketMsgAtElement extends PacketMsgTextElement { } }]; } + 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 { @@ -152,6 +194,22 @@ export class PacketMsgReplyElement extends IPacketMsgElement { }]; } + 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 '[回复消息]'; } @@ -207,6 +265,46 @@ export class PacketMsgFaceElement extends IPacketMsgElement { } } + 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 '[表情]'; } @@ -295,6 +393,60 @@ export class PacketMsgPicElement extends IPacketMsgElement { }]; } + static override parseElement = (elem: NapProtoDecodeStructType): ParseElementFnR => { + if (elem?.commonElem?.serviceType === 48 || [10, 20].includes(elem?.commonElem?.businessType ?? 0)) { + 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.UNKNOWN, + 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; } diff --git a/src/core/packet/transformer/highway/DownloadGroupImage.ts b/src/core/packet/transformer/highway/DownloadGroupImage.ts new file mode 100644 index 00000000..8b6aaa32 --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadGroupImage.ts @@ -0,0 +1,50 @@ +import * as proto from '@/core/packet/transformer/proto'; +import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core'; +import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base'; +import OidbBase from '@/core/packet/transformer/oidb/oidbBase'; +import { IndexNode } from '@/core/packet/transformer/proto'; + +class DownloadGroupImage extends PacketTransformer { + constructor() { + super(); + } + + build(group_uin: number, node: NapProtoEncodeStructType): OidbPacket { + const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 200 + }, + scene: { + requestType: 2, + businessType: 1, + sceneType: 2, + group: { + groupUin: group_uin + } + }, + client: { + agentType: 2, + } + }, + download: { + node: node, + download: { + video: { + busiType: 0, + sceneType: 0 + } + } + } + }); + return OidbBase.build(0x11C4, 200, body, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new DownloadGroupImage(); diff --git a/src/core/packet/transformer/highway/DownloadImage.ts b/src/core/packet/transformer/highway/DownloadImage.ts index 389e69bd..dc42cfbd 100644 --- a/src/core/packet/transformer/highway/DownloadImage.ts +++ b/src/core/packet/transformer/highway/DownloadImage.ts @@ -14,7 +14,7 @@ class DownloadImage extends PacketTransformer { reqHead: { common: { requestId: 1, - command: 100 + command: 200 }, scene: { requestType: 2, diff --git a/src/core/packet/transformer/highway/index.ts b/src/core/packet/transformer/highway/index.ts index f9170cea..7ca56645 100644 --- a/src/core/packet/transformer/highway/index.ts +++ b/src/core/packet/transformer/highway/index.ts @@ -12,3 +12,4 @@ export { default as UploadPrivateImage } from './UploadPrivateImage'; export { default as UploadPrivatePtt } from './UploadPrivatePtt'; export { default as UploadPrivateVideo } from './UploadPrivateVideo'; export { default as DownloadImage } from './DownloadImage'; +export { default as DownloadGroupImage } from './DownloadGroupImage'; diff --git a/src/core/packet/transformer/message/DownloadForwardMsg.ts b/src/core/packet/transformer/message/DownloadForwardMsg.ts new file mode 100644 index 00000000..2f869ced --- /dev/null +++ b/src/core/packet/transformer/message/DownloadForwardMsg.ts @@ -0,0 +1,37 @@ +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 DownloadForwardMsg extends PacketTransformer { + constructor() { + super(); + } + + build(uid: string, resId: string): OidbPacket { + const req = new NapProtoMsg(proto.RecvLongMsgReq).encode({ + info: { + uid: { + uid: uid + }, + resId: resId, + acquire: true + }, + settings: { + field1: 2, + field2: 0, + field3: 0, + field4: 0 + } + }); + return { + cmd: 'trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg', + data: PacketHexStrBuilder(req) + }; + } + + parse(data: Buffer) { + return new NapProtoMsg(proto.RecvLongMsgResp).decode(data); + } +} + +export default new DownloadForwardMsg(); diff --git a/src/core/packet/transformer/message/index.ts b/src/core/packet/transformer/message/index.ts index 88148753..16a5d338 100644 --- a/src/core/packet/transformer/message/index.ts +++ b/src/core/packet/transformer/message/index.ts @@ -1 +1,2 @@ export { default as UploadForwardMsg } from './UploadForwardMsg'; +export { default as DownloadForwardMsg } from './DownloadForwardMsg'; \ No newline at end of file diff --git a/src/core/packet/transformer/proto/message/action.ts b/src/core/packet/transformer/proto/message/action.ts index 74f47132..7bdbf7f2 100644 --- a/src/core/packet/transformer/proto/message/action.ts +++ b/src/core/packet/transformer/proto/message/action.ts @@ -2,7 +2,7 @@ import { ProtoField, ScalarType } from '@napneko/nap-proto-core'; import { PushMsgBody } from '@/core/packet/transformer/proto'; export const LongMsgResult = { - action: ProtoField(2, () => LongMsgAction) + action: ProtoField(2, () => LongMsgAction, false, true) }; export const LongMsgAction = { diff --git a/src/onebot/action/go-cqhttp/GetForwardMsg.ts b/src/onebot/action/go-cqhttp/GetForwardMsg.ts index f1bb51cb..5552229b 100644 --- a/src/onebot/action/go-cqhttp/GetForwardMsg.ts +++ b/src/onebot/action/go-cqhttp/GetForwardMsg.ts @@ -3,6 +3,7 @@ import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, import { ActionName } from '@/onebot/action/router'; import { MessageUnique } from '@/common/message-unique'; import { Static, Type } from '@sinclair/typebox'; +import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from '@/core'; const SchemaData = Type.Object({ message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])), @@ -57,24 +58,72 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction { + return { + chatType: ChatType.KCHATTYPEGROUP, + elements: [{ + elementType: ElementType.MULTIFORWARD, + elementId: '', + multiForwardMsgElement: { + resId: res_id, + fileName: '', + xmlContent: '', + } + }], + guildId: '', + isOnlineMsg: false, + msgId: '', // TODO: no necessary + msgRandom: '0', + msgSeq: '', + msgTime: '', + msgType: NTMsgType.KMSGTYPEMIX, + parentMsgIdList: [], + parentMsgPeer: { + chatType: ChatType.KCHATTYPEGROUP, + peerUid: '', + }, + peerName: '', + peerUid: '284840486', + peerUin: '284840486', + recallTime: '0', + records: [], + sendNickName: '', + sendRemarkName: '', + senderUid: '', + senderUin: '1094950020', + sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN, + subMsgType: 1, + } as RawMessage; + }; + + const protocolFallbackLogic = async (res_id: string) => { + const ob = (await this.obContext.apis.MsgApi.parseMessageV2(fakeForwardMsg(res_id)))?.arrayMsg; + if (ob) { + return { + messages: (ob?.message?.[0] as OB11MessageForward)?.data?.content + }; + } + throw new Error('protocolFallbackLogic: 找不到相关的聊天记录'); + }; + const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString()); const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId); if (!rootMsg) { - throw new Error('msg not found'); + return await protocolFallbackLogic(msgId.toString()); } const data = await this.core.apis.MsgApi.getMsgsByMsgId(rootMsg.Peer, [rootMsg.MsgId]); if (!data || data.result !== 0) { - throw new Error('找不到相关的聊天记录' + data?.errMsg); + return await protocolFallbackLogic(msgId.toString()); } const singleMsg = data.msgList[0]; if (!singleMsg) { - throw new Error('找不到相关的聊天记录'); + return await protocolFallbackLogic(msgId.toString()); } const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;//强制array 以便处理 if (!(resMsg?.message?.[0] as OB11MessageForward)?.data?.content) { - throw new Error('找不到相关的聊天记录'); + return await protocolFallbackLogic(msgId.toString()); } return { messages: (resMsg?.message?.[0] as OB11MessageForward)?.data?.content diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index e3361f82..d65a7171 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -1,41 +1,50 @@ -import { FileNapCatOneBotUUID } from '@/common/file-uuid'; -import { MessageUnique } from '@/common/message-unique'; +import {FileNapCatOneBotUUID} from '@/common/file-uuid'; +import {MessageUnique} from '@/common/message-unique'; import { - NTMsgAtType, ChatType, CustomMusicSignPostData, ElementType, FaceIndex, + FaceType, + GrayTipElement, + GroupNotify, IdMusicSignPostData, MessageElement, NapCatCore, NTGrayTipElementSubTypeV2, + NTMsgAtType, Peer, RawMessage, SendMessageElement, SendTextElement, - FaceType, - GrayTipElement, - GroupNotify, } from '@/core'; import faceConfig from '@/core/external/face_config.json'; -import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageFileBase, OB11MessageForward, OB11MessageImage, OB11MessageVideo, } from '@/onebot'; -import { OB11Construct } from '@/onebot/helper/data'; -import { EventType } from '@/onebot/event/OneBotEvent'; -import { encodeCQCode } from '@/onebot/helper/cqcode'; -import { uriToLocalFile } from '@/common/file'; -import { RequestUtil } from '@/common/request'; -import fsPromise, { constants } from 'node:fs/promises'; -import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; -import { ForwardMsgBuilder } from '@/common/forward-msg-builder'; -import { NapProtoMsg } from '@napneko/nap-proto-core'; -import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent'; -import { OB11GroupDecreaseEvent, GroupDecreaseSubType } from '../event/notice/OB11GroupDecreaseEvent'; -import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin'; -import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent'; -import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto'; -import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest'; -import { LRUCache } from '@/common/lru-cache'; +import { + NapCatOneBot11Adapter, + OB11Message, + OB11MessageData, + OB11MessageDataType, + OB11MessageFileBase, + OB11MessageForward, + OB11MessageImage, + OB11MessageVideo, +} from '@/onebot'; +import {OB11Construct} from '@/onebot/helper/data'; +import {EventType} from '@/onebot/event/OneBotEvent'; +import {encodeCQCode} from '@/onebot/helper/cqcode'; +import {uriToLocalFile} from '@/common/file'; +import {RequestUtil} from '@/common/request'; +import fsPromise, {constants} from 'node:fs/promises'; +import {OB11FriendAddNoticeEvent} from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; +import {ForwardMsgBuilder} from '@/common/forward-msg-builder'; +import {NapProtoMsg} from '@napneko/nap-proto-core'; +import {OB11GroupIncreaseEvent} from '../event/notice/OB11GroupIncreaseEvent'; +import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from '../event/notice/OB11GroupDecreaseEvent'; +import {GroupAdmin} from '@/core/packet/transformer/proto/message/groupAdmin'; +import {OB11GroupAdminNoticeEvent} from '../event/notice/OB11GroupAdminNoticeEvent'; +import {GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody} from '@/core/packet/transformer/proto'; +import {OB11GroupRequestEvent} from '../event/request/OB11GroupRequest'; +import {LRUCache} from '@/common/lru-cache'; type RawToOb11Converters = { [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( @@ -84,12 +93,12 @@ export class OneBotMsgApi { } return { type: OB11MessageDataType.text, - data: { text }, + data: {text}, }; } else { let qq: string = 'all'; if (element.atType !== NTMsgAtType.ATTYPEALL) { - const { atNtUid, atUid } = element; + const {atNtUid, atUid} = element; qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid; } return { @@ -197,7 +206,7 @@ export class OneBotMsgApi { peerUid: msg.peerUid, guildId: '', }; - const { emojiId } = _; + const {emojiId} = _; const dir = emojiId.substring(0, 2); const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`; const filename = `${dir}-${emojiId}.gif`; @@ -264,7 +273,6 @@ export class OneBotMsgApi { } - // 丢弃该消息段 if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { this.core.context.logger.logError( @@ -355,18 +363,25 @@ export class OneBotMsgApi { }; }, - multiForwardMsgElement: async (_, msg, _wrapper, context) => { + multiForwardMsgElement: async (element, msg, _wrapper, context) => { const parentMsgPeer = msg.parentMsgPeer ?? { chatType: msg.chatType, guildId: '', peerUid: msg.peerUid, }; - const multiMsgs = await this.getMultiMessages(msg, parentMsgPeer); + let multiMsgs = await this.getMultiMessages(msg, parentMsgPeer); // 拉取失败则跳过 - if (!multiMsgs) return null; + if (!multiMsgs || multiMsgs.length === 0) { + try { + multiMsgs = await this.core.apis.PacketApi.pkt.operation.FetchForwardMsg(element.resId); + } catch (e) { + this.core.context.logger.logError('Protocol FetchForwardMsg fallback failed!', e); + return null; + } + } const forward: OB11MessageForward = { type: OB11MessageDataType.forward, - data: { id: msg.msgId } + data: {id: msg.msgId} }; if (!context.parseMultMsg) return forward; forward.data.content = await this.parseMultiMessageContent( @@ -397,7 +412,7 @@ export class OneBotMsgApi { }; ob11ToRawConverters: Ob11ToRawConverters = { - [OB11MessageDataType.text]: async ({ data: { text } }) => ({ + [OB11MessageDataType.text]: async ({data: {text}}) => ({ elementType: ElementType.TEXT, elementId: '', textElement: { @@ -409,7 +424,7 @@ export class OneBotMsgApi { }, }), - [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => { + [OB11MessageDataType.at]: async ({data: {qq: atQQ}}, context) => { function at(atUid: string, atNtUid: string, atType: NTMsgAtType, atName: string): SendTextElement { return { elementType: ElementType.TEXT, @@ -436,7 +451,7 @@ export class OneBotMsgApi { return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || ''); }, - [OB11MessageDataType.reply]: async ({ data: { id } }) => { + [OB11MessageDataType.reply]: async ({data: {id}}) => { const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id)); if (!replyMsgM) { this.core.context.logger.logWarn('回复消息不存在', id); @@ -458,7 +473,7 @@ export class OneBotMsgApi { undefined; }, - [OB11MessageDataType.face]: async ({ data: { id, resultId, chainCount } }) => { + [OB11MessageDataType.face]: async ({data: {id, resultId, chainCount}}) => { const parsedFaceId = +id; // 从face_config.json中获取表情名称 const sysFaces = faceConfig.sysface; @@ -522,12 +537,12 @@ export class OneBotMsgApi { }, [OB11MessageDataType.file]: async (sendMsg, context) => { - const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context); + const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context); return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName); }, [OB11MessageDataType.video]: async (sendMsg, context) => { - const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context); + const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context); let thumb = sendMsg.data.thumb; if (thumb) { @@ -545,7 +560,7 @@ export class OneBotMsgApi { this.core.apis.FileApi.createValidSendPttElement( (await this.handleOb11FileLikeMessage(sendMsg, context)).path), - [OB11MessageDataType.json]: async ({ data: { data } }) => ({ + [OB11MessageDataType.json]: async ({data: {data}}) => ({ elementType: ElementType.ARK, elementId: '', arkElement: { @@ -588,13 +603,13 @@ export class OneBotMsgApi { }), // Need signing - [OB11MessageDataType.markdown]: async ({ data: { content } }) => ({ + [OB11MessageDataType.markdown]: async ({data: {content}}) => ({ elementType: ElementType.MARKDOWN, elementId: '', - markdownElement: { content }, + markdownElement: {content}, }), - [OB11MessageDataType.music]: async ({ data }, context) => { + [OB11MessageDataType.music]: async ({data}, context) => { // 保留, 直到...找到更好的解决方案 if (data.id !== undefined) { if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) { @@ -618,8 +633,8 @@ export class OneBotMsgApi { let postData: IdMusicSignPostData | CustomMusicSignPostData; if (data.id === undefined && data.content) { - const { content, ...others } = data; - postData = { singer: content, ...others }; + const {content, ...others} = data; + postData = {singer: content, ...others}; } else { postData = data; } @@ -631,7 +646,7 @@ export class OneBotMsgApi { try { const musicJson = await RequestUtil.HttpGetJson(signUrl, 'POST', postData); return this.ob11ToRawConverters.json({ - data: { data: musicJson }, + data: {data: musicJson}, type: OB11MessageDataType.json }, context); } catch (e) { @@ -642,10 +657,10 @@ export class OneBotMsgApi { [OB11MessageDataType.node]: async () => undefined, - [OB11MessageDataType.forward]: async ({ data }, context) => { + [OB11MessageDataType.forward]: async ({data}, context) => { const jsonData = ForwardMsgBuilder.fromResId(data.id); return this.ob11ToRawConverters.json({ - data: { data: JSON.stringify(jsonData) }, + data: {data: JSON.stringify(jsonData)}, type: OB11MessageDataType.json }, context); }, @@ -665,17 +680,17 @@ export class OneBotMsgApi { [OB11MessageDataType.miniapp]: async () => undefined, - [OB11MessageDataType.contact]: async ({ data: { type = 'qq', id } }, context) => { + [OB11MessageDataType.contact]: async ({data: {type = 'qq', id}}, context) => { if (type === 'qq') { const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), ''); return this.ob11ToRawConverters.json({ - data: { data: arkJson.arkMsg }, + data: {data: arkJson.arkMsg}, type: OB11MessageDataType.json }, context); } else if (type === 'group') { const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString()); return this.ob11ToRawConverters.json({ - data: { data: arkJson.arkJson }, + data: {data: arkJson.arkJson}, type: OB11MessageDataType.json }, context); } @@ -692,7 +707,10 @@ export class OneBotMsgApi { if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) { if (grayTipElement.jsonGrayTipElement.busiId == 1061) { const PokeEvent = await this.obContext.apis.FriendApi.parsePrivatePokeEvent(grayTipElement, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid))); - if (PokeEvent) { return PokeEvent; }; + if (PokeEvent) { + return PokeEvent; + } + ; } else if (grayTipElement.jsonGrayTipElement.busiId == 19324 && msg.peerUid !== '') { return new OB11FriendAddNoticeEvent(this.core, Number(await this.core.apis.UserApi.getUinByUidV2(msg.peerUid))); } @@ -849,7 +867,7 @@ export class OneBotMsgApi { element[key], msg, element, - { parseMultMsg } + {parseMultMsg} ); if (key === 'faceElement' && !parsedElement) { return null; @@ -902,13 +920,13 @@ export class OneBotMsgApi { ) => Promise; const callResult = converter( sendMsg, - { peer, deleteAfterSentFiles }, + {peer, deleteAfterSentFiles}, )?.catch(undefined); callResultList.push(callResult); } const ret = await Promise.all(callResultList); const sendElements: SendMessageElement[] = ret.filter(ele => !!ele); - return { sendElements, deleteAfterSentFiles }; + return {sendElements, deleteAfterSentFiles}; } async sendMsgWithOb11UniqueId(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[]) { @@ -970,8 +988,8 @@ export class OneBotMsgApi { } private async handleOb11FileLikeMessage( - { data: inputdata }: OB11MessageFileBase, - { deleteAfterSentFiles }: SendMessageContext + {data: inputdata}: OB11MessageFileBase, + {deleteAfterSentFiles}: SendMessageContext ) { let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? ''; if (!realUri) { @@ -980,28 +998,29 @@ export class OneBotMsgApi { } const downloadFile = async (uri: string) => { - const { path, fileName, errMsg, success } = await uriToLocalFile(this.core.NapCatTempPath, uri); + const {path, fileName, errMsg, success} = await uriToLocalFile(this.core.NapCatTempPath, uri); if (!success) { this.core.context.logger.logError('文件下载失败', errMsg); throw new Error('文件下载失败: ' + errMsg); } - return { path, fileName }; + return {path, fileName}; }; try { - const { path, fileName } = await downloadFile(realUri); + const {path, fileName} = await downloadFile(realUri); deleteAfterSentFiles.push(path); - return { path, fileName: inputdata.name ?? fileName }; + return {path, fileName: inputdata.name ?? fileName}; } catch { realUri = await this.handleObfuckName(realUri); - const { path, fileName } = await downloadFile(realUri); + const {path, fileName} = await downloadFile(realUri); deleteAfterSentFiles.push(path); - return { path, fileName: inputdata.name ?? fileName }; + return {path, fileName: inputdata.name ?? fileName}; } } + async handleObfuckName(name: string) { const contextMsgFile = FileNapCatOneBotUUID.decode(name); if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) { - const { peer, msgId, elementId } = contextMsgFile; + const {peer, msgId, elementId} = contextMsgFile; const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId); const mixElement = rawMessage?.elements.find(e => e.elementId === elementId); const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement; @@ -1009,18 +1028,19 @@ export class OneBotMsgApi { let url = ''; if (mixElement?.picElement && rawMessage) { const tempData = - await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined; + await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageImage | undefined; url = tempData?.data.url ?? ''; } if (mixElement?.videoElement && rawMessage) { const tempData = - await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined; + await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageVideo | undefined; url = tempData?.data.url ?? ''; } return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', ''); } throw new Error('文件名解析失败'); } + groupChangDecreseType2String(type: number): GroupDecreaseSubType { switch (type) { case 130: