diff --git a/package-lock.json b/package-lock.json index bad3962..3b11f41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "devDependencies": { "@babel/preset-env": "^7.23.2", "@types/express": "^4.17.20", + "@types/uuid": "^9.0.8", "babel-loader": "^9.1.3", "electron": "^27.0.2", "ts-loader": "^9.5.0", @@ -2305,6 +2306,12 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://mirrors.cloud.tencent.com/npm/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@types/yauzl": { "version": "2.10.2", "resolved": "https://mirrors.cloud.tencent.com/npm/@types/yauzl/-/yauzl-2.10.2.tgz", diff --git a/package.json b/package.json index cd84ed2..f3327b2 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@babel/preset-env": "^7.23.2", "@types/express": "^4.17.20", + "@types/uuid": "^9.0.8", "babel-loader": "^9.1.3", "electron": "^27.0.2", "ts-loader": "^9.5.0", diff --git a/src/common/IPCChannel.ts b/src/common/channels.ts similarity index 100% rename from src/common/IPCChannel.ts rename to src/common/channels.ts diff --git a/src/main/config.ts b/src/common/config.ts similarity index 94% rename from src/main/config.ts rename to src/common/config.ts index 3ef443d..44b73d5 100644 --- a/src/main/config.ts +++ b/src/common/config.ts @@ -1,4 +1,4 @@ -import {Config} from "../common/types"; +import {Config} from "./types"; const fs = require("fs") diff --git a/src/common/data.ts b/src/common/data.ts new file mode 100644 index 0000000..c9aa843 --- /dev/null +++ b/src/common/data.ts @@ -0,0 +1,30 @@ +import {Group, MessageElement, RawMessage, SelfInfo, User} from "./types"; + +export let groups: Group[] = [] +export let friends: User[] = [] + +export function getFriend(qq: string): User | undefined { + return friends.find(friend => friend.uin === qq) +} + +export function getGroup(qq: string): Group | undefined { + return groups.find(group => group.uid === qq) +} + +export function getGroupMember(groupQQ: string, memberQQ: string) { + const group = getGroup(groupQQ) + if (group) { + return group.members?.find(member => member.uin === memberQQ) + } +} + +export let selfInfo: SelfInfo = { + user_id: "", + nickname: "" +} + +export let msgHistory: Record<string, RawMessage> = {} + +export function getHistoryMsgBySeq(seq: string) { + return Object.values(msgHistory).find(msg => msg.msgSeq === seq) +} diff --git a/src/common/types.ts b/src/common/types.ts index 1a00311..ae79f8a 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,3 +1,5 @@ +import {OB11ApiName, OB11MessageData} from "../onebot11/types"; + export enum AtType { notAt = 0, atAll = 1, @@ -24,12 +26,6 @@ export interface GroupMemberInfo { uin: string; // QQ号 } -export const OnebotGroupMemberRole = { - 4: 'owner', - 3: 'admin', - 2: 'member' -} - export interface SelfInfo { user_id: string; @@ -79,42 +75,48 @@ export interface PttElement { waveAmplitudes: number[] } -export interface ArkElement{ +export interface ArkElement { bytesData: string } +export interface RawMessage { + msgId: string, + msgTime: string, + msgSeq: string, + senderUin: string; // 发送者QQ号 + peerUid: string; // 群号 或者 QQ uid + peerUin: string; // 群号 或者 发送者QQ号 + sendNickName: string; + sendMemberName?: string; // 发送者群名片 + chatType: ChatType, + elements: { + replyElement: { + senderUid: string, // 原消息发送者QQ号 + sourceMsgIsIncPic: boolean; // 原消息是否有图片 + sourceMsgText: string; + replayMsgSeq: string; // 源消息的msgSeq,可以通过这个找到源消息的msgId + }, + textElement: { + atType: AtType + atUid: string, + content: string, + atNtUid: string + }, + picElement: { + sourcePath: string // 图片本地路径 + picWidth: number + picHeight: number + fileSize: number + fileName: string + fileUuid: string + }, + pttElement: PttElement, + arkElement: ArkElement + }[] +} + export interface MessageElement { - raw: { - msgId: string, - msgTime: string, - msgSeq: string, - senderUin: string; // 发送者QQ号 - chatType: ChatType, - elements: { - replyElement: { - senderUid: string, // 原消息发送者QQ号 - sourceMsgIsIncPic: boolean; // 原消息是否有图片 - sourceMsgText: string; - replayMsgSeq: string; // 源消息的msgSeq,可以通过这个找到源消息的msgId - }, - textElement: { - atType: AtType - atUid: string, - content: string, - atNtUid: string - }, - picElement: { - sourcePath: string // 图片本地路径 - picWidth: number - picHeight: number - fileSize: number - fileName: string - fileUuid: string - }, - pttElement: PttElement, - arkElement: ArkElement - }[] - } + raw: RawMessage peer: Peer, sender: { uid: string // 一串加密的字符串 @@ -123,60 +125,17 @@ export interface MessageElement { } } -export enum MessageType { - text = "text", - image = "image", - voice = "record", - at = "at", - reply = "reply", - json = "json" -} - -export type SendMessage = { - type: MessageType.text, - content: string, - data?: { - text: string, // 纯文本 - } -} | { - type: "image" | "voice" | "record", - file: string, // 本地路径 - data?: { - file: string // 本地路径 - } -} | { - type: MessageType.at, - atType?: AtType, - content?: string, - atUid?: string, - atNtUid?: string, - data?: { - qq: string // at的qq号 - } -} | { - type: MessageType.reply, - msgId: string, - msgSeq: string, - senderUin: string, - data: { - id: string, - } -} - -export type PostDataAction = "send_private_msg" | "send_group_msg" | "get_group_list" - | "get_friend_list" | "delete_msg" | "get_login_info" | "get_group_member_list" | "get_group_member_info" - export interface PostDataSendMsg { - action: PostDataAction + action: OB11ApiName message_type?: "private" | "group" params?: { user_id: string, group_id: string, - message: SendMessage[]; + message: OB11MessageData[]; }, user_id: string, group_id: string, - message: SendMessage[]; + message?: OB11MessageData[]; ipc_uuid?: string } @@ -189,9 +148,3 @@ export interface Config { log?: boolean } -export interface SendMsgResult { - status: number - retcode: number - data: any - message: string -} \ No newline at end of file diff --git a/src/main/utils.ts b/src/common/utils.ts similarity index 60% rename from src/main/utils.ts rename to src/common/utils.ts index bfafa66..04262b2 100644 --- a/src/main/utils.ts +++ b/src/common/utils.ts @@ -2,6 +2,7 @@ import * as path from "path"; import {json} from "express"; import {selfInfo} from "./data"; import {ConfigUtil} from "./config"; +import util from "util"; const fs = require('fs'); @@ -12,7 +13,7 @@ export function getConfigUtil() { return new ConfigUtil(configFilePath) } -export function log(msg: any) { +export function log(...msg: any[]) { if (!getConfigUtil().getConfig().log){ return } @@ -23,7 +24,17 @@ export function log(msg: any) { const day = date.getDate(); const currentDate = `${year}-${month}-${day}`; const userInfo = selfInfo.user_id ? `${selfInfo.nickname}(${selfInfo.user_id})` : "" - fs.appendFile(path.join(CONFIG_DIR , `llonebot-${currentDate}.log`), currentDateTime + ` ${userInfo}:` + JSON.stringify(msg) + "\n", (err: any) => { + let logMsg = ""; + for (let msgItem of msg){ + // 判断是否是对象 + if (typeof msgItem === "object"){ + logMsg += JSON.stringify(msgItem) + " "; + continue; + } + logMsg += msgItem + " "; + } + logMsg = `${currentDateTime} ${userInfo}: ${logMsg}\n` + fs.appendFile(path.join(CONFIG_DIR , `llonebot-${currentDate}.log`), logMsg, (err: any) => { }) } @@ -55,3 +66,29 @@ export function checkFileReceived(path: string, timeout: number=3000): Promise<v check(); }); } + +export async function file2base64(path: string){ + const readFile = util.promisify(fs.readFile); + let result = { + err: "", + data: "" + } + try { + // 读取文件内容 + // if (!fs.existsSync(path)){ + // path = path.replace("\\Ori\\", "\\Thumb\\"); + // } + try { + await checkFileReceived(path, 5000); + } catch (e: any) { + result.err = e.toString(); + return result; + } + const data = await readFile(path); + // 转换为Base64编码 + result.data = data.toString('base64'); + } catch (err) { + result.err = err.toString(); + } + return result; +} diff --git a/src/global.d.ts b/src/global.d.ts index 221edbf..646642c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -6,11 +6,13 @@ import { Peer, PostDataSendMsg, PttElement, SelfInfo, - SendMessage, SendMsgResult, User } from "./common/types"; +import {OB11Return, OB11MessageData} from "./onebot11/types"; + + declare var LLAPI: { on(event: "new-messages" | "new-send-messages", callback: (data: MessageElement[]) => void): void; on(event: "context-msg-menu", callback: (event: any, target: any, msgIds:any) => void): void; @@ -20,7 +22,7 @@ declare var LLAPI: { }> getUserInfo(uid: string): Promise<User>; // uid是一串加密的字符串 - sendMessage(peer: Peer, message: SendMessage[]): Promise<any>; + sendMessage(peer: Peer, message: OB11MessageData[]): Promise<any>; recallMessage(peer: Peer, msgIds: string[]): Promise<void>; getGroupsList(forced: boolean): Promise<Group[]> getFriendsList(forced: boolean): Promise<User[]> @@ -47,7 +49,7 @@ declare var llonebot: { downloadFile(arg: {uri: string, fileName: string}):Promise<{errMsg: string, path: string}>; deleteFile(path: string[]):Promise<void>; getRunningStatus(): Promise<boolean>; - sendSendMsgResult(sessionId: string, msgResult: SendMsgResult): void; + sendSendMsgResult(sessionId: string, msgResult: OB11Return): void; file2base64(path: string): Promise<{err: string, data: string}>; }; diff --git a/src/main/data.ts b/src/main/data.ts deleted file mode 100644 index 49c09f8..0000000 --- a/src/main/data.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {Group, SelfInfo, User} from "../common/types"; - -export let groups: Group[] = [] -export let friends: User[] = [] - -export let selfInfo: SelfInfo = { - user_id: "", - nickname: "" -} \ No newline at end of file diff --git a/src/main/IPCSend.ts b/src/main/ipcsend.ts similarity index 74% rename from src/main/IPCSend.ts rename to src/main/ipcsend.ts index 2bf00c4..1a026af 100644 --- a/src/main/IPCSend.ts +++ b/src/main/ipcsend.ts @@ -1,8 +1,11 @@ import {ipcMain, webContents} from 'electron'; -import {PostDataSendMsg, SendMsgResult} from "../common/types"; -import {CHANNEL_RECALL_MSG, CHANNEL_SEND_MSG} from "../common/IPCChannel"; +import {PostDataSendMsg} from "../common/types"; +import {CHANNEL_RECALL_MSG, CHANNEL_SEND_MSG} from "../common/channels"; import {v4 as uuid4} from "uuid"; -import {log} from "./utils"; +import {log} from "../common/utils"; + + +import {OB11Return} from "../onebot11/types"; function sendIPCMsg(channel: string, data: any) { let contents = webContents.getAllWebContents(); @@ -16,10 +19,10 @@ function sendIPCMsg(channel: string, data: any) { } -export function sendIPCSendQQMsg(postData: PostDataSendMsg, handleSendResult: (data: SendMsgResult) => void) { +export function sendIPCSendQQMsg(postData: PostDataSendMsg, handleSendResult: (data: OB11Return<any>) => void) { const onceSessionId = "llonebot_send_msg_" + uuid4(); postData.ipc_uuid = onceSessionId; - ipcMain.once(onceSessionId, (event: any, sendResult: SendMsgResult) => { + ipcMain.once(onceSessionId, (event: any, sendResult: OB11Return<any>) => { // log("llonebot send msg ipcMain.once:" + JSON.stringify(sendResult)); try { handleSendResult(sendResult) diff --git a/src/main/main.ts b/src/main/main.ts index c0840cd..8bd3457 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,10 +1,10 @@ // 运行在 Electron 主进程 下的插件入口 import * as path from "path"; -import {ipcMain} from 'electron'; +import {BrowserWindow, ipcMain} from 'electron'; import * as util from 'util'; -import {Config, Group, SelfInfo, User} from "../common/types"; +import {Config, Group, RawMessage, SelfInfo, User} from "../common/types"; import { CHANNEL_DOWNLOAD_FILE, CHANNEL_GET_CONFIG, @@ -15,12 +15,14 @@ import { CHANNEL_START_HTTP_SERVER, CHANNEL_UPDATE_FRIENDS, CHANNEL_UPDATE_GROUPS, CHANNEL_DELETE_FILE, CHANNEL_GET_RUNNING_STATUS, CHANNEL_FILE2BASE64 -} from "../common/IPCChannel"; -import {ConfigUtil} from "./config"; -import {startExpress} from "./HttpServer"; -import {checkFileReceived, CONFIG_DIR, getConfigUtil, isGIF, log} from "./utils"; -import {friends, groups, selfInfo} from "./data"; +} from "../common/channels"; +import {ConfigUtil} from "../common/config"; +import {postMsg, startExpress} from "../server/httpserver"; +import {checkFileReceived, CONFIG_DIR, file2base64, getConfigUtil, isGIF, log} from "../common/utils"; +import {friends, groups, msgHistory, selfInfo} from "../common/data"; import {} from "../global"; +import {hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook"; +import {OB11Construct} from "../onebot11/construct"; const fs = require('fs'); @@ -29,7 +31,7 @@ let running = false; // 加载插件时触发 function onLoad() { - log("main onLoaded"); + log("llonebot main onLoad"); // const config_dir = browserWindow.LiteLoader.plugins["LLOneBot"].path.data; @@ -138,24 +140,7 @@ function onLoad() { }) ipcMain.on(CHANNEL_POST_ONEBOT_DATA, (event: any, arg: any) => { - for (const host of getConfigUtil().getConfig().hosts) { - try { - fetch(host, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-self-id": selfInfo.user_id - }, - body: JSON.stringify(arg) - }).then((res: any) => { - log(`新消息事件上传成功: ${host} ` + JSON.stringify(arg)); - }, (err: any) => { - log(`新消息事件上传失败: ${host} ` + err + JSON.stringify(arg)); - }); - } catch (e: any) { - log(e.toString()) - } - } + postMsg(arg); }) ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { @@ -179,36 +164,26 @@ function onLoad() { }) ipcMain.handle(CHANNEL_FILE2BASE64, async (event: any, path: string): Promise<{err: string, data: string}> => { - const readFile = util.promisify(fs.readFile); - let result = { - err: "", - data: "" + return await file2base64(path); + }) + + registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { + for (const message of payload.msgList) { + OB11Construct.constructMessage(message).then((msg) => { + postMsg(msg); + }); } - try { - // 读取文件内容 - // if (!fs.existsSync(path)){ - // path = path.replace("\\Ori\\", "\\Thumb\\"); - // } - try { - await checkFileReceived(path, 5000); - } catch (e: any) { - result.err = e.toString(); - return result; - } - const data = await readFile(path); - // 转换为Base64编码 - result.data = data.toString('base64'); - } catch (err) { - result.err = err.toString(); - } - return result; }) } // 创建窗口时触发 -function onBrowserWindowCreated(window: any) { - +function onBrowserWindowCreated(window: BrowserWindow) { + try { + hookNTQQApiReceive(window); + } catch (e){ + log("llonebot hook error: ", e.toString()) + } } try { diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts new file mode 100644 index 0000000..9a077e9 --- /dev/null +++ b/src/ntqqapi/hook.ts @@ -0,0 +1,69 @@ +import {BrowserWindow} from 'electron'; +import {log} from "../common/utils"; +import {NTQQApiClass} from "./ntcall"; +import {RawMessage} from "../common/types"; +import {msgHistory} from "../common/data"; + +export enum ReceiveCmd { + UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate", + NEW_MSG = "nodeIKernelMsgListener/onRecvMsg" +} + +interface NTQQApiReturnData extends Array<any> { + 0: { + "type": "request", + "eventName": NTQQApiClass + }, + 1: + { + cmdName: ReceiveCmd, + cmdType: "event", + payload: unknown + }[] +} + +let receiveHooks: Array<{ + method: ReceiveCmd, + hookFunc: (payload: unknown) => void +}> = [] + +export function hookNTQQApiReceive(window: BrowserWindow) { + const originalSend = window.webContents.send; + const patchSend = (channel: string, ...args: NTQQApiReturnData) => { + // 判断是否是列表 + if (args?.[1] instanceof Array) { + for (let receiveData of args?.[1]) { + const ntQQApiMethodName = receiveData.cmdName; + log(`received ntqq api message: ${channel} ${ntQQApiMethodName}`, JSON.stringify(receiveData)) + for (let hook of receiveHooks) { + if (hook.method === ntQQApiMethodName) { + hook.hookFunc(receiveData.payload); + } + } + } + } + + return originalSend.call(window.webContents, channel, ...args); + } + window.webContents.send = patchSend; +} + +export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void) { + receiveHooks.push({ + method, + hookFunc + }) +} + +registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.UPDATE_MSG, (payload) => { + for (const message of payload.msgList) { + msgHistory[message.msgId] = message; + } +}) + +registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => { + for (const message of payload.msgList) { + log("收到新消息,push到历史记录", message) + msgHistory[message.msgId] = message; + } +}) \ No newline at end of file diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts new file mode 100644 index 0000000..dfdccff --- /dev/null +++ b/src/ntqqapi/ntcall.ts @@ -0,0 +1,65 @@ +import {ipcMain} from "electron"; +import {v4 as uuidv4} from "uuid"; + +interface IPCReceiveEvent { + eventName: string + callbackId: string +} + +export type IPCReceiveDetail = [ + { + cmdName: NTQQApiMethod + payload: unknown + }, +] + +export enum NTQQApiClass { + NT_API = "ns-ntApi", + NT_FS_API = "ns-FsApi", +} + +export enum NTQQApiMethod { + LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", + UPDATE_MSG = "nodeIKernelMsgListener/onMsgInfoListUpdate" +} + +enum NTQQApiChannel { + IPC_UP_2 = "IPC_UP_2", + IPC_UP_3 = "IPC_UP_3", + IPC_UP_1 = "IPC_UP_1", +} + + +function callNTQQApi(channel: NTQQApiChannel, className: NTQQApiClass, methodName: NTQQApiMethod, args: unknown[]=[]) { + const uuid = uuidv4(); + return new Promise((resolve, reject) => { + ipcMain.emit( + channel, + { + sender: { + send: (args: [string, IPCReceiveEvent, IPCReceiveDetail]) => { + resolve(args) + }, + }, + }, + {type: 'request', callbackId: uuid, eventName: className + "-" + channel[channel.length - 1]}, + [methodName, ...args], + ) + }) +} + + +export class NTQQApi { + // static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND) + static likeFriend(uid: string, count = 1) { + return callNTQQApi(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND, [{ + doLikeUserInfo: { + friendUid: uid, + sourceId: 71, + doLikeCount: count, + doLikeTollCount: 0 + } + }, + null]) + } +} \ No newline at end of file diff --git a/src/onebot11/construct.ts b/src/onebot11/construct.ts new file mode 100644 index 0000000..fd32c18 --- /dev/null +++ b/src/onebot11/construct.ts @@ -0,0 +1,134 @@ +import {OB11MessageDataType, OB11GroupMemberRole, OB11Message, OB11MessageData} from "./types"; +import {AtType, ChatType, RawMessage} from "../common/types"; +import {getFriend, getGroupMember, getHistoryMsgBySeq, msgHistory, selfInfo} from "../common/data"; +import {file2base64, getConfigUtil} from "../common/utils"; + + +export class OB11Construct { + static async constructMessage(msg: RawMessage): Promise<OB11Message> { + const {debug, enableBase64} = getConfigUtil().getConfig() + const message_type = msg.chatType == ChatType.group ? "group" : "private"; + const resMsg: OB11Message = { + self_id: selfInfo.user_id, + user_id: msg.senderUin, + time: parseInt(msg.msgTime) || 0, + message_id: msg.msgId, + real_id: msg.msgId, + message_type: msg.chatType == ChatType.group ? "group" : "private", + sender: { + user_id: msg.senderUin, + nickname: msg.sendNickName, + card: msg.sendMemberName || "", + }, + raw_message: "", + font: 14, + sub_type: "friend", + message: [], + post_type: "message", + } + if (msg.chatType == ChatType.group) { + resMsg.group_id = msg.peerUin + const member = getGroupMember(msg.peerUin, msg.senderUin); + if (member) { + resMsg.sender.role = OB11Construct.constructGroupMemberRole(member.role); + } + } else if (msg.chatType == ChatType.friend) { + resMsg.sub_type = "friend" + const friend = getFriend(msg.senderUin); + if (friend) { + resMsg.sender.nickname = friend.nickName; + } + } else if (msg.chatType == ChatType.temp) { + resMsg.sub_type = "group" + } + + + if (debug) { + resMsg.raw = msg + } + + for (let element of msg.elements) { + let message_data: any = { + data: {}, + type: "unknown" + } + if (element.textElement && element.textElement?.atType !== AtType.notAt) { + message_data["type"] = OB11MessageDataType.at + if (element.textElement.atType == AtType.atAll) { + message_data["data"]["mention"] = "all" + message_data["data"]["qq"] = "all" + } else { + let uid = element.textElement.atNtUid + let atMember = getGroupMember(msg.peerUin, uid) + message_data["data"]["mention"] = atMember!.uin + message_data["data"]["qq"] = atMember!.uin + } + } else if (element.textElement) { + message_data["type"] = "text" + message_data["data"]["text"] = element.textElement.content + } else if (element.picElement) { + message_data["type"] = "image" + message_data["data"]["file_id"] = element.picElement.fileUuid + message_data["data"]["path"] = element.picElement.sourcePath + message_data["data"]["file"] = element.picElement.sourcePath + } else if (element.replyElement) { + message_data["type"] = "reply" + const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq) + if (replyMsg) { + message_data["data"]["id"] = replyMsg.msgId + } + else{ + continue + } + } else if (element.pttElement) { + message_data["type"] = OB11MessageDataType.voice; + message_data["data"]["file"] = element.pttElement.filePath + message_data["data"]["file_id"] = element.pttElement.fileUuid + // console.log("收到语音消息", message.raw.msgId, message.peer, element.pttElement) + // window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { + // console.log("语音转文字结果", text); + // }).catch(err => { + // console.log("语音转文字失败", err); + // }) + } else if (element.arkElement) { + message_data["type"] = OB11MessageDataType.json; + message_data["data"]["data"] = element.arkElement.bytesData; + } + if (message_data.data.file) { + let filePath: string = message_data.data.file; + message_data.data.file = "file://" + filePath + if (enableBase64) { + // filePath = filePath.replace("\\Ori\\", "\\Thumb\\") + let {err, data} = await file2base64(filePath); + if (err) { + console.log("文件转base64失败", err) + } else { + message_data.data.file = "base64://" + data + } + } + } + if (message_data.type !== "unknown" && message_data.data) { + resMsg.message.push(message_data); + } + } + // if (msgHistory.length > 10000) { + // msgHistory.splice(0, 100) + // } + // msgHistory.push(message) + // if (!reportSelfMessage && onebot_message_data["user_id"] == self_qq) { + // console.log("开启了不上传自己发送的消息,进行拦截 ", onebot_message_data); + // } else { + // console.log("发送上传消息给ipc main", onebot_message_data); + // window.llonebot.postData(onebot_message_data); + // } + return resMsg; + } + + static constructGroupMemberRole(role: number): OB11GroupMemberRole { + return { + 4: OB11GroupMemberRole.owner, + 3: OB11GroupMemberRole.admin, + 2: OB11GroupMemberRole.member + }[role] + } +} \ No newline at end of file diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts new file mode 100644 index 0000000..34975d6 --- /dev/null +++ b/src/onebot11/types.ts @@ -0,0 +1,105 @@ +import {AtType, RawMessage} from "../common/types"; + +export enum OB11UserSex{ + male = "male", + female = "female", + unknown = "unknown" +} + +export enum OB11GroupMemberRole{ + owner = "owner", + admin = "admin", + member = "member", +} + +interface OB11Sender { + user_id: string, + nickname: string, + sex?: OB11UserSex, + age?: number, + card?: string, // 群名片 + level?: string, // 群等级 + role?: OB11GroupMemberRole +} + +export enum OB11MessageType { + private = "private", + group = "group" +} + +export interface OB11Message { + self_id?: string, + time: number, + message_id: string, + real_id: string, + user_id: string, + group_id?: string, + message_type: "private" | "group", + sub_type?: "friend" | "group" | "other", + sender: OB11Sender, + message: OB11MessageData[], + raw_message: string, + font: number, + post_type?: "message", + raw?: RawMessage +} + +export type OB11ApiName = + "send_private_msg" + | "send_group_msg" + | "get_group_list" + | "get_friend_list" + | "delete_msg" + | "get_login_info" + | "get_group_member_list" + | "get_group_member_info" + | "get_msg" + +export interface OB11Return<DataType> { + status: number + retcode: number + data: DataType + message: string +} + +export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{} + +export enum OB11MessageDataType { + text = "text", + image = "image", + voice = "record", + at = "at", + reply = "reply", + json = "json" +} + +export type OB11MessageData = { + type: OB11MessageDataType.text, + content: string, + data?: { + text: string, // 纯文本 + } +} | { + type: "image" | "voice" | "record", + file: string, // 本地路径 + data?: { + file: string // 本地路径 + } +} | { + type: OB11MessageDataType.at, + atType?: AtType, + content?: string, + atUid?: string, + atNtUid?: string, + data?: { + qq: string // at的qq号 + } +} | { + type: OB11MessageDataType.reply, + msgId: string, + msgSeq: string, + senderUin: string, + data: { + id: string, + } +} \ No newline at end of file diff --git a/src/preload.ts b/src/preload.ts index 93be597..06922ab 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,6 +1,6 @@ // Electron 主进程 与 渲染进程 交互的桥梁 -import {Config, Group, PostDataSendMsg, SelfInfo, SendMsgResult, User} from "./common/types"; +import {Config, Group, PostDataSendMsg, SelfInfo, User} from "./common/types"; import { CHANNEL_DOWNLOAD_FILE, CHANNEL_GET_CONFIG, @@ -15,7 +15,10 @@ import { CHANNEL_UPDATE_GROUPS, CHANNEL_DELETE_FILE, CHANNEL_GET_RUNNING_STATUS, CHANNEL_FILE2BASE64 -} from "./common/IPCChannel"; +} from "./common/channels"; + + +import {OB11Return, OB11SendMsgReturn} from "./onebot11/types"; const {contextBridge} = require("electron"); @@ -33,7 +36,7 @@ contextBridge.exposeInMainWorld("llonebot", { updateFriends: (friends: User[]) => { ipcRenderer.send(CHANNEL_UPDATE_FRIENDS, friends); }, - sendSendMsgResult: (sessionId: string, msgResult: SendMsgResult)=>{ + sendSendMsgResult: (sessionId: string, msgResult: OB11SendMsgReturn)=>{ ipcRenderer.send(sessionId, msgResult); }, listenSendMessage: (handle: (jsonData: PostDataSendMsg) => void) => { diff --git a/src/renderer.ts b/src/renderer.ts index 9c50b0e..041ed23 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -4,14 +4,14 @@ import { AtType, ChatType, Group, - MessageElement, MessageType, - OnebotGroupMemberRole, - Peer, + MessageElement, Peer, PostDataSendMsg, - SendMsgResult, User } from "./common/types"; + +import {OB11Return, OB11SendMsgReturn, OB11MessageDataType} from "./onebot11/types"; + let self_qq: string = "" let groups: Group[] = [] let friends: User[] = [] @@ -120,142 +120,15 @@ async function getGroupMember(group_qq: string, member_uid: string) { } } -async function handleNewMessage(messages: MessageElement[]) { - console.log("llonebot 收到消息:", messages); - const {debug, enableBase64, reportSelfMessage} = await window.llonebot.getConfig(); - for (let message of messages) { - let onebot_message_data: any = { - self: { - platform: "qq", - user_id: self_qq - }, - self_id: self_qq, - time: parseInt(message.raw.msgTime || "0"), - type: "message", - post_type: "message", - message_type: message.peer.chatType, - detail_type: message.peer.chatType, - message_id: message.raw.msgId, - sub_type: "", - message: [], - raw_message: "", - font: 14 - } - if (debug) { - onebot_message_data.raw = JSON.parse(JSON.stringify(message)) - } - if (message.raw.chatType == ChatType.group) { - let group_id = message.peer.uid - let group = (await getGroup(group_id))! - onebot_message_data.detail_type = onebot_message_data.message_type = onebot_message_data.sub_type = "group" - onebot_message_data["group_id"] = message.peer.uid - let groupMember = await getGroupMember(group_id, message.sender.uid) - onebot_message_data["user_id"] = groupMember!.uin - onebot_message_data.sender = { - user_id: groupMember!.uin, - nickname: groupMember!.nick, - card: groupMember!.cardName, - role: OnebotGroupMemberRole[groupMember!.role] - } - // console.log("收到群消息", onebot_message_data) - } else if (message.raw.chatType == ChatType.friend) { - onebot_message_data["user_id"] = message.raw.senderUin; - onebot_message_data.detail_type = onebot_message_data.message_type = "private" - onebot_message_data.sub_type = "friend" - let friend = await getFriend(message.raw.senderUin); - onebot_message_data.sender = { - user_id: friend!.uin, - nickname: friend!.nickName - } - } else if (message.raw.chatType == ChatType.temp) { - let senderQQ = message.raw.senderUin; - let senderUid = message.sender.uid; - let sender = await getUserInfo(senderUid); - onebot_message_data["user_id"] = senderQQ; - onebot_message_data.detail_type = onebot_message_data.message_type = "private" - onebot_message_data.sub_type = "group"; - onebot_message_data.sender = { - user_id: senderQQ, - nickname: sender.nickName - } - } - for (let element of message.raw.elements) { - let message_data: any = { - data: {}, - type: "unknown" - } - if (element.textElement && element.textElement?.atType !== AtType.notAt) { - message_data["type"] = "at" - if (element.textElement.atType == AtType.atAll) { - message_data["data"]["mention"] = "all" - message_data["data"]["qq"] = "all" - } else { - let uid = element.textElement.atNtUid - let atMember = await getGroupMember(message.peer.uid, uid) - message_data["data"]["mention"] = atMember!.uin - message_data["data"]["qq"] = atMember!.uin - } - } else if (element.textElement) { - message_data["type"] = "text" - message_data["data"]["text"] = element.textElement.content - } else if (element.picElement) { - message_data["type"] = "image" - message_data["data"]["file_id"] = element.picElement.fileUuid - message_data["data"]["path"] = element.picElement.sourcePath - message_data["data"]["file"] = element.picElement.sourcePath - } else if (element.replyElement) { - message_data["type"] = "reply" - message_data["data"]["id"] = msgHistory.find(msg => msg.raw.msgSeq == element.replyElement.replayMsgSeq)?.raw.msgId - } else if (element.pttElement) { - message_data["type"] = MessageType.voice; - message_data["data"]["file"] = element.pttElement.filePath - message_data["data"]["file_id"] = element.pttElement.fileUuid - // console.log("收到语音消息", message.raw.msgId, message.peer, element.pttElement) - // window.LLAPI.Ptt2Text(message.raw.msgId, message.peer, messages).then(text => { - // console.log("语音转文字结果", text); - // }).catch(err => { - // console.log("语音转文字失败", err); - // }) - } else if (element.arkElement) { - message_data["type"] = MessageType.json; - message_data["data"]["data"] = element.arkElement.bytesData; - } - if (message_data.data.file) { - let filePath: string = message_data.data.file; - message_data.data.file = "file://" + filePath - if (enableBase64) { - // filePath = filePath.replace("\\Ori\\", "\\Thumb\\") - let {err, data} = await window.llonebot.file2base64(filePath); - if (err) { - console.log("文件转base64失败", err) - } else { - message_data.data.file = "base64://" + data - } - } - } - if (message_data.type !== "unknown"){ - onebot_message_data.message.push(message_data); - } - } - if (msgHistory.length > 10000) { - msgHistory.splice(0, 100) - } - msgHistory.push(message) - if (!reportSelfMessage && onebot_message_data["user_id"] == self_qq){ - console.log("开启了不上传自己发送的消息,进行拦截 ", onebot_message_data); - } else { - console.log("发送上传消息给ipc main", onebot_message_data); - window.llonebot.postData(onebot_message_data); - } - } -} async function listenSendMessage(postData: PostDataSendMsg) { console.log("收到发送消息请求", postData); - let sendMsgResult: SendMsgResult = { + let sendMsgResult: OB11SendMsgReturn = { retcode: 0, status: 0, - data: {}, + data: { + message_id: "" + }, message: "发送成功" } if (postData.action == "send_private_msg" || postData.action == "send_group_msg") { @@ -319,8 +192,7 @@ async function listenSendMessage(postData: PostDataSendMsg) { message.atType = AtType.atAll atUid = "0"; message.content = `@全体成员` - } - else { + } else { let group = await getGroup(postData.params.group_id) let atMember = group.members.find(member => member.uin == atUid) message.atNtUid = atMember.uid @@ -374,11 +246,13 @@ async function listenSendMessage(postData: PostDataSendMsg) { window.llonebot.sendSendMsgResult(postData.ipc_uuid, sendMsgResult) return; } - window.LLAPI.sendMessage(peer, postData.params.message).then(res => { - console.log("消息发送成功:", peer, postData.params.message) + window.LLAPI.sendMessage(peer, postData.params.message).then( + (res: MessageElement) => { + console.log("消息发送成功:", res, peer, postData.params.message) if (sendFiles.length) { window.llonebot.deleteFile(sendFiles); } + sendMsgResult.data.message_id = res.raw.msgId; window.llonebot.sendSendMsgResult(postData.ipc_uuid, sendMsgResult) }, err => { @@ -421,19 +295,6 @@ async function getGroupsMembers(groupsArg: Group[]) { } } -function onNewMessages(messages: MessageElement[]) { - async function func(messages: MessageElement[]) { - console.log("收到新消息", messages) - if (!self_qq) { - self_qq = (await window.LLAPI.getAccountInfo()).uin - } - await handleNewMessage(messages); - } - - func(messages).then(() => { - }) - // console.log("chatListEle", chatListEle) -} async function initAccountInfo() { let accountInfo = await window.LLAPI.getAccountInfo(); @@ -471,8 +332,6 @@ function onLoad() { }); }); } - window.LLAPI.on("new-messages", onNewMessages); - window.LLAPI.on("new-send-messages", onNewMessages); window.llonebot.log("llonebot render start"); window.llonebot.startExpress(); diff --git a/src/main/HttpServer.ts b/src/server/httpserver.ts similarity index 67% rename from src/main/HttpServer.ts rename to src/server/httpserver.ts index f3fa148..105aa20 100644 --- a/src/main/HttpServer.ts +++ b/src/server/httpserver.ts @@ -1,13 +1,20 @@ -import {log} from "./utils"; +import {getConfigUtil, log} from "../common/utils"; + +// const express = require("express"); +import express from "express"; +import {Request} from 'express'; +import {Response} from 'express'; -const express = require("express"); const JSONbig = require('json-bigint'); -import {sendIPCRecallQQMsg, sendIPCSendQQMsg} from "./IPCSend"; -import {OnebotGroupMemberRole, PostDataAction, PostDataSendMsg, SendMessage, SendMsgResult} from "../common/types"; -import {friends, groups, selfInfo} from "./data"; +import {sendIPCRecallQQMsg, sendIPCSendQQMsg} from "../main/ipcsend"; +import {PostDataSendMsg} from "../common/types"; +import {friends, groups, msgHistory, selfInfo} from "../common/data"; +import {OB11ApiName, OB11Message, OB11Return, OB11MessageData} from "../onebot11/types"; +import {OB11Construct} from "../onebot11/construct"; + // @SiberianHusky 2021-08-15 -function checkSendMessage(sendMsgList: SendMessage[]) { +function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkUri(uri: string): boolean { const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/; return pattern.test(uri); @@ -44,7 +51,17 @@ function checkSendMessage(sendMsgList: SendMessage[]) { // ==end== -function handlePost(jsonData: any, handleSendResult: (data: SendMsgResult) => void) { +function constructReturnData(status: number, data: any = {}, message: string = "") { + return { + status: status, + retcode: status, + data: data, + message: message + } + +} + +function handlePost(jsonData: any, handleSendResult: (data: OB11Return<any>) => void) { log("API receive post:" + JSON.stringify(jsonData)) if (!jsonData.params) { jsonData.params = JSON.parse(JSON.stringify(jsonData)); @@ -109,7 +126,7 @@ function handlePost(jsonData: any, handleSendResult: (data: SendMsgResult) => vo user_display_name: member.cardName || member.nick, nickname: member.nick, card: member.cardName, - role: OnebotGroupMemberRole[member.role], + role: OB11Construct.constructGroupMemberRole(member.role), } } else if (jsonData.action == "get_group_member_list") { let group = groups.find(group => group.uid == jsonData.params.group_id) @@ -121,7 +138,7 @@ function handlePost(jsonData: any, handleSendResult: (data: SendMsgResult) => vo user_display_name: member.cardName || member.nick, nickname: member.nick, card: member.cardName, - role: OnebotGroupMemberRole[member.role], + role: OB11Construct.constructGroupMemberRole(member.role), } }) || [] @@ -162,11 +179,24 @@ export function startExpress(port: number) { } }); - function parseToOnebot12(action: PostDataAction) { - app.post('/' + action, (req: any, res: any) => { + async function registerRouter<PayloadType, ReturnDataType>(action: OB11ApiName, handle: (payload: PayloadType) => Promise<OB11Return<ReturnDataType>>) { + async function _handle(res: Response, payload: PayloadType) { + res.send(await handle(payload)) + } + + app.post('/' + action, (req: Request, res: Response) => { + _handle(res, req.body).then() + }); + app.get('/' + action, (req: Request, res: Response) => { + _handle(res, req.query as any).then() + }); + } + + function parseToOnebot12(action: OB11ApiName) { + app.post('/' + action, (req: Request, res: Response) => { let jsonData: PostDataSendMsg = req.body; jsonData.action = action - let resData = handlePost(jsonData, (data: SendMsgResult) => { + let resData = handlePost(jsonData, (data: OB11Return<any>) => { res.send(data) }) if (resData) { @@ -175,29 +205,29 @@ export function startExpress(port: number) { }); } - const actionList: PostDataAction[] = ["get_login_info", "send_private_msg", "send_group_msg", + const actionList: OB11ApiName[] = ["get_login_info", "send_private_msg", "send_group_msg", "get_group_list", "get_friend_list", "delete_msg", "get_group_member_list", "get_group_member_info"] for (const action of actionList) { - parseToOnebot12(action as PostDataAction) + parseToOnebot12(action as OB11ApiName) } - app.get('/', (req: any, res: any) => { + app.get('/', (req: Request, res: Response) => { res.send('llonebot已启动'); }) // 处理POST请求的路由 - app.post('/', (req: any, res: any) => { + app.post('/', (req: Request, res: Response) => { let jsonData: PostDataSendMsg = req.body; - let resData = handlePost(jsonData, (data: SendMsgResult) => { + let resData = handlePost(jsonData, (data: OB11Return<any>) => { res.send(data) }) if (resData) { res.send(resData) } }); - app.post('/send_msg', (req: any, res: any) => { + app.post('/send_msg', (req: Request, res: Response) => { let jsonData: PostDataSendMsg = req.body; if (jsonData.message_type == "private") { jsonData.action = "send_private_msg" @@ -210,7 +240,7 @@ export function startExpress(port: number) { jsonData.action = "send_private_msg" } } - let resData = handlePost(jsonData, (data: SendMsgResult) => { + let resData = handlePost(jsonData, (data: OB11Return<any>) => { res.send(data) }) if (resData) { @@ -218,7 +248,38 @@ export function startExpress(port: number) { } }) + registerRouter<{ message_id: string }, OB11Message>("get_msg", async (payload) => { + const msg = msgHistory[payload.message_id.toString()] + if (msg) { + const msgData = await OB11Construct.constructMessage(msg); + return constructReturnData(0, msgData) + } else { + return constructReturnData(1, {}, "消息不存在") + } + }).then(()=>{ + + }) + app.listen(port, "0.0.0.0", () => { console.log(`llonebot started 0.0.0.0:${port}`); }); +} + + +export function postMsg(msg: OB11Message) { + for (const host of getConfigUtil().getConfig().hosts) { + fetch(host, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-self-id": selfInfo.user_id + }, + body: JSON.stringify(msg) + }).then((res: any) => { + log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg)); + }, (err: any) => { + log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg)); + }); + } + } \ No newline at end of file