feat: add websocket support

This commit is contained in:
Disy 2024-02-14 22:11:07 +08:00
parent 9bb69058c2
commit c875cfda15
11 changed files with 273 additions and 106 deletions

35
package-lock.json generated
View File

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

View File

@ -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"
},

View File

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

View File

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

View File

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

View File

@ -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<PayloadType, ReturnDataType> {
actionName: ActionName
@ -11,20 +11,33 @@ class BaseAction<PayloadType, ReturnDataType> {
}
public async handle(payload: PayloadType): Promise<OB11Return<ReturnDataType | null>> {
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<OB11WebsocketReturn<ReturnDataType | null>> {
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<ReturnDataType> {
throw `pleas override ${this.actionName} _handle`
throw `pleas override ${this.actionName} _handle`;
}
}

View File

@ -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()
]
]
function initActionMap() {
const actionMap = new Map<string, BaseAction<any, any>>();
for (const action of actionHandlers) {
actionMap.set(action.actionName, action);
}
return actionMap
}
export const actionMap = initActionMap();

View File

@ -1,18 +1,36 @@
import { OB11Return } from '../types';
import {OB11Return, OB11WebsocketReturn} from '../types';
export class OB11Response {
static res<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {
return {
status: status,
retcode: status,
retcode: retcode,
data: data,
message: message
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(data)
return OB11Response.res<T>(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<T>(data: T, status: string, retcode: number, echo: string, message: string = ""): OB11WebsocketReturn<T> {
return {
status: status,
retcode: retcode,
data: data,
echo: echo,
message: message
}
}
static ok<T>(data: T, echo: string = "") {
return OB11WebsocketResponse.res<T>(data, "ok", 0, echo)
}
static error(err: string, retcode: number, echo: string = "") {
return OB11WebsocketResponse.res(null, "failed", retcode, echo, err)
}
}

View File

@ -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<T>(data: T, status: number = 0, message: string = ""): OB11Return<T> {
return {
status: status,
retcode: status,
data: data,
message: message
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(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<string, (payload: any) => Promise<OB11Return<any>>> = {};
@ -143,7 +212,7 @@ function registerRouter(action: string, handle: (payload: any) => Promise<any>)
}
catch (e) {
log(e.stack);
res.send(OB11Response.error(e.stack.toString()))
res.send(OB11Response.error(e.stack.toString(), 200))
}
}

View File

@ -86,12 +86,20 @@ export type OB11ApiName =
| "get_msg"
export interface OB11Return<DataType> {
status: number
status: string
retcode: number
data: DataType
message: string
}
export interface OB11WebsocketReturn<DataType> {
status: string
retcode: number
data: DataType
echo: string
message: string
}
export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{}
export enum OB11MessageDataType {

View File

@ -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) {
<setting-list class="wrap">
<setting-item class="vertical-list-item" data-direction="row">
<setting-text></setting-text>
<input id="port" type="number" value="${config.port}"/>
<input id="port" type="number" value="${config.httpPort}"/>
</setting-item>
<div>
<button id="addHost" class="q-button"></button>
@ -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("保存成功");
})