feat: http post quick operation

This commit is contained in:
linyuchen 2024-03-23 19:16:07 +08:00
parent 89c3f07cba
commit 1735babb7d
6 changed files with 306 additions and 151 deletions

View File

@ -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;

View File

@ -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";

View File

@ -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<Payload, any> {
protected async _handle(payload: Payload): Promise<any> {
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") {

View File

@ -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<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
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<OB11PostSendMsg, ReturnDataType> {
} 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<OB11PostSendMsg, ReturnDataType> {
}
}
// 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<OB11PostSendMsg, ReturnDataType> {
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<OB11PostSendMsg, ReturnDataType> {
}
// 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<OB11PostSendMsg, ReturnDataType> {
}
}
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 = {

View File

@ -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);
});

View File

@ -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)