diff --git a/src/laana/action/message.ts b/src/laana/action/message.ts index 51c718b3..ce17c096 100644 --- a/src/laana/action/message.ts +++ b/src/laana/action/message.ts @@ -2,9 +2,12 @@ import { NapCatLaanaAdapter } from '..'; import { NapCatCore } from '@/core'; import { LaanaActionHandler } from '../action'; import fs from 'fs'; -import { ForwardMessagePing_Operation } from '@laana-proto/def'; +import { + ForwardMessagePing_Operation, + LaanaPeer, + OutgoingMessage, +} from '@laana-proto/def'; -// TODO: separate implementation and handler export class LaanaMessageActionHandler { constructor( public core: NapCatCore, @@ -13,188 +16,53 @@ export class LaanaMessageActionHandler { impl: LaanaActionHandler = { sendMessage: async (params) => { - const { elements, fileCacheRecords } = await this.laana.utils.msg.laanaMessageToRaw(params.message!, params); - - let cacheSize = 0; - try { - for (const cacheRecord of fileCacheRecords) { - cacheSize += fs.statSync(await this.laana.utils.file.toLocalPath(cacheRecord.cacheId)).size; - } - } catch (e) { - this.core.context.logger.logWarn('文件缓存大小计算失败', e); - } - const estimatedSendMsgTimeout = - cacheSize / 1024 / 256 * 1000 + // file upload time - 1000 * fileCacheRecords.length + // request timeout - 10000; // fallback timeout - - const sentMsgOrEmpty = await this.core.apis.MsgApi.sendMsg( - await this.laana.utils.msg.laanaPeerToRaw(params.targetPeer!), - elements, - true, // TODO: add 'wait complete' (bool) field - estimatedSendMsgTimeout, - ); - - fileCacheRecords.forEach(record => { - if (record.originalType !== 'cacheId') { - this.laana.utils.file.destroyCache(record.cacheId); - } - }); - - if (!sentMsgOrEmpty) { - throw Error('消息发送失败'); - } - return { - msgId: this.laana.utils.msg.encodeMsgToLaanaMsgId( - sentMsgOrEmpty.msgId, - sentMsgOrEmpty.chatType, - sentMsgOrEmpty.peerUid, - ), - }; + return { msgId: await this.sendMessage(params.message!, params.targetPeer!) }; }, sendPackedMessages: async (params) => { // first send every single message to self, then forward them to target peer - - // send message one by one const sendMsgIds: string[] = []; for (const message of params.messages) { - sendMsgIds.push((await this.impl.sendMessage!({ targetPeer: params.targetPeer, message })).msgId); + sendMsgIds.push(await this.sendMessage(message, params.targetPeer!)); } - - await this.impl.forwardMessage!({ - msgIds: sendMsgIds, - targetPeer: params.targetPeer, - operation: ForwardMessagePing_Operation.AS_PACKED, - }); - + const packedMsgId = await this.forwardMessageAsPacked(sendMsgIds, params.targetPeer!); return { - packedMsgId: '', // unimplemented + packedMsgId, msgIds: sendMsgIds, }; }, getMessage: async (params) => { - const { msgId, chatType, peerUid } = this.laana.utils.msg.decodeLaanaMsgId(params.msgId); - const msgListWrapper = await this.core.apis.MsgApi.getMsgsByMsgId( - { chatType, peerUid, guildId: '' }, - [msgId], - ); - if (msgListWrapper.msgList.length === 0) { - throw new Error('消息不存在'); - } - const msg = msgListWrapper.msgList[0]; - return { - message: await this.laana.utils.msg.rawMessageToLaana(msg), - }; + return { message: await this.getMessage(params.msgId) }; }, getMessages: async (params) => { if (params.msgIds.length === 0) { throw new Error('消息 ID 列表不能为空'); } - - const msgIdWrappers = params.msgIds.map(msgId => this.laana.utils.msg.decodeLaanaMsgId(msgId)); - - // check whether chatType and peerUid for each message are the same - const firstMsg = msgIdWrappers[0]; - if (msgIdWrappers.some(msg => msg.chatType !== firstMsg.chatType || msg.peerUid !== firstMsg.peerUid)) { - return { - // one request per message - messages: await Promise.all( - params.msgIds.map(msgId => this.laana.utils.msg.decodeLaanaMsgId(msgId)) - .map(async ({ msgId, chatType, peerUid }) => { - const msgListWrapper = await this.core.apis.MsgApi.getMsgsByMsgId( - { chatType, peerUid, guildId: '' }, - [msgId], - ); - if (msgListWrapper.msgList.length === 0) { - throw new Error('消息不存在'); - } - return await this.laana.utils.msg.rawMessageToLaana(msgListWrapper.msgList[0]); - }) - ) - }; - } else { - // a single request for all messages - const msgListWrapper = await this.core.apis.MsgApi.getMsgsByMsgId( - { chatType: firstMsg.chatType, peerUid: firstMsg.peerUid, guildId: '' }, - msgIdWrappers.map(msg => msg.msgId), - ); - return { - messages: await Promise.all( - msgListWrapper.msgList.map(msg => this.laana.utils.msg.rawMessageToLaana(msg)), - ), - }; - } + return { messages: await this.getMessages(params.msgIds) }; }, getForwardedMessages: async (params) => { - const { rootMsgLaanaId, currentMsgId } = this.laana.utils.msg.decodeLaanaForwardMsgRefId(params.refId); - const decodedRootMsgId = this.laana.utils.msg.decodeLaanaMsgId(rootMsgLaanaId); - const rawForwardedMessages = await this.core.apis.MsgApi.getMultiMsg( - { - chatType: decodedRootMsgId.chatType, - peerUid: decodedRootMsgId.peerUid, - guildId: '', - }, - decodedRootMsgId.msgId, - currentMsgId, - ); - if (!rawForwardedMessages || rawForwardedMessages.result !== 0 || rawForwardedMessages.msgList.length === 0) { - throw new Error('获取转发消息失败'); - } return { forwardMessage: { refId: params.refId, - messages: await Promise.all( - rawForwardedMessages.msgList.map(async msg => { - return await this.laana.utils.msg.rawMessageToLaana(msg, rootMsgLaanaId); - }), - ), + messages: await this.getForwardedMessages(params.refId) } }; }, - getHistoryMessages: async (params) => { // TODO: add 'reverseOrder' field - const { msgId } = this.laana.utils.msg.decodeLaanaMsgId(params.lastMsgId); - const msgListWrapper = await this.core.apis.MsgApi.getMsgHistory( - await this.laana.utils.msg.laanaPeerToRaw(params.targetPeer!), - msgId, - params.count, - ); - if (msgListWrapper.msgList.length === 0) { - this.core.context.logger.logWarn('获取历史消息失败', params.targetPeer!.uin); - } - return { // TODO: check order - messages: await Promise.all( - msgListWrapper.msgList.map(async msg => { - return await this.laana.utils.msg.rawMessageToLaana(msg); - }), - ), - }; + getHistoryMessages: async (params) => { + return { messages: await this.getHistoryMessages(params.targetPeer!, params.lastMsgId, params.count) }; }, withdrawMessage: async (params) => { - const { msgId, chatType, peerUid } = this.laana.utils.msg.decodeLaanaMsgId(params.msgId); - try { - await this.core.apis.MsgApi.recallMsg( - { chatType, peerUid, guildId: '' }, - msgId, - ); - } catch (e) { - throw new Error(`消息撤回失败: ${e}`); - } + await this.withdrawMessage(params.msgId); return { success: true }; }, markPeerMessageAsRead: async (params) => { - const { chatType, peerUid } = await this.laana.utils.msg.laanaPeerToRaw(params.peer!); - try { - await this.core.apis.MsgApi.setMsgRead({ chatType, peerUid }); - } catch (e) { - throw new Error(`标记消息已读失败: ${e}`); - } + await this.markPeerMessageAsRead(params.peer!); return { success: true }; }, @@ -202,26 +70,211 @@ export class LaanaMessageActionHandler { if (params.msgIds.length === 0) { throw new Error('消息 ID 列表不能为空'); } - const { chatType, peerUid } = this.laana.utils.msg.decodeLaanaMsgId(params.msgIds[0]); - const msgIdList = params.msgIds - .map(msgId => this.laana.utils.msg.decodeLaanaMsgId(msgId).msgId); - const destPeer = await this.laana.utils.msg.laanaPeerToRaw(params.targetPeer!); - if (params.operation === ForwardMessagePing_Operation.AS_SINGLETONS) { - const ret = await this.core.apis.MsgApi.forwardMsg( - { chatType, peerUid, guildId: '' }, - destPeer, - msgIdList, - ); - if (ret.result !== 0) { - throw new Error(`转发消息失败 ${ret.errMsg}`); - } + await this.forwardMessageAsSingletons(params.msgIds, params.targetPeer!); } else { - throw new Error('unimplemented'); - // TODO: refactor NTQQMsgApi.multiForwardMsg + await this.forwardMessageAsPacked(params.msgIds, params.targetPeer!); } - return { success: true }; }, }; + + /** + * Send a message to a peer. + * @param msg The message to send. + * @param targetPeer The peer to send the message to. + * @returns The Laana-styled msgId of the message sent. + */ + async sendMessage(msg: OutgoingMessage, targetPeer: LaanaPeer) { + const { elements, fileCacheRecords } = await this.laana.utils.msg.laanaMessageToRaw(msg, targetPeer); + + let cacheSize = 0; + try { + for (const cacheRecord of fileCacheRecords) { + cacheSize += fs.statSync(await this.laana.utils.file.toLocalPath(cacheRecord.cacheId)).size; + } + } catch (e) { + this.core.context.logger.logWarn('文件缓存大小计算失败', e); + } + const estimatedSendMsgTimeout = + cacheSize / 1024 / 256 * 1000 + // file upload time + 1000 * fileCacheRecords.length + // request timeout + 10000; // fallback timeout + + const sentMsgOrEmpty = await this.core.apis.MsgApi.sendMsg( + await this.laana.utils.msg.laanaPeerToRaw(targetPeer), + elements, + true, // TODO: add 'wait complete' (bool) field + estimatedSendMsgTimeout, + ); + + fileCacheRecords.forEach(record => { + if (record.originalType !== 'cacheId') { + this.laana.utils.file.destroyCache(record.cacheId); + } + }); + + if (!sentMsgOrEmpty) { + throw Error('消息发送失败'); + } + return this.laana.utils.msg.encodeMsgToLaanaMsgId( + sentMsgOrEmpty.msgId, + sentMsgOrEmpty.chatType, + sentMsgOrEmpty.peerUid, + ); + } + + /** + * Get a message by its Laana-styled msgId. + * @param laanaMsgId The Laana-styled msgId of the message. + */ + async getMessage(laanaMsgId: string) { + const { msgId, chatType, peerUid } = this.laana.utils.msg.decodeLaanaMsgId(laanaMsgId); + const msgListWrapper = await this.core.apis.MsgApi.getMsgsByMsgId( + { chatType, peerUid, guildId: '' }, + [msgId], + ); + if (msgListWrapper.msgList.length === 0) { + throw new Error('消息不存在'); + } + const msg = msgListWrapper.msgList[0]; + return await this.laana.utils.msg.rawMessageToLaana(msg); + } + + /** + * Get multiple messages by their Laana-styled msgIds. + * This method is optimized for fetching multiple messages at once. + * @param laanaMsgIds The Laana-styled msgIds of the messages. + */ + async getMessages(laanaMsgIds: string[]) { + const msgIdWrappers = laanaMsgIds.map(msgId => this.laana.utils.msg.decodeLaanaMsgId(msgId)); + // check whether chatType and peerUid for each message are the same + const firstMsg = msgIdWrappers[0]; + if (msgIdWrappers.some(msg => msg.chatType !== firstMsg.chatType || msg.peerUid !== firstMsg.peerUid)) { + // one request for each message + return await Promise.all(msgIdWrappers.map(async ({ msgId, chatType, peerUid }) => { + const msgListWrapper = await this.core.apis.MsgApi.getMsgsByMsgId( + { chatType, peerUid, guildId: '' }, + [msgId], + ); + if (msgListWrapper.msgList.length === 0) { + throw new Error('消息不存在'); + } + return await this.laana.utils.msg.rawMessageToLaana(msgListWrapper.msgList[0]); + })); + } else { + // a single request for all messages + const msgList = (await this.core.apis.MsgApi.getMsgsByMsgId( + { chatType: firstMsg.chatType, peerUid: firstMsg.peerUid, guildId: '' }, + msgIdWrappers.map(msg => msg.msgId), + )).msgList; + return await Promise.all(msgList.map(msg => this.laana.utils.msg.rawMessageToLaana(msg))); + } + } + + /** + * Get forwarded messages by a Laana-styled refId. + * @param refId The Laana-styled refId of the message. + */ + async getForwardedMessages(refId: string) { + const { rootMsgLaanaId, currentMsgId } = this.laana.utils.msg.decodeLaanaForwardMsgRefId(refId); + const decodedRootMsgId = this.laana.utils.msg.decodeLaanaMsgId(rootMsgLaanaId); + const rawForwardedMessages = await this.core.apis.MsgApi.getMultiMsg( + { + chatType: decodedRootMsgId.chatType, + peerUid: decodedRootMsgId.peerUid, + guildId: '', + }, + decodedRootMsgId.msgId, + currentMsgId, + ); + if (!rawForwardedMessages || rawForwardedMessages.result !== 0 || rawForwardedMessages.msgList.length === 0) { + throw new Error('获取转发消息失败'); + } + return await Promise.all( + rawForwardedMessages.msgList.map(async msg => { + return await this.laana.utils.msg.rawMessageToLaana(msg, rootMsgLaanaId); + }), + ); + } + + /** + * Get history messages of a peer. + * @param peer The peer to get history messages from. + * @param lastMsgId The Laana-styled msgId of the last message. + * @param count The number of messages to get. + */ + async getHistoryMessages(peer: LaanaPeer, lastMsgId: string, count: number) { + const { msgId } = this.laana.utils.msg.decodeLaanaMsgId(lastMsgId); + const msgListWrapper = await this.core.apis.MsgApi.getMsgHistory( + await this.laana.utils.msg.laanaPeerToRaw(peer), + msgId, + count, + ); + if (msgListWrapper.msgList.length === 0) { + this.core.context.logger.logWarn('获取历史消息失败', peer.uin); + } + return await Promise.all( + msgListWrapper.msgList.map(async msg => { + return await this.laana.utils.msg.rawMessageToLaana(msg); + }), + ); + } + + /** + * Withdraw a message by its Laana-styled msgId. + * @param laanaMsgId The Laana-styled msgId of the message. + */ + async withdrawMessage(laanaMsgId: string) { + const { msgId, chatType, peerUid } = this.laana.utils.msg.decodeLaanaMsgId(laanaMsgId); + await this.core.apis.MsgApi.recallMsg( + { chatType, peerUid, guildId: '' }, + msgId, + ); + } + + /** + * Mark a peer's messages as read. + * @param peer The peer to mark messages as read. + */ + async markPeerMessageAsRead(peer: LaanaPeer) { + const { chatType, peerUid } = await this.laana.utils.msg.laanaPeerToRaw(peer); + await this.core.apis.MsgApi.setMsgRead({ chatType, peerUid }); + } + + /** + * Forward messages as singletons to a target peer. + * @param laanaMsgIds The Laana-styled msgIds of the messages to forward. + * @param targetPeer The peer to forward the messages to. + */ + async forwardMessageAsSingletons(laanaMsgIds: string[], targetPeer: LaanaPeer) { + const { chatType, peerUid } = this.laana.utils.msg.decodeLaanaMsgId(laanaMsgIds[0]); + const msgIdList = laanaMsgIds.map(msgId => this.laana.utils.msg.decodeLaanaMsgId(msgId).msgId); + const destPeer = await this.laana.utils.msg.laanaPeerToRaw(targetPeer); + const ret = await this.core.apis.MsgApi.forwardMsg( + { chatType, peerUid, guildId: '' }, + destPeer, + msgIdList, + ); + if (ret.result !== 0) { + throw new Error(`转发消息失败 ${ret.errMsg}`); + } + } + + /** + * Forward messages as packed to a target peer. + * @param laanaMsgIds The Laana-styled msgIds of the messages to forward. + * @param targetPeer The peer to forward the messages to. + */ + async forwardMessageAsPacked(laanaMsgIds: string[], targetPeer: LaanaPeer) { + const { chatType, peerUid } = this.laana.utils.msg.decodeLaanaMsgId(laanaMsgIds[0]); + const msgIdList = laanaMsgIds.map(msgId => this.laana.utils.msg.decodeLaanaMsgId(msgId).msgId); + const destPeer = await this.laana.utils.msg.laanaPeerToRaw(targetPeer); + const retMsg = await this.core.apis.MsgApi.multiForwardMsg( + { chatType, peerUid, guildId: '' }, + destPeer, + msgIdList, + ); + return this.laana.utils.msg.encodeMsgToLaanaMsgId(retMsg.msgId, retMsg.chatType, retMsg.peerUid); + } } diff --git a/src/laana/utils/message.ts b/src/laana/utils/message.ts index c2322b0e..1971a02a 100644 --- a/src/laana/utils/message.ts +++ b/src/laana/utils/message.ts @@ -33,7 +33,7 @@ type Laana2RawConverters = { // eslint-disable-next-line // @ts-ignore msgContent: Extract[key], - params: SendMessagePing, + targetPeer: LaanaPeer, ) => PromiseLike<{ elements: SendMessageElement[], fileCacheRecords: SentMessageFileCacheRecord[], @@ -56,7 +56,7 @@ export class LaanaMessageUtils { } l2r: Laana2RawConverters = { - bubble: async (msgContent, params) => { + bubble: async (msgContent, targetPeer) => { function at(atUid: string, atNtUid: string, atType: AtType, atName: string): SendTextElement { return { elementType: ElementType.TEXT, @@ -77,7 +77,7 @@ export class LaanaMessageUtils { if (msgContent.repliedMsgId) { const replyMsg = ( await this.core.apis.MsgApi.getMsgsByMsgId( - await this.laanaPeerToRaw(params.targetPeer!), + await this.laanaPeerToRaw(targetPeer), [msgContent.repliedMsgId] ) ).msgList[0]; @@ -112,7 +112,7 @@ export class LaanaMessageUtils { }, }); } else if (content.oneofKind === 'at') { - if (params.targetPeer?.type !== LaanaPeer_Type.GROUP) { + if (targetPeer.type !== LaanaPeer_Type.GROUP) { throw Error('试图在私聊会话中使用 At'); } @@ -125,7 +125,7 @@ export class LaanaMessageUtils { } const atMember = await this.core.apis.GroupApi - .getGroupMember(params.targetPeer.uin, content.at.uin); + .getGroupMember(targetPeer.uin, content.at.uin); if (atMember) { elements.push(at( content.at.uin, @@ -298,7 +298,7 @@ export class LaanaMessageUtils { }; } - async laanaMessageToRaw(msg: OutgoingMessage, params: SendMessagePing) { + async laanaMessageToRaw(msg: OutgoingMessage, targetPeer: LaanaPeer) { if (!msg.content.oneofKind) { throw Error('消息内容类型未知'); } @@ -306,7 +306,7 @@ export class LaanaMessageUtils { // eslint-disable-next-line // @ts-ignore msg.content[msg.content.oneofKind], - params + targetPeer ); }