diff --git a/src/common/utils/MessageUnique.ts b/src/common/utils/MessageUnique.ts index 71198eb5..725c51de 100644 --- a/src/common/utils/MessageUnique.ts +++ b/src/common/utils/MessageUnique.ts @@ -101,7 +101,7 @@ class MessageUniqueWrapper { return ret.map((t) => t?.MsgId).filter((t) => t !== undefined); } - createMsg(peer: Peer, msgId: string): number | undefined { + createMsg(peer: Peer, msgId: string) { const key = `${msgId}|${peer.chatType}|${peer.peerUid}`; const hash = crypto.createHash('md5').update(key).digest(); //设置第一个bit为0 保证shortId为正数 diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index e327522e..b9f52d1b 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -8,6 +8,11 @@ import { IMAGE_HTTP_HOST_NT, Peer, PicElement, + PicType, + SendFileElement, + SendPicElement, + SendPttElement, + SendVideoElement, } from '@/core/entities'; import path from 'path'; import fs from 'fs'; @@ -18,7 +23,12 @@ import imageSize from 'image-size'; import { ISizeCalculationResult } from 'image-size/dist/types/interface'; import { NodeIKernelSearchService } from '../services/NodeIKernelSearchService'; import { RkeyManager } from '../helper/rkey'; -import { calculateFileMD5 } from '@/common/utils/file'; +import { calculateFileMD5, isGIF } from '@/common/utils/file'; +import pathLib from 'node:path'; +import { defaultVideoThumbB64, getVideoInfo } from '@/common/utils/video'; +import ffmpeg from 'fluent-ffmpeg'; +import fsnormal from 'node:fs'; +import { encodeSilk } from '@/common/utils/audio'; export class NTQQFileApi { @@ -84,6 +94,200 @@ export class NTQQFileApi { }; } + async createValidSendFileElement( + filePath: string, + fileName: string = '', + folderId: string = '' + ): Promise { + const { fileName: _fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + return { + elementType: ElementType.FILE, + elementId: '', + fileElement: { + fileName: fileName || _fileName, + folderId: folderId, + 'filePath': path!, + 'fileSize': (fileSize).toString(), + }, + }; + } + + async createValidSendPicElement( + picPath: string, + summary: string = '', + subType: 0 | 1 = 0 + ): Promise { + const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + const imageSize = await this.core.apis.FileApi.getImageSize(picPath); + const picElement: any = { + md5HexStr: md5, + fileSize: fileSize.toString(), + picWidth: imageSize?.width, + picHeight: imageSize?.height, + fileName: fileName, + sourcePath: path, + original: true, + picType: isGIF(picPath) ? PicType.gif : PicType.jpg, + picSubType: subType, + fileUuid: '', + fileSubId: '', + thumbFileSize: 0, + summary, + }; + return { + elementType: ElementType.PIC, + elementId: '', + picElement, + }; + } + + async createValidSendVideoElement( + filePath: string, + fileName: string = '', + diyThumbPath: string = '', + ): Promise { + const logger = this.core.context.logger; + const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`); + thumb = pathLib.dirname(thumb); + // log("thumb 目录", thumb) + let videoInfo = { + width: 1920, height: 1080, + time: 15, + format: 'mp4', + size: fileSize, + filePath, + }; + try { + videoInfo = await getVideoInfo(path, logger); + //logDebug('视频信息', videoInfo); + } catch (e) { + logger.logError('获取视频信息失败', e); + } + const createThumb = new Promise((resolve, reject) => { + const thumbFileName = `${md5}_0.png`; + const thumbPath = pathLib.join(thumb, thumbFileName); + ffmpeg(filePath) + .on('error', (err) => { + logger.logDebug('获取视频封面失败,使用默认封面', err); + if (diyThumbPath) { + fsPromises.copyFile(diyThumbPath, thumbPath).then(() => { + resolve(thumbPath); + }).catch(reject); + } else { + fsnormal.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64')); + resolve(thumbPath); + } + }) + .screenshots({ + timestamps: [0], + filename: thumbFileName, + folder: thumb, + size: videoInfo.width + 'x' + videoInfo.height, + }).on('end', () => { + resolve(thumbPath); + }); + }); + const thumbPath = new Map(); + const _thumbPath = await createThumb; + const thumbSize = _thumbPath ? (await fsPromises.stat(_thumbPath)).size : 0; + // log("生成缩略图", _thumbPath) + thumbPath.set(0, _thumbPath); + const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : ""; + // "fileElement": { + // "fileMd5": "", + // "fileName": "1.mp4", + // "filePath": "C:\\Users\\nanae\\OneDrive\\Desktop\\1.mp4", + // "fileSize": "1847007", + // "picHeight": 1280, + // "picWidth": 720, + // "picThumbPath": {}, + // "file10MMd5": "", + // "fileSha": "", + // "fileSha3": "", + // "fileUuid": "", + // "fileSubId": "", + // "thumbFileSize": 750 + // } + return { + elementType: ElementType.VIDEO, + elementId: '', + videoElement: { + fileName: fileName || _fileName, + filePath: path, + videoMd5: md5, + thumbMd5, + fileTime: videoInfo.time, + thumbPath: thumbPath, + thumbSize, + thumbWidth: videoInfo.width, + thumbHeight: videoInfo.height, + fileSize: '' + fileSize, + // fileFormat: videotype + // fileUuid: "", + // transferStatus: 0, + // progress: 0, + // invalidState: 0, + // fileSubId: "", + // fileBizId: null, + // originVideoMd5: "", + // fileFormat: 2, + // import_rich_media_context: null, + // sourceVideoCodecFormat: 2 + }, + }; + } + + async createValidSendPttElement(pttPath: string): Promise { + const { + converted, + path: silkPath, + duration, + } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger); + // log("生成语音", silkPath, duration); + if (!silkPath) { + throw '语音转换失败, 请检查语音文件是否正常'; + } + const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(silkPath!, ElementType.PTT); + if (fileSize === 0) { + throw '文件异常,大小为0'; + } + if (converted) { + fsPromises.unlink(silkPath); + } + return { + elementType: ElementType.PTT, + elementId: '', + pttElement: { + fileName: fileName, + filePath: path, + md5HexStr: md5, + fileSize: fileSize, + // duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算 + duration: duration || 1, + formatType: 1, + voiceType: 1, + voiceChangeType: 0, + canConvert2Text: true, + waveAmplitudes: [ + 0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17, + ], + fileSubId: '', + playState: 1, + autoConvertText: 0, + }, + }; + } + async downloadMediaByUuid() { //napCatCore.session.getRichMediaService().downloadFileForFileUuid(); } diff --git a/src/onebot/action/go-cqhttp/UploadGroupFile.ts b/src/onebot/action/go-cqhttp/UploadGroupFile.ts index 3107c1a6..e7e8cc5b 100644 --- a/src/onebot/action/go-cqhttp/UploadGroupFile.ts +++ b/src/onebot/action/go-cqhttp/UploadGroupFile.ts @@ -1,11 +1,10 @@ import BaseAction from '../BaseAction'; import { ActionName } from '../types'; -import { ChatType, SendFileElement } from '@/core/entities'; +import { ChatType } from '@/core/entities'; import fs from 'fs'; import { sendMsg } from '@/onebot/action/msg/SendMsg'; import { uri2local } from '@/common/utils/file'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { SendMsgElementConstructor } from '@/onebot/helper/genMessage'; const SchemaData = { type: 'object', @@ -34,7 +33,7 @@ export default class GoCQHTTPUploadGroupFile extends BaseAction { if (!downloadResult.success) { throw new Error(downloadResult.errMsg); } - const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(this.CoreContext, downloadResult.path, payload.name, payload.folder_id); + const sendFileEle = await this.CoreContext.apis.FileApi.createValidSendFileElement(downloadResult.path, payload.name, payload.folder_id); await sendMsg(this.CoreContext, { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString(), diff --git a/src/onebot/action/go-cqhttp/UploadPrivareFile.ts b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts similarity index 90% rename from src/onebot/action/go-cqhttp/UploadPrivareFile.ts rename to src/onebot/action/go-cqhttp/UploadPrivateFile.ts index 70800939..287b59e0 100644 --- a/src/onebot/action/go-cqhttp/UploadPrivareFile.ts +++ b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts @@ -5,7 +5,6 @@ import fs from 'fs'; import { sendMsg } from '@/onebot/action/msg/SendMsg'; import { uri2local } from '@/common/utils/file'; import { FromSchema, JSONSchema } from 'json-schema-to-ts'; -import { SendMsgElementConstructor } from '@/onebot/helper/genMessage'; const SchemaData = { type: 'object', @@ -47,7 +46,7 @@ export default class GoCQHTTPUploadPrivateFile extends BaseAction if (!downloadResult.success) { throw new Error(downloadResult.errMsg); } - const sendFileEle: SendFileElement = await SendMsgElementConstructor.file(this.CoreContext, downloadResult.path, payload.name); + const sendFileEle: SendFileElement = await this.CoreContext.apis.FileApi.createValidSendFileElement(downloadResult.path, payload.name); await sendMsg(this.CoreContext, peer, [sendFileEle], [], true); return null; } diff --git a/src/onebot/action/index.ts b/src/onebot/action/index.ts index f6a45c27..f2d4ced2 100644 --- a/src/onebot/action/index.ts +++ b/src/onebot/action/index.ts @@ -71,7 +71,7 @@ import GetRecentContact from './user/GetRecentContact'; import { GetProfileLike } from './extends/GetProfileLike'; import SetGroupPortrait from './go-cqhttp/SetGroupPortrait'; import { FetchCustomFace } from './extends/FetchCustomFace'; -import GoCQHTTPUploadPrivateFile from './go-cqhttp/UploadPrivareFile'; +import GoCQHTTPUploadPrivateFile from './go-cqhttp/UploadPrivateFile'; import { FetchEmojiLike } from './extends/FetchEmojiLike'; import { NapCatCore } from '@/core'; diff --git a/src/onebot/action/msg/SendMsg/index.ts b/src/onebot/action/msg/SendMsg.ts similarity index 52% rename from src/onebot/action/msg/SendMsg/index.ts rename to src/onebot/action/msg/SendMsg.ts index 9daafe68..b9ad6e07 100644 --- a/src/onebot/action/msg/SendMsg/index.ts +++ b/src/onebot/action/msg/SendMsg.ts @@ -9,11 +9,9 @@ import { ActionName, BaseCheckResult } from '@/onebot/action/types'; import fs from 'node:fs'; import fsPromise from 'node:fs/promises'; import { decodeCQCode } from '@/onebot/helper/cqcode'; -import createSendElements from './create-send-elements'; import { MessageUnique } from '@/common/utils/MessageUnique'; -import { ChatType, ElementType, NapCatCore, Peer, SendMessageElement } from '@/core'; -import BaseAction from '../../BaseAction'; -import { handleForwardNode } from './handle-forward-node'; +import { ChatType, ElementType, NapCatCore, Peer, RawMessage, SendMessageElement } from '@/core'; +import BaseAction from '../BaseAction'; export interface ReturnDataType { message_id: number; @@ -34,8 +32,6 @@ export function normalize(message: OB11MessageMixType, autoEscape = false): OB11 ) : Array.isArray(message) ? message : [message]; } -export { createSendElements }; - export async function sendMsg(coreContext: NapCatCore, peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) { const NTQQMsgApi = coreContext.apis.MsgApi; const logger = coreContext.context.logger; @@ -144,9 +140,6 @@ export class SendMsg extends BaseAction { contextMode = ContextMode.Normal; protected async check(payload: OB11PostSendMsg): Promise { - const NTQQGroupApi = this.CoreContext.apis.GroupApi; - const NTQQFriendApi = this.CoreContext.apis.FriendApi; - const NTQQUserApi = this.CoreContext.apis.UserApi; const messages = normalize(payload.message); const nodeElementLength = getSpecialMsgNum(payload, OB11MessageDataType.node); if (nodeElementLength > 0 && nodeElementLength != messages.length) { @@ -156,9 +149,9 @@ export class SendMsg extends BaseAction { }; } if (payload.user_id && payload.message_type !== 'group') { - const uid = await NTQQUserApi.getUidByUinV2(payload.user_id.toString()); - const isBuddy = await NTQQFriendApi.isBuddy(uid!); - //if (!isBuddy) { } + // const uid = await this.CoreContext.apis.UserApi.getUidByUinV2(payload.user_id.toString()); + // const isBuddy = await NTQQFriendApi.isBuddy(uid!); + // if (!isBuddy) { } } return { valid: true }; } @@ -174,7 +167,7 @@ export class SendMsg extends BaseAction { ); if (getSpecialMsgNum(payload, OB11MessageDataType.node)) { - const returnMsg = await handleForwardNode(this.CoreContext, this.OneBotContext, peer, messages as OB11MessageNode[]); + const returnMsg = await this.handleForwardedNodes(peer, messages as OB11MessageNode[]); if (returnMsg) { const msgShortId = MessageUnique.createMsg({ guildId: '', @@ -194,11 +187,137 @@ export class SendMsg extends BaseAction { } // log("send msg:", peer, sendElements) - const { sendElements, deleteAfterSentFiles } = await createSendElements(this.CoreContext, this.OneBotContext, messages, peer); - //console.log(peer, JSON.stringify(sendElements,null,2)); + const { sendElements, deleteAfterSentFiles } = await this.OneBotContext.apiContext.MsgApi + .createSendElements(messages, peer); const returnMsg = await sendMsg(this.CoreContext, peer, sendElements, deleteAfterSentFiles); return { message_id: returnMsg!.id! }; } + + private async handleForwardedNodes(destPeer: Peer, messageNodes: OB11MessageNode[]): Promise { + const NTQQMsgApi = this.CoreContext.apis.MsgApi; + const selfPeer = { + chatType: ChatType.KCHATTYPEC2C, + peerUid: this.CoreContext.selfInfo.uid, + }; + let nodeMsgIds: string[] = []; + const logger = this.CoreContext.context.logger; + for (const messageNode of messageNodes) { + const nodeId = messageNode.data.id; + if (nodeId) { + //对Mgsid和OB11ID混用情况兜底 + const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId); + if (!nodeMsg) { + logger.logError('转发消息失败,未找到消息', nodeId); + continue; + } + nodeMsgIds.push(nodeMsg.MsgId); + } else { + // 自定义的消息 + try { + const OB11Data = normalize(messageNode.data.content); + //筛选node消息 + const isNodeMsg = OB11Data.filter(e => e.type === OB11MessageDataType.node).length;//找到子转发消息 + if (isNodeMsg !== 0) { + if (isNodeMsg !== OB11Data.length) { + logger.logError('子消息中包含非node消息 跳过不合法部分'); + continue; + } + const nodeMsg = await this.handleForwardedNodes(selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node)); + if (nodeMsg) { + nodeMsgIds.push(nodeMsg.msgId); + MessageUnique.createMsg(selfPeer, nodeMsg.msgId); + } + //完成子卡片生成跳过后续 + continue; + } + const { sendElements } = await this.OneBotContext.apiContext.MsgApi + .createSendElements(OB11Data, destPeer); + //拆分消息 + const MixElement = sendElements.filter(element => element.elementType !== ElementType.FILE && element.elementType !== ElementType.VIDEO); + const SingleElement = sendElements.filter(element => element.elementType === ElementType.FILE || element.elementType === ElementType.VIDEO).map(e => [e]); + const AllElement: SendMessageElement[][] = [MixElement, ...SingleElement].filter(e => e !== undefined && e.length !== 0); + const MsgNodeList: Promise[] = []; + for (const sendElementsSplitElement of AllElement) { + MsgNodeList.push(sendMsg(this.CoreContext, selfPeer, sendElementsSplitElement, [], true).catch(_ => new Promise((resolve) => { + resolve(undefined); + }))); + } + (await Promise.allSettled(MsgNodeList)).map((result) => { + if (result.status === 'fulfilled' && result.value) { + nodeMsgIds.push(result.value.msgId); + MessageUnique.createMsg(selfPeer, result.value.msgId); + } + }); + } catch (e) { + logger.logDebug('生成转发消息节点失败', e); + } + } + } + const nodeMsgArray: Array = []; + let srcPeer: Peer | undefined = undefined; + let needSendSelf = false; + //检测是否处于同一个Peer 不在同一个peer则全部消息由自身发送 + for (const msgId of nodeMsgIds) { + const nodeMsgPeer = MessageUnique.getPeerByMsgId(msgId); + if (!nodeMsgPeer) { + logger.logError('转发消息失败,未找到消息', msgId); + continue; + } + const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]; + srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }; + if (srcPeer.peerUid !== nodeMsg.peerUid) { + needSendSelf = true; + } + nodeMsgArray.push(nodeMsg); + } + nodeMsgIds = nodeMsgArray.map(msg => msg.msgId); + let retMsgIds: string[] = []; + if (needSendSelf) { + for (const [, msg] of nodeMsgArray.entries()) { + if (msg.peerUid === this.CoreContext.selfInfo.uid){ + retMsgIds.push(msg.msgId); + continue; + } + const ClonedMsg = await this.cloneMsg(msg); + if (ClonedMsg) retMsgIds.push(ClonedMsg.msgId); + } + } else { + retMsgIds = nodeMsgIds; + } + if (retMsgIds.length === 0) throw Error('转发消息失败,生成节点为空'); + try { + logger.logDebug('开发转发', srcPeer, destPeer, retMsgIds); + return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds); + } catch (e) { + logger.logError('forward failed', e); + return null; + } + } + + async cloneMsg(msg: RawMessage): Promise { + const selfPeer = { + chatType: ChatType.KCHATTYPEC2C, + peerUid: this.CoreContext.selfInfo.uid, + }; + const logger = this.CoreContext.context.logger; + const NTQQMsgApi = this.CoreContext.apis.MsgApi; + //logDebug('克隆的目标消息', msg); + + const sendElements: SendMessageElement[] = []; + + for (const element of msg.elements) { + sendElements.push(element as SendMessageElement); + } + + if (sendElements.length === 0) { + logger.logDebug('需要clone的消息无法解析,将会忽略掉', msg); + } + try { + return await NTQQMsgApi.sendMsg(selfPeer, sendElements, true); + } catch (e) { + logger.logError(e, '克隆转发消息失败,将忽略本条消息', msg); + } + } } export default SendMsg; diff --git a/src/onebot/action/msg/SendMsg/check-send-message.ts b/src/onebot/action/msg/SendMsg/check-send-message.ts deleted file mode 100644 index ddbc0a2b..00000000 --- a/src/onebot/action/msg/SendMsg/check-send-message.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { OB11MessageData } from '@/onebot/types'; - -function checkSendMessage(sendMsgList: OB11MessageData[]) { - function checkUri(uri: string): boolean { - const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/; - return pattern.test(uri); - } - - for (const msg of sendMsgList) { - if (msg['type'] && msg['data']) { - const type = msg['type']; - const data = msg['data']; - if (type === 'text' && !data['text']) { - return 400; - } else if (['image', 'voice', 'record'].includes(type)) { - if (!data['file']) { - return 400; - } else { - if (checkUri(data['file'])) { - return 200; - } else { - return 400; - } - } - - } else if (type === 'at' && !data['qq']) { - return 400; - } else if (type === 'reply' && !data['id']) { - return 400; - } - } else { - return 400; - } - } - return 200; -} diff --git a/src/onebot/action/msg/SendMsg/create-send-elements.ts b/src/onebot/action/msg/SendMsg/create-send-elements.ts deleted file mode 100644 index d3bc3917..00000000 --- a/src/onebot/action/msg/SendMsg/create-send-elements.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { OB11MessageData, OB11MessageDataType, OB11MessageFileBase } from '@/onebot/types'; -import { uri2local } from '@/common/utils/file'; -import { RequestUtil } from '@/common/utils/request'; -import { MessageUnique } from '@/common/utils/MessageUnique'; -import { AtType, ChatType, CustomMusicSignPostData, IdMusicSignPostData, NapCatCore, Peer, SendMessageElement } from '@/core'; -import { SendMsgElementConstructor } from '@/onebot/helper/genMessage'; -import { NapCatOneBot11Adapter } from '@/onebot'; - -export type MessageContext = { - deleteAfterSentFiles: string[], - peer: Peer -} - -async function handleOb11FileLikeMessage( - coreContext: NapCatCore, - obContext: NapCatOneBot11Adapter, - { data: inputdata }: OB11MessageFileBase, - { deleteAfterSentFiles }: MessageContext, -) { - //inputdata?.url || inputdata.file - const isBlankUrl = !inputdata.url || inputdata.url === ''; - const isBlankFile = !inputdata.file || inputdata.file === ''; - if (isBlankUrl && isBlankFile) { - coreContext.context.logger.logError('文件消息缺少参数', inputdata); - throw Error('文件消息缺少参数'); - } - const fileOrUrl = (isBlankUrl ? inputdata.file : inputdata.url) || ""; - const { - path, - isLocal, - fileName, - errMsg, - success, - } = (await uri2local(coreContext.NapCatTempPath, fileOrUrl)); - - if (!success) { - coreContext.context.logger.logError('文件下载失败', errMsg); - throw Error('文件下载失败' + errMsg); - } - - if (!isLocal) { // 只删除http和base64转过来的文件 - deleteAfterSentFiles.push(path); - } - - return { path, fileName: inputdata.name || fileName }; -} - -const _handlers: { - [Key in OB11MessageDataType]: ( - CoreContext: NapCatCore, - obContext: NapCatOneBot11Adapter, - sendMsg: Extract, - // This picks the correct message type out - // How great the type system of TypeScript is! - context: MessageContext, - ) => Promise -} = { - [OB11MessageDataType.text]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { text } }) => SendMsgElementConstructor.text(coreContext, text), - - [OB11MessageDataType.at]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { qq: atQQ } }, context) => { - if (!context.peer || context.peer.chatType == ChatType.KCHATTYPEC2C) return undefined; - if (atQQ === 'all') return SendMsgElementConstructor.at(coreContext, atQQ, atQQ, AtType.atAll, '全体成员'); - const NTQQGroupApi = coreContext.apis.GroupApi; - const NTQQUserApi = coreContext.apis.UserApi; - const atMember = await NTQQGroupApi.getGroupMember(context.peer.peerUid, atQQ); - if (atMember) { - return SendMsgElementConstructor.at(coreContext, atQQ, atMember.uid, AtType.atUser, atMember.nick || atMember.cardName); - } - const uid = await NTQQUserApi.getUidByUinV2(`${atQQ}`); - if (!uid) throw new Error('Get Uid Error'); - const info = await NTQQUserApi.getUserDetailInfo(uid); - return SendMsgElementConstructor.at(coreContext, atQQ, uid, AtType.atUser, info.nick || ''); - }, - [OB11MessageDataType.reply]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { id } }) => { - const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id)); - if (!replyMsgM) { - coreContext.context.logger.logWarn('回复消息不存在', id); - return undefined; - } - const NTQQMsgApi = coreContext.apis.MsgApi; - const replyMsg = (await NTQQMsgApi.getMsgsByMsgId( - replyMsgM.Peer, [replyMsgM.MsgId!])).msgList[0]; - return replyMsg ? - SendMsgElementConstructor.reply(coreContext, replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) : - undefined; - }, - - [OB11MessageDataType.face]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { id } }) => SendMsgElementConstructor.face(coreContext, parseInt(id)), - - [OB11MessageDataType.mface]: async (coreContext, obContext: NapCatOneBot11Adapter, { - data: { - emoji_package_id, emoji_id, key, summary, - }, - }) => SendMsgElementConstructor.mface(coreContext, emoji_package_id, emoji_id, key, summary), - - // File service - [OB11MessageDataType.image]: async (coreContext, obContext: NapCatOneBot11Adapter, sendMsg, context) => { - const PicEle = await SendMsgElementConstructor.pic( - coreContext, - (await handleOb11FileLikeMessage(coreContext, obContext, sendMsg, context)).path, - sendMsg.data.summary || '', - sendMsg.data.subType || 0, - ); - context.deleteAfterSentFiles.push(PicEle.picElement.sourcePath); - return PicEle; - }, // currently not supported - [OB11MessageDataType.file]: async (coreContext, obContext: NapCatOneBot11Adapter, sendMsg, context) => { - const { path, fileName } = await handleOb11FileLikeMessage(coreContext, obContext, sendMsg, context); - //logDebug('发送文件', path, fileName); - const FileEle = await SendMsgElementConstructor.file(coreContext, path, fileName); - // 清除Upload的应该 - // context.deleteAfterSentFiles.push(fileName || FileEle.fileElement.filePath); - return FileEle; - }, - - [OB11MessageDataType.video]: async (coreContext, obContext, sendMsg, context) => { - const { path, fileName } = await handleOb11FileLikeMessage(coreContext, obContext, sendMsg, context); - - //logDebug('发送视频', path, fileName); - let thumb = sendMsg.data.thumb; - if (thumb) { - const uri2LocalRes = await uri2local(coreContext.NapCatTempPath, thumb); - if (uri2LocalRes.success) thumb = uri2LocalRes.path; - } - const videoEle = await SendMsgElementConstructor.video(coreContext, path, fileName, thumb); - //未测试 - context.deleteAfterSentFiles.push(videoEle.videoElement.filePath); - return videoEle; - }, - - [OB11MessageDataType.voice]: async (coreContext, obContext: NapCatOneBot11Adapter, sendMsg, context) => SendMsgElementConstructor.ptt(coreContext, (await handleOb11FileLikeMessage(coreContext, obContext, sendMsg, context)).path), - - [OB11MessageDataType.json]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { data } }) => SendMsgElementConstructor.ark(coreContext, data), - - [OB11MessageDataType.dice]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { result } }) => SendMsgElementConstructor.dice(coreContext, result), - - [OB11MessageDataType.RPS]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { result } }) => SendMsgElementConstructor.rps(coreContext, result), - - [OB11MessageDataType.markdown]: async (coreContext, obContext: NapCatOneBot11Adapter, { data: { content } }) => SendMsgElementConstructor.markdown(coreContext, content), - - [OB11MessageDataType.music]: async (coreContext, obContext: NapCatOneBot11Adapter, { data }) => { - // 保留, 直到...找到更好的解决方案 - if (data.type === 'custom') { - if (!data.url) { - coreContext.context.logger.logError('自定义音卡缺少参数url'); - return undefined; - } - if (!data.audio) { - coreContext.context.logger.logError('自定义音卡缺少参数audio'); - return undefined; - } - if (!data.title) { - coreContext.context.logger.logError('自定义音卡缺少参数title'); - return undefined; - } - } else { - if (!['qq', '163'].includes(data.type)) { - coreContext.context.logger.logError('音乐卡片type错误, 只支持qq、163、custom,当前type:', data.type); - return undefined; - } - if (!data.id) { - coreContext.context.logger.logError('音乐卡片缺少参数id'); - return undefined; - } - } - - let postData: IdMusicSignPostData | CustomMusicSignPostData; - if (data.type === 'custom' && data.content) { - const { content, ...others } = data; - postData = { singer: content, ...others }; - } else { - postData = data; - } - // Mlikiowa V2.2.8 Refactor Todo - const signUrl = obContext.configLoader.configData.musicSignUrl; - if (!signUrl) { - if (data.type === 'qq') { - //const musicJson = (await SignMusicWrapper(data.id.toString())).data.arkResult.slice(0, -1); - //return SendMsgElementConstructor.ark(musicJson); - } - throw Error('音乐消息签名地址未配置'); - } - try { - const musicJson = await RequestUtil.HttpGetJson(signUrl, 'POST', postData); - return SendMsgElementConstructor.ark(coreContext, musicJson); - } catch (e) { - coreContext.context.logger.logError('生成音乐消息失败', e); - } - }, - - [OB11MessageDataType.node]: async (coreContext, obContext: NapCatOneBot11Adapter) => undefined, - - [OB11MessageDataType.forward]: async (coreContext, obContext: NapCatOneBot11Adapter) => undefined, - - [OB11MessageDataType.xml]: async (coreContext, obContext: NapCatOneBot11Adapter) => undefined, - - [OB11MessageDataType.poke]: async (coreContext, obContext: NapCatOneBot11Adapter) => undefined, - - [OB11MessageDataType.Location]: async (coreContext, obContext: NapCatOneBot11Adapter) => { - return SendMsgElementConstructor.location(coreContext); - }, - [OB11MessageDataType.miniapp]: function (CoreContext: NapCatCore, obContext: NapCatOneBot11Adapter, sendMsg: never, context: MessageContext): Promise { - throw new Error('Function not implemented.'); - }, -}; - -const handlers = <{ - [Key in OB11MessageDataType]: ( - coreContext: NapCatCore, - obContext: NapCatOneBot11Adapter, - sendMsg: OB11MessageData, - context: MessageContext, - ) => Promise -}>_handlers; - -export default async function createSendElements( - CoreContext: NapCatCore, - obContext: NapCatOneBot11Adapter, - messageData: OB11MessageData[], - peer: Peer, - ignoreTypes: OB11MessageDataType[] = [], -) { - const deleteAfterSentFiles: string[] = []; - const callResultList: Array> = []; - for (const sendMsg of messageData) { - if (ignoreTypes.includes(sendMsg.type)) { - continue; - } - const callResult = handlers[sendMsg.type]( - CoreContext, - obContext, - sendMsg, - { peer, deleteAfterSentFiles }, - )?.catch(undefined); - callResultList.push(callResult); - } - const ret = await Promise.all(callResultList); - const sendElements: SendMessageElement[] = ret.filter(ele => !!ele); - return { sendElements, deleteAfterSentFiles }; -} - -export async function createSendElementsParallel( - CoreContext: NapCatCore, - obContext: NapCatOneBot11Adapter, - messageData: OB11MessageData[], - peer: Peer, - ignoreTypes: OB11MessageDataType[] = [], -) { - const deleteAfterSentFiles: string[] = []; - const sendElements = ( - await Promise.all( - messageData.map(async sendMsg => ignoreTypes.includes(sendMsg.type) ? - undefined : - handlers[sendMsg.type](CoreContext, obContext, sendMsg, { peer, deleteAfterSentFiles })), - ).then( - results => results.filter( - element => element !== undefined, - ), - ) - ); - return { sendElements, deleteAfterSentFiles }; -} diff --git a/src/onebot/action/msg/SendMsg/handle-forward-node.ts b/src/onebot/action/msg/SendMsg/handle-forward-node.ts deleted file mode 100644 index a2668de7..00000000 --- a/src/onebot/action/msg/SendMsg/handle-forward-node.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { ChatType, ElementType, NapCatCore, Peer, RawMessage, SendMessageElement } from '@/core'; -import { MessageUnique } from '@/common/utils/MessageUnique'; -import { OB11MessageDataType, OB11MessageNode } from '@/onebot/types'; -import createSendElements from './create-send-elements'; -import { normalize, sendMsg } from '../SendMsg/index'; -import { NapCatOneBot11Adapter } from '@/onebot'; - -async function cloneMsg(coreContext: NapCatCore, msg: RawMessage): Promise { - const selfPeer = { - chatType: ChatType.KCHATTYPEC2C, - peerUid: coreContext.selfInfo.uid, - }; - const logger = coreContext.context.logger; - const NTQQMsgApi = coreContext.apis.MsgApi; - //logDebug('克隆的目标消息', msg); - - const sendElements: SendMessageElement[] = []; - - for (const element of msg.elements) { - sendElements.push(element as SendMessageElement); - } - - if (sendElements.length === 0) { - logger.logDebug('需要clone的消息无法解析,将会忽略掉', msg); - } - try { - const nodeMsg = await NTQQMsgApi.sendMsg(selfPeer, sendElements, true); - return nodeMsg; - } catch (e) { - logger.logError(e, '克隆转发消息失败,将忽略本条消息', msg); - } -} - -export async function handleForwardNode(coreContext: NapCatCore, obContext: NapCatOneBot11Adapter, destPeer: Peer, messageNodes: OB11MessageNode[]): Promise { - const NTQQMsgApi = coreContext.apis.MsgApi; - const selfPeer = { - chatType: ChatType.KCHATTYPEC2C, - peerUid: coreContext.selfInfo.uid, - }; - let nodeMsgIds: string[] = []; - const logger = coreContext.context.logger; - for (const messageNode of messageNodes) { - const nodeId = messageNode.data.id; - if (nodeId) { - //对Mgsid和OB11ID混用情况兜底 - const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId); - if (!nodeMsg) { - logger.logError('转发消息失败,未找到消息', nodeId); - continue; - } - nodeMsgIds.push(nodeMsg.MsgId); - } else { - // 自定义的消息 - try { - const OB11Data = normalize(messageNode.data.content); - //筛选node消息 - const isNodeMsg = OB11Data.filter(e => e.type === OB11MessageDataType.node).length;//找到子转发消息 - if (isNodeMsg !== 0) { - if (isNodeMsg !== OB11Data.length) { - logger.logError('子消息中包含非node消息 跳过不合法部分'); - continue; - } - const nodeMsg = await handleForwardNode(coreContext, obContext, selfPeer, OB11Data.filter(e => e.type === OB11MessageDataType.node)); - if (nodeMsg) { - nodeMsgIds.push(nodeMsg.msgId); - MessageUnique.createMsg(selfPeer, nodeMsg.msgId); - } - //完成子卡片生成跳过后续 - continue; - } - const { sendElements } = await createSendElements(coreContext, obContext, OB11Data, destPeer); - //拆分消息 - const MixElement = sendElements.filter(element => element.elementType !== ElementType.FILE && element.elementType !== ElementType.VIDEO); - const SingleElement = sendElements.filter(element => element.elementType === ElementType.FILE || element.elementType === ElementType.VIDEO).map(e => [e]); - const AllElement: SendMessageElement[][] = [MixElement, ...SingleElement].filter(e => e !== undefined && e.length !== 0); - const MsgNodeList: Promise[] = []; - for (const sendElementsSplitElement of AllElement) { - MsgNodeList.push(sendMsg(coreContext, selfPeer, sendElementsSplitElement, [], true).catch(e => new Promise((resolve, reject) => { - resolve(undefined); - }))); - } - (await Promise.allSettled(MsgNodeList)).map((result) => { - if (result.status === 'fulfilled' && result.value) { - nodeMsgIds.push(result.value.msgId); - MessageUnique.createMsg(selfPeer, result.value.msgId); - } - }); - } catch (e) { - logger.logDebug('生成转发消息节点失败', e); - } - } - } - const nodeMsgArray: Array = []; - let srcPeer: Peer | undefined = undefined; - let needSendSelf = false; - //检测是否处于同一个Peer 不在同一个peer则全部消息由自身发送 - for (const msgId of nodeMsgIds) { - const nodeMsgPeer = MessageUnique.getPeerByMsgId(msgId); - if (!nodeMsgPeer) { - logger.logError('转发消息失败,未找到消息', msgId); - continue; - } - const nodeMsg = (await NTQQMsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0]; - srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }; - if (srcPeer.peerUid !== nodeMsg.peerUid) { - needSendSelf = true; - } - nodeMsgArray.push(nodeMsg); - } - nodeMsgIds = nodeMsgArray.map(msg => msg.msgId); - let retMsgIds: string[] = []; - if (needSendSelf) { - for (const [, msg] of nodeMsgArray.entries()) { - if (msg.peerUid === coreContext.selfInfo.uid){ - retMsgIds.push(msg.msgId); - continue; - } - const ClonedMsg = await cloneMsg(coreContext, msg); - if (ClonedMsg) retMsgIds.push(ClonedMsg.msgId); - } - } else { - retMsgIds = nodeMsgIds; - } - if (retMsgIds.length === 0) throw Error('转发消息失败,生成节点为空'); - try { - logger.logDebug('开发转发', srcPeer, destPeer, retMsgIds); - return await NTQQMsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds); - } catch (e) { - logger.logError('forward failed', e); - return null; - } -} diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index 1ffba3af..102956a8 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -1,15 +1,660 @@ import { UUIDConverter } from '@/common/utils/helper'; import { MessageUnique } from '@/common/utils/MessageUnique'; -import { AtType, ChatType, FaceIndex, MessageElement, NapCatCore, RawMessage } from '@/core'; -import { NapCatOneBot11Adapter, OB11Message, OB11MessageData, OB11MessageDataType } from '@/onebot'; +import { + AtType, + ChatType, + CustomMusicSignPostData, + ElementType, + FaceIndex, + FaceType, + IdMusicSignPostData, + MessageElement, + NapCatCore, + Peer, + RawMessage, + SendMessageElement, SendTextElement, +} from '@/core'; +import faceConfig from '@/core/external/face_config.json'; +import { + NapCatOneBot11Adapter, + OB11Message, + OB11MessageData, + OB11MessageDataType, + OB11MessageFileBase, + OB11MessageForward, + OB11MessageReply, +} from '@/onebot'; import { OB11Constructor } from '../helper'; import { EventType } from '@/onebot/event/OB11BaseEvent'; import { encodeCQCode } from '@/onebot/helper/cqcode'; +import { uri2local } from '@/common/utils/file'; +import { RequestUtil } from '@/common/utils/request'; + +type RawToOb11Converters = { + [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( + element: Exclude, + msg: RawMessage, + elementWrapper: MessageElement + ) => PromiseLike +} + +type Ob11ToRawConverters = { + [Key in OB11MessageDataType]: ( + sendMsg: Extract, + context: MessageContext, + ) => Promise +} + +export type MessageContext = { + deleteAfterSentFiles: string[], + peer: Peer +} + +function keyCanBeParsed(key: string, parser: RawToOb11Converters): key is keyof RawToOb11Converters { + return key in parser; +} export class OneBotMsgApi { obContext: NapCatOneBot11Adapter; coreContext: NapCatCore; + rawToOb11Converters: RawToOb11Converters = { + textElement: async element => { + if (element.atType === AtType.notAt) { + let text = element.content; + if (!text.trim()) { + return null; + } + // 兼容 9.7.x 换行符 + if (text.indexOf('\n') === -1 && text.indexOf('\r\n') === -1) { + text = text.replace(/\r/g, '\n'); + } + return { + type: OB11MessageDataType.text, + data: { text } + }; + } else { + let qq: string = 'all'; + if (element.atType !== AtType.atAll) { + const { atNtUid, /* content */ } = element; + let atQQ = element.atUid; + if (!atQQ || atQQ === '0') { + atQQ = await this.coreContext.apis.UserApi.getUinByUidV2(atNtUid); + } + if (atQQ) { + qq = atQQ as `${number}`; + } + } + return { + type: OB11MessageDataType.at, + data: { + qq: qq, + // name: content.slice(1); + }, + }; + } + }, + + picElement: async (element, msg) => { + try { + return { + type: OB11MessageDataType.image, + data: { + file: element.fileName, + sub_type: element.picSubType, + file_id: UUIDConverter.encode(msg.peerUin, msg.msgId), + url: await this.coreContext.apis.FileApi.getImageUrl(element), + file_size: element.fileSize, + }, + }; + } catch (e: any) { + this.coreContext.context.logger.logError('获取图片url失败', e.stack); + return null; + } + }, + + fileElement: async (element, msg, elementWrapper) => { + await this.coreContext.apis.FileApi.addFileCache( + { + peerUid: msg.peerUid, + chatType: msg.chatType, + guildId: '', + }, + msg.msgId, + msg.msgSeq, + msg.senderUid, + elementWrapper.elementId, + elementWrapper.elementType.toString(), + element.fileSize, + element.fileName, + ); + return { + type: OB11MessageDataType.file, + data: { + file: element.fileName, + path: element.filePath, + url: element.filePath, + file_id: UUIDConverter.encode(msg.peerUin, msg.msgId), + file_size: element.fileSize, + } + }; + }, + + faceElement: async element => { + const faceIndex = element.faceIndex; + if (faceIndex === FaceIndex.dice) { + return { + type: OB11MessageDataType.dice, + data: { + result: element.resultId!, + } + }; + } else if (faceIndex === FaceIndex.RPS) { + return { + type: OB11MessageDataType.RPS, + data: { + result: element.resultId!, + } + }; + } else { + return { + type: OB11MessageDataType.face, + data: { + id: element.faceIndex.toString() + } + }; + } + }, + + marketFaceElement: async (_, msg, elementWrapper) => { + await this.coreContext.apis.FileApi.addFileCache( + { + peerUid: msg.peerUid, + chatType: msg.chatType, + guildId: '', + }, + msg.msgId, + msg.msgSeq, + msg.senderUid, + elementWrapper.elementId, + elementWrapper.elementType.toString(), + '0', + 'marketface', + ); + return { + type: OB11MessageDataType.image, + data: { + file: 'marketface', + file_id: UUIDConverter.encode(msg.peerUin, msg.msgId), + path: elementWrapper.elementId, + url: elementWrapper.elementId, + } + }; + }, + + replyElement: async (element, msg) => { + const NTQQMsgApi = this.coreContext.apis.MsgApi; + const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords); + const peer = { + chatType: msg.chatType, + peerUid: msg.peerUid, + guildId: '', + }; + if (!records) { + this.coreContext.context.logger.logError('获取不到引用的消息', element.replayMsgSeq); + return null; + } + let replyMsg: RawMessage | undefined; + // Attempt 1 + replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({ + peerUid: msg.peerUid, + guildId: '', + chatType: msg.chatType, + }, element.replayMsgSeq, 1, true, true)) + .msgList + .find(msg => msg.msgRandom === records.msgRandom); + + if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { + // Attempt 2 + replyMsg = (await NTQQMsgApi.getSingleMsg(peer, element.replayMsgSeq)).msgList[0]; + + if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { + // Attempt 3 + const replyMsgList = (await NTQQMsgApi.getMsgExBySeq(peer, records.msgSeq)).msgList; + if (replyMsgList.length < 1) { + this.coreContext.context.logger.logError('回复消息消息验证失败', element.replayMsgSeq); + return null; + } + replyMsg = replyMsgList.filter(e => e.msgSeq == records.msgSeq) + .sort((a, b) => parseInt(a.msgTime) - parseInt(b.msgTime))[0]; + } + } + + return { + type: OB11MessageDataType.reply, + data: { + id: MessageUnique.createMsg({ + peerUid: msg.peerUid, + guildId: '', + chatType: msg.chatType, + }, replyMsg.msgId).toString() + } + }; + }, + + videoElement: async (element, msg, elementWrapper) => { + const NTQQFileApi = this.coreContext.apis.FileApi; + + //读取视频链接并兜底 + let videoUrlWrappers: Awaited> | undefined; + + if (msg.peerUin === '284840486') { + //TODO: 合并消息内部 应该进行特殊处理 可能需要重写peer 待测试与研究 Mlikiowa Tagged + } + try { + videoUrlWrappers = await NTQQFileApi.getVideoUrl({ + chatType: msg.chatType, + peerUid: msg.peerUid, + guildId: '0', + }, msg.msgId, elementWrapper.elementId); + } catch (error) { + this.coreContext.context.logger.logWarn('获取视频 URL 失败'); + } + + //读取在线URL + let videoDownUrl: string | undefined; + + if (videoUrlWrappers) { + const videoDownUrlTemp = videoUrlWrappers.find((urlWrapper) => { + return !!(urlWrapper.url); + }); + if (videoDownUrlTemp) { + videoDownUrl = videoDownUrlTemp.url; + } + } + + //开始兜底 + if (!videoDownUrl) { + videoDownUrl = element.filePath; + } + + await NTQQFileApi.addFileCache( + { + peerUid: msg.peerUid, + chatType: msg.chatType, + guildId: '', + }, + msg.msgId, + msg.msgSeq, + msg.senderUid, + elementWrapper.elementId, + elementWrapper.elementType.toString(), + element.fileSize ?? '0', + element.fileName, + ); + + return { + type: OB11MessageDataType.video, + data: { + file: element.fileName, + path: videoDownUrl, + url: videoDownUrl, + file_id: UUIDConverter.encode(msg.peerUin, msg.msgId), + file_size: element.fileSize, + } + }; + }, + + pttElement: async (element, msg, elementWrapper) => { + await this.coreContext.apis.FileApi.addFileCache( + { + peerUid: msg.peerUid, + chatType: msg.chatType, + guildId: '', + }, + msg.msgId, + msg.msgSeq, + msg.senderUid, + elementWrapper.elementId, + elementWrapper.elementType.toString(), + element.fileSize || '0', + element.fileUuid || '', + ); + return { + type: OB11MessageDataType.voice, + data: { + file: element.fileName, + path: element.filePath, + file_id: UUIDConverter.encode(msg.peerUin, msg.msgId), + file_size: element.fileSize, + } + }; + }, + + multiForwardMsgElement: async (_, msg) => { + const NTQQMsgApi = this.coreContext.apis.MsgApi; + const message_data: OB11MessageForward = { + data: {} as any, + type: OB11MessageDataType.forward, + }; + message_data.data.id = msg.msgId; + const parentMsgPeer = msg.parentMsgPeer ?? { + chatType: msg.chatType, + guildId: '', + peerUid: msg.peerUid, + }; + //判断是否在合并消息内 + msg.parentMsgIdList = msg.parentMsgIdList ?? []; + //首次列表不存在则开始创建 + msg.parentMsgIdList.push(msg.msgId); + //let parentMsgId = msg.parentMsgIdList[msg.parentMsgIdList.length - 2 < 0 ? 0 : msg.parentMsgIdList.length - 2]; + //加入自身MsgId + const multiMsgs = (await NTQQMsgApi.getMultiMsg(parentMsgPeer, msg.parentMsgIdList[0], msg.msgId))?.msgList; + //拉取下级消息 + if (!multiMsgs) return null; + //拉取失败则跳过 + + return { + type: OB11MessageDataType.forward, + data: { + id: msg.msgId, + content: (await Promise.all(multiMsgs.map( + async multiMsgItem => { + multiMsgItem.parentMsgPeer = parentMsgPeer; + multiMsgItem.parentMsgIdList = msg.parentMsgIdList; + multiMsgItem.id = MessageUnique.createMsg(parentMsgPeer, multiMsgItem.msgId); //该ID仅用查看 无法调用 + return await this.parseMessage(multiMsgItem); + } + ))).filter(item => item !== undefined), + } + }; + }, + + arkElement: async (element) => { + return { + type: OB11MessageDataType.json, + data: { + data: element.bytesData + } + }; + }, + + markdownElement: async (element) => { + return { + type: OB11MessageDataType.markdown, + data: { + content: element.content + } + }; + } + }; + + ob11ToRawConverters: Ob11ToRawConverters = { + [OB11MessageDataType.text]: async ({ data: { text } }) => ({ + elementType: ElementType.TEXT, + elementId: '', + textElement: { + content: text, + atType: AtType.notAt, + atUid: '', + atTinyId: '', + atNtUid: '', + }, + }), + + [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => { + function at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { + return { + elementType: ElementType.TEXT, + elementId: '', + textElement: { + content: `@${atName}`, + atType, + atUid, + atTinyId: '', + atNtUid, + }, + }; + } + + if (!context.peer || context.peer.chatType == ChatType.KCHATTYPEC2C) return undefined; + if (atQQ === 'all') return at(atQQ, atQQ, AtType.atAll, '全体成员'); + const NTQQGroupApi = this.coreContext.apis.GroupApi; + const NTQQUserApi = this.coreContext.apis.UserApi; + const atMember = await NTQQGroupApi.getGroupMember(context.peer.peerUid, atQQ); + if (atMember) { + return at(atQQ, atMember.uid, AtType.atUser, atMember.nick || atMember.cardName); + } + const uid = await NTQQUserApi.getUidByUinV2(`${atQQ}`); + if (!uid) throw new Error('Get Uid Error'); + const info = await NTQQUserApi.getUserDetailInfo(uid); + return at(atQQ, uid, AtType.atUser, info.nick || ''); + }, + + [OB11MessageDataType.reply]: async ({ data: { id } }) => { + const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id)); + if (!replyMsgM) { + this.coreContext.context.logger.logWarn('回复消息不存在', id); + return undefined; + } + const NTQQMsgApi = this.coreContext.apis.MsgApi; + const replyMsg = (await NTQQMsgApi.getMsgsByMsgId( + replyMsgM.Peer, [replyMsgM.MsgId!])).msgList[0]; + return replyMsg ? + { + elementType: ElementType.REPLY, + elementId: '', + replyElement: { + replayMsgSeq: replyMsg.msgSeq, // raw.msgSeq + replayMsgId: replyMsg.msgId, // raw.msgId + senderUin: replyMsg.senderUin!, + senderUinStr: replyMsg.senderUin!, + }, + } : + undefined; + }, + + [OB11MessageDataType.face]: async ({ data: { id } }) => { + let parsedFaceId = parseInt(id); + // 从face_config.json中获取表情名称 + const sysFaces = faceConfig.sysface; + // const emojiFaces = faceConfig.emoji; + const face: any = sysFaces.find((systemFace) => systemFace.QSid === parsedFaceId.toString()); + parsedFaceId = parseInt(parsedFaceId.toString()); + // let faceType = parseInt(parsedFaceId.toString().substring(0, 1)); + let faceType = 1; + if (parsedFaceId >= 222) { + faceType = 2; + } + if (face.AniStickerType) { + faceType = 3; + } + return { + elementType: ElementType.FACE, + elementId: '', + faceElement: { + faceIndex: parsedFaceId, + faceType, + faceText: face.QDes, + stickerId: face.AniStickerId, + stickerType: face.AniStickerType, + packId: face.AniStickerPackId, + sourceType: 1, + }, + }; + }, + + [OB11MessageDataType.mface]: async ({ + data: { + emoji_package_id, emoji_id, key, summary, + }, + }) => ({ + elementType: ElementType.MFACE, + marketFaceElement: { + emojiPackageId: emoji_package_id, + emojiId: emoji_id, + key, + faceName: summary || '[商城表情]', + }, + }), + + // File service + [OB11MessageDataType.image]: async (sendMsg, context) => { + const sendPicElement = await this.coreContext.apis.FileApi.createValidSendPicElement( + (await this.handleOb11FileLikeMessage(sendMsg, context)).path, + sendMsg.data.summary, + sendMsg.data.sub_type, + ); + context.deleteAfterSentFiles.push(sendPicElement.picElement.sourcePath); + return sendPicElement; + }, + + [OB11MessageDataType.file]: async (sendMsg, context) => { + const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context); + //logDebug('发送文件', path, fileName); + // context.deleteAfterSentFiles.push(fileName || FileEle.fileElement.filePath); + return await this.coreContext.apis.FileApi.createValidSendFileElement(path, fileName); + }, + + [OB11MessageDataType.video]: async (sendMsg, context) => { + const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context); + + let thumb = sendMsg.data.thumb; + if (thumb) { + const uri2LocalRes = await uri2local(this.coreContext.NapCatTempPath, thumb); + if (uri2LocalRes.success) thumb = uri2LocalRes.path; + } + const videoEle = await this.coreContext.apis.FileApi.createValidSendVideoElement(path, fileName, thumb); + + context.deleteAfterSentFiles.push(videoEle.videoElement.filePath); + return videoEle; + }, + + [OB11MessageDataType.voice]: async (sendMsg, context) => + this.coreContext.apis.FileApi.createValidSendPttElement( + (await this.handleOb11FileLikeMessage(sendMsg, context)).path), + + [OB11MessageDataType.json]: async ({ data: { data } }) => ({ + elementType: ElementType.ARK, + elementId: '', + arkElement: { + bytesData: typeof data === 'string' ? data : JSON.stringify(data), + linkInfo: null, + subElementType: null, + }, + }), + + [OB11MessageDataType.dice]: async () => ({ + elementType: ElementType.FACE, + elementId: '', + faceElement: { + faceIndex: FaceIndex.dice, + faceType: FaceType.dice, + 'faceText': '[骰子]', + 'packId': '1', + 'stickerId': '33', + 'sourceType': 1, + 'stickerType': 2, + // resultId: resultId.toString(), + 'surpriseId': '', + // "randomType": 1, + }, + }), + + [OB11MessageDataType.RPS]: async () => ({ + elementType: ElementType.FACE, + elementId: '', + faceElement: { + 'faceIndex': FaceIndex.RPS, + 'faceText': '[包剪锤]', + 'faceType': 3, + 'packId': '1', + 'stickerId': '34', + 'sourceType': 1, + 'stickerType': 2, + // 'resultId': resultId.toString(), + 'surpriseId': '', + // "randomType": 1, + }, + }), + + // Need signing + [OB11MessageDataType.markdown]: async ({ data: { content } }) => ({ + elementType: ElementType.MARKDOWN, + elementId: '', + markdownElement: { content }, + }), + + [OB11MessageDataType.music]: async ({ data }, context) => { + // 保留, 直到...找到更好的解决方案 + if (data.type === 'custom') { + if (!data.url) { + this.coreContext.context.logger.logError('自定义音卡缺少参数url'); + return undefined; + } + if (!data.audio) { + this.coreContext.context.logger.logError('自定义音卡缺少参数audio'); + return undefined; + } + if (!data.title) { + this.coreContext.context.logger.logError('自定义音卡缺少参数title'); + return undefined; + } + } else { + if (!['qq', '163'].includes(data.type)) { + this.coreContext.context.logger.logError('音乐卡片type错误, 只支持qq、163、custom,当前type:', data.type); + return undefined; + } + if (!data.id) { + this.coreContext.context.logger.logError('音乐卡片缺少参数id'); + return undefined; + } + } + + let postData: IdMusicSignPostData | CustomMusicSignPostData; + if (data.type === 'custom' && data.content) { + const { content, ...others } = data; + postData = { singer: content, ...others }; + } else { + postData = data; + } + // Mlikiowa V2.2.7 Refactor Todo + const signUrl = this.obContext.configLoader.configData.musicSignUrl; + if (!signUrl) { + if (data.type === 'qq') { + //const musicJson = (await SignMusicWrapper(data.id.toString())).data.arkResult.slice(0, -1); + //return SendMsgElementConstructor.ark(musicJson); + } + throw Error('音乐消息签名地址未配置'); + } + try { + const musicJson = await RequestUtil.HttpGetJson(signUrl, 'POST', postData); + return this.ob11ToRawConverters.json(musicJson, context); + } catch (e) { + this.coreContext.context.logger.logError('生成音乐消息失败', e); + } + }, + + [OB11MessageDataType.node]: async () => undefined, + + [OB11MessageDataType.forward]: async () => undefined, + + [OB11MessageDataType.xml]: async () => undefined, + + [OB11MessageDataType.poke]: async () => undefined, + + [OB11MessageDataType.Location]: async () => ({ + elementType: ElementType.SHARELOCATION, + elementId: '', + shareLocationElement: { + text: '测试', + ext: '', + }, + }), + + [OB11MessageDataType.miniapp]: async () => undefined, + }; + constructor(obContext: NapCatOneBot11Adapter, coreContext: NapCatCore) { this.obContext = obContext; this.coreContext = coreContext; @@ -17,10 +662,10 @@ export class OneBotMsgApi { async parseMessage( msg: RawMessage, - messagePostFormat: string = this.obContext.configLoader.configData.messagePostFormat + messagePostFormat: string = this.obContext.configLoader.configData.messagePostFormat, ) { - if (msg.senderUin == "0" || msg.senderUin == "") return; - if (msg.peerUin == "0" || msg.peerUin == "") return; + if (msg.senderUin == '0' || msg.senderUin == '') return; + if (msg.peerUin == '0' || msg.peerUin == '') return; //跳过空消息 const NTQQGroupApi = this.coreContext.apis.GroupApi; const NTQQUserApi = this.coreContext.apis.UserApi; @@ -65,404 +710,89 @@ export class OneBotMsgApi { resMsg.sender.nickname = ret.tmpChatInfo!.fromNick; } else { resMsg.group_id = 284840486; //兜底数据 - resMsg.sender.nickname = "临时会话"; + resMsg.sender.nickname = '临时会话'; } } - for (const element of msg.elements) { - let message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - if (element.textElement && element.textElement?.atType !== AtType.notAt) { - const textAtMsgData = await this.obContext.apiContext.MsgApi.parseTextElemntWithAt(msg, element); - if (textAtMsgData) message_data = textAtMsgData; - } else if (element.textElement) { - const textMsgData = await this.obContext.apiContext.MsgApi.parseTextElement(msg, element); - if (textMsgData) message_data = textMsgData; - } else if (element.replyElement) { - const replyMsgData = await this.obContext.apiContext.MsgApi.parseReplyElement(msg, element); - if (replyMsgData) message_data = replyMsgData; - } else if (element.picElement) { - const PicMsgData = await this.obContext.apiContext.MsgApi.parsePicElement(msg, element); - if (PicMsgData) message_data = PicMsgData; - } else if (element.fileElement) { - const FileMsgData = await this.obContext.apiContext.MsgApi.parseFileElement(msg, element); - if (FileMsgData) message_data = FileMsgData; - } else if (element.videoElement) { - const videoMsgData = await this.obContext.apiContext.MsgApi.parseVideoElement(msg, element); - if (videoMsgData) message_data = videoMsgData; - } else if (element.pttElement) { - const pttMsgData = await this.obContext.apiContext.MsgApi.parsePTTElement(msg, element); - if (pttMsgData) message_data = pttMsgData; - } else if (element.arkElement) { - const arkMsgData = await this.obContext.apiContext.MsgApi.parseArkElement(msg, element); - if (arkMsgData) message_data = arkMsgData; - } else if (element.faceElement) { - const faceMsgData = await this.obContext.apiContext.MsgApi.parseFaceElement(msg, element); - if (faceMsgData) message_data = faceMsgData; - } else if (element.marketFaceElement) { - const marketFaceMsgData = await this.obContext.apiContext.MsgApi.parseMarketFaceElement(msg, element); - if (marketFaceMsgData) message_data = marketFaceMsgData; - } else if (element.markdownElement) { - message_data['type'] = OB11MessageDataType.markdown; - message_data['data']['data'] = element.markdownElement.content; - } else if (element.multiForwardMsgElement) { - const multiForwardMsgData = await this.obContext.apiContext.MsgApi.parseMultForwardElement(msg, element, messagePostFormat); - if (multiForwardMsgData) message_data = multiForwardMsgData; - } - if ((message_data.type as string) !== 'unknown' && message_data.data) { - const cqCode = encodeCQCode(message_data); - if (messagePostFormat === 'string') { - (resMsg.message as string) += cqCode; - } else (resMsg.message as OB11MessageData[]).push(message_data); - resMsg.raw_message += cqCode; + const msgSegments = (await Promise.all(msg.elements.map( + async (element) => { + for (const key in element) { + if (keyCanBeParsed(key, this.rawToOb11Converters) && this.rawToOb11Converters[key]) { + return await this.rawToOb11Converters[key]?.( + // eslint-disable-next-line + // @ts-ignore + element[key], + msg, + element + ); + } + } } + ))).filter(entry => !!entry); + const msgAsCQCode = msgSegments.map(msg => encodeCQCode(msg)).join('').trim(); + + if (messagePostFormat === 'string') { + resMsg.message = msgAsCQCode; + } else { + resMsg.message = msgSegments; + resMsg.raw_message = msgAsCQCode; } - resMsg.raw_message = resMsg.raw_message.trim(); return resMsg; } - async parseFileElement(msg: RawMessage, element: MessageElement) { - const fileElement = element.fileElement; - if (!fileElement) return undefined; - const NTQQFileApi = this.coreContext.apis.FileApi; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - message_data['type'] = OB11MessageDataType.file; - message_data['data']['file'] = fileElement.fileName; - message_data['data']['path'] = fileElement.filePath; - message_data['data']['url'] = fileElement.filePath; - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId); - message_data['data']['file_size'] = fileElement.fileSize; - await NTQQFileApi.addFileCache( - { - peerUid: msg.peerUid, - chatType: msg.chatType, - guildId: '', - }, - msg.msgId, - msg.msgSeq, - msg.senderUid, - element.elementId, - element.elementType.toString(), - fileElement.fileSize, - fileElement.fileName - ); - return message_data; - } - async parseTextElemntWithAt(msg: RawMessage, element: MessageElement) { - const textElement = element.textElement; - if (!textElement) return undefined; - const NTQQUserApi = this.coreContext.apis.UserApi; - let qq: `${number}` | 'all'; - // let name: string | undefined; - if (textElement.atType == AtType.atAll) { - qq = 'all'; - } else { - const { atNtUid, content } = textElement; - let atQQ = textElement.atUid; - if (!atQQ || atQQ === '0') { - atQQ = await NTQQUserApi.getUinByUidV2(atNtUid); - } - if (atQQ) { - qq = atQQ as `${number}`; - // name = content.replace('@', ''); + async createSendElements( + messageData: OB11MessageData[], + peer: Peer, + ignoreTypes: OB11MessageDataType[] = [], + ) { + const deleteAfterSentFiles: string[] = []; + const callResultList: Array> = []; + for (const sendMsg of messageData) { + if (ignoreTypes.includes(sendMsg.type)) { + continue; } + const callResult = this.ob11ToRawConverters[sendMsg.type]( + // eslint-disable-next-line + // @ts-ignore + sendMsg, + { peer, deleteAfterSentFiles }, + )?.catch(undefined); + callResultList.push(callResult); } - - return { - type: OB11MessageDataType.at, - data: { - qq: qq!, - // name, - }, - }; + const ret = await Promise.all(callResultList); + const sendElements: SendMessageElement[] = ret.filter(ele => !!ele); + return { sendElements, deleteAfterSentFiles }; } - async parseTextElement(msg: RawMessage, element: MessageElement) { - const textElement = element.textElement; - if (!textElement) return undefined; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - message_data['type'] = OB11MessageDataType.text; - let text = textElement.content; - if (!text.trim()) { - return false; + private async handleOb11FileLikeMessage( + { data: inputdata }: OB11MessageFileBase, + { deleteAfterSentFiles }: MessageContext, + ) { + const isBlankUrl = !inputdata.url || inputdata.url === ''; + const isBlankFile = !inputdata.file || inputdata.file === ''; + if (isBlankUrl && isBlankFile) { + this.coreContext.context.logger.logError('文件消息缺少参数', inputdata); + throw Error('文件消息缺少参数'); } - // 兼容 9.7.x 换行符 - if (text.indexOf('\n') === -1 && text.indexOf('\r\n') === -1) { - text = text.replace(/\r/g, '\n'); - } - message_data['data']['text'] = text; - return message_data; - } - async parsePicElement(msg: RawMessage, element: MessageElement) { - const picElement = element.picElement; - if (!picElement) return undefined; - const NTQQFileApi = this.coreContext.apis.FileApi; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - message_data['type'] = OB11MessageDataType.image; - // message_data["data"]["file"] = element.picElement.sourcePath - message_data['data']['file'] = picElement.fileName; - message_data['data']['subType'] = picElement.picSubType; - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId); - // message_data["data"]["path"] = element.picElement.sourcePath - try { - message_data['data']['url'] = await NTQQFileApi.getImageUrl(picElement); - } catch (e: any) { - this.coreContext.context.logger.logError('获取图片url失败', e.stack); - } - //console.log(message_data['data']['url']) - // message_data["data"]["file_id"] = element.picElement.fileUuid - message_data['data']['file_size'] = picElement.fileSize; - return message_data; - } - async parseMarketFaceElement(msg: RawMessage, element: MessageElement) { - const NTQQFileApi = this.coreContext.apis.FileApi; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - message_data['type'] = OB11MessageDataType.image; - message_data['data']['file'] = 'marketface'; - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId); - message_data['data']['path'] = element.elementId; - message_data['data']['url'] = element.elementId; - await NTQQFileApi.addFileCache( - { - peerUid: msg.peerUid, - chatType: msg.chatType, - guildId: '', - }, - msg.msgId, - msg.msgSeq, - msg.senderUid, - element.elementId, - element.elementType.toString(), - '0', - 'marketface' - ); - return message_data; - } - async parseReplyElement(msg: RawMessage, element: MessageElement) { - const replyElement = element.replyElement; - if (!replyElement) return undefined; - const NTQQMsgApi = this.coreContext.apis.MsgApi; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - message_data['type'] = OB11MessageDataType.reply; - //log("收到回复消息", element.replyElement); - try { - const records = msg.records.find(msgRecord => msgRecord.msgId === replyElement?.sourceMsgIdInRecords); - const peer = { - chatType: msg.chatType, - peerUid: msg.peerUid, - guildId: '', - }; - let replyMsg: RawMessage | undefined; - if (!records) throw new Error('找不到回复消息'); - replyMsg = (await NTQQMsgApi.getMsgsBySeqAndCount({ - peerUid: msg.peerUid, - guildId: '', - chatType: msg.chatType, - }, replyElement.replayMsgSeq, 1, true, true)).msgList.find(msg => msg.msgRandom === records.msgRandom); - if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { - replyMsg = (await NTQQMsgApi.getSingleMsg(peer, replyElement.replayMsgSeq)).msgList[0]; - } - if (msg.peerUin == '284840486') { - //合并消息内侧 消息具体定位不到 - } - if ((!replyMsg || records.msgRandom !== replyMsg.msgRandom) && msg.peerUin !== '284840486') { - const replyMsgList = (await NTQQMsgApi.getMsgExBySeq(peer, records.msgSeq)).msgList; - if (replyMsgList.length < 1) { - throw new Error('回复消息消息验证失败'); - } - replyMsg = replyMsgList.filter(e => e.msgSeq == records.msgSeq).sort((a, b) => parseInt(a.msgTime) - parseInt(b.msgTime))[0]; - } - message_data['data']['id'] = MessageUnique.createMsg({ - peerUid: msg.peerUid, - guildId: '', - chatType: msg.chatType, - }, replyMsg.msgId)?.toString(); - //log("找到回复消息", message_data['data']['id'], replyMsg.msgList[0].msgId) - } catch (e: any) { - message_data['type'] = 'unknown' as any; - message_data['data'] = undefined; - this.coreContext.context.logger.logError('获取不到引用的消息', e.stack, replyElement.replayMsgSeq); - return undefined; - } - return message_data; - } - async parseVideoElement(msg: RawMessage, element: MessageElement) { - const videoElement = element.videoElement; - if (!videoElement) return undefined; - const NTQQFileApi = this.coreContext.apis.FileApi; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - //读取视频链接并兜底 - let videoUrl; //Array - if (msg.peerUin === '284840486') { - //合并消息内部 应该进行特殊处理 可能需要重写peer 待测试与研究 Mlikiowa Taged TODO - } - try { + const fileOrUrl = (isBlankUrl ? inputdata.file : inputdata.url) || ""; + const { + path, + isLocal, + fileName, + errMsg, + success, + } = (await uri2local(this.coreContext.NapCatTempPath, fileOrUrl)); - videoUrl = await NTQQFileApi.getVideoUrl({ - chatType: msg.chatType, - peerUid: msg.peerUid, - guildId: '0', - }, msg.msgId, element.elementId); - } catch (error) { - videoUrl = undefined; + if (!success) { + this.coreContext.context.logger.logError('文件下载失败', errMsg); + throw Error('文件下载失败' + errMsg); } - //读取在线URL - let videoDownUrl = undefined; - if (videoUrl) { - const videoDownUrlTemp = videoUrl.find((url) => { - return !!url.url; - }); - if (videoDownUrlTemp) { - videoDownUrl = videoDownUrlTemp.url; - } + if (!isLocal) { // 只删除http和base64转过来的文件 + deleteAfterSentFiles.push(path); } - //开始兜底 - if (!videoDownUrl) { - videoDownUrl = videoElement.filePath; - } - message_data['type'] = OB11MessageDataType.video; - message_data['data']['file'] = videoElement.fileName; - message_data['data']['path'] = videoDownUrl; - message_data['data']['url'] = videoDownUrl; - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId); - message_data['data']['file_size'] = videoElement.fileSize; - await NTQQFileApi.addFileCache( - { - peerUid: msg.peerUid, - chatType: msg.chatType, - guildId: '', - }, - msg.msgId, - msg.msgSeq, - msg.senderUid, - element.elementId, - element.elementType.toString(), - videoElement.fileSize || '0', - videoElement.fileName - ); - return message_data; - } - async parsePTTElement(msg: RawMessage, element: MessageElement) { - const pttElement = element.pttElement; - if (!pttElement) return undefined; - const NTQQFileApi = this.coreContext.apis.FileApi; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - - message_data['type'] = OB11MessageDataType.voice; - message_data['data']['file'] = pttElement.fileName; - message_data['data']['path'] = pttElement.filePath; - //message_data['data']['file_id'] = element.pttElement.fileUuid; - message_data['data']['file_id'] = UUIDConverter.encode(msg.peerUin, msg.msgId); - message_data['data']['file_size'] = pttElement.fileSize; - await NTQQFileApi.addFileCache({ - peerUid: msg.peerUid, - chatType: msg.chatType, - guildId: '', - }, - msg.msgId, - msg.msgSeq, - msg.senderUid, - element.elementId, - element.elementType.toString(), - pttElement.fileSize || '0', - pttElement.fileUuid || '' - ); - //以uuid作为文件名 - return message_data; - } - async parseFaceElement(msg: RawMessage, element: MessageElement) { - const faceElement = element.faceElement; - if (!faceElement) return undefined; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - const faceId = faceElement.faceIndex; - if (faceId === FaceIndex.dice) { - message_data['type'] = OB11MessageDataType.dice; - message_data['data']['result'] = faceElement.resultId; - } else if (faceId === FaceIndex.RPS) { - message_data['type'] = OB11MessageDataType.RPS; - message_data['data']['result'] = faceElement.resultId; - } else { - message_data['type'] = OB11MessageDataType.face; - message_data['data']['id'] = faceElement.faceIndex.toString(); - } - return message_data; - } - async parseMultForwardElement(msg: RawMessage, element: MessageElement, messagePostFormat: any) { - const NTQQMsgApi = this.coreContext.apis.MsgApi; - const faceElement = element.multiForwardMsgElement; - if (!faceElement) return undefined; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - message_data['type'] = OB11MessageDataType.forward; - message_data['data']['id'] = msg.msgId; - const ParentMsgPeer = msg.parentMsgPeer ?? { - chatType: msg.chatType, - guildId: '', - peerUid: msg.peerUid, - }; - //判断是否在合并消息内 - msg.parentMsgIdList = msg.parentMsgIdList ?? []; - //首次列表不存在则开始创建 - msg.parentMsgIdList.push(msg.msgId); - //let parentMsgId = msg.parentMsgIdList[msg.parentMsgIdList.length - 2 < 0 ? 0 : msg.parentMsgIdList.length - 2]; - //加入自身MsgId - const MultiMsgs = (await NTQQMsgApi.getMultiMsg(ParentMsgPeer, msg.parentMsgIdList[0], msg.msgId))?.msgList; - //拉取下级消息 - if (!MultiMsgs) return undefined; - //拉取失败则跳过 - message_data['data']['content'] = []; - for (const MultiMsg of MultiMsgs) { - //对每条拉取的消息传递ParentMsgPeer修正Peer - MultiMsg.parentMsgPeer = ParentMsgPeer; - MultiMsg.parentMsgIdList = msg.parentMsgIdList; - MultiMsg.id = MessageUnique.createMsg(ParentMsgPeer, MultiMsg.msgId); //该ID仅用查看 无法调用 - const msgList = await this.parseMessage(MultiMsg, messagePostFormat); - if (!msgList) continue; - message_data['data']['content'].push(msgList); - //console.log("合并消息", msgList); - } - return message_data; - } - async parseArkElement(msg: RawMessage, element: MessageElement) { - const arkElement = element.arkElement; - if (!arkElement) return undefined; - const message_data: OB11MessageData = { - data: {} as any, - type: 'unknown' as any, - }; - message_data['type'] = OB11MessageDataType.json; - message_data['data']['data'] = arkElement.bytesData; - return message_data; + return { path, fileName: inputdata.name || fileName }; } } diff --git a/src/onebot/helper/genMessage.ts b/src/onebot/helper/genMessage.ts deleted file mode 100644 index 711b5112..00000000 --- a/src/onebot/helper/genMessage.ts +++ /dev/null @@ -1,422 +0,0 @@ -import { - AtType, - ElementType, - FaceIndex, - FaceType, - NapCatCore, - PicType, - SendArkElement, - SendFaceElement, - SendFileElement, - SendMarkdownElement, - SendMarketFaceElement, - SendPicElement, - SendPttElement, - SendReplyElement, - sendShareLocationElement, - SendTextElement, - SendVideoElement, - viedo_type, -} from '@/core'; -import * as fsnormal from 'node:fs'; -import { promises as fs } from 'node:fs'; -import ffmpeg from 'fluent-ffmpeg'; -import { calculateFileMD5, isGIF } from '@/common/utils/file'; -import { defaultVideoThumbB64, getVideoInfo } from '@/common/utils/video'; -import { encodeSilk } from '@/common/utils/audio'; -import faceConfig from '@/core/external/face_config.json'; -import * as pathLib from 'node:path'; - -export class SendMsgElementConstructor { - static location(CoreContext: NapCatCore): sendShareLocationElement { - return { - elementType: ElementType.SHARELOCATION, - elementId: '', - shareLocationElement: { - text: '测试', - ext: '', - }, - }; - } - - static text(CoreContext: NapCatCore, content: string): SendTextElement { - return { - elementType: ElementType.TEXT, - elementId: '', - textElement: { - content, - atType: AtType.notAt, - atUid: '', - atTinyId: '', - atNtUid: '', - }, - }; - } - - static at(CoreContext: NapCatCore, atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { - return { - elementType: ElementType.TEXT, - elementId: '', - textElement: { - content: `@${atName}`, - atType, - atUid, - atTinyId: '', - atNtUid, - }, - }; - } - - static reply(CoreContext: NapCatCore, msgSeq: string, msgId: string, senderUin: string, senderUinStr: string): SendReplyElement { - return { - elementType: ElementType.REPLY, - elementId: '', - replyElement: { - replayMsgSeq: msgSeq, // raw.msgSeq - replayMsgId: msgId, // raw.msgId - senderUin: senderUin, - senderUinStr: senderUinStr, - }, - }; - } - - static async pic(coreContext: NapCatCore, picPath: string, summary: string = '', subType: 0 | 1 = 0): Promise { - const NTQQGroupApi = coreContext.apis.GroupApi; - const NTQQUserApi = coreContext.apis.UserApi; - const NTQQFileApi = coreContext.apis.FileApi; - const NTQQMsgApi = coreContext.apis.MsgApi; - const NTQQFriendApi = coreContext.apis.FriendApi; - const logger = coreContext.context.logger; - const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(picPath, ElementType.PIC, subType); - if (fileSize === 0) { - throw '文件异常,大小为0'; - } - const imageSize = await NTQQFileApi.getImageSize(picPath); - const picElement: any = { - md5HexStr: md5, - fileSize: fileSize.toString(), - picWidth: imageSize?.width, - picHeight: imageSize?.height, - fileName: fileName, - sourcePath: path, - original: true, - picType: isGIF(picPath) ? PicType.gif : PicType.jpg, - picSubType: subType, - fileUuid: '', - fileSubId: '', - thumbFileSize: 0, - summary, - }; - //logDebug('图片信息', picElement); - return { - elementType: ElementType.PIC, - elementId: '', - picElement, - }; - } - - static async file(coreContext: NapCatCore, filePath: string, fileName: string = '', folderId: string = ''): Promise { - const NTQQGroupApi = coreContext.apis.GroupApi; - const NTQQUserApi = coreContext.apis.UserApi; - const NTQQFileApi = coreContext.apis.FileApi; - const NTQQMsgApi = coreContext.apis.MsgApi; - const NTQQFriendApi = coreContext.apis.FriendApi; - const logger = coreContext.context.logger; - const { md5, fileName: _fileName, path, fileSize } = await NTQQFileApi.uploadFile(filePath, ElementType.FILE); - if (fileSize === 0) { - throw '文件异常,大小为0'; - } - const element: SendFileElement = { - elementType: ElementType.FILE, - elementId: '', - fileElement: { - fileName: fileName || _fileName, - folderId: folderId, - 'filePath': path!, - 'fileSize': (fileSize).toString(), - }, - }; - - return element; - } - - static async video(coreContext: NapCatCore, filePath: string, fileName: string = '', diyThumbPath: string = '', videotype: viedo_type = viedo_type.VIDEO_FORMAT_MP4): Promise { - const NTQQFileApi = coreContext.apis.FileApi; - const logger = coreContext.context.logger; - const { fileName: _fileName, path, fileSize, md5 } = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); - if (fileSize === 0) { - throw '文件异常,大小为0'; - } - let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`); - thumb = pathLib.dirname(thumb); - // log("thumb 目录", thumb) - let videoInfo = { - width: 1920, height: 1080, - time: 15, - format: 'mp4', - size: fileSize, - filePath, - }; - try { - videoInfo = await getVideoInfo(path, logger); - //logDebug('视频信息', videoInfo); - } catch (e) { - logger.logError('获取视频信息失败', e); - } - const createThumb = new Promise((resolve, reject) => { - const thumbFileName = `${md5}_0.png`; - const thumbPath = pathLib.join(thumb, thumbFileName); - ffmpeg(filePath) - .on('end', () => { - }) - .on('error', (err) => { - logger.logDebug('获取视频封面失败,使用默认封面', err); - if (diyThumbPath) { - fs.copyFile(diyThumbPath, thumbPath).then(() => { - resolve(thumbPath); - }).catch(reject); - } else { - fsnormal.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64')); - resolve(thumbPath); - } - }) - .screenshots({ - timestamps: [0], - filename: thumbFileName, - folder: thumb, - size: videoInfo.width + 'x' + videoInfo.height, - }).on('end', () => { - resolve(thumbPath); - }); - }); - const thumbPath = new Map(); - const _thumbPath = await createThumb; - const thumbSize = _thumbPath ? (await fs.stat(_thumbPath)).size : 0; - // log("生成缩略图", _thumbPath) - thumbPath.set(0, _thumbPath); - const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : ""; - const element: SendVideoElement = { - elementType: ElementType.VIDEO, - elementId: '', - videoElement: { - fileName: fileName || _fileName, - filePath: path, - videoMd5: md5, - thumbMd5, - fileTime: videoInfo.time, - thumbPath: thumbPath, - thumbSize, - thumbWidth: videoInfo.width, - thumbHeight: videoInfo.height, - fileSize: '' + fileSize, - //fileFormat: videotype - // fileUuid: "", - // transferStatus: 0, - // progress: 0, - // invalidState: 0, - // fileSubId: "", - // fileBizId: null, - // originVideoMd5: "", - // fileFormat: 2, - // import_rich_media_context: null, - // sourceVideoCodecFormat: 2 - }, - }; - // "fileElement": { - // "fileMd5": "", - // "fileName": "1.mp4", - // "filePath": "C:\\Users\\nanae\\OneDrive\\Desktop\\1.mp4", - // "fileSize": "1847007", - // "picHeight": 1280, - // "picWidth": 720, - // "picThumbPath": {}, - // "file10MMd5": "", - // "fileSha": "", - // "fileSha3": "", - // "fileUuid": "", - // "fileSubId": "", - // "thumbFileSize": 750 - // } - return element; - } - - static async ptt(coreContext: NapCatCore, pttPath: string): Promise { - const NTQQGroupApi = coreContext.apis.GroupApi; - const NTQQUserApi = coreContext.apis.UserApi; - const NTQQFileApi = coreContext.apis.FileApi; - const NTQQMsgApi = coreContext.apis.MsgApi; - const NTQQFriendApi = coreContext.apis.FriendApi; - const logger = coreContext.context.logger; - const { - converted, - path: silkPath, - duration, - } = await encodeSilk(pttPath, coreContext.NapCatTempPath, coreContext.context.logger); - // log("生成语音", silkPath, duration); - if (!silkPath) { - throw '语音转换失败, 请检查语音文件是否正常'; - } - const { md5, fileName, path, fileSize } = await NTQQFileApi.uploadFile(silkPath!, ElementType.PTT); - if (fileSize === 0) { - throw '文件异常,大小为0'; - } - if (converted) { - fs.unlink(silkPath).then(); - } - return { - elementType: ElementType.PTT, - elementId: '', - pttElement: { - fileName: fileName, - filePath: path, - md5HexStr: md5, - fileSize: fileSize, - // duration: Math.max(1, Math.round(fileSize / 1024 / 3)), // 一秒钟大概是3kb大小, 小于1秒的按1秒算 - duration: duration || 1, - formatType: 1, - voiceType: 1, - voiceChangeType: 0, - canConvert2Text: true, - waveAmplitudes: [ - 0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17, - ], - fileSubId: '', - playState: 1, - autoConvertText: 0, - }, - }; - } - - // NodeIQQNTWrapperSession sendMsg [ - // "0", - // { - // "peerUid": "u_e_RIxgTs2NaJ68h0PwOPSg", - // "chatType": 1, - // "guildId": "" - // }, - // [ - // { - // "elementId": "0", - // "elementType": 6, - // "faceElement": { - // "faceIndex": 0, - // "faceType": 5, - // "msgType": 0, - // "pokeType": 1, - // "pokeStrength": 0 - // } - // } - // ], - // {} - // ] - static face(CoreContext: NapCatCore, faceId: number): SendFaceElement { - // 从face_config.json中获取表情名称 - const sysFaces = faceConfig.sysface; - const emojiFaces = faceConfig.emoji; - const face: any = sysFaces.find((face) => face.QSid === faceId.toString()); - faceId = parseInt(faceId.toString()); - // let faceType = parseInt(faceId.toString().substring(0, 1)); - let faceType = 1; - if (faceId >= 222) { - faceType = 2; - } - if (face.AniStickerType) { - faceType = 3; - } - return { - elementType: ElementType.FACE, - elementId: '', - faceElement: { - faceIndex: faceId, - faceType, - faceText: face.QDes, - stickerId: face.AniStickerId, - stickerType: face.AniStickerType, - packId: face.AniStickerPackId, - sourceType: 1, - }, - }; - } - - static mface(CoreContext: NapCatCore, emojiPackageId: number, emojiId: string, key: string, faceName: string): SendMarketFaceElement { - return { - elementType: ElementType.MFACE, - marketFaceElement: { - emojiPackageId, - emojiId, - key, - faceName: faceName || '[商城表情]', - }, - }; - } - - static dice(CoreContext: NapCatCore, resultId: number | null): SendFaceElement { - // 实际测试并不能控制结果 - - // 随机1到6 - // if (isNull(resultId)) resultId = Math.floor(Math.random() * 6) + 1; - return { - elementType: ElementType.FACE, - elementId: '', - faceElement: { - faceIndex: FaceIndex.dice, - faceType: FaceType.dice, - 'faceText': '[骰子]', - 'packId': '1', - 'stickerId': '33', - 'sourceType': 1, - 'stickerType': 2, - // resultId: resultId.toString(), - 'surpriseId': '', - // "randomType": 1, - }, - }; - } - - // 猜拳(石头剪刀布)表情 - static rps(CoreContext: NapCatCore, resultId: number | null): SendFaceElement { - // 实际测试并不能控制结果 - // if (isNull(resultId)) resultId = Math.floor(Math.random() * 3) + 1; - return { - elementType: ElementType.FACE, - elementId: '', - faceElement: { - 'faceIndex': FaceIndex.RPS, - 'faceText': '[包剪锤]', - 'faceType': 3, - 'packId': '1', - 'stickerId': '34', - 'sourceType': 1, - 'stickerType': 2, - // 'resultId': resultId.toString(), - 'surpriseId': '', - // "randomType": 1, - }, - }; - } - - static ark(CoreContext: NapCatCore, data: any): SendArkElement { - if (typeof data !== 'string') { - data = JSON.stringify(data); - } - return { - elementType: ElementType.ARK, - elementId: '', - arkElement: { - bytesData: data, - linkInfo: null, - subElementType: null, - }, - }; - } - - static markdown(CoreContext: NapCatCore, content: string): SendMarkdownElement { - return { - elementType: ElementType.MARKDOWN, - elementId: '', - markdownElement: { - content, - }, - }; - } -} diff --git a/src/onebot/helper/index.ts b/src/onebot/helper/index.ts index 3cfdbea9..3a8f5bb9 100644 --- a/src/onebot/helper/index.ts +++ b/src/onebot/helper/index.ts @@ -1,5 +1,4 @@ export * from './config'; export * from './converter'; export * from './quick'; -export * from './genMessage'; export * from './event'; diff --git a/src/onebot/helper/quick.ts b/src/onebot/helper/quick.ts index d228290e..650b3c98 100644 --- a/src/onebot/helper/quick.ts +++ b/src/onebot/helper/quick.ts @@ -13,7 +13,7 @@ import { QuickActionGroupRequest, } from '../types'; import { isNull } from '@/common/utils/helper'; -import { createSendElements, normalize, sendMsg } from '../action/msg/SendMsg'; +import { normalize, sendMsg } from '../action/msg/SendMsg'; import { NapCatOneBot11Adapter } from '..'; async function handleMsg(coreContext: NapCatCore, obContext: NapCatOneBot11Adapter, msg: OB11Message, quickAction: QuickAction) { @@ -53,7 +53,7 @@ async function handleMsg(coreContext: NapCatCore, obContext: NapCatOneBot11Adapt } } replyMessage = replyMessage.concat(normalize(reply, quickAction.auto_escape)); - const { sendElements, deleteAfterSentFiles } = await createSendElements(coreContext, obContext, replyMessage, peer); + const { sendElements, deleteAfterSentFiles } = await obContext.apiContext.MsgApi.createSendElements(replyMessage, peer); sendMsg(coreContext, peer, sendElements, deleteAfterSentFiles, false).then().catch(coreContext.context.logger.logError); } } diff --git a/src/onebot/types/message.ts b/src/onebot/types/message.ts index 25465dea..17b8a383 100644 --- a/src/onebot/types/message.ts +++ b/src/onebot/types/message.ts @@ -94,7 +94,7 @@ export interface OB11MessageImage extends OB11MessageFileBase { type: OB11MessageDataType.image data: OB11MessageFileBase['data'] & { summary?: string; // 图片摘要 - subType?: PicSubType + sub_type?: PicSubType }, } @@ -113,7 +113,7 @@ export interface OB11MessageVideo extends OB11MessageFileBase { export interface OB11MessageAt { type: OB11MessageDataType.at; data: { - qq: `${number}` | 'all' + qq: string, // `${number}` | 'all' name?: string }; } @@ -156,20 +156,20 @@ export interface OB11MessageCustomMusic { export interface OB11MessageJson { type: OB11MessageDataType.json; - data: { config: { token: string }, data: string }; + data: { config?: { token: string }, data: string }; } export interface OB11MessageDice { type: OB11MessageDataType.dice, data: { - result: number + result: number /* intended */ | string /* in fact */ } } export interface OB11MessageRPS { type: OB11MessageDataType.RPS, data: { - result: number + result: number | string } }