From c875cfda15b3597941ee2ff0cd6f8a7900dd502e Mon Sep 17 00:00:00 2001 From: Disy Date: Wed, 14 Feb 2024 22:11:07 +0800 Subject: [PATCH] feat: add websocket support --- package-lock.json | 35 +++++++ package.json | 3 +- src/common/config.ts | 19 ++-- src/common/types.ts | 13 ++- src/main/main.ts | 57 +++++----- src/onebot11/actions/BaseAction.ts | 27 +++-- src/onebot11/actions/index.ts | 14 ++- src/onebot11/actions/utils.ts | 30 ++++-- src/onebot11/server.ts | 163 ++++++++++++++++++++--------- src/onebot11/types.ts | 10 +- src/renderer.ts | 8 +- 11 files changed, 273 insertions(+), 106 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19da495..35582a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", + "express-ws": "^5.0.2", "json-bigint": "^1.0.0", "uuid": "^9.0.1" }, @@ -3123,6 +3124,20 @@ "node": ">= 0.10.0" } }, + "node_modules/express-ws": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/express-ws/-/express-ws-5.0.2.tgz", + "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", + "dependencies": { + "ws": "^7.4.6" + }, + "engines": { + "node": ">=4.5.0" + }, + "peerDependencies": { + "express": "^4.0.0 || ^5.0.0-alpha.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4691,6 +4706,26 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 1ec8b58..b5de92b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "postinstall": "ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm install electron --no-save", + "postinstall": "set ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save", "build": "npm run build-main && npm run build-preload && npm run build-renderer", "build-main": "webpack --config webpack.main.config.js", "build-preload": "webpack --config webpack.preload.config.js", @@ -19,6 +19,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", + "express-ws": "^5.0.2", "json-bigint": "^1.0.0", "uuid": "^9.0.1" }, diff --git a/src/common/config.ts b/src/common/config.ts index 44b73d5..e390a4b 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,6 +1,6 @@ import {Config} from "./types"; -const fs = require("fs") +const fs = require("fs"); export class ConfigUtil{ configPath: string; @@ -9,20 +9,25 @@ export class ConfigUtil{ this.configPath = configPath; } - getConfig(): Config{ + getConfig(): Config { if (!fs.existsSync(this.configPath)) { - return {port:3000, hosts: ["http://192.168.1.2:5000/"]} + return { + httpPort: 3000, + httpHosts: ["http://127.0.0.1:5000/"], + wsPort: 3001, + wsHosts: ["ws://127.0.0.1:3002/"] + } } else { const data = fs.readFileSync(this.configPath, "utf-8"); - let jsonData =JSON.parse(data); - if (!jsonData.hosts){ - jsonData.hosts = [] + let jsonData = JSON.parse(data); + if (!jsonData.hosts) { + jsonData.hosts = []; } return jsonData; } } setConfig(config: Config){ - fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8") + fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8"); } } diff --git a/src/common/types.ts b/src/common/types.ts index 5b892ab..54964dd 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,9 +1,14 @@ export interface Config { - port: number - hosts: string[] + httpPort: number + httpHosts: string[] + wsPort: number + wsHosts: string[] + enableHttp?: boolean + enableHttpPost?: boolean + enableWs?: boolean + enableWsReverse?: boolean enableBase64?: boolean debug?: boolean reportSelfMessage?: boolean log?: boolean -} - +} \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index 43b5400..c2ab87e 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -10,7 +10,7 @@ import { CHANNEL_LOG, CHANNEL_SET_CONFIG, } from "../common/channels"; -import { postMsg, startExpress } from "../onebot11/server"; +import {initWebsocket, postMsg, startExpress, startWebsocketServer} from "../onebot11/server"; import { CONFIG_DIR, getConfigUtil, log } from "../common/utils"; import { addHistoryMsg, msgHistory, selfInfo } from "../common/data"; import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; @@ -29,56 +29,53 @@ function onLoad() { // const config_dir = browserWindow.LiteLoader.plugins["LLOneBot"].path.data; - if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, {recursive: true}); } ipcMain.handle(CHANNEL_GET_CONFIG, (event: any, arg: any) => { - return getConfigUtil().getConfig() + return getConfigUtil().getConfig(); }) ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => { - getConfigUtil().setConfig(arg) + getConfigUtil().setConfig(arg); }) ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { - log(arg) + log(arg); }) function postRawMsg(msgList: RawMessage[]) { const {debug, reportSelfMessage} = getConfigUtil().getConfig(); for (let message of msgList) { - message.msgShortId = msgHistory[message.msgId]?.msgShortId + message.msgShortId = msgHistory[message.msgId]?.msgShortId; if (!message.msgShortId) { - addHistoryMsg(message) + addHistoryMsg(message); } OB11Constructor.message(message).then((msg) => { if (debug) { msg.raw = message; } if (msg.user_id == selfInfo.uin && !reportSelfMessage) { - return + return; } postMsg(msg); - // log("post msg", msg) }).catch(e => log("constructMessage error: ", e.toString())); } } function start() { - log("llonebot start") + log("llonebot start"); registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, (payload) => { try { - // log("received msg length", payload.msgList.length); postRawMsg(payload.msgList); } catch (e) { - log("report message error: ", e.toString()) + log("report message error: ", e.toString()); } }) registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => { - const {reportSelfMessage} = getConfigUtil().getConfig() + const {reportSelfMessage} = getConfigUtil().getConfig(); if (!reportSelfMessage) { return } @@ -86,42 +83,46 @@ function onLoad() { try { postRawMsg([payload.msgRecord]); } catch (e) { - log("report self message error: ", e.toString()) + log("report self message error: ", e.toString()); } }) - NTQQApi.getGroups(true).then() - startExpress(getConfigUtil().getConfig().port) + NTQQApi.getGroups(true).then(); + + const config = getConfigUtil().getConfig(); + startExpress(config.httpPort); + startWebsocketServer(config.wsPort); + initWebsocket(); } const init = async () => { try { - const _ = await NTQQApi.getSelfInfo() - Object.assign(selfInfo, _) - selfInfo.nick = selfInfo.uin - log("get self simple info", _) + const _ = await NTQQApi.getSelfInfo(); + Object.assign(selfInfo, _); + selfInfo.nick = selfInfo.uin; + log("get self simple info", _); } catch (e) { - log("retry get self info") + log("retry get self info"); } if (selfInfo.uin) { try { - const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid)) + const userInfo = (await NTQQApi.getUserInfo(selfInfo.uid)); log("self info", userInfo); if (userInfo) { - selfInfo.nick = userInfo.nick + selfInfo.nick = userInfo.nick; } else { - return setTimeout(init, 1000) + return setTimeout(init, 1000); } } catch (e) { - log("get self nickname failed", e.toString()) - return setTimeout(init, 1000) + log("get self nickname failed", e.toString()); + return setTimeout(init, 1000); } start(); } else{ - setTimeout(init, 1000) + setTimeout(init, 1000); } } - setTimeout(init, 1000) + setTimeout(init, 1000); } diff --git a/src/onebot11/actions/BaseAction.ts b/src/onebot11/actions/BaseAction.ts index ab69555..9806ea0 100644 --- a/src/onebot11/actions/BaseAction.ts +++ b/src/onebot11/actions/BaseAction.ts @@ -1,6 +1,6 @@ import {ActionName, BaseCheckResult} from "./types" -import { OB11Response } from "./utils" -import { OB11Return } from "../types"; +import {OB11Response, OB11WebsocketResponse} from "./utils" +import {OB11Return, OB11WebsocketReturn} from "../types"; class BaseAction { actionName: ActionName @@ -11,20 +11,33 @@ class BaseAction { } public async handle(payload: PayloadType): Promise> { + const result = await this.check(payload); + if (!result.valid) { + return OB11Response.error(result.message, 400); + } + try { + const resData = await this._handle(payload); + return OB11Response.ok(resData); + } catch (e) { + return OB11Response.error(e.toString(), 200); + } + } + + public async websocketHandle(payload: PayloadType, echo: string): Promise> { const result = await this.check(payload) if (!result.valid) { - return OB11Response.error(result.message) + return OB11WebsocketResponse.error(result.message, 1400) } try { const resData = await this._handle(payload) - return OB11Response.ok(resData) - }catch (e) { - return OB11Response.error(e.toString()) + return OB11WebsocketResponse.ok(resData, echo); + } catch (e) { + return OB11WebsocketResponse.error(e.toString(), 1200) } } protected async _handle(payload: PayloadType): Promise { - throw `pleas override ${this.actionName} _handle` + throw `pleas override ${this.actionName} _handle`; } } diff --git a/src/onebot11/actions/index.ts b/src/onebot11/actions/index.ts index 42fcda6..dfd90c5 100644 --- a/src/onebot11/actions/index.ts +++ b/src/onebot11/actions/index.ts @@ -9,6 +9,7 @@ import SendGroupMsg from './SendGroupMsg' import SendPrivateMsg from './SendPrivateMsg' import SendMsg from './SendMsg' import DeleteMsg from "./DeleteMsg"; +import BaseAction from "./BaseAction"; export const actionHandlers = [ new GetMsg(), @@ -17,4 +18,15 @@ export const actionHandlers = [ new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(), new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), new DeleteMsg() -] \ No newline at end of file +] + +function initActionMap() { + const actionMap = new Map>(); + for (const action of actionHandlers) { + actionMap.set(action.actionName, action); + } + + return actionMap +} + +export const actionMap = initActionMap(); \ No newline at end of file diff --git a/src/onebot11/actions/utils.ts b/src/onebot11/actions/utils.ts index 8120d7f..e87b394 100644 --- a/src/onebot11/actions/utils.ts +++ b/src/onebot11/actions/utils.ts @@ -1,18 +1,36 @@ -import { OB11Return } from '../types'; +import {OB11Return, OB11WebsocketReturn} from '../types'; export class OB11Response { - static res(data: T, status: number = 0, message: string = ""): OB11Return { + static res(data: T, status: string, retcode: number, message: string = ""): OB11Return { return { status: status, - retcode: status, + retcode: retcode, data: data, message: message } } static ok(data: T) { - return OB11Response.res(data) + return OB11Response.res(data, "ok", 0) } - static error(err: string) { - return OB11Response.res(null, -1, err) + static error(err: string, retcode: number) { + return OB11Response.res(null, "failed", retcode, err) + } +} + +export class OB11WebsocketResponse { + static res(data: T, status: string, retcode: number, echo: string, message: string = ""): OB11WebsocketReturn { + return { + status: status, + retcode: retcode, + data: data, + echo: echo, + message: message + } + } + static ok(data: T, echo: string = "") { + return OB11WebsocketResponse.res(data, "ok", 0, echo) + } + static error(err: string, retcode: number, echo: string = "") { + return OB11WebsocketResponse.res(null, "failed", retcode, echo, err) } } diff --git a/src/onebot11/server.ts b/src/onebot11/server.ts index 95b67f6..74e819a 100644 --- a/src/onebot11/server.ts +++ b/src/onebot11/server.ts @@ -1,13 +1,16 @@ import { getConfigUtil, log } from "../common/utils"; const express = require("express"); +const expressWs = require("express-ws"); + import { Request } from 'express'; import { Response } from 'express'; const JSONbig = require('json-bigint')({ storeAsString: true }); import { selfInfo } from "../common/data"; import { OB11Message, OB11Return, OB11MessageData } from './types'; -import { actionHandlers } from "./actions"; +import {actionHandlers, actionMap} from "./actions"; +import {OB11Response, OB11WebsocketResponse} from "./actions/utils"; // @SiberianHusky 2021-08-15 @@ -48,27 +51,12 @@ function checkSendMessage(sendMsgList: OB11MessageData[]) { // ==end== - -class OB11Response { - static res(data: T, status: number = 0, message: string = ""): OB11Return { - return { - status: status, - retcode: status, - data: data, - message: message - } - } - static ok(data: T) { - return OB11Response.res(data) - } - static error(err: string) { - return OB11Response.res(null, -1, err) - } -} - const expressAPP = express(); expressAPP.use(express.urlencoded({ extended: true, limit: "500mb" })); +const expressWsApp = express(); +const websocketClientConnections = []; + expressAPP.use((req, res, next) => { let data = ''; req.on('data', chunk => { @@ -86,12 +74,6 @@ expressAPP.use((req, res, next) => { next(); }); }); -// expressAPP.use(express.json({ -// limit: '500mb', -// verify: (req: any, res: any, buf: any, encoding: any) => { -// req.rawBody = buf; -// } -// })); export function startExpress(port: number) { @@ -99,33 +81,120 @@ export function startExpress(port: number) { res.send('llonebot已启动'); }) - expressAPP.listen(port, "0.0.0.0", () => { - console.log(`llonebot started 0.0.0.0:${port}`); - }); + if (getConfigUtil().getConfig().enableHttp) { + expressAPP.listen(port, "0.0.0.0", () => { + console.log(`llonebot http service started 0.0.0.0:${port}`); + }); + } +} + +export function startWebsocketServer(port: number) { + const config = getConfigUtil().getConfig(); + if (config.enableWs) { + expressWs(expressWsApp) + expressWsApp.listen(getConfigUtil().getConfig().wsPort, function () { + console.log(`llonebot websocket service started 0.0.0.0:${port}`); + }); + } +} + +export function initWebsocket() { + if (getConfigUtil().getConfig().enableWs) { + expressWsApp.ws("/api", onWebsocketMessage); + expressWsApp.ws("/", onWebsocketMessage); + } + + initReverseWebsocket(); +} + +function initReverseWebsocket() { + const config = getConfigUtil().getConfig(); + if (config.enableWsReverse) { + for (const url of config.wsHosts) { + try { + const wsClient = new WebSocket(url); + websocketClientConnections.push(wsClient); + + wsClient.onclose = function (ev) { + let index = websocketClientConnections.indexOf(wsClient); + if (index !== -1) { + websocketClientConnections.splice(index, 1); + } + } + + wsClient.onmessage = async function (ev) { + let message = ev.data; + if (typeof message === "string") { + try { + let recv = JSON.parse(message); + let echo = recv.echo ?? ""; + + if (actionMap.has(recv.action)) { + let action = actionMap.get(recv.action); + const result = await action.websocketHandle(recv.params, echo); + wsClient.send(JSON.stringify(result)); + } + else { + wsClient.send(JSON.stringify(OB11WebsocketResponse.error("Bad Request", 1400, echo))); + } + } catch (e) { + log(e.stack); + wsClient.send(JSON.stringify(OB11WebsocketResponse.error(e.stack.toString(), 1200))); + } + } + } + } + catch (e) {} + } + } +} + +function onWebsocketMessage(ws, req) { + ws.on("message", async function (message) { + try { + let recv = JSON.parse(message); + let echo = recv.echo ?? ""; + + if (actionMap.has(recv.action)) { + let action = actionMap.get(recv.action) + const result = await action.websocketHandle(recv.params, echo); + ws.send(JSON.stringify(result)); + } + else { + ws.send(JSON.stringify(OB11WebsocketResponse.error("Bad Request", 1400, echo))); + } + } catch (e) { + log(e.stack); + ws.send(JSON.stringify(OB11WebsocketResponse.error(e.stack.toString(), 1200))); + } + }) } export function postMsg(msg: OB11Message) { - const { reportSelfMessage } = getConfigUtil().getConfig() - if (!reportSelfMessage) { - if (msg.user_id == selfInfo.uin) { - return + const config = getConfigUtil().getConfig(); + if (config.enableHttpPost) { + if (!config.reportSelfMessage) { + if (msg.user_id == selfInfo.uin) { + return + } + } + for (const host of config.httpHosts) { + fetch(host, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-self-id": selfInfo.uin + }, + body: JSON.stringify(msg) + }).then((res: any) => { + log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg)); + }, (err: any) => { + log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg)); + }); } } - for (const host of getConfigUtil().getConfig().hosts) { - fetch(host, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-self-id": selfInfo.uin - }, - body: JSON.stringify(msg) - }).then((res: any) => { - log(`新消息事件上报成功: ${host} ` + JSON.stringify(msg)); - }, (err: any) => { - log(`新消息事件上报失败: ${host} ` + err + JSON.stringify(msg)); - }); - } + } let routers: Record Promise>> = {}; @@ -143,7 +212,7 @@ function registerRouter(action: string, handle: (payload: any) => Promise) } catch (e) { log(e.stack); - res.send(OB11Response.error(e.stack.toString())) + res.send(OB11Response.error(e.stack.toString(), 200)) } } diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index f07552f..f78f3ab 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -86,12 +86,20 @@ export type OB11ApiName = | "get_msg" export interface OB11Return { - status: number + status: string retcode: number data: DataType message: string } +export interface OB11WebsocketReturn { + status: string + retcode: number + data: DataType + echo: string + message: string +} + export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{} export enum OB11MessageDataType { diff --git a/src/renderer.ts b/src/renderer.ts index 83651f1..cdb3fdd 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -20,7 +20,7 @@ async function onSettingWindowCreated(view: Element) { } let hostsEleStr = "" - for (const host of config.hosts) { + for (const host of config.httpHosts) { hostsEleStr += creatHostEleStr(host); } let html = ` @@ -30,7 +30,7 @@ async function onSettingWindowCreated(view: Element) { 监听端口 - +
@@ -135,8 +135,8 @@ async function onSettingWindowCreated(view: Element) { hosts.push(hostEle.value); } } - config.port = parseInt(port); - config.hosts = hosts; + config.httpPort = parseInt(port); + config.httpHosts = hosts; window.llonebot.setConfig(config); alert("保存成功"); })