From 1735babb7d84229cdd8364c51dc24e76ff1b3f35 Mon Sep 17 00:00:00 2001 From: linyuchen Date: Sat, 23 Mar 2024 19:16:07 +0800 Subject: [PATCH] feat: http post quick operation --- src/common/config.ts | 2 +- src/ntqqapi/api/group.ts | 1 - src/onebot11/action/llonebot/Debug.ts | 17 +- src/onebot11/action/msg/SendMsg.ts | 293 +++++++++++++------------- src/onebot11/server/postOB11Event.ts | 115 +++++++++- test/quick_action/server.py | 29 +++ 6 files changed, 306 insertions(+), 151 deletions(-) create mode 100644 test/quick_action/server.py diff --git a/src/common/config.ts b/src/common/config.ts index 84442bb..f1613b4 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -6,7 +6,7 @@ import path from "node:path"; import {selfInfo} from "./data"; import {DATA_DIR} from "./utils"; -export const HOOK_LOG = true; +export const HOOK_LOG = false; export const ALLOW_SEND_TEMP_MSG = false; diff --git a/src/ntqqapi/api/group.ts b/src/ntqqapi/api/group.ts index fcb45a7..f23836a 100644 --- a/src/ntqqapi/api/group.ts +++ b/src/ntqqapi/api/group.ts @@ -2,7 +2,6 @@ import {ReceiveCmdS} from "../hook"; import {Group, GroupMember, GroupMemberRole, GroupNotifies, GroupNotify, GroupRequestOperateTypes} from "../types"; import {callNTQQApi, GeneralCallResult, NTQQApiClass, NTQQApiMethod} from "../ntcall"; import {uidMaps} from "../../common/data"; -import {BrowserWindow} from "electron"; import {dbUtil} from "../../common/db"; import {log} from "../../common/utils/log"; import {NTQQWindowApi, NTQQWindows} from "./window"; diff --git a/src/onebot11/action/llonebot/Debug.ts b/src/onebot11/action/llonebot/Debug.ts index 7402612..7ac1964 100644 --- a/src/onebot11/action/llonebot/Debug.ts +++ b/src/onebot11/action/llonebot/Debug.ts @@ -1,5 +1,14 @@ import BaseAction from "../BaseAction"; -import * as ntqqApi from "../../../ntqqapi/api"; +// import * as ntqqApi from "../../../ntqqapi/api"; +import { + NTQQMsgApi, + NTQQFriendApi, + NTQQGroupApi, + NTQQUserApi, + NTQQFileApi, + NTQQFileCacheApi, + NTQQWindowApi, +} from "../../../ntqqapi/api"; import {ActionName} from "../types"; import {log} from "../../../common/utils/log"; @@ -13,8 +22,10 @@ export default class Debug extends BaseAction { protected async _handle(payload: Payload): Promise { log("debug call ntqq api", payload); - for (const ntqqApiClass in ntqqApi) { - const method = ntqqApi[ntqqApiClass][payload.method] + const ntqqApi = [NTQQMsgApi, NTQQFriendApi, NTQQGroupApi, NTQQUserApi, NTQQFileApi, NTQQFileCacheApi, NTQQWindowApi] + for (const ntqqApiClass of ntqqApi) { + log("ntqqApiClass", ntqqApiClass) + const method = ntqqApiClass[payload.method] if (method) { const result = method(...payload.args); if (method.constructor.name === "AsyncFunction") { diff --git a/src/onebot11/action/msg/SendMsg.ts b/src/onebot11/action/msg/SendMsg.ts index 18cb65c..7bfadea 100644 --- a/src/onebot11/action/msg/SendMsg.ts +++ b/src/onebot11/action/msg/SendMsg.ts @@ -75,11 +75,156 @@ export interface ReturnDataType { message_id: number } +export function convertMessage2List(message: OB11MessageMixType, autoEscape = false) { + if (typeof message === "string") { + if (!autoEscape) { + message = decodeCQCode(message.toString()) + } else { + message = [{ + type: OB11MessageDataType.text, + data: { + text: message + } + }] + } + } else if (!Array.isArray(message)) { + message = [message] + } + return message; +} + +export async function createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) { + let sendElements: SendMessageElement[] = [] + let deleteAfterSentFiles: string[] = [] + for (let 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: { + let 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.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.getFileCache(file) + if (cache) { + if (fs.existsSync(cache.filePath)) { + file = "file://" + cache.filePath + } else if (cache.downloadFunc) { + await cache.downloadFunc() + file = cache.filePath; + } else if (cache.url) { + file = cache.url + } + log("找到文件缓存", file); + } + const {path, isLocal, fileName, errMsg} = (await uri2local(file)) + if (errMsg) { + throw errMsg + } + if (path) { + if (!isLocal) { // 只删除http和base64转过来的文件 + deleteAfterSentFiles.push(path) + } + if (sendMsg.type === OB11MessageDataType.file) { + log("发送文件", path, payloadFileName || fileName) + sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName)); + } else if (sendMsg.type === OB11MessageDataType.video) { + log("发送视频", path, payloadFileName || fileName) + let thumb = sendMsg.data?.thumb; + if (thumb) { + let 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 || "")); + } + } + } + } + break; + case OB11MessageDataType.json: { + sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data)) + } + break + } + + } + + return { + sendElements, + deleteAfterSentFiles + } +} + +export async function sendMsg(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) { + if (!sendElements.length) { + throw ("消息体无法解析") + } + const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000); + log("消息发送结果", returnMsg) + returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) + deleteAfterSentFiles.map(f => fs.unlink(f, () => { + })) + return returnMsg +} + export class SendMsg extends BaseAction { actionName = ActionName.SendMsg protected async check(payload: OB11PostSendMsg): Promise { - const messages = this.convertMessage2List(payload.message); + const messages = convertMessage2List(payload.message); const fmNum = this.getSpecialMsgNum(payload, OB11MessageDataType.node) if (fmNum && fmNum != messages.length) { return { @@ -149,7 +294,7 @@ export class SendMsg extends BaseAction { } else { throw ("发送消息参数错误, 请指定group_id或user_id") } - const messages = this.convertMessage2List(payload.message); + const messages = convertMessage2List(payload.message); if (this.getSpecialMsgNum(payload, OB11MessageDataType.node)) { try { const returnMsg = await this.handleForwardNode(peer, messages as OB11MessageNode[], group) @@ -173,27 +318,13 @@ export class SendMsg extends BaseAction { } } // log("send msg:", peer, sendElements) - const {sendElements, deleteAfterSentFiles} = await this.createSendElements(messages, group) - const returnMsg = await this.send(peer, sendElements, deleteAfterSentFiles) + const {sendElements, deleteAfterSentFiles} = await createSendElements(messages, group) + const returnMsg = await sendMsg(peer, sendElements, deleteAfterSentFiles) deleteAfterSentFiles.map(f => fs.unlink(f, () => { })); return {message_id: returnMsg.msgShortId} } - protected convertMessage2List(message: OB11MessageMixType) { - if (typeof message === "string") { - // message = [{ - // type: OB11MessageDataType.text, - // data: { - // text: message - // } - // }] as OB11MessageData[] - message = decodeCQCode(message.toString()) - } else if (!Array.isArray(message)) { - message = [message] - } - return message; - } private getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number { if (Array.isArray(payload.message)) { @@ -262,7 +393,7 @@ export class SendMsg extends BaseAction { const { sendElements, deleteAfterSentFiles - } = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group); + } = await createSendElements(convertMessage2List(messageNode.data.content), group); log("开始生成转发节点", sendElements); let sendElementsSplit: SendMessageElement[][] = [] let splitIndex = 0; @@ -284,7 +415,7 @@ export class SendMsg extends BaseAction { } // log("分割后的转发节点", sendElementsSplit) for (const eles of sendElementsSplit) { - const nodeMsg = await this.send(selfPeer, eles, [], true); + const nodeMsg = await sendMsg(selfPeer, eles, [], true); nodeMsgIds.push(nodeMsg.msgId) await sleep(500); log("转发节点生成成功", nodeMsg.msgId); @@ -346,130 +477,8 @@ export class SendMsg extends BaseAction { } } - private async createSendElements(messageData: OB11MessageData[], group: Group | undefined, ignoreTypes: OB11MessageDataType[] = []) { - let sendElements: SendMessageElement[] = [] - let deleteAfterSentFiles: string[] = [] - for (let 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: { - let 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.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.getFileCache(file) - if (cache) { - if (fs.existsSync(cache.filePath)) { - file = "file://" + cache.filePath - } else if (cache.downloadFunc) { - await cache.downloadFunc() - file = cache.filePath; - } else if (cache.url) { - file = cache.url - } - log("找到文件缓存", file); - } - const {path, isLocal, fileName, errMsg} = (await uri2local(file)) - if (errMsg) { - throw errMsg - } - if (path) { - if (!isLocal) { // 只删除http和base64转过来的文件 - deleteAfterSentFiles.push(path) - } - if (sendMsg.type === OB11MessageDataType.file) { - log("发送文件", path, payloadFileName || fileName) - sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName)); - } else if (sendMsg.type === OB11MessageDataType.video) { - log("发送视频", path, payloadFileName || fileName) - let thumb = sendMsg.data?.thumb; - if (thumb){ - let 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 || "")); - } - } - } - } break; - case OB11MessageDataType.json: { - sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data)) - }break - } - } - - return { - sendElements, - deleteAfterSentFiles - } - } - - private async send(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[], waitComplete = true) { - if (!sendElements.length) { - throw ("消息体无法解析") - } - const returnMsg = await NTQQMsgApi.sendMsg(peer, sendElements, waitComplete, 20000); - log("消息发送结果", returnMsg) - returnMsg.msgShortId = await dbUtil.addMsg(returnMsg) - deleteAfterSentFiles.map(f => fs.unlink(f, () => { - })) - return returnMsg - } private genMusicElement(url: string, audio: string, title: string, content: string, image: string): SendArkElement { const musicJson = { diff --git a/src/onebot11/server/postOB11Event.ts b/src/onebot11/server/postOB11Event.ts index fc5fd62..8c3a2b7 100644 --- a/src/onebot11/server/postOB11Event.ts +++ b/src/onebot11/server/postOB11Event.ts @@ -1,5 +1,5 @@ -import {OB11Message} from "../types"; -import {selfInfo} from "../../common/data"; +import {OB11Message, OB11MessageAt, OB11MessageData} from "../types"; +import {getGroup, selfInfo} from "../../common/data"; import {OB11BaseMetaEvent} from "../event/meta/OB11BaseMetaEvent"; import {OB11BaseNoticeEvent} from "../event/notice/OB11BaseNoticeEvent"; import {WebSocket as WebSocketClass} from "ws"; @@ -7,9 +7,47 @@ import {wsReply} from "./ws/reply"; import {log} from "../../common/utils/log"; import {getConfigUtil} from "../../common/config"; import crypto from 'crypto'; +import {NTQQFriendApi, NTQQGroupApi, NTQQMsgApi, Peer} from "../../ntqqapi/api"; +import {ChatType, Group, GroupRequestOperateTypes} from "../../ntqqapi/types"; +import {convertMessage2List, createSendElements, sendMsg} from "../action/msg/SendMsg"; +import {dbUtil} from "../../common/db"; +import {OB11FriendRequestEvent} from "../event/request/OB11FriendRequest"; +import {OB11GroupRequestEvent} from "../event/request/OB11GroupRequest"; +import {isNull} from "../../common/utils"; export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent +interface QuickActionPrivateMessage { + reply?: string; + auto_escape?: boolean; +} + +interface QuickActionGroupMessage extends QuickActionPrivateMessage { + // 回复群消息 + at_sender?: boolean + delete?: boolean + kick?: boolean + ban?: boolean + ban_duration?: number + // +} + +interface QuickActionFriendRequest { + approve?: boolean + remark?: string +} + +interface QuickActionGroupRequest { + approve?: boolean + reason?: string +} + +type QuickAction = + QuickActionPrivateMessage + & QuickActionGroupMessage + & QuickActionFriendRequest + & QuickActionGroupRequest + const eventWSList: WebSocketClass[] = []; export function registerWsEventSender(ws: WebSocketClass) { @@ -56,8 +94,77 @@ export function postOB11Event(msg: PostEventType, reportSelf = false) { method: "POST", headers, body: msgStr - }).then((res: any) => { - log(`新消息事件HTTP上报成功: ${host} ` + msgStr); + }).then(async (res) => { + log(`新消息事件HTTP上报成功: ${host} `, msgStr); + // todo: 处理不够优雅,应该使用高级泛型进行QuickAction类型识别 + let resJson: QuickAction; + try { + resJson = await res.json(); + log(`新消息事件HTTP上报返回快速操作: `, JSON.stringify(resJson)) + } catch (e) { + log(`新消息事件HTTP上报没有返回快速操作,不需要处理`) + return + } + if (msg.post_type === "message") { + msg = msg as OB11Message; + const rawMessage = await dbUtil.getMsgByShortId(msg.message_id) + resJson = resJson as QuickActionPrivateMessage | QuickActionGroupMessage + const reply = resJson.reply + let peer: Peer = { + chatType: ChatType.friend, + peerUid: msg.user_id.toString() + } + if (msg.message_type == "private") { + if (msg.sub_type === "group") { + peer.chatType = ChatType.temp + } + } else { + peer.chatType = ChatType.group + peer.peerUid = msg.group_id.toString() + } + if (reply) { + let group: Group = null + let replyMessage: OB11MessageData[] = [] + + if (msg.message_type == "group") { + group = await getGroup(msg.group_id.toString()) + if ((resJson as QuickActionGroupMessage).at_sender) { + replyMessage.push({ + type: "at", + data: { + qq: msg.user_id.toString() + } + } as OB11MessageAt) + } + } + replyMessage = replyMessage.concat(convertMessage2List(reply, resJson.auto_escape)) + const {sendElements, deleteAfterSentFiles} = await createSendElements(replyMessage, group) + sendMsg(peer, sendElements, deleteAfterSentFiles, false).then() + } else if (resJson.delete) { + NTQQMsgApi.recallMsg(peer, [rawMessage.msgId]).then() + } else if (resJson.kick) { + NTQQGroupApi.kickMember(peer.peerUid, [rawMessage.senderUid]).then() + } else if (resJson.ban) { + NTQQGroupApi.banMember(peer.peerUid, [{ + uid: rawMessage.senderUid, + timeStamp: resJson.ban_duration || 60 * 30 + }],).then() + } + + } else if (msg.post_type === "request") { + if ((msg as OB11FriendRequestEvent).request_type === "friend") { + resJson = resJson as QuickActionFriendRequest + if (!isNull(resJson.approve)) { + // todo: set remark + NTQQFriendApi.handleFriendRequest(parseInt((msg as OB11FriendRequestEvent).flag), resJson.approve).then() + } + } else if ((msg as OB11GroupRequestEvent).request_type === "group") { + resJson = resJson as QuickActionGroupRequest + if (!isNull(resJson.approve)) { + NTQQGroupApi.handleGroupRequest((msg as OB11FriendRequestEvent).flag, resJson.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject, resJson.reason).then() + } + } + } }, (err: any) => { log(`新消息事件HTTP上报失败: ${host} `, err, msg); }); diff --git a/test/quick_action/server.py b/test/quick_action/server.py new file mode 100644 index 0000000..b8dbc01 --- /dev/null +++ b/test/quick_action/server.py @@ -0,0 +1,29 @@ +import uvicorn +from fastapi import FastAPI, Request + +app = FastAPI() + +@app.post("/") +async def root(request: Request): + data = await request.json() + print(data) + if (data["post_type"] == "message"): + text = list(filter(lambda x: x["type"] == "text", data["message"]))[0]["data"]["text"] + if text == "禁言": + return {"ban": True, "ban_duration": 10} + elif text == "踢我": + return {"kick": True} + elif text == "撤回": + return {"delete": True} +# print(data["message"]) + return {"reply": "Hello World"} + elif data["post_type"] == "request": + if data["request_type"] == "group": + return {"approve": False, "reason": "不让你进群"} + else: + # 加好友 + return {"approve": True} + return {} + +if __name__ == "__main__": + uvicorn.run(app, host="", port=8000) \ No newline at end of file