From 4fcf3aa2bdb484b72a1877b42aa2d142c9e38d24 Mon Sep 17 00:00:00 2001 From: "Wesley F. Young" Date: Wed, 15 May 2024 14:53:58 +0800 Subject: [PATCH] refactor: better type inferring; move createSendElement into another file --- .../msg/SendMsg/create-send-elements.ts | 243 +++++++++++++ .../msg/{SendMsg.ts => SendMsg/index.ts} | 339 +++--------------- src/onebot11/types.ts | 4 +- 3 files changed, 286 insertions(+), 300 deletions(-) create mode 100644 src/onebot11/action/msg/SendMsg/create-send-elements.ts rename src/onebot11/action/msg/{SendMsg.ts => SendMsg/index.ts} (54%) diff --git a/src/onebot11/action/msg/SendMsg/create-send-elements.ts b/src/onebot11/action/msg/SendMsg/create-send-elements.ts new file mode 100644 index 00000000..34192112 --- /dev/null +++ b/src/onebot11/action/msg/SendMsg/create-send-elements.ts @@ -0,0 +1,243 @@ +import { OB11MessageData, OB11MessageDataType, OB11MessageFileBase } from '@/onebot11/types'; +import { + AtType, + CustomMusicSignPostData, + Group, + IdMusicSignPostData, + NTQQFileApi, + SendMessageElement, + SendMsgElementConstructor +} from '@/core'; +import { getGroupMember } from '@/core/data'; +import { dbUtil } from '@/core/utils/db'; +import { logDebug, logError } from '@/common/utils/log'; +import { uri2local } from '@/common/utils/file'; +import { ob11Config } from '@/onebot11/config'; +import { RequestUtil } from '@/common/utils/request'; +import fs from 'node:fs'; + +export type MessageContext = { + group?: Group, + deleteAfterSentFiles: string[], +} + +async function handleOb11FileLikeMessage( + { data: { file, name: payloadFileName } }: OB11MessageFileBase, + { deleteAfterSentFiles }: MessageContext +) { + let uri = file; + + const cache = await dbUtil.getFileCacheByName(file); + if (cache) { + if (fs.existsSync(cache.path)) { + uri = 'file://' + cache.path; + } else if (cache.url) { + uri = cache.url; + } else { + const fileMsg = await dbUtil.getMsgByLongId(cache.msgId); + if (fileMsg) { + cache.path = await NTQQFileApi.downloadMedia( + fileMsg.msgId, fileMsg.chatType, fileMsg.peerUid, + cache.elementId, '', '' + ); + uri = 'file://' + cache.path; + dbUtil.updateFileCache(cache); + } + } + logDebug('找到文件缓存', uri); + } + + const { path, isLocal, fileName, errMsg } = (await uri2local(uri)); + + if (errMsg) { + logError('文件下载失败', errMsg); + throw Error('文件下载失败' + errMsg); + } + + if (!isLocal) { // 只删除http和base64转过来的文件 + deleteAfterSentFiles.push(path); + } + + return { path, fileName: payloadFileName || fileName }; +} + +const _handlers: { + [Key in OB11MessageDataType]: ( + sendMsg: Extract, + // This picks the correct message type out + // How great the type system of TypeScript is! + context: MessageContext + ) => SendMessageElement | undefined | Promise +} = { + [OB11MessageDataType.text]: ({ data: { text } }) => SendMsgElementConstructor.text(text), + + [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => { + if (!context.group) return undefined; + + if (atQQ === 'all') return SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员'); + + // then the qq is a group member + const atMember = await getGroupMember(context.group.groupCode, atQQ); + return atMember ? + SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick) : + undefined; + }, + + [OB11MessageDataType.reply]: async ({ data: { id } }) => { + const replyMsg = await dbUtil.getMsgByShortId(parseInt(id)); + return replyMsg ? + SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!) : + undefined; + }, + + [OB11MessageDataType.face]: ({ data: { id } }) => SendMsgElementConstructor.face(parseInt(id)), + + [OB11MessageDataType.mface]: ({ + data: { + emoji_package_id, + emoji_id, + key, + summary + } + }) => SendMsgElementConstructor.mface(emoji_package_id, emoji_id, key, summary), + + // File service + + [OB11MessageDataType.image]: async (sendMsg, context) => + SendMsgElementConstructor.pic( + (await handleOb11FileLikeMessage(sendMsg, context)).path, + sendMsg.data.summary || '', + sendMsg.data.subType || 0 + ), // currently not supported + + [OB11MessageDataType.file]: async (sendMsg, context) => { + const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context); + logDebug('发送文件', path, fileName); + return SendMsgElementConstructor.file(path, fileName); + }, + + [OB11MessageDataType.video]: async (sendMsg, context) => { + const { path, fileName } = await handleOb11FileLikeMessage(sendMsg, context); + + logDebug('发送视频', path, fileName); + let thumb = sendMsg.data.thumb; + if (thumb) { + const uri2LocalRes = await uri2local(thumb); + if (uri2LocalRes.success) thumb = uri2LocalRes.path; + } + + return SendMsgElementConstructor.video(path, fileName, thumb); + }, + + [OB11MessageDataType.voice]: async (sendMsg, context) => + SendMsgElementConstructor.ptt((await handleOb11FileLikeMessage(sendMsg, context)).path), + + [OB11MessageDataType.json]: ({ data: { data } }) => SendMsgElementConstructor.ark(data), + + [OB11MessageDataType.dice]: ({ data: { result } }) => SendMsgElementConstructor.dice(result), + + [OB11MessageDataType.RPS]: ({ data: { result } }) => SendMsgElementConstructor.rps(result), + + [OB11MessageDataType.markdown]: ({ data: { content } }) => SendMsgElementConstructor.markdown(content), + + [OB11MessageDataType.music]: async ({ data }) => { + // 保留, 直到...找到更好的解决方案 + if (data.type === 'custom') { + if (!data.url) { + logError('自定义音卡缺少参数url'); + return undefined; + } + if (!data.audio) { + logError('自定义音卡缺少参数audio'); + return undefined; + } + if (!data.title) { + logError('自定义音卡缺少参数title'); + return undefined; + } + } else { + if (!['qq', '163'].includes(data.type)) { + logError('音乐卡片type错误, 只支持qq、163、custom,当前type:', data.type); + return undefined; + } + if (!data.id) { + 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; + } + + const signUrl = ob11Config.musicSignUrl; + if (!signUrl) { + throw Error('音乐消息签名地址未配置'); + } + try { + const musicJson = await RequestUtil.HttpGetJson(signUrl, 'POST', postData); + return SendMsgElementConstructor.ark(musicJson); + } catch (e) { + logError('生成音乐消息失败', e); + } + }, + + [OB11MessageDataType.node]: () => undefined, + + [OB11MessageDataType.forward]: () => undefined, + + [OB11MessageDataType.xml]: () => undefined, + + [OB11MessageDataType.poke]: () => undefined, +}; + +const handlers = <{ + [Key in OB11MessageDataType]: ( + sendMsg: OB11MessageData, + context: MessageContext + ) => SendMessageElement | undefined | Promise +}>_handlers; + +export default async function createSendElements( + messageData: OB11MessageData[], + group?: Group, + ignoreTypes: OB11MessageDataType[] = [] +) { + const sendElements: SendMessageElement[] = []; + const deleteAfterSentFiles: string[] = []; + for (const sendMsg of messageData) { + if (ignoreTypes.includes(sendMsg.type)) { + continue; + } + const callResult = await handlers[sendMsg.type]( + sendMsg, + { group, deleteAfterSentFiles } + ); + if (callResult) sendElements.push(callResult); + } + return { sendElements, deleteAfterSentFiles }; +} + +export async function createSendElementsParallel( + messageData: OB11MessageData[], + group?: Group, + ignoreTypes: OB11MessageDataType[] = [] +) { + const deleteAfterSentFiles: string[] = []; + const sendElements = ( + await Promise.all( + messageData.map(async sendMsg => ignoreTypes.includes(sendMsg.type) ? + undefined : + handlers[sendMsg.type](sendMsg, { group, deleteAfterSentFiles })) + ).then( + results => results.filter( + element => element !== undefined + ) + ) + ); + return { sendElements, deleteAfterSentFiles }; +} diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg/index.ts similarity index 54% rename from src/onebot11/action/msg/SendMsg.ts rename to src/onebot11/action/msg/SendMsg/index.ts index 68a53053..18cf2e0f 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg/index.ts @@ -1,37 +1,35 @@ -import { - AtType, - ChatType, - ElementType, - Group, PicSubType, - RawMessage, - SendArkElement, - SendMessageElement, - Peer -} from '@/core/entities'; - +import BaseAction from '@/onebot11/action/BaseAction'; import { OB11MessageCustomMusic, OB11MessageData, - OB11MessageDataType, OB11MessageIdMusic, + OB11MessageDataType, OB11MessageMixType, OB11MessageNode, OB11PostSendMsg -} from '../../types'; -import { SendMsgElementConstructor } from '@/core/entities/constructor'; -import BaseAction from '../BaseAction'; -import { ActionName, BaseCheckResult } from '../types'; -import * as fs from 'node:fs'; -import { decodeCQCode } from '../../cqcode'; +} from '@/onebot11/types'; +import { ActionName, BaseCheckResult } from '@/onebot11/action/types'; +import { getFriend, getGroup, getUidByUin, selfInfo } from '@/core/data'; import { dbUtil } from '@/core/utils/db'; -import { log, logDebug, logError } from '@/common/utils/log'; +import { + ChatType, + CustomMusicSignPostData, + ElementType, + Group, + IdMusicSignPostData, + NTQQMsgApi, + Peer, + RawMessage, + SendArkElement, + SendMessageElement, + SendMsgElementConstructor +} from '@/core'; +import fs from 'node:fs'; +import { logDebug, logError } from '@/common/utils/log'; import { sleep } from '@/common/utils/helper'; -import { uri2local } from '@/common/utils/file'; -import { getFriend, getGroup, getGroupMember, getUidByUin, selfInfo } from '@/core/data'; -import { NTQQMsgApi } from '@/core/apis'; -import { NTQQFileApi } from '@/core/apis'; import { ob11Config } from '@/onebot11/config'; -import { CustomMusicSignPostData, IdMusicSignPostData } from '@/core/apis/sign'; import { RequestUtil } from '@/common/utils/request'; +import { decodeCQCode } from '@/onebot11/cqcode'; +import createSendElements from './create-send-elements'; const ALLOW_SEND_TEMP_MSG = false; @@ -47,29 +45,23 @@ function checkSendMessage(sendMsgList: OB11MessageData[]) { const data = msg['data']; if (type === 'text' && !data['text']) { return 400; - } - else if (['image', 'voice', 'record'].includes(type)) { + } else if (['image', 'voice', 'record'].includes(type)) { if (!data['file']) { return 400; - } - else { + } else { if (checkUri(data['file'])) { return 200; - } - else { + } else { return 400; } } - } - else if (type === 'at' && !data['qq']) { + } else if (type === 'at' && !data['qq']) { + return 400; + } else if (type === 'reply' && !data['id']) { return 400; } - else if (type === 'reply' && !data['id']) { - return 400; - } - } - else { + } else { return 400; } } @@ -89,254 +81,15 @@ export function convertMessage2List(message: OB11MessageMixType, autoEscape = fa text: message } }]; - } - else { + } else { message = decodeCQCode(message.toString()); } - } - else if (!Array.isArray(message)) { + } else if (!Array.isArray(message)) { message = [message]; } return message; } -async function genMusicElement(postData: IdMusicSignPostData | CustomMusicSignPostData): Promise { - // const musicJson = { - // app: 'com.tencent.structmsg', - // config: { - // ctime: 1709689928, - // forward: 1, - // token: '5c1e4905f926dd3a64a4bd3841460351', - // type: 'normal' - // }, - // extra: { app_type: 1, appid: 100497308, uin: selfInfo.uin }, - // meta: { - // news: { - // action: '', - // android_pkg_name: '', - // app_type: 1, - // appid: 100497308, - // ctime: 1709689928, - // desc: content || title, - // jumpUrl: url, - // musicUrl: audio, - // preview: image, - // source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0', - // source_url: '', - // tag: 'QQ音乐', - // title: title, - // uin: selfInfo.uin, - // } - // }, - // prompt: content || title, - // ver: '0.0.0.1', - // view: 'news' - // }; - const signUrl = ob11Config.musicSignUrl; - if (!signUrl) { - throw Error('音乐消息签名地址未配置'); - } - try { - //const musicJson = await new MusicSign(signUrl).sign(postData); - // 待测试 - const musicJson = await RequestUtil.HttpGetJson(signUrl, 'POST', postData); - return SendMsgElementConstructor.ark(musicJson); - } catch (e) { - logError('生成音乐消息失败', e); - } -} - - -export async function createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) { - const sendElements: SendMessageElement[] = []; - const deleteAfterSentFiles: string[] = []; - for (const sendMsg of messageData) { - if (ignoreTypes.includes(sendMsg.type)) { - continue; - } - switch (sendMsg.type) { - case OB11MessageDataType.text: { - const text = sendMsg.data?.text; - if (text) { - sendElements.push(SendMsgElementConstructor.text(sendMsg.data!.text)); - } - } - break; - case OB11MessageDataType.at: { - if (!group) { - continue; - } - let atQQ = sendMsg.data?.qq; - if (atQQ) { - atQQ = atQQ.toString(); - if (atQQ === 'all') { - sendElements.push(SendMsgElementConstructor.at(atQQ, atQQ, AtType.atAll, '全体成员')); - } - else { - // const atMember = group?.members.find(m => m.uin == atQQ) - const atMember = await getGroupMember(group?.groupCode, atQQ); - if (atMember) { - sendElements.push(SendMsgElementConstructor.at(atQQ, atMember.uid, AtType.atUser, atMember.cardName || atMember.nick)); - } - } - } - } - break; - case OB11MessageDataType.reply: { - const replyMsgId = sendMsg.data.id; - if (replyMsgId) { - const replyMsg = await dbUtil.getMsgByShortId(parseInt(replyMsgId)); - if (replyMsg) { - sendElements.push(SendMsgElementConstructor.reply(replyMsg.msgSeq, replyMsg.msgId, replyMsg.senderUin!, replyMsg.senderUin!)); - } - } - } - break; - case OB11MessageDataType.face: { - const faceId = sendMsg.data?.id; - if (faceId) { - sendElements.push(SendMsgElementConstructor.face(parseInt(faceId))); - } - } - break; - case OB11MessageDataType.mface: { - sendElements.push( - SendMsgElementConstructor.mface(sendMsg.data.emoji_package_id, sendMsg.data.emoji_id, sendMsg.data.key, sendMsg.data.summary), - ); - } - break; - case OB11MessageDataType.image: - case OB11MessageDataType.file: - case OB11MessageDataType.video: - case OB11MessageDataType.voice: { - let file = sendMsg.data?.file; - const payloadFileName = sendMsg.data?.name; - if (file) { - const cache = await dbUtil.getFileCacheByName(file); - if (cache) { - if (fs.existsSync(cache.path)) { - file = 'file://' + cache.path; - } - else if (cache.url) { - file = cache.url; - } - else { - const fileMsg = await dbUtil.getMsgByLongId(cache.msgId); - if (fileMsg) { - const downloadPath = await NTQQFileApi.downloadMedia(fileMsg.msgId, fileMsg.chatType, fileMsg.peerUid, - cache.elementId, '', ''); - cache.path = downloadPath!; - dbUtil.updateFileCache(cache).then(); - file = 'file://' + cache.path; - } - // await sleep(1000); - - // log('download result', downloadPath); - // log('下载完成后的msg', msg); - } - logDebug('找到文件缓存', file); - } - const { path, isLocal, fileName, errMsg } = (await uri2local(file)); - if (errMsg) { - logError('文件下载失败', errMsg); - throw Error('文件下载失败' + errMsg); - // throw (errMsg); - // continue - } - if (path) { - if (!isLocal) { // 只删除http和base64转过来的文件 - deleteAfterSentFiles.push(path); - } - if (sendMsg.type === OB11MessageDataType.file) { - logDebug('发送文件', path, payloadFileName || fileName); - sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName)); - } - else if (sendMsg.type === OB11MessageDataType.video) { - logDebug('发送视频', path, payloadFileName || fileName); - let thumb = sendMsg.data?.thumb; - if (thumb) { - const uri2LocalRes = await uri2local(thumb); - if (uri2LocalRes.success) { - thumb = uri2LocalRes.path; - } - } - sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb)); - } - else if (sendMsg.type === OB11MessageDataType.voice) { - sendElements.push(await SendMsgElementConstructor.ptt(path)); - } - else if (sendMsg.type === OB11MessageDataType.image) { - sendElements.push(await SendMsgElementConstructor.pic(path, sendMsg.data.summary || '', parseInt(sendMsg.data?.subType?.toString() || '0'))); - } - } - } - } - break; - case OB11MessageDataType.json: { - sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data)); - } - break; - case OB11MessageDataType.dice: { - const resultId = sendMsg.data?.result; - sendElements.push(SendMsgElementConstructor.dice(resultId)); - } - break; - case OB11MessageDataType.RPS: { - const resultId = sendMsg.data?.result; - sendElements.push(SendMsgElementConstructor.rps(resultId)); - } - break; - case OB11MessageDataType.markdown: { - const content = sendMsg.data?.content; - sendElements.push(SendMsgElementConstructor.markdown(content)); - } - break; - case OB11MessageDataType.music: { - const musicData = sendMsg.data; - if (musicData.type === 'custom') { - if (!musicData.url) { - logError('自定义音卡缺少参数url'); - break; - } - if (!musicData.audio) { - logError('自定义音卡缺少参数audio'); - break; - } - if (!musicData.title) { - logError('自定义音卡缺少参数title'); - break; - } - } - else { - if (!['qq', '163'].includes(musicData.type)) { - logError('音乐卡片type错误, 只支持qq、163、custom,当前type:', musicData.type); - break; - } - if (!musicData.id) { - logError('音乐卡片缺少参数id'); - break; - } - } - const postData = { ...sendMsg.data } as IdMusicSignPostData | CustomMusicSignPostData; - if (sendMsg.data.type === 'custom' && sendMsg.data.content) { - (postData as CustomMusicSignPostData).singer = sendMsg.data.content; - delete (postData as OB11MessageCustomMusic['data']).content; - } - const musicMsgElement = await genMusicElement(postData); - logDebug('生成音乐消息', musicMsgElement); - if (musicMsgElement) { - sendElements.push(musicMsgElement); - } - } - } - } - - return { - sendElements, - deleteAfterSentFiles - }; -} - export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) { if (!sendElements.length) { throw ('消息体无法解析, 请检查是否发送了不支持的消息类型'); @@ -417,8 +170,7 @@ export class SendMsg extends BaseAction { if (friend) { // peer.name = friend.nickName peer.peerUid = friend.uid; - } - else { + } else { peer.chatType = ChatType.temp; const tempUserUid = getUidByUin(payload.user_id.toString()); if (!tempUserUid) { @@ -432,14 +184,11 @@ export class SendMsg extends BaseAction { if (payload?.group_id && payload.message_type === 'group') { await genGroupPeer(); - } - else if (payload?.user_id) { + } else if (payload?.user_id) { await genFriendPeer(); - } - else if (payload.group_id) { + } else if (payload.group_id) { await genGroupPeer(); - } - else { + } else { throw ('发送消息参数错误, 请指定group_id或user_id'); } const messages = convertMessage2List(payload.message, payload.auto_escape === true || payload.auto_escape === 'true'); @@ -449,15 +198,13 @@ export class SendMsg extends BaseAction { if (returnMsg) { const msgShortId = await dbUtil.addMsg(returnMsg!, false); return { message_id: msgShortId }; - } - else { + } else { throw Error('发送转发消息失败'); } } catch (e: any) { throw Error('发送转发消息失败 ' + e.toString()); } - } - else { + } else { if (this.getSpecialMsgNum(payload, OB11MessageDataType.music)) { const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic; // if (music) { @@ -528,8 +275,7 @@ export class SendMsg extends BaseAction { const nodeMsg = await dbUtil.getMsgByShortId(parseInt(nodeId)); if (!needClone) { nodeMsgIds.push(nodeMsg!.msgId); - } - else { + } else { if (nodeMsg!.peerUid !== selfInfo.uid) { const cloneMsg = await this.cloneMsg(nodeMsg!); if (cloneMsg) { @@ -537,8 +283,7 @@ export class SendMsg extends BaseAction { } } } - } - else { + } else { // 自定义的消息 // 提取消息段,发给自己生成消息id try { @@ -560,8 +305,7 @@ export class SendMsg extends BaseAction { } sendElementsSplit[splitIndex] = [ele]; splitIndex++; - } - else { + } else { sendElementsSplit[splitIndex].push(ele); } logDebug(sendElementsSplit); @@ -597,8 +341,7 @@ export class SendMsg extends BaseAction { nodeMsgArray.push(nodeMsg); if (!srcPeer) { srcPeer = { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid }; - } - else if (srcPeer.peerUid !== nodeMsg.peerUid) { + } else if (srcPeer.peerUid !== nodeMsg.peerUid) { needSendSelf = true; srcPeer = selfPeer; } diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index 8918101e..679bada6 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -143,7 +143,7 @@ export interface OB11MessageText { } } -interface OB11MessageFileBase { +export interface OB11MessageFileBase { data: { thumb?: string; name?: string; @@ -176,7 +176,7 @@ export interface OB11MessageVideo extends OB11MessageFileBase { export interface OB11MessageAt { type: OB11MessageDataType.at data: { - qq: string | 'all' + qq: `${number}` | 'all' } }