diff --git a/src/common/forward-msg-builder.ts b/src/common/forward-msg-builder.ts index 256e0c35..78c98a4b 100644 --- a/src/common/forward-msg-builder.ts +++ b/src/common/forward-msg-builder.ts @@ -56,7 +56,7 @@ export class ForwardMsgBuilder { if (!source) { source = isGroupMsg ? "群聊的聊天记录" : msg.length - ? Array.from(new Set(msg.map(m => m.senderName))) + ? Array.from(new Set(msg.slice(0, 4).map(m => m.senderName))) .join('和') + '的聊天记录' : '聊天记录'; } diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 5202850a..cd193c98 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -6,6 +6,7 @@ import { Peer, PicElement, PicType, + RawMessage, SendFileElement, SendPicElement, SendPttElement, @@ -238,7 +239,7 @@ export class NTQQFileApi { fileName: fileName, filePath: path, md5HexStr: md5, - fileSize: fileSize, + fileSize: fileSize.toString(), duration: duration ?? 1, formatType: 1, voiceType: 1, @@ -267,6 +268,53 @@ export class NTQQFileApi { return fileTransNotifyInfo.filePath; } + async downloadRawMsgMedia(msg: RawMessage[]) { + const res = await Promise.all( + msg.map(m => + Promise.all( + m.elements + .filter(element => + element.elementType === ElementType.PIC || + element.elementType === ElementType.VIDEO || + element.elementType === ElementType.PTT || + element.elementType === ElementType.FILE + ) + .map(element => + this.downloadMedia(m.msgId, m.chatType, m.peerUid, element.elementId, '', '', 1000 * 60 * 2, true) + ) + ) + ) + ); + msg.forEach((m, msgIndex) => { + const elementResults = res[msgIndex]; + let elementIndex = 0; + m.elements.forEach(element => { + if ( + element.elementType === ElementType.PIC || + element.elementType === ElementType.VIDEO || + element.elementType === ElementType.PTT || + element.elementType === ElementType.FILE + ) { + switch (element.elementType) { + case ElementType.PIC: + element.picElement!.sourcePath = elementResults[elementIndex]; + break; + case ElementType.VIDEO: + element.videoElement!.filePath = elementResults[elementIndex]; + break; + case ElementType.PTT: + element.pttElement!.filePath = elementResults[elementIndex]; + break; + case ElementType.FILE: + element.fileElement!.filePath = elementResults[elementIndex]; + break; + } + elementIndex++; + } + }); + }); + } + async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) { // 用于下载收到的消息中的图片等 if (sourcePath && fs.existsSync(sourcePath)) { @@ -296,7 +344,7 @@ export class NTQQFileApi { filePath: thumbPath, }], () => true, - (arg) => arg.msgId === msgId, + (arg) => arg.msgElementId === elementId && arg.msgId === msgId, 1, timeout, ); diff --git a/src/core/entities/msg.ts b/src/core/entities/msg.ts index 0c77e3b7..1a4a38ce 100644 --- a/src/core/entities/msg.ts +++ b/src/core/entities/msg.ts @@ -27,94 +27,70 @@ export interface GetFileListParam { export enum ElementType { UNKNOWN = 0, - TEXT = 1, - PIC = 2, - FILE = 3, - PTT = 4, - VIDEO = 5, - FACE = 6, - REPLY = 7, - + GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等 WALLET = 9, - - /** - * “小灰条”,包括拍一拍 (Poke)、撤回提示等 - */ - GreyTip = 8, - ARK = 10, - MFACE = 11, - LIVEGIFT = 12, - STRUCTLONGMSG = 13, - MARKDOWN = 14, - GIPHY = 15, - MULTIFORWARD = 16, - INLINEKEYBOARD = 17, - INTEXTGIFT = 18, - CALENDAR = 19, - YOLOGAMERESULT = 20, - AVRECORD = 21, - FEED = 22, - TOFURECORD = 23, - ACEBUBBLE = 24, - ACTIVITY = 25, - TOFU = 26, - FACEBUBBLE = 27, - SHARELOCATION = 28, - TASKTOPMSG = 29, - RECOMMENDEDMSG = 43, - ACTIONBAR = 44 } +type ElementFullBase = Omit; + +type ElementBase< + K extends keyof ElementFullBase, + S extends Partial<{ [P in K]: keyof NonNullable | Array> }> = {} +> = { + [P in K]: + S[P] extends Array + ? Pick, U & keyof NonNullable> + : S[P] extends keyof NonNullable + ? Pick, S[P]> + : NonNullable; +}; + +export interface SendElementBase { + elementType: ET; + elementId: string; + extBufForUI?: string; +} + export interface ActionBarElement { rows: InlineKeyboardRow[]; botAppid: string; } -export interface SendActionBarElement { - elementType: ElementType.ACTIONBAR; - elementId: string; - actionBarElement: ActionBarElement; -} - export interface RecommendedMsgElement { rows: InlineKeyboardRow[]; botAppid: string; } -export interface SendRecommendedMsgElement { - elementType: ElementType.RECOMMENDEDMSG; - elementId: string; - recommendedMsgElement: RecommendedMsgElement; -} +export type SendRecommendedMsgElement = SendElementBase & ElementBase<'recommendedMsgElement'>; export interface InlineKeyboardButton { id: string; @@ -171,11 +147,7 @@ export enum NTMsgType { KMSGTYPEWALLET = 10 } -export interface SendTaskTopMsgElement { - elementType: ElementType.TASKTOPMSG; - elementId: string; - taskTopMsgElement: TaskTopMsgElement; -} +export type SendTaskTopMsgElement = SendElementBase & ElementBase<'taskTopMsgElement'>; export interface TofuRecordElement { type: number; @@ -194,11 +166,7 @@ export interface TofuRecordElement { onscreennotify: boolean; } -export interface SendTofuRecordElement { - elementType: ElementType.TOFURECORD; - elementId: string; - tofuRecordElement: TofuRecordElement; -} +export type SendTofuRecordElement = SendElementBase & ElementBase<'tofuRecordElement'>; export interface FaceBubbleElement { faceCount: number; @@ -216,12 +184,7 @@ export interface FaceBubbleElement { }; } -export interface SendFaceBubbleElement { - elementType: ElementType.FACEBUBBLE; - elementId: string; - faceBubbleElement: FaceBubbleElement; - -} +export type SendFaceBubbleElement = SendElementBase & ElementBase<'faceBubbleElement'>; export interface AvRecordElement { type: number; @@ -232,11 +195,7 @@ export interface AvRecordElement { extraType: number; } -export interface SendavRecordElement { - elementType: ElementType.AVRECORD; - elementId: string; - avRecordElement: AvRecordElement; -} +export type SendAvRecordElement = SendElementBase & ElementBase<'avRecordElement'>; export interface YoloUserInfo { uid: string; @@ -245,24 +204,13 @@ export interface YoloUserInfo { bizId: string; } -export interface SendInlineKeyboardElement { - elementType: ElementType.INLINEKEYBOARD; - elementId: string; - inlineKeyboardElement: { - rows: number; - botAppid: string; - }; - -} +export type SendInlineKeyboardElement = SendElementBase & ElementBase<'inlineKeyboardElement'>; export interface YoloGameResultElement { UserInfo: YoloUserInfo[]; } -export interface SendYoloGameResultElement { - elementType: ElementType.YOLOGAMERESULT; - yoloGameResultElement: YoloGameResultElement; -} +export type SendYoloGameResultElement = SendElementBase & ElementBase<'yoloGameResultElement'>; export interface GiphyElement { id: string; @@ -271,17 +219,9 @@ export interface GiphyElement { height: number; } -export interface SendGiphyElement { - elementType: ElementType.GIPHY; - elementId: string; - giphyElement: GiphyElement; -} +export type SendGiphyElement = SendElementBase & ElementBase<'giphyElement'>; -export interface SendWalletElement { - elementType: ElementType.UNKNOWN;//不做 设置位置 - elementId: string; - walletElement: Record; -} +export type SendWalletElement = SendElementBase & ElementBase<'walletElement'>; export interface CalendarElement { summary: string; @@ -291,49 +231,16 @@ export interface CalendarElement { schema: string; } -export interface SendCalendarElement { - elementType: ElementType.CALENDAR; - elementId: string; - calendarElement: CalendarElement; -} +export type SendCalendarElement = SendElementBase & ElementBase<'calendarElement'>; -export interface SendliveGiftElement { - elementType: ElementType.LIVEGIFT; - elementId: string; - liveGiftElement: Record; -} +export type SendLiveGiftElement = SendElementBase & ElementBase<'liveGiftElement'>; -export interface SendTextElement { - elementType: ElementType.TEXT; - elementId: string; - textElement: { - content: string; - atType: number; - atUid: string; - atTinyId: string; - atNtUid: string; - }; -} +export type SendTextElement = SendElementBase & ElementBase<'textElement'>; -export interface SendPttElement { - elementType: ElementType.PTT; - elementId: string; - pttElement: { - fileName: string; - filePath: string; - md5HexStr: string; - fileSize: number; - duration: number; // 单位是秒 - formatType: number; - voiceType: number; - voiceChangeType: number; - canConvert2Text: boolean; - waveAmplitudes: number[]; - fileSubId: string; - playState: number; - autoConvertText: number; - }; -} +export type SendPttElement = SendElementBase & ElementBase<'pttElement', { + pttElement: ['fileName', 'filePath', 'md5HexStr', 'fileSize', 'duration', 'formatType', 'voiceType', + 'voiceChangeType', 'canConvert2Text', 'waveAmplitudes', 'fileSubId', 'playState', 'autoConvertText'] +}>; export enum PicType { gif = 2000, @@ -359,11 +266,7 @@ export enum NTMsgAtType { ATTYPEUNKNOWN = 0 } -export interface SendPicElement { - elementType: ElementType.PIC; - elementId: string; - picElement: PicElement; -} +export type SendPicElement = SendElementBase & ElementBase<'picElement'>; export interface ReplyElement { sourceMsgIdInRecords?: string; @@ -375,53 +278,27 @@ export interface ReplyElement { replyMsgClientSeq?: string; } -export interface SendReplyElement { - elementType: ElementType.REPLY; - elementId: string; - replyElement: ReplyElement; -} +export type SendReplyElement = SendElementBase & ElementBase<'replyElement'>; -export interface SendFaceElement { - elementType: ElementType.FACE; - elementId: string; - faceElement: FaceElement; -} +export type SendFaceElement = SendElementBase & ElementBase<'faceElement'>; -export interface SendMarketFaceElement { - elementType: ElementType.MFACE; - marketFaceElement: MarketFaceElement; -} +export type SendMarketFaceElement = SendElementBase & ElementBase<'marketFaceElement'>; -export interface SendStructLongMsgElement { - elementType: ElementType.STRUCTLONGMSG; - elementId: string; - structLongMsgElement: StructLongMsgElement; -} +export type SendStructLongMsgElement = SendElementBase & ElementBase<'structLongMsgElement'>; export interface StructLongMsgElement { xmlContent: string; resId: string; } -export interface SendactionBarElement { - elementType: ElementType.ACTIONBAR; - elementId: string; - actionBarElement: { - rows: number; - botAppid: string; - }; -} +export type SendActionBarElement = SendElementBase & ElementBase<'actionBarElement'>; export interface ShareLocationElement { text: string; ext: string; } -export interface SendShareLocationElement { - elementType: ElementType.SHARELOCATION; - elementId: string; - shareLocationElement?: ShareLocationElement; -} +export type SendShareLocationElement = SendElementBase & ElementBase<'shareLocationElement'>; export interface FileElement { fileMd5?: string; @@ -441,29 +318,13 @@ export interface FileElement { fileBizId?: number; } -export interface SendFileElement { - elementType: ElementType.FILE; - elementId: string; - fileElement: FileElement; -} +export type SendFileElement = SendElementBase & ElementBase<'fileElement'>; -export interface SendVideoElement { - elementType: ElementType.VIDEO; - elementId: string; - videoElement: VideoElement; -} +export type SendVideoElement = SendElementBase & ElementBase<'videoElement'>; -export interface SendArkElement { - elementType: ElementType.ARK; - elementId: string; - arkElement: ArkElement; -} +export type SendArkElement = SendElementBase & ElementBase<'arkElement'>; -export interface SendMarkdownElement { - elementType: ElementType.MARKDOWN; - elementId: string; - markdownElement: MarkdownElement; -} +export type SendMarkdownElement = SendElementBase & ElementBase<'markdownElement'>; export type SendMessageElement = SendTextElement | SendPttElement | SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement | @@ -480,7 +341,7 @@ export interface TextElement { export interface MessageElement { elementType: ElementType, elementId: string, - extBufForUI: string,//"0x", + extBufForUI?: string, //"0x", textElement?: TextElement; faceElement?: FaceElement, marketFaceElement?: MarketFaceElement, @@ -509,7 +370,6 @@ export interface MessageElement { taskTopMsgElement?: TaskTopMsgElement, recommendedMsgElement?: RecommendedMsgElement, actionBarElement?: ActionBarElement - } export enum AtType { @@ -578,7 +438,7 @@ export interface PttElement { fileSize: string; // "4261" fileSubId: string; // "0" fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV" - formatType: string; // 1 + formatType: number; // 1 invalidState: number; // 0 md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6" playState: number; // 0 @@ -589,6 +449,7 @@ export interface PttElement { voiceChangeType: number; // 0 voiceType: number; // 0 waveAmplitudes: number[]; + autoConvertText: number; } export interface ArkElement { @@ -794,7 +655,8 @@ export interface InlineKeyboardElementRowButton { export interface InlineKeyboardElement { rows: [{ buttons: InlineKeyboardElementRowButton[] - }]; + }], + botAppid: string; } export interface TipAioOpGrayTipElement { // 这是什么提示来着? diff --git a/src/core/packet/msg/converter.ts b/src/core/packet/msg/converter.ts index 8e2091ec..f1b13278 100644 --- a/src/core/packet/msg/converter.ts +++ b/src/core/packet/msg/converter.ts @@ -1,4 +1,7 @@ import { + Peer, + ChatType, + ElementType, MessageElement, RawMessage, SendArkElement, @@ -28,30 +31,42 @@ import { PacketMsgVideoElement, PacketMultiMsgElement } from "@/core/packet/msg/element"; -import { PacketMsg, PacketSendMsgElement } from "@/core/packet/msg/message"; -import { LogWrapper } from "@/common/log"; +import {PacketMsg, PacketSendMsgElement} from "@/core/packet/msg/message"; +import {LogWrapper} from "@/common/log"; -type SendMessageElementMap = { - textElement: SendTextElement, - picElement: SendPicElement, - replyElement: SendReplyElement, - faceElement: SendFaceElement, - marketFaceElement: SendMarketFaceElement, - videoElement: SendVideoElement, - fileElement: SendFileElement, - pttElement: SendPttElement, - arkElement: SendArkElement, - markdownElement: SendMarkdownElement, - structLongMsgElement: SendStructLongMsgElement +const SupportedElementTypes = [ + ElementType.TEXT, + ElementType.PIC, + ElementType.REPLY, + ElementType.FACE, + ElementType.MFACE, + ElementType.VIDEO, + ElementType.FILE, + ElementType.PTT, + ElementType.ARK, + ElementType.MARKDOWN, + ElementType.STRUCTLONGMSG +]; + +type SendMessageTypeElementMap = { + [ElementType.TEXT]: SendTextElement, + [ElementType.PIC]: SendPicElement, + [ElementType.FILE]: SendFileElement, + [ElementType.PTT]: SendPttElement, + [ElementType.VIDEO]: SendVideoElement, + [ElementType.FACE]: SendFaceElement, + [ElementType.REPLY]: SendReplyElement, + [ElementType.ARK]: SendArkElement, + [ElementType.MFACE]: SendMarketFaceElement, + [ElementType.STRUCTLONGMSG]: SendStructLongMsgElement, + [ElementType.MARKDOWN]: SendMarkdownElement, }; -type RawToPacketMsgConverters = { - [K in keyof SendMessageElementMap]: ( - element: SendMessageElementMap[K], - msg?: RawMessage, - elementWrapper?: MessageElement, - ) => IPacketMsgElement | null; -}; +type ElementToPacketMsgConverters = { + [K in keyof SendMessageTypeElementMap]: ( + sendElement: MessageElement + ) => IPacketMsgElement; +} export type rawMsgWithSendMsg = { senderUin: number; @@ -69,6 +84,10 @@ export class PacketMsgConverter { this.logger = logger; } + private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters { + return SupportedElementTypes.includes(type); + } + rawMsgWithSendMsgToPacketMsg(msg: rawMsgWithSendMsg): PacketMsg { return { senderUid: msg.senderUid ?? '', @@ -77,55 +96,68 @@ export class PacketMsgConverter { groupId: msg.groupId, time: msg.time, msg: msg.msg.map((element) => { - const key = (Object.keys(this.rawToPacketMsgConverters) as Array).find( - (k) => (element as any)[k] !== undefined // TODO: - ); - if (key) { - const elementData = (element as any)[key]; // TODO: - if (elementData) return this.rawToPacketMsgConverters[key](element as any); - } - return null; + if (!this.isValidElementType(element.elementType)) return null; + return this.rawToPacketMsgConverters[element.elementType](element as MessageElement); }).filter((e) => e !== null) }; } - private rawToPacketMsgConverters: RawToPacketMsgConverters = { - textElement: (element: SendTextElement) => { - if (element.textElement.atType) { - return new PacketMsgAtElement(element); + rawMsgToPacketMsg(msg: RawMessage, ctxPeer: Peer): PacketMsg { + return { + seq: +msg.msgSeq, + groupId: ctxPeer.chatType === ChatType.KCHATTYPEGROUP ? +msg.peerUid : undefined, + senderUid: msg.senderUid, + senderUin: +msg.senderUin, + senderName: msg.sendMemberName && msg.sendMemberName !== '' + ? msg.sendMemberName + : msg.sendNickName && msg.sendNickName !== '' + ? msg.sendNickName + : "QQ用户", + time: +msg.msgTime, + msg: msg.elements.map((element) => { + if (!this.isValidElementType(element.elementType)) return null; + return this.rawToPacketMsgConverters[element.elementType](element); + }).filter((e) => e !== null) + } + } + + private rawToPacketMsgConverters: ElementToPacketMsgConverters = { + [ElementType.TEXT]: (element) => { + if (element.textElement?.atType) { + return new PacketMsgAtElement(element as SendTextElement); } - return new PacketMsgTextElement(element); + return new PacketMsgTextElement(element as SendTextElement); }, - picElement: (element: SendPicElement) => { - return new PacketMsgPicElement(element); + [ElementType.PIC]: (element) => { + return new PacketMsgPicElement(element as SendPicElement); }, - replyElement: (element: SendReplyElement) => { - return new PacketMsgReplyElement(element); + [ElementType.REPLY]: (element) => { + return new PacketMsgReplyElement(element as SendReplyElement); }, - faceElement: (element: SendFaceElement) => { - return new PacketMsgFaceElement(element); + [ElementType.FACE]: (element) => { + return new PacketMsgFaceElement(element as SendFaceElement); }, - marketFaceElement: (element: SendMarketFaceElement) => { - return new PacketMsgMarkFaceElement(element); + [ElementType.MFACE]: (element) => { + return new PacketMsgMarkFaceElement(element as SendMarketFaceElement); }, - videoElement: (element: SendVideoElement) => { - return new PacketMsgVideoElement(element); + [ElementType.VIDEO]: (element) => { + return new PacketMsgVideoElement(element as SendVideoElement); }, - fileElement: (element: SendFileElement) => { - return new PacketMsgFileElement(element); + [ElementType.FILE]: (element) => { + return new PacketMsgFileElement(element as SendFileElement); }, - pttElement: (element: SendPttElement) => { - return new PacketMsgPttElement(element); + [ElementType.PTT]: (element) => { + return new PacketMsgPttElement(element as SendPttElement); }, - arkElement: (element: SendArkElement) => { - return new PacketMsgLightAppElement(element); + [ElementType.ARK]: (element) => { + return new PacketMsgLightAppElement(element as SendArkElement); }, - markdownElement: (element: SendMarkdownElement) => { - return new PacketMsgMarkDownElement(element); + [ElementType.MARKDOWN]: (element) => { + return new PacketMsgMarkDownElement(element as SendMarkdownElement); }, // TODO: check this logic, move it in arkElement? - structLongMsgElement: (element: SendStructLongMsgElement) => { - return new PacketMultiMsgElement(element); + [ElementType.STRUCTLONGMSG]: (element) => { + return new PacketMultiMsgElement(element as SendStructLongMsgElement); } }; } diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts index 92633dd6..d2f79292 100644 --- a/src/onebot/action/msg/SendMsg.ts +++ b/src/onebot/action/msg/SendMsg.ts @@ -168,7 +168,7 @@ export class SendMsg extends BaseAction { logger.logWarn('转发消息深度超过3层,将停止解析!'); break; } - if ((node.data.id && typeof node.data.content !== "string") || !node.data.id) { + if (!node.data.id) { const OB11Data = normalize(node.type === OB11MessageDataType.node ? node.data.content : node); let sendElements: SendMessageElement[]; @@ -190,9 +190,22 @@ export class SendMsg extends BaseAction { time: Number(node.data.time) || Date.now(), msg: sendElements, }; - logger.logDebug(`handleForwardedNodesPacket 开始转换 ${JSON.stringify(packetMsgElements)}`); + logger.logDebug(`handleForwardedNodesPacket[SendRaw] 开始转换 ${JSON.stringify(packetMsgElements)}`); const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements); - logger.logDebug(`handleForwardedNodesPacket 转换为 ${JSON.stringify(transformedMsg)}`); + logger.logDebug(`handleForwardedNodesPacket[SendRaw] 转换为 ${JSON.stringify(transformedMsg)}`); + packetMsg.push(transformedMsg!); + } else if (node.data.id) { + const id = node.data.id; + const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(+id) || MessageUnique.getPeerByMsgId(id); + if (!nodeMsg) { + logger.logError.bind(this.core.context.logger)('转发消息失败,未找到消息', id); + continue; + } + const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0]; + logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${JSON.stringify(msg)}`); + await this.core.apis.FileApi.downloadRawMsgMedia([msg]); + const transformedMsg = this.core.apis.PacketApi.packetSession?.packer.packetConverter.rawMsgToPacketMsg(msg, msgPeer); + logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${JSON.stringify(transformedMsg)}`); packetMsg.push(transformedMsg!); } else { logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${JSON.stringify(node)}`); @@ -262,6 +275,7 @@ export class SendMsg extends BaseAction { logger.logError.bind(this.core.context.logger)('子消息中包含非node消息 跳过不合法部分'); continue; } + // @ts-ignore const nodeMsg = await this.handleForwardedNodes(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node)); if (nodeMsg) { nodeMsgIds.push(nodeMsg.message!.msgId); diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index b4b008a3..1b4cd02b 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -466,6 +466,7 @@ export class OneBotMsgApi { }, }) => ({ elementType: ElementType.MFACE, + elementId: '', marketFaceElement: { emojiPackageId: emoji_package_id, emojiId: emoji_id,