diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 209d6844..026fe88d 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -63,6 +63,74 @@ export class NTQQFileApi { } } + async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined) { + if (this.core.apis.PacketApi.available) { + try { + if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) { + return this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+peer, fileUUID); + } else if (file10MMd5 && fileUUID) { + return this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(peer, fileUUID, file10MMd5); + } + } catch (error) { + this.context.logger.logError('获取文件URL失败', (error as Error).message); + } + } + throw new Error('fileUUID or file10MMd5 is undefined'); + } + + async getPttUrl(chatType: ChatType, peer: string, fileUUID?: string) { + if (this.core.apis.PacketApi.available) { + try { + if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) { + return this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+peer, { + fileUuid: fileUUID, + storeId: 1, + uploadTime: 0, + ttl: 0, + subType: 0, + }); + } else if (fileUUID) { + return this.core.apis.PacketApi.pkt.operation.GetPttUrl(peer, { + fileUuid: fileUUID, + storeId: 1, + uploadTime: 0, + ttl: 0, + subType: 0, + }); + } + } catch (error) { + this.context.logger.logError('获取文件URL失败', (error as Error).message); + } + } + throw new Error('packet cant get ptt url'); + } + + async getVideoUrlPacket(chatType: ChatType, peer: string, fileUUID?: string) { + if (this.core.apis.PacketApi.available) { + try { + if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) { + return this.core.apis.PacketApi.pkt.operation.GetGroupVideoUrl(+peer, { + fileUuid: fileUUID, + storeId: 1, + uploadTime: 0, + ttl: 0, + subType: 0, + }); + } else if (fileUUID) { + return this.core.apis.PacketApi.pkt.operation.GetVideoUrl(peer, { + fileUuid: fileUUID, + storeId: 1, + uploadTime: 0, + ttl: 0, + subType: 0, + }); + } + } catch (error) { + this.context.logger.logError('获取文件URL失败', (error as Error).message); + } + } + throw new Error('packet cant get video url'); + } async copyFile(filePath: string, destPath: string) { await this.core.util.copyFile(filePath, destPath); diff --git a/src/core/apis/msg.ts b/src/core/apis/msg.ts index 5dd4d969..7c2d3614 100644 --- a/src/core/apis/msg.ts +++ b/src/core/apis/msg.ts @@ -71,6 +71,7 @@ export class NTQQMsgApi { async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) { return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, { chatInfo: peer, + //searchFields: 3, filterMsgType: [], filterSendersUid: [], filterMsgToTime: '0', @@ -84,6 +85,7 @@ export class NTQQMsgApi { return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, { chatInfo: peer, filterMsgType: [], + //searchFields: 3, filterSendersUid: SendersUid, filterMsgToTime: MsgTime, filterMsgFromTime: MsgTime, @@ -100,6 +102,7 @@ export class NTQQMsgApi { filterMsgToTime: '0', filterMsgFromTime: '0', isReverseOrder: false, + //searchFields: 3, isIncludeCurrent: true, pageLimit: 1, }); @@ -110,6 +113,7 @@ export class NTQQMsgApi { filterMsgType: [], filterSendersUid: [], filterMsgToTime: '0', + //searchFields: 3, filterMsgFromTime: '0', isReverseOrder: true, isIncludeCurrent: true, @@ -128,6 +132,7 @@ export class NTQQMsgApi { chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa filterMsgType: [], filterSendersUid: [], + //searchFields: 3, filterMsgToTime: filterMsgToTime, filterMsgFromTime: filterMsgFromTime, isReverseOrder: false, @@ -142,6 +147,7 @@ export class NTQQMsgApi { chatInfo: peer, filterMsgType: [], filterSendersUid: SendersUid, + //searchFields: 3, filterMsgToTime: '0', filterMsgFromTime: '0', isReverseOrder: true, diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts index d89e8899..76866709 100644 --- a/src/core/packet/context/operationContext.ts +++ b/src/core/packet/context/operationContext.ts @@ -124,6 +124,20 @@ export class PacketOperationContext { return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } + async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType) { + const req = trans.DownloadPtt.build(selfUid, node); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.DownloadPtt.parse(resp); + return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; + } + + async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType) { + const req = trans.DownloadVideo.build(selfUid, node); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.DownloadVideo.parse(resp); + return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; + } + async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType) { const req = trans.DownloadGroupImage.build(groupUin, node); const resp = await this.context.client.sendOidbPacket(req, true); @@ -131,6 +145,21 @@ export class PacketOperationContext { return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; } + async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType) { + const req = trans.DownloadGroupPtt.build(groupUin, node); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.DownloadImage.parse(resp); + return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; + } + + async GetGroupVideoUrl(groupUin: number, node: NapProtoEncodeStructType) { + const req = trans.DownloadGroupVideo.build(groupUin, node); + const resp = await this.context.client.sendOidbPacket(req, true); + const res = trans.DownloadImage.parse(resp); + return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; + } + + async ImageOCR(imgUrl: string) { const req = trans.ImageOCR.build(imgUrl); const resp = await this.context.client.sendOidbPacket(req, true); @@ -154,7 +183,7 @@ export class PacketOperationContext { private async SendPreprocess(msg: PacketMsg[], groupUin: number = 0) { const ps = msg.map((m) => { - return m.msg.map(async(e) => { + return m.msg.map(async (e) => { if (e instanceof PacketMsgReplyElement && !e.targetElems) { this.context.logger.debug(`Cannot find reply element's targetElems, prepare to fetch it...`); if (!e.targetPeer?.peerUid) { @@ -222,6 +251,7 @@ export class PacketOperationContext { const res = trans.DownloadGroupFile.parse(resp); return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`; } + async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) { const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5); const resp = await this.context.client.sendOidbPacket(req, true); @@ -229,13 +259,6 @@ export class PacketOperationContext { return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`; } - async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType) { - const req = trans.DownloadGroupPtt.build(groupUin, node); - const resp = await this.context.client.sendOidbPacket(req, true); - const res = trans.DownloadGroupPtt.parse(resp); - return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`; - } - async GetMiniAppAdaptShareInfo(param: MiniAppReqParams) { const req = trans.GetMiniAppAdaptShareInfo.build(param); const resp = await this.context.client.sendOidbPacket(req, true); diff --git a/src/core/packet/transformer/highway/DownloadGroupVideo.ts b/src/core/packet/transformer/highway/DownloadGroupVideo.ts new file mode 100644 index 00000000..22fe2e8e --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadGroupVideo.ts @@ -0,0 +1,50 @@ +import * as proto from '@/core/packet/transformer/proto'; +import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core'; +import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base'; +import OidbBase from '@/core/packet/transformer/oidb/oidbBase'; +import { IndexNode } from '@/core/packet/transformer/proto'; + +class DownloadGroupVideo extends PacketTransformer { + constructor() { + super(); + } + + build(groupUin: number, node: NapProtoEncodeStructType): OidbPacket { + const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 200 + }, + scene: { + requestType: 2, + businessType: 2, + sceneType: 2, + group: { + groupUin: groupUin + } + }, + client: { + agentType: 2, + } + }, + download: { + node: node, + download: { + video: { + busiType: 0, + sceneType: 0 + } + } + } + }); + return OidbBase.build(0x11EA, 200, body, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new DownloadGroupVideo(); diff --git a/src/core/packet/transformer/highway/DownloadPtt.ts b/src/core/packet/transformer/highway/DownloadPtt.ts new file mode 100644 index 00000000..41ab6c7e --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadPtt.ts @@ -0,0 +1,51 @@ +import * as proto from '@/core/packet/transformer/proto'; +import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core'; +import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base'; +import OidbBase from '@/core/packet/transformer/oidb/oidbBase'; +import { IndexNode } from '@/core/packet/transformer/proto'; + +class DownloadPtt extends PacketTransformer { + constructor() { + super(); + } + + build(selfUid: string, node: NapProtoEncodeStructType): OidbPacket { + const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 200 + }, + scene: { + requestType: 1, + businessType: 3, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: selfUid + }, + }, + client: { + agentType: 2, + } + }, + download: { + node: node, + download: { + video: { + busiType: 0, + sceneType: 0 + } + } + } + }); + return OidbBase.build(0x126D, 200, body, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new DownloadPtt(); diff --git a/src/core/packet/transformer/highway/DownloadVideo.ts b/src/core/packet/transformer/highway/DownloadVideo.ts new file mode 100644 index 00000000..0731b258 --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadVideo.ts @@ -0,0 +1,51 @@ +import * as proto from '@/core/packet/transformer/proto'; +import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core'; +import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base'; +import OidbBase from '@/core/packet/transformer/oidb/oidbBase'; +import { IndexNode } from '@/core/packet/transformer/proto'; + +class DownloadVideo extends PacketTransformer { + constructor() { + super(); + } + + build(selfUid: string, node: NapProtoEncodeStructType): OidbPacket { + const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({ + reqHead: { + common: { + requestId: 1, + command: 200 + }, + scene: { + requestType: 2, + businessType: 2, + sceneType: 1, + c2C: { + accountType: 2, + targetUid: selfUid + }, + }, + client: { + agentType: 2, + } + }, + download: { + node: node, + download: { + video: { + busiType: 0, + sceneType: 0 + } + } + } + }); + return OidbBase.build(0x11E9, 200, body, true, false); + } + + parse(data: Buffer) { + const oidbBody = OidbBase.parse(data).body; + return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody); + } +} + +export default new DownloadVideo(); diff --git a/src/core/packet/transformer/highway/index.ts b/src/core/packet/transformer/highway/index.ts index 7ca56645..ef202112 100644 --- a/src/core/packet/transformer/highway/index.ts +++ b/src/core/packet/transformer/highway/index.ts @@ -13,3 +13,6 @@ export { default as UploadPrivatePtt } from './UploadPrivatePtt'; export { default as UploadPrivateVideo } from './UploadPrivateVideo'; export { default as DownloadImage } from './DownloadImage'; export { default as DownloadGroupImage } from './DownloadGroupImage'; +export { default as DownloadVideo } from './DownloadVideo'; +export { default as DownloadGroupVideo } from './DownloadGroupVideo'; +export { default as DownloadPtt } from './DownloadPtt'; \ No newline at end of file diff --git a/src/core/services/NodeIKernelMsgService.ts b/src/core/services/NodeIKernelMsgService.ts index abd6c969..34dd8e0c 100644 --- a/src/core/services/NodeIKernelMsgService.ts +++ b/src/core/services/NodeIKernelMsgService.ts @@ -148,10 +148,11 @@ export interface NodeIKernelMsgService { msgList: RawMessage[] }>; - //@deprecated - getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise; + // getMsgService/getMsgs { chatType: 2, peerUid: '975206796', privilegeFlag: 336068800 } 0 20 true + getMsgs(peer: Peer & { privilegeFlag: number }, msgId: string, count: number, queryOrder: boolean): Promise; - //@deprecated getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise; diff --git a/src/core/types/msg.ts b/src/core/types/msg.ts index 9e542ad8..e949bbd9 100644 --- a/src/core/types/msg.ts +++ b/src/core/types/msg.ts @@ -508,7 +508,8 @@ export interface RawMessage { * 查询消息参数接口 */ export interface QueryMsgsParams { - chatInfo: Peer; + chatInfo: Peer & { privilegeFlag?: number }; + //searchFields: number; filterMsgType: Array<{ type: NTMsgType, subType: Array }>; filterSendersUid: string[]; filterMsgFromTime: string; diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index 90ddc16d..6191c57d 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -150,12 +150,31 @@ export class OneBotMsgApi { }; FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid); FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName); + if (this.core.apis.PacketApi.available) { + let url; + try { + url = await this.core.apis.FileApi.getFileUrl(msg.chatType, msg.peerUid, element.fileUuid, element.file10MMd5) + } catch (error) { + url = ''; + } + if (url) { + return { + type: OB11MessageDataType.file, + data: { + file: element.fileName, + file_id: element.fileUuid, + file_size: element.fileSize, + url: url, + }, + } + } + } return { type: OB11MessageDataType.file, data: { file: element.fileName, file_id: element.fileUuid, - file_size: element.fileSize, + file_size: element.fileSize }, }; }, @@ -225,17 +244,13 @@ export class OneBotMsgApi { }, replyElement: async (element, msg) => { - const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords); const peer = { chatType: msg.chatType, peerUid: msg.peerUid, guildId: '', }; - if (!records || !element.replyMsgTime || !element.senderUidStr) { - this.core.context.logger.logError('似乎是旧版客户端,获取不到引用的消息', element.replayMsgSeq); - return null; - } + // 创建回复数据的通用方法 const createReplyData = (msgId: string): OB11MessageData => ({ type: OB11MessageDataType.reply, data: { @@ -243,48 +258,96 @@ export class OneBotMsgApi { }, }); - if (records.peerUin === '284840486' || records.peerUin === '1094950020') { + // 查找记录 + const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords); + + // 特定账号的特殊处理 + if (records && (records.peerUin === '284840486' || records.peerUin === '1094950020')) { return createReplyData(records.msgId); } - let replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(peer, element.replayMsgSeq, records.msgTime, [element.senderUidStr])).msgList; - let replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom); - if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { - this.core.context.logger.logError( - '筛选结果,筛选消息失败,将使用Fallback-1 Seq: ', + // 获取消息的通用方法组 + const tryFetchMethods = async (msgSeq: string, senderUid?: string, msgTime?: string, msgRandom?: string): Promise => { + try { + // 方法1:通过序号和时间筛选 + if (senderUid && msgTime) { + const replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2( + peer, msgSeq, msgTime, [senderUid] + )).msgList; + + const replyMsg = msgRandom + ? replyMsgList.find(msg => msg.msgRandom === msgRandom) + : replyMsgList.find(msg => msg.msgSeq === msgSeq); + + if (replyMsg) return replyMsg; + + this.core.context.logger.logWarn(`方法1查询失败,序号: ${msgSeq}, 消息数: ${replyMsgList.length}`); + } + + // 方法2:直接通过序号获取 + const replyMsgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount( + peer, msgSeq, 1, true, true + )).msgList; + + const replyMsg = msgRandom + ? replyMsgList.find(msg => msg.msgRandom === msgRandom) + : replyMsgList.find(msg => msg.msgSeq === msgSeq); + + if (replyMsg) return replyMsg; + + this.core.context.logger.logWarn(`方法2查询失败,序号: ${msgSeq}, 消息数: ${replyMsgList.length}`); + + // 方法3:另一种筛选方式 + if (senderUid) { + const replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3( + peer, msgSeq, [senderUid] + )).msgList; + + const replyMsg = msgRandom + ? replyMsgList.find(msg => msg.msgRandom === msgRandom) + : replyMsgList.find(msg => msg.msgSeq === msgSeq); + + if (replyMsg) return replyMsg; + + this.core.context.logger.logWarn(`方法3查询失败,序号: ${msgSeq}, 消息数: ${replyMsgList.length}`); + } + + return undefined; + } catch (error) { + this.core.context.logger.logError('查询回复消息出错', error); + return undefined; + } + }; + + // 有记录情况下,使用完整信息查询 + if (records && element.replyMsgTime && element.senderUidStr) { + const replyMsg = await tryFetchMethods( element.replayMsgSeq, - ',消息长度:', - replyMsgList.length + element.senderUidStr, + records.msgTime, + records.msgRandom ); - replyMsgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount(peer, element.replayMsgSeq, 1, true, true)).msgList; - replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom); + + if (replyMsg) { + return createReplyData(replyMsg.msgId); + } + + this.core.context.logger.logError('所有查找方法均失败,获取不到带记录的引用消息', element.replayMsgSeq); + } else { + // 旧版客户端或不完整记录的情况,也尝试使用相同流程 + this.core.context.logger.logWarn('似乎是旧版客户端,尝试仅通过序号获取引用消息', element.replayMsgSeq); + + const replyMsg = await tryFetchMethods(element.replayMsgSeq); + + if (replyMsg) { + return createReplyData(replyMsg.msgId); + } + + this.core.context.logger.logError('所有查找方法均失败,获取不到旧客户端的引用消息', element.replayMsgSeq); } - if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { - this.core.context.logger.logWarn( - '筛选消息失败,将使用Fallback-2 Seq:', - element.replayMsgSeq, - ',消息长度:', - replyMsgList.length - ); - replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(peer, element.replayMsgSeq, [element.senderUidStr])).msgList; - replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom); - } - - - // 丢弃该消息段 - if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) { - this.core.context.logger.logError( - '最终筛选结果,筛选消息失败,获取不到引用的消息 Seq: ', - element.replayMsgSeq, - ',消息长度:', - replyMsgList.length - ); - return null; - } - return createReplyData(replyMsg.msgId); + return null; }, - videoElement: async (element, msg, elementWrapper) => { const peer = { chatType: msg.chatType, @@ -331,7 +394,17 @@ export class OneBotMsgApi { //开始兜底 if (!videoDownUrl) { - videoDownUrl = element.filePath; + if (this.core.apis.PacketApi.available) { + try { + videoDownUrl = await this.core.apis.FileApi.getVideoUrlPacket(msg.chatType, msg.peerUid, element.fileUuid); + } catch (e) { + this.core.context.logger.logError('获取视频url失败', (e as Error).stack); + videoDownUrl = element.filePath; + } + } else { + videoDownUrl = element.filePath; + } + } const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName); return { @@ -351,6 +424,28 @@ export class OneBotMsgApi { guildId: '', }; const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, '', element.fileName); + let pttUrl = ''; + if (this.core.apis.PacketApi.available) { + try { + pttUrl = await this.core.apis.FileApi.getPttUrl(msg.chatType, msg.peerUid, element.fileUuid); + } catch (e) { + this.core.context.logger.logError('获取语音url失败', (e as Error).stack); + pttUrl = element.filePath; + } + } else { + pttUrl = element.filePath; + } + if (pttUrl) { + return { + type: OB11MessageDataType.voice, + data: { + file: fileCode, + path: element.filePath, + url: pttUrl, + file_size: element.fileSize, + }, + } + } return { type: OB11MessageDataType.voice, data: {