diff --git a/README.md b/README.md index 2c6c8fe..050e511 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ LiteLoaderQQNT的OneBot11协议插件 - [x] http调用api - [x] http事件上报 - [x] 正向websocket -- [ ] 反向websocket +- [x] 反向websocket 主要功能: - [x] 发送好友消息 @@ -108,7 +108,7 @@ LiteLoaderQQNT的OneBot11协议插件 ## TODO - [x] 重构摆脱LLAPI,目前调用LLAPI只能在renderer进程调用,需重构成在main进程调用 -- [x] 支持正向websocket +- [x] 支持正、反向websocket(感谢@disymayufei的PR) - [ ] 转发消息记录 - [ ] 好友点赞api diff --git a/manifest.json b/manifest.json index 4d0e65c..4ffec7f 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "LiteLoaderQQNT的OneBotApi", - "version": "3.3.1", + "version": "3.4.0", "thumbnail": "./icon.png", "authors": [ { diff --git a/package-lock.json b/package-lock.json index 90e7cca..4bc8916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", + "express-ws": "^5.0.2", "json-bigint": "^1.0.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "ws": "^8.16.0" }, "devDependencies": { "@babel/preset-env": "^7.23.2", @@ -3134,6 +3136,40 @@ "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/express-ws/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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index fda21bd..667e78e 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": "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", @@ -20,7 +20,8 @@ "dependencies": { "express": "^4.18.2", "json-bigint": "^1.0.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "ws": "^8.16.0" }, "devDependencies": { "@babel/preset-env": "^7.23.2", diff --git a/src/common/config.ts b/src/common/config.ts index a751771..7988a8e 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,50 +1,79 @@ -import { Config } from "./types"; - -const fs = require("fs") +import fs from "fs"; +import {Config, OB11Config} from "./types"; +import {mergeNewProperties} from "./utils"; export class ConfigUtil { - configPath: string; + private readonly configPath: string; + private config: Config | null = null; constructor(configPath: string) { this.configPath = configPath; } - getConfig(): Config { - let defaultConfig: Config = { - port: 3000, + getConfig(cache=true) { + if (this.config && cache) { + return this.config; + } + + return this.reloadConfig(); + } + + reloadConfig(): Config { + let ob11Default: OB11Config = { + httpPort: 3000, + httpHosts: [], wsPort: 3001, - hosts: [], + wsHosts: [], + enableHttp: true, + enableHttpPost: true, + enableWs: true, + enableWsReverse: false + } + let defaultConfig: Config = { + ob11: ob11Default, + heartInterval: 5000, token: "", enableBase64: false, debug: false, log: false, reportSelfMessage: false - } + }; + if (!fs.existsSync(this.configPath)) { - return defaultConfig + this.config = defaultConfig; + return this.config; } else { const data = fs.readFileSync(this.configPath, "utf-8"); let jsonData: Config = defaultConfig; try { jsonData = JSON.parse(data) + } catch (e) { + this.config = defaultConfig; + return this.config; } - catch (e){ - - } - if (!jsonData.hosts) { - jsonData.hosts = [] - } - if (!jsonData.wsPort){ - jsonData.wsPort = 3001 - } - if (!jsonData.token){ - jsonData.token = "" - } - return jsonData; + mergeNewProperties(defaultConfig, jsonData); + this.checkOldConfig(jsonData.ob11, jsonData, "httpPort", "port"); + this.checkOldConfig(jsonData.ob11, jsonData, "httpHosts", "hosts"); + this.checkOldConfig(jsonData.ob11, jsonData, "wsPort", "wsPort"); + // console.log("get config", jsonData); + this.config = jsonData; + return this.config; } } setConfig(config: Config) { + this.config = config; fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8") } + + private checkOldConfig(currentConfig: Config | OB11Config, + oldConfig: Config | OB11Config, + currentKey: string, oldKey: string) { + // 迁移旧的配置到新配置,避免用户重新填写配置 + const oldValue = oldConfig[oldKey]; + if (oldValue) { + currentConfig[currentKey] = oldValue; + delete oldConfig[oldKey]; + } + } } diff --git a/src/common/data.ts b/src/common/data.ts index dcc559d..e070a41 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -1,6 +1,5 @@ -import { NTQQApi } from '../ntqqapi/ntcall'; -import { Friend, Group, GroupMember, RawMessage, SelfInfo } from "../ntqqapi/types"; -import { log } from "./utils"; +import {NTQQApi} from '../ntqqapi/ntcall'; +import {Friend, Group, GroupMember, RawMessage, SelfInfo} from "../ntqqapi/types"; export let groups: Group[] = [] export let friends: Friend[] = [] @@ -88,5 +87,4 @@ export function getStrangerByUin(uin: string) { } } -export const version = "v3.3.1" -export const heartInterval = 15000 // 毫秒 \ No newline at end of file +export const version = "v3.4.0" diff --git a/src/common/types.ts b/src/common/types.ts index bb98810..1952099 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,11 +1,20 @@ -export interface Config { - port: number +export interface OB11Config { + httpPort: number + httpHosts: string[] wsPort: number - hosts: string[] + wsHosts: string[] + enableHttp?: boolean + enableHttpPost?: boolean + enableWs?: boolean + enableWsReverse?: boolean +} + +export interface Config { + ob11: OB11Config token?: string + heartInterval?: number // ms enableBase64?: boolean debug?: boolean reportSelfMessage?: boolean log?: boolean -} - +} \ No newline at end of file diff --git a/src/common/utils.ts b/src/common/utils.ts index 0962619..43c77fe 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -2,6 +2,7 @@ import * as path from "path"; import {selfInfo} from "./data"; import {ConfigUtil} from "./config"; import util from "util"; + const fs = require('fs'); export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; @@ -47,6 +48,10 @@ export function isGIF(path: string) { return buffer.toString() === 'GIF8' } +export function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + // 定义一个异步函数来检查文件是否存在 export function checkFileReceived(path: string, timeout: number=3000): Promise { @@ -93,5 +98,21 @@ export async function file2base64(path: string){ return result; } -export const sleep = (ms: number): Promise => - new Promise((resolve) => setTimeout(resolve, ms)) \ No newline at end of file + +// 在保证老对象已有的属性不变化的情况下将新对象的属性复制到老对象 +export function mergeNewProperties(newObj: any, oldObj: any) { + Object.keys(newObj).forEach(key => { + // 如果老对象不存在当前属性,则直接复制 + if (!oldObj.hasOwnProperty(key)) { + oldObj[key] = newObj[key]; + } else { + // 如果老对象和新对象的当前属性都是对象,则递归合并 + if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') { + mergeNewProperties(newObj[key], oldObj[key]); + } else if(typeof oldObj[key] === 'object' || typeof newObj[key] === 'object'){ + // 属性冲突,有一方不是对象,直接覆盖 + oldObj[key] = newObj[key]; + } + } + }); +} \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index d88bd91..000cd50 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,7 +4,7 @@ import {LLOneBot} from "./preload"; declare global { interface Window { - llonebot: LLOneBot; + llonebot: typeof llonebot; LiteLoader: any; } } \ No newline at end of file diff --git a/src/main/ipcsend.ts b/src/main/ipcsend.ts index 303e9d2..37d04b9 100644 --- a/src/main/ipcsend.ts +++ b/src/main/ipcsend.ts @@ -1,10 +1,11 @@ import {webContents} from 'electron'; -function sendIPCMsg(channel: string, data: any) { + +function sendIPCMsg(channel: string, ...data: any) { let contents = webContents.getAllWebContents(); for (const content of contents) { try { - content.send(channel, data) + content.send(channel, ...data) } catch (e) { console.log("llonebot send ipc msg to render error:", e) } diff --git a/src/main/main.ts b/src/main/main.ts index 07961d8..d5f24e5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -7,7 +7,7 @@ import { CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG, } from "../common/ import { postMsg, setToken, startHTTPServer, startWSServer } from "../onebot11/server"; import { CONFIG_DIR, getConfigUtil, log } from "../common/utils"; import { addHistoryMsg, getGroupMember, msgHistory, selfInfo, uidMaps } from "../common/data"; -import { hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; +import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; import { OB11Constructor } from "../onebot11/constructor"; import { NTQQApi } from "../ntqqapi/ntcall"; import { ChatType, RawMessage } from "../ntqqapi/types"; @@ -29,19 +29,22 @@ function onLoad() { ipcMain.on(CHANNEL_SET_CONFIG, (event: any, arg: Config) => { let oldConfig = getConfigUtil().getConfig(); getConfigUtil().setConfig(arg) - if (arg.port != oldConfig.port) { - startHTTPServer(arg.port) + if (arg.ob11.httpPort != oldConfig.ob11.httpPort && arg.ob11.enableHttp) { + ob11HTTPServer.restart(arg.ob11.httpPort); } - if (arg.wsPort != oldConfig.wsPort) { - startWSServer(arg.wsPort) + if (!arg.ob11.enableHttp){ + ob11HTTPServer.stop() } - if (arg.token != oldConfig.token) { - setToken(arg.token); + else{ + ob11HTTPServer.start(arg.ob11.httpPort); + } + if (arg.ob11.wsPort != oldConfig.ob11.wsPort) { + initWebsocket(arg.ob11.wsPort) } }) ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { - log(arg) + log(arg); }) @@ -51,7 +54,7 @@ function onLoad() { // log("收到新消息", message) message.msgShortId = msgHistory[message.msgId]?.msgShortId if (!message.msgShortId) { - addHistoryMsg(message) + addHistoryMsg(message); } OB11Constructor.message(message).then((msg) => { if (debug) { @@ -70,10 +73,9 @@ function onLoad() { async function 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<{ msgList: Array }>(ReceiveCmd.UPDATE_MSG, async (payload) => { @@ -86,8 +88,8 @@ function onLoad() { continue } if (message.chatType == ChatType.friend) { - const friendRecallEvent = OB11Constructor.friendRecallEvent(message.senderUin, oriMessage.msgShortId) - postMsg(friendRecallEvent) + const friendRecallEvent = new OB11FriendRecallNoticeEvent(parseInt(message.senderUin), oriMessage.msgShortId); + postMsg(friendRecallEvent); } else if (message.chatType == ChatType.group) { let operatorId = message.senderUin for (const element of message.elements) { @@ -95,13 +97,14 @@ function onLoad() { const operator = await getGroupMember(message.peerUin, null, operatorUid) operatorId = operator.uin } - const groupRecallEvent = OB11Constructor.groupRecallEvent( - message.peerUin, - message.senderUin, - operatorId, + const groupRecallEvent = new OB11GroupRecallNoticeEvent( + parseInt(message.peerUin), + parseInt(message.senderUin), + parseInt(operatorId), oriMessage.msgShortId ) - postMsg(groupRecallEvent) + + postMsg(groupRecallEvent); } continue } @@ -109,7 +112,7 @@ function onLoad() { } }) registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, (payload) => { - const {reportSelfMessage} = getConfigUtil().getConfig() + const {reportSelfMessage} = getConfigUtil().getConfig(); if (!reportSelfMessage) { return } @@ -117,45 +120,48 @@ 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() const config = getConfigUtil().getConfig() - startHTTPServer(config.port) - startWSServer(config.wsPort) - setToken(config.token) + try { + ob11HTTPServer.start(config.ob11.httpPort) + initWebsocket(config.ob11.wsPort); + }catch (e) { + console.log("start failed", e) + } log("LLOneBot start") } 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().then(); } else { setTimeout(init, 1000) } } - setTimeout(init, 1000) + setTimeout(init, 1000); } diff --git a/src/ntqqapi/constructor.ts b/src/ntqqapi/constructor.ts index a966026..3267e27 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -1,13 +1,13 @@ import { + AtType, ElementType, + SendFaceElement, SendPicElement, SendPttElement, SendReplyElement, - SendTextElement, - AtType, - SendFaceElement + SendTextElement } from "./types"; -import { NTQQApi } from "./ntcall"; +import {NTQQApi} from "./ntcall"; export class SendMsgElementConstructor { diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 5601127..b56733b 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -4,6 +4,15 @@ import { NTQQApi, NTQQApiClass, sendMessagePool } from "./ntcall"; import { Group, RawMessage, User } from "./types"; import { addHistoryMsg, friends, groups, msgHistory } from "../common/data"; import { v4 as uuidv4 } from 'uuid'; +import {BrowserWindow} from 'electron'; +import {log, sleep} from "../common/utils"; +import {NTQQApi, NTQQApiClass, sendMessagePool} from "./ntcall"; +import {Group, RawMessage, User} from "./types"; +import {addHistoryMsg, friends, groups, msgHistory} from "../common/data"; +import {v4 as uuidv4} from 'uuid'; +import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent"; +import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent"; +import {postMsg} from "../onebot11/server"; export let hookApiCallbacks: Record void> = {} @@ -110,25 +119,103 @@ export function removeReceiveHook(id: string) { receiveHooks.splice(index, 1); } -async function updateGroups(_groups: Group[]) { +async function updateGroups(_groups: Group[], needUpdate: boolean = true) { for (let group of _groups) { - let existGroup = groups.find(g => g.groupCode == group.groupCode) - if (!existGroup) { - NTQQApi.getGroupMembers(group.groupCode).then(members => { - if (members) { - group.members = members - } - }) - groups.push(group) - log("update group members", group.members) - } else { - Object.assign(existGroup, group) + let existGroup = groups.find(g => g.groupCode == group.groupCode); + if (existGroup) { + Object.assign(existGroup, group); + } + else { + groups.push(group); + existGroup = group; + } + + if (needUpdate) { + const members = await NTQQApi.getGroupMembers(group.groupCode); + + if (members) { + existGroup.members = members; + } } } } -registerReceiveHook<{ groupList: Group[] }>(ReceiveCmd.GROUPS, (payload) => updateGroups(payload.groupList).then()) -registerReceiveHook<{ groupList: Group[] }>(ReceiveCmd.GROUPS_UNIX, (payload) => updateGroups(payload.groupList).then()) +async function processGroupEvent(payload) { + try { + const newGroupList = payload.groupList; + for (const group of newGroupList) { + let existGroup = groups.find(g => g.groupCode == group.groupCode); + if (existGroup) { + if (existGroup.memberCount > group.memberCount) { + const oldMembers = existGroup.members; + + await sleep(200); // 如果请求QQ API的速度过快,通常无法正确拉取到最新的群信息,因此这里人为引入一个延时 + const newMembers = await NTQQApi.getGroupMembers(group.groupCode); + + group.members = newMembers; + const newMembersSet = new Set(); // 建立索引降低时间复杂度 + + for (const member of newMembers) { + newMembersSet.add(member.uin); + } + + for (const member of oldMembers) { + if (!newMembersSet.has(member.uin)) { + postMsg(new OB11GroupDecreaseEvent(group.groupCode, parseInt(member.uin))); + break; + } + } + + } + else if (existGroup.memberCount < group.memberCount) { + const oldMembers = existGroup.members; + const oldMembersSet = new Set(); + for (const member of oldMembers) { + oldMembersSet.add(member.uin); + } + + await sleep(200); + const newMembers = await NTQQApi.getGroupMembers(group.groupCode); + + group.members = newMembers; + for (const member of newMembers) { + if (!oldMembersSet.has(member.uin)) { + postMsg(new OB11GroupIncreaseEvent(group.groupCode, parseInt(member.uin))); + break; + } + } + } + } + } + + updateGroups(newGroupList, false).then(); + } + catch (e) { + updateGroups(payload.groupList).then(); + console.log(e); + } +} + +registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS, (payload) => { + if (payload.updateType != 2) { + updateGroups(payload.groupList).then(); + } + else { + if (process.platform == "win32") { + processGroupEvent(payload).then(); + } + } +}) +registerReceiveHook<{ groupList: Group[], updateType: number }>(ReceiveCmd.GROUPS_UNIX, (payload) => { + if (payload.updateType != 2) { + updateGroups(payload.groupList).then(); + } + else { + if (process.platform != "win32") { + processGroupEvent(payload).then(); + } + } +}) registerReceiveHook<{ data: { categoryId: number, categroyName: string, categroyMbCount: number, buddyList: User[] }[] }>(ReceiveCmd.FRIENDS, payload => { @@ -145,11 +232,6 @@ registerReceiveHook<{ } }) -// registerReceiveHook(ReceiveCmd.USER_INFO, (payload)=>{ -// log("user info", payload); -// }) - - registerReceiveHook<{ msgList: Array }>(ReceiveCmd.NEW_MSG, (payload) => { for (const message of payload.msgList) { // log("收到新消息,push到历史记录", message) diff --git a/src/onebot11/ReconnectingWebsocket.ts b/src/onebot11/ReconnectingWebsocket.ts new file mode 100644 index 0000000..ecf5c5e --- /dev/null +++ b/src/onebot11/ReconnectingWebsocket.ts @@ -0,0 +1,56 @@ +import {log} from "../common/utils"; + +const WebSocket = require("ws"); + +export class ReconnectingWebsocket { + private websocket; + private readonly url: string; + + public constructor(url: string) { + this.url = url; + this.reconnect() + } + + public onopen = function (){} + + public onmessage = function (msg){} + + public onclose = function () {} + + public send(msg) { + if (this.websocket && this.websocket.readyState == WebSocket.OPEN) { + this.websocket.send(msg); + } + } + + private reconnect() { + this.websocket = new WebSocket(this.url, { + handshakeTimeout: 2000, + perMessageDeflate: false + }); + + console.log("Trying to connect to the websocket server: " + this.url); + + const instance = this; + + this.websocket.on("open", function open() { + console.log("Connected to the websocket server: " + instance.url); + instance.onopen(); + }); + + this.websocket.on("message", function message(data) { + instance.onmessage(data.toString()); + }); + + this.websocket.on("error", console.error); + + this.websocket.on("close", function close() { + console.log("The websocket connection: " + instance.url + " closed, trying reconnecting..."); + instance.onclose(); + + setTimeout(() => { + instance.reconnect(); + }, 3000); // TODO: 重连间隔在配置文件中实现 + }); + } +} \ No newline at end of file diff --git a/src/onebot11/action/BaseAction.ts b/src/onebot11/action/BaseAction.ts new file mode 100644 index 0000000..9806ea0 --- /dev/null +++ b/src/onebot11/action/BaseAction.ts @@ -0,0 +1,44 @@ +import {ActionName, BaseCheckResult} from "./types" +import {OB11Response, OB11WebsocketResponse} from "./utils" +import {OB11Return, OB11WebsocketReturn} from "../types"; + +class BaseAction { + actionName: ActionName + protected async check(payload: PayloadType): Promise { + return { + valid: true, + } + } + + 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 OB11WebsocketResponse.error(result.message, 1400) + } + try { + const resData = await this._handle(payload) + 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`; + } +} + +export default BaseAction \ No newline at end of file diff --git a/src/onebot11/actions/CanSendImage.ts b/src/onebot11/action/CanSendImage.ts similarity index 83% rename from src/onebot11/actions/CanSendImage.ts rename to src/onebot11/action/CanSendImage.ts index f8c6b2c..d018808 100644 --- a/src/onebot11/actions/CanSendImage.ts +++ b/src/onebot11/action/CanSendImage.ts @@ -1,4 +1,4 @@ -import { ActionName } from "./types"; +import {ActionName} from "./types"; import CanSendRecord from "./CanSendRecord"; interface ReturnType{ diff --git a/src/onebot11/actions/CanSendRecord.ts b/src/onebot11/action/CanSendRecord.ts similarity index 89% rename from src/onebot11/actions/CanSendRecord.ts rename to src/onebot11/action/CanSendRecord.ts index 4d94d17..6019253 100644 --- a/src/onebot11/actions/CanSendRecord.ts +++ b/src/onebot11/action/CanSendRecord.ts @@ -1,5 +1,5 @@ import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; interface ReturnType{ yes: boolean diff --git a/src/onebot11/actions/DeleteMsg.ts b/src/onebot11/action/DeleteMsg.ts similarity index 73% rename from src/onebot11/actions/DeleteMsg.ts rename to src/onebot11/action/DeleteMsg.ts index 7c999c3..8ed6c1c 100644 --- a/src/onebot11/actions/DeleteMsg.ts +++ b/src/onebot11/action/DeleteMsg.ts @@ -1,7 +1,7 @@ -import { ActionName } from "./types"; +import {ActionName} from "./types"; import BaseAction from "./BaseAction"; -import { NTQQApi } from "../../ntqqapi/ntcall"; -import { getHistoryMsgByShortId, msgHistory } from "../../common/data"; +import {NTQQApi} from "../../ntqqapi/ntcall"; +import {getHistoryMsgByShortId} from "../../common/data"; interface Payload { message_id: number diff --git a/src/onebot11/actions/GetFriendList.ts b/src/onebot11/action/GetFriendList.ts similarity index 61% rename from src/onebot11/actions/GetFriendList.ts rename to src/onebot11/action/GetFriendList.ts index 6827728..4aff82a 100644 --- a/src/onebot11/actions/GetFriendList.ts +++ b/src/onebot11/action/GetFriendList.ts @@ -1,8 +1,8 @@ -import { OB11User } from '../types'; -import { OB11Constructor } from "../constructor"; -import { friends } from "../../common/data"; +import {OB11User} from '../types'; +import {OB11Constructor} from "../constructor"; +import {friends} from "../../common/data"; import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; class GetFriendList extends BaseAction { diff --git a/src/onebot11/actions/GetGroupInfo.ts b/src/onebot11/action/GetGroupInfo.ts similarity index 74% rename from src/onebot11/actions/GetGroupInfo.ts rename to src/onebot11/action/GetGroupInfo.ts index 0ff09a1..952972c 100644 --- a/src/onebot11/actions/GetGroupInfo.ts +++ b/src/onebot11/action/GetGroupInfo.ts @@ -1,8 +1,8 @@ -import { OB11Group } from '../types'; -import { getGroup } from "../../common/data"; -import { OB11Constructor } from "../constructor"; +import {OB11Group} from '../types'; +import {getGroup} from "../../common/data"; +import {OB11Constructor} from "../constructor"; import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; interface PayloadType { group_id: number diff --git a/src/onebot11/actions/GetGroupList.ts b/src/onebot11/action/GetGroupList.ts similarity index 61% rename from src/onebot11/actions/GetGroupList.ts rename to src/onebot11/action/GetGroupList.ts index d56cf12..bfbdc71 100644 --- a/src/onebot11/actions/GetGroupList.ts +++ b/src/onebot11/action/GetGroupList.ts @@ -1,9 +1,8 @@ -import { OB11Group } from '../types'; -import { OB11Constructor } from "../constructor"; -import { groups } from "../../common/data"; +import {OB11Group} from '../types'; +import {OB11Constructor} from "../constructor"; +import {groups} from "../../common/data"; import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; - +import {ActionName} from "./types"; class GetGroupList extends BaseAction { diff --git a/src/onebot11/actions/GetGroupMemberInfo.ts b/src/onebot11/action/GetGroupMemberInfo.ts similarity index 77% rename from src/onebot11/actions/GetGroupMemberInfo.ts rename to src/onebot11/action/GetGroupMemberInfo.ts index 7010e9a..7546c1c 100644 --- a/src/onebot11/actions/GetGroupMemberInfo.ts +++ b/src/onebot11/action/GetGroupMemberInfo.ts @@ -1,8 +1,8 @@ -import { OB11GroupMember } from '../types'; -import { getGroupMember } from "../../common/data"; -import { OB11Constructor } from "../constructor"; +import {OB11GroupMember} from '../types'; +import {getGroupMember} from "../../common/data"; +import {OB11Constructor} from "../constructor"; import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; export interface PayloadType { diff --git a/src/onebot11/actions/GetGroupMemberList.ts b/src/onebot11/action/GetGroupMemberList.ts similarity index 75% rename from src/onebot11/actions/GetGroupMemberList.ts rename to src/onebot11/action/GetGroupMemberList.ts index f9d9b55..0f97973 100644 --- a/src/onebot11/actions/GetGroupMemberList.ts +++ b/src/onebot11/action/GetGroupMemberList.ts @@ -1,9 +1,9 @@ -import { OB11GroupMember } from '../types'; -import { getGroup } from "../../common/data"; -import { NTQQApi } from "../../ntqqapi/ntcall"; -import { OB11Constructor } from "../constructor"; +import {OB11GroupMember} from '../types'; +import {getGroup} from "../../common/data"; +import {NTQQApi} from "../../ntqqapi/ntcall"; +import {OB11Constructor} from "../constructor"; import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; export interface PayloadType { group_id: number diff --git a/src/onebot11/actions/GetLoginInfo.ts b/src/onebot11/action/GetLoginInfo.ts similarity index 61% rename from src/onebot11/actions/GetLoginInfo.ts rename to src/onebot11/action/GetLoginInfo.ts index 56dbfe1..ab8694f 100644 --- a/src/onebot11/actions/GetLoginInfo.ts +++ b/src/onebot11/action/GetLoginInfo.ts @@ -1,8 +1,8 @@ -import { OB11User } from '../types'; -import { OB11Constructor } from "../constructor"; -import { selfInfo } from "../../common/data"; +import {OB11User} from '../types'; +import {OB11Constructor} from "../constructor"; +import {selfInfo} from "../../common/data"; import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; class GetLoginInfo extends BaseAction { diff --git a/src/onebot11/actions/GetMsg.ts b/src/onebot11/action/GetMsg.ts similarity index 74% rename from src/onebot11/actions/GetMsg.ts rename to src/onebot11/action/GetMsg.ts index 7dedbcd..0be0ddf 100644 --- a/src/onebot11/actions/GetMsg.ts +++ b/src/onebot11/action/GetMsg.ts @@ -1,9 +1,8 @@ -import { getHistoryMsgByShortId, msgHistory } from "../../common/data"; -import { OB11Message } from '../types'; -import { OB11Constructor } from "../constructor"; -import { log } from "../../common/utils"; +import {getHistoryMsgByShortId} from "../../common/data"; +import {OB11Message} from '../types'; +import {OB11Constructor} from "../constructor"; import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; export interface PayloadType { diff --git a/src/onebot11/actions/GetStatus.ts b/src/onebot11/action/GetStatus.ts similarity index 89% rename from src/onebot11/actions/GetStatus.ts rename to src/onebot11/action/GetStatus.ts index 2df98db..c3f038c 100644 --- a/src/onebot11/actions/GetStatus.ts +++ b/src/onebot11/action/GetStatus.ts @@ -1,6 +1,6 @@ import BaseAction from "./BaseAction"; import {OB11Status} from "../types"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; export default class GetStatus extends BaseAction { diff --git a/src/onebot11/actions/GetVersionInfo.ts b/src/onebot11/action/GetVersionInfo.ts similarity index 83% rename from src/onebot11/actions/GetVersionInfo.ts rename to src/onebot11/action/GetVersionInfo.ts index 5fadb22..83ba16b 100644 --- a/src/onebot11/actions/GetVersionInfo.ts +++ b/src/onebot11/action/GetVersionInfo.ts @@ -1,7 +1,7 @@ import BaseAction from "./BaseAction"; -import { OB11Version } from "../types"; +import {OB11Version} from "../types"; import {version} from "../../common/data"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; export default class GetVersionInfo extends BaseAction{ actionName = ActionName.GetVersionInfo diff --git a/src/onebot11/actions/SendGroupMsg.ts b/src/onebot11/action/SendGroupMsg.ts similarity index 78% rename from src/onebot11/actions/SendGroupMsg.ts rename to src/onebot11/action/SendGroupMsg.ts index 13df317..3bb09b5 100644 --- a/src/onebot11/actions/SendGroupMsg.ts +++ b/src/onebot11/action/SendGroupMsg.ts @@ -1,5 +1,5 @@ import SendMsg from "./SendMsg"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; class SendGroupMsg extends SendMsg{ diff --git a/src/onebot11/actions/SendMsg.ts b/src/onebot11/action/SendMsg.ts similarity index 89% rename from src/onebot11/actions/SendMsg.ts rename to src/onebot11/action/SendMsg.ts index 4e28109..519ce0b 100644 --- a/src/onebot11/actions/SendMsg.ts +++ b/src/onebot11/action/SendMsg.ts @@ -10,6 +10,41 @@ import {ActionName, BaseCheckResult} from "./types"; import * as fs from "fs"; import {log, sleep} from "../../common/utils"; +function checkSendMessage(sendMsgList: OB11MessageData[]) { + function checkUri(uri: string): boolean { + const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/; + return pattern.test(uri); + } + + for (let msg of sendMsgList) { + if (msg["type"] && msg["data"]) { + let type = msg["type"]; + let data = msg["data"]; + if (type === "text" && !data["text"]) { + return 400; + } else if (["image", "voice", "record"].includes(type)) { + if (!data["file"]) { + return 400; + } else { + if (checkUri(data["file"])) { + return 200; + } else { + return 400; + } + } + + } else if (type === "at" && !data["qq"]) { + return 400; + } else if (type === "reply" && !data["id"]) { + return 400; + } + } else { + return 400 + } + } + return 200; +} + export interface ReturnDataType { message_id: number } @@ -233,8 +268,6 @@ class SendMsg extends BaseAction { })) return returnMsg } - - } export default SendMsg \ No newline at end of file diff --git a/src/onebot11/actions/SendPrivateMsg.ts b/src/onebot11/action/SendPrivateMsg.ts similarity index 79% rename from src/onebot11/actions/SendPrivateMsg.ts rename to src/onebot11/action/SendPrivateMsg.ts index 26a8b22..5ce5b95 100644 --- a/src/onebot11/actions/SendPrivateMsg.ts +++ b/src/onebot11/action/SendPrivateMsg.ts @@ -1,5 +1,5 @@ import SendMsg from "./SendMsg"; -import { ActionName } from "./types"; +import {ActionName} from "./types"; class SendPrivateMsg extends SendMsg { actionName = ActionName.SendPrivateMsg diff --git a/src/onebot11/actions/index.ts b/src/onebot11/action/index.ts similarity index 78% rename from src/onebot11/actions/index.ts rename to src/onebot11/action/index.ts index 2baa5dc..33c6d2d 100644 --- a/src/onebot11/actions/index.ts +++ b/src/onebot11/action/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"; import GetVersionInfo from "./GetVersionInfo"; import CanSendRecord from "./CanSendRecord"; import CanSendImage from "./CanSendImage"; @@ -27,4 +28,15 @@ export const actionHandlers = [ new CanSendRecord(), new CanSendImage(), new GetStatus() -] \ 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(); diff --git a/src/onebot11/actions/types.ts b/src/onebot11/action/types.ts similarity index 92% rename from src/onebot11/actions/types.ts rename to src/onebot11/action/types.ts index b604b24..d747cf5 100644 --- a/src/onebot11/actions/types.ts +++ b/src/onebot11/action/types.ts @@ -1,5 +1,3 @@ -import GetVersionInfo from "./GetVersionInfo"; - export type BaseCheckResult = ValidCheckResult | InvalidCheckResult export interface ValidCheckResult { @@ -13,7 +11,7 @@ export interface InvalidCheckResult { [k: string | number]: any } -export enum ActionName{ +export enum ActionName { TestForwardMsg = "test_forward_msg", GetLoginInfo = "get_login_info", GetFriendList = "get_friend_list", diff --git a/src/onebot11/action/utils.ts b/src/onebot11/action/utils.ts new file mode 100644 index 0000000..e87b394 --- /dev/null +++ b/src/onebot11/action/utils.ts @@ -0,0 +1,36 @@ +import {OB11Return, OB11WebsocketReturn} from '../types'; + +export class OB11Response { + static res(data: T, status: string, retcode: number, message: string = ""): OB11Return { + return { + status: status, + retcode: retcode, + data: data, + message: message + } + } + static ok(data: T) { + return OB11Response.res(data, "ok", 0) + } + 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/actions/BaseAction.ts b/src/onebot11/actions/BaseAction.ts deleted file mode 100644 index ab69555..0000000 --- a/src/onebot11/actions/BaseAction.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {ActionName, BaseCheckResult} from "./types" -import { OB11Response } from "./utils" -import { OB11Return } from "../types"; - -class BaseAction { - actionName: ActionName - protected async check(payload: PayloadType): Promise { - return { - valid: true, - } - } - - public async handle(payload: PayloadType): Promise> { - const result = await this.check(payload) - if (!result.valid) { - return OB11Response.error(result.message) - } - try { - const resData = await this._handle(payload) - return OB11Response.ok(resData) - }catch (e) { - return OB11Response.error(e.toString()) - } - } - - protected async _handle(payload: PayloadType): Promise { - throw `pleas override ${this.actionName} _handle` - } -} - -export default BaseAction \ No newline at end of file diff --git a/src/onebot11/actions/TestForwdMsg.ts b/src/onebot11/actions/TestForwdMsg.ts deleted file mode 100644 index 24b3fd0..0000000 --- a/src/onebot11/actions/TestForwdMsg.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { OB11Message } from '../types'; -import BaseAction from "./BaseAction"; -import { ActionName } from "./types"; -import { NTQQApi, Peer } from "../../ntqqapi/ntcall"; -import { ChatType } from "../../ntqqapi/types"; -import { selfInfo } from "../../common/data"; -import { SendMsgElementConstructor } from "../../ntqqapi/constructor"; -import {sleep} from "../../common/utils"; - - -export interface PayloadType { - message: string, - group_id: string -} - -export default class TestForwardMsg extends BaseAction { - actionName = ActionName.TestForwardMsg - - protected async _handle(payload: PayloadType) { - // log("history msg ids", Object.keys(msgHistory)); - const selfPeer: Peer = { - chatType: ChatType.friend, - peerUid: selfInfo.uid - } - const sendMsg = await NTQQApi.sendMsg(selfPeer, [SendMsgElementConstructor.text(payload.message)]) - const sendMsg2 = await NTQQApi.sendMsg(selfPeer, [SendMsgElementConstructor.text(payload.message)]) - await NTQQApi.multiForwardMsg( - selfPeer, - {chatType: ChatType.group, peerUid: payload.group_id, guildId: ""}, - [sendMsg.msgId, sendMsg2.msgId] - ) - return null - } -} \ No newline at end of file diff --git a/src/onebot11/actions/utils.ts b/src/onebot11/actions/utils.ts deleted file mode 100644 index 9edfc90..0000000 --- a/src/onebot11/actions/utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { OB11Return } from '../types'; - -export class OB11Response { - static res(data: T, status: number = 0, message: string = "", echo=""): OB11Return { - return { - status: status, - retcode: status, - data: data, - message: message, - echo, - } - } - static ok(data: T) { - return OB11Response.res(data) - } - static error(err: string, status=-1) { - return OB11Response.res(null, status, err) - } -} diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index d06cbd3..2a2c59a 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -1,19 +1,11 @@ -import { - OB11MessageDataType, - OB11GroupMemberRole, - OB11Message, - OB11Group, - OB11GroupMember, - OB11User -} from "./types"; -import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/types'; -import { getFriend, getGroupMember, getHistoryMsgBySeq, heartInterval, msgHistory, selfInfo } from '../common/data'; -import { file2base64, getConfigUtil, log } from "../common/utils"; -import { NTQQApi } from "../ntqqapi/ntcall"; -import {OB11EventConstructor} from "./events/constructor"; +import {OB11Group, OB11GroupMember, OB11GroupMemberRole, OB11Message, OB11MessageDataType, OB11User} from "./types"; +import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types'; +import {getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo} from '../common/data'; +import {file2base64, getConfigUtil, log} from "../common/utils"; +import {NTQQApi} from "../ntqqapi/ntcall"; -export class OB11Constructor extends OB11EventConstructor{ +export class OB11Constructor { static async message(msg: RawMessage): Promise { const {enableBase64} = getConfigUtil().getConfig() diff --git a/src/onebot11/event/OB11BaseEvent.ts b/src/onebot11/event/OB11BaseEvent.ts new file mode 100644 index 0000000..f45577f --- /dev/null +++ b/src/onebot11/event/OB11BaseEvent.ts @@ -0,0 +1,15 @@ +import {selfInfo} from "../../common/data"; + +export enum EventType { + META = "meta_event", + REQUEST = "request", + NOTICE = "notice", + MESSAGE = "message" +} + + +export abstract class OB11BaseEvent { + time = new Date().getTime(); + self_id = selfInfo.uin; + post_type: EventType; +} \ No newline at end of file diff --git a/src/onebot11/event/manager.ts b/src/onebot11/event/manager.ts new file mode 100644 index 0000000..93ce191 --- /dev/null +++ b/src/onebot11/event/manager.ts @@ -0,0 +1,24 @@ +import * as websocket from "ws"; +import {PostMsgType, wsReply} from "../server"; +import {ReconnectingWebsocket} from "../ReconnectingWebsocket"; + +const websocketList = []; + +export function registerEventSender(ws: websocket.WebSocket | ReconnectingWebsocket) { + websocketList.push(ws); +} + +export function unregisterEventSender(ws: websocket.WebSocket | ReconnectingWebsocket) { + let index = websocketList.indexOf(ws); + if (index !== -1) { + websocketList.splice(index, 1); + } +} + +export function callEvent(event: PostMsgType) { + new Promise(() => { + for (const ws of websocketList) { + wsReply(ws, event); + } + }).then() +} \ No newline at end of file diff --git a/src/onebot11/event/message/OB11BaseMessageEvent.ts b/src/onebot11/event/message/OB11BaseMessageEvent.ts new file mode 100644 index 0000000..6018aeb --- /dev/null +++ b/src/onebot11/event/message/OB11BaseMessageEvent.ts @@ -0,0 +1,5 @@ +import {EventType, OB11BaseEvent} from "../OB11BaseEvent"; + +export abstract class OB11BaseMessageEvent extends OB11BaseEvent { + post_type = EventType.MESSAGE; +} \ No newline at end of file diff --git a/src/onebot11/event/meta/OB11BaseMetaEvent.ts b/src/onebot11/event/meta/OB11BaseMetaEvent.ts new file mode 100644 index 0000000..d3946e6 --- /dev/null +++ b/src/onebot11/event/meta/OB11BaseMetaEvent.ts @@ -0,0 +1,6 @@ +import {EventType, OB11BaseEvent} from "../OB11BaseEvent"; + +export abstract class OB11BaseMetaEvent extends OB11BaseEvent { + post_type = EventType.META; + meta_event_type: string; +} \ No newline at end of file diff --git a/src/onebot11/event/meta/OB11HeartbeatEvent.ts b/src/onebot11/event/meta/OB11HeartbeatEvent.ts new file mode 100644 index 0000000..42fa024 --- /dev/null +++ b/src/onebot11/event/meta/OB11HeartbeatEvent.ts @@ -0,0 +1,21 @@ +import {OB11BaseMetaEvent} from "./OB11BaseMetaEvent"; + +interface HeartbeatStatus { + online: boolean | null, + good: boolean +} + +export class OB11HeartbeatEvent extends OB11BaseMetaEvent { + meta_event_type = "heartbeat"; + status: HeartbeatStatus; + interval: number; + + public constructor(isOnline: boolean | null, isGood: boolean, interval: number) { + super(); + this.interval = interval; + this.status = { + online: isOnline, + good: isGood + } + } +} \ No newline at end of file diff --git a/src/onebot11/event/meta/OB11LifeCycleEvent.ts b/src/onebot11/event/meta/OB11LifeCycleEvent.ts new file mode 100644 index 0000000..e9243ca --- /dev/null +++ b/src/onebot11/event/meta/OB11LifeCycleEvent.ts @@ -0,0 +1,17 @@ +import {OB11BaseMetaEvent} from "./OB11BaseMetaEvent"; + +export enum LifeCycleSubType { + ENABLE = "enable", + DISABLE = "disable", + CONNECT = "connect" +} + +export class OB11LifeCycleEvent extends OB11BaseMetaEvent { + meta_event_type = "lifecycle"; + sub_type: LifeCycleSubType; + + public constructor(subType: LifeCycleSubType) { + super(); + this.sub_type = subType; + } +} \ No newline at end of file diff --git a/src/onebot11/event/notice/OB11BaseNoticeEvent.ts b/src/onebot11/event/notice/OB11BaseNoticeEvent.ts new file mode 100644 index 0000000..9429d2c --- /dev/null +++ b/src/onebot11/event/notice/OB11BaseNoticeEvent.ts @@ -0,0 +1,5 @@ +import {EventType, OB11BaseEvent} from "../OB11BaseEvent"; + +export abstract class OB11BaseNoticeEvent extends OB11BaseEvent { + post_type = EventType.NOTICE; +} \ No newline at end of file diff --git a/src/onebot11/event/notice/OB11FriendRecallNoticeEvent.ts b/src/onebot11/event/notice/OB11FriendRecallNoticeEvent.ts new file mode 100644 index 0000000..9344b44 --- /dev/null +++ b/src/onebot11/event/notice/OB11FriendRecallNoticeEvent.ts @@ -0,0 +1,13 @@ +import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent"; + +export class OB11FriendRecallNoticeEvent extends OB11BaseNoticeEvent { + notice_type = "friend_recall" + user_id: number + message_id: number + + public constructor(userId: number, messageId: number) { + super(); + this.user_id = userId; + this.message_id = messageId; + } +} \ No newline at end of file diff --git a/src/onebot11/event/notice/OB11GroupAdminNoticeEvent.ts b/src/onebot11/event/notice/OB11GroupAdminNoticeEvent.ts new file mode 100644 index 0000000..f30739e --- /dev/null +++ b/src/onebot11/event/notice/OB11GroupAdminNoticeEvent.ts @@ -0,0 +1,6 @@ +import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent"; + +export class OB11GroupAdminNoticeEvent extends OB11BaseNoticeEvent { + notice_type = "group_admin" + sub_type: string // "set" | "unset" +} \ No newline at end of file diff --git a/src/onebot11/event/notice/OB11GroupDecreaseEvent.ts b/src/onebot11/event/notice/OB11GroupDecreaseEvent.ts new file mode 100644 index 0000000..f9518d2 --- /dev/null +++ b/src/onebot11/event/notice/OB11GroupDecreaseEvent.ts @@ -0,0 +1,14 @@ +import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent"; + +export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent { + notice_type = "group_decrease"; + sub_type = "leave"; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me") + operate_id: number; + + constructor(groupId: number, userId: number) { + super(); + this.group_id = groupId; + this.operate_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被踢出的,还是自己主动退出的 + this.user_id = userId; + } +} diff --git a/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts b/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts new file mode 100644 index 0000000..0b07394 --- /dev/null +++ b/src/onebot11/event/notice/OB11GroupIncreaseEvent.ts @@ -0,0 +1,14 @@ +import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent"; + +export class OB11GroupIncreaseEvent extends OB11GroupNoticeEvent { + notice_type = "group_increase"; + sub_type = "approve"; // TODO: 实现其他几种子类型的识别 ("approve" | "invite") + operate_id: number; + + constructor(groupId: number, userId: number) { + super(); + this.group_id = groupId; + this.operate_id = userId; // 实际上不应该这么实现,但是现在还没有办法识别用户是被邀请的,还是主动加入的 + this.user_id = userId; + } +} \ No newline at end of file diff --git a/src/onebot11/event/notice/OB11GroupNoticeEvent.ts b/src/onebot11/event/notice/OB11GroupNoticeEvent.ts new file mode 100644 index 0000000..31c1d6c --- /dev/null +++ b/src/onebot11/event/notice/OB11GroupNoticeEvent.ts @@ -0,0 +1,6 @@ +import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent"; + +export abstract class OB11GroupNoticeEvent extends OB11BaseNoticeEvent { + group_id: number; + user_id: number; +} \ No newline at end of file diff --git a/src/onebot11/event/notice/OB11GroupRecallNoticeEvent.ts b/src/onebot11/event/notice/OB11GroupRecallNoticeEvent.ts new file mode 100644 index 0000000..11c180a --- /dev/null +++ b/src/onebot11/event/notice/OB11GroupRecallNoticeEvent.ts @@ -0,0 +1,15 @@ +import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent"; + +export class OB11GroupRecallNoticeEvent extends OB11GroupNoticeEvent { + notice_type = "group_recall" + operator_id: number + message_id: number + + constructor(groupId: number, userId: number, operatorId: number, messageId: number) { + super(); + this.group_id = groupId; + this.user_id = userId; + this.operator_id = operatorId; + this.message_id = messageId; + } +} \ No newline at end of file diff --git a/src/onebot11/events/constructor.ts b/src/onebot11/events/constructor.ts deleted file mode 100644 index 06cd850..0000000 --- a/src/onebot11/events/constructor.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - OB11EventBase, - OB11EventPostType, OB11FriendRecallNoticeEvent, - OB11GroupRecallNoticeEvent, - OB11HeartEvent, - OB11LifeCycleEvent, OB11MetaEvent, OB11NoticeEvent -} from "./types"; -import { heartInterval, selfInfo } from "../../common/data"; - -function eventBase(post_type: OB11EventPostType): OB11EventBase { - return { - time: Math.floor(Date.now() / 1000), - self_id: parseInt(selfInfo.uin), - post_type - } -} - -export class OB11EventConstructor { - static lifeCycleEvent(): OB11LifeCycleEvent { - return { - ...eventBase(OB11EventPostType.META) as OB11MetaEvent, - meta_event_type: "lifecycle", - sub_type: "connect" - } - } - - static heartEvent(): OB11HeartEvent { - return { - ...eventBase(OB11EventPostType.META) as OB11MetaEvent, - meta_event_type: "heartbeat", - status: { - online: true, - good: true - }, - interval: heartInterval - } - } - - static groupRecallEvent(group_id: string, user_id: string, operator_id: string, message_id: number): OB11GroupRecallNoticeEvent { - return { - ...eventBase(OB11EventPostType.NOTICE) as OB11NoticeEvent, - notice_type: "group_recall", - group_id: parseInt(group_id), - user_id: parseInt(user_id), - operator_id: parseInt(operator_id), - message_id - } - } - - static friendRecallEvent(user_id: string, message_id: number): OB11FriendRecallNoticeEvent { - return { - ...eventBase(OB11EventPostType.NOTICE) as OB11NoticeEvent, - notice_type: "friend_recall", - user_id: parseInt(user_id), - message_id - } - } -} \ No newline at end of file diff --git a/src/onebot11/events/types.ts b/src/onebot11/events/types.ts deleted file mode 100644 index 5902114..0000000 --- a/src/onebot11/events/types.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { OB11Status } from "../types"; - -export enum OB11EventPostType{ - META = "meta_event", - NOTICE = "notice" -} - -export interface OB11EventBase { - time: number - self_id: number - post_type: OB11EventPostType -} - -export interface OB11MetaEvent extends OB11EventBase{ - post_type: OB11EventPostType.META - meta_event_type: "lifecycle" | "heartbeat" -} - -export interface OB11NoticeEvent extends OB11EventBase{ - post_type: OB11EventPostType.NOTICE - notice_type: "group_admin" | "group_decrease" | "group_increase" | "group_ban" | "friend_add" | "group_recall" | "friend_recall" -} - -interface OB11GroupNoticeBase extends OB11NoticeEvent{ - group_id: number - user_id: number -} - -export interface OB11GroupAdminNoticeEvent extends OB11GroupNoticeBase{ - notice_type: "group_admin" - sub_type: "set" | "unset" -} - -export interface OB11GroupMemberDecNoticeEvent extends OB11GroupNoticeBase{ - notice_type: "group_decrease" - sub_type: "leave" | "kick" | "kick_me" - operator_id: number -} - -export interface OB11GroupMemberIncNoticeEvent extends OB11GroupNoticeBase{ - notice_type: "group_increase" - sub_type: "approve" | "invite" - operator_id: number -} - -export interface OB11GroupRecallNoticeEvent extends OB11GroupNoticeBase{ - notice_type: "group_recall" - operator_id: number - message_id: number -} - -export interface OB11FriendRecallNoticeEvent extends OB11NoticeEvent{ - notice_type: "friend_recall" - user_id: number - message_id: number -} - -export interface OB11LifeCycleEvent extends OB11MetaEvent { - meta_event_type: "lifecycle" - sub_type: "enable" | "disable" | "connect" -} - -export interface OB11HeartEvent extends OB11MetaEvent { - meta_event_type: "heartbeat" - status: OB11Status - interval: number -} \ No newline at end of file diff --git a/src/onebot11/server.ts b/src/onebot11/server.ts index 56f6391..df87476 100644 --- a/src/onebot11/server.ts +++ b/src/onebot11/server.ts @@ -1,200 +1,185 @@ -import * as http from "http"; import * as websocket from "ws"; import urlParse from "url"; -import express from "express"; -import { Request } from 'express'; -import { Response } from 'express'; -import { getConfigUtil, log } from "../common/utils"; -import { heartInterval, selfInfo } from "../common/data"; -import { OB11Message, OB11Return, OB11MessageData } from './types'; -import { actionHandlers } from "./actions"; -import { OB11Response } from "./actions/utils"; -import { ActionName } from "./actions/types"; -import BaseAction from "./actions/BaseAction"; -import { OB11Constructor } from "./constructor"; -import { OB11EventBase, OB11LifeCycleEvent, OB11MetaEvent, OB11NoticeEvent } from "./events/types"; +import {getConfigUtil, log} from "../common/utils"; +import {selfInfo} from "../common/data"; +import {OB11Message} from './types'; +import {actionMap} from "./action"; +import {OB11WebsocketResponse} from "./action/utils"; +import {callEvent, registerEventSender, unregisterEventSender} from "./event/manager"; +import {ReconnectingWebsocket} from "./ReconnectingWebsocket"; +import {ActionName} from "./action/types"; +import {OB11BaseMetaEvent} from "./event/meta/OB11BaseMetaEvent"; +import {OB11BaseNoticeEvent} from "./event/notice/OB11BaseNoticeEvent"; +import BaseAction from "./action/BaseAction"; +import {LifeCycleSubType, OB11LifeCycleEvent} from "./event/meta/OB11LifeCycleEvent"; +import {OB11HeartbeatEvent} from "./event/meta/OB11HeartbeatEvent"; -let wsServer: websocket.Server = null; -let accessToken = "" +let heartbeatRunning = false; +let websocketServer = null; -const JSONbig = require('json-bigint')({storeAsString: true}); -const expressAPP = express(); -let httpServer: http.Server = null; -expressAPP.use(express.urlencoded({extended: true, limit: "500mb"})); +export function initWebsocket(port: number) { + const {heartInterval, ob11: {enableWs}, token} = getConfigUtil().getConfig() + if (!heartbeatRunning) { + setInterval(() => { + callEvent(new OB11HeartbeatEvent(true, true, heartInterval)); + }, heartInterval); // 心跳包 -expressAPP.use((req, res, next) => { - let data = ''; - req.on('data', chunk => { - data += chunk.toString(); - }); - req.on('end', () => { - if (data) { - try { - // log("receive raw", data) - req.body = JSONbig.parse(data); - } catch (e) { - return next(e); - } + heartbeatRunning = true; + } + if (enableWs) { + if (websocketServer) { + websocketServer.close((err) => { + log("ws server close failed!", err) + }) } - next(); - }); -}); -const expressAuthorize = (req: Request, res: Response, next: () => void) => { - try { - let token = "" - const authHeader = req.get("authorization") - if (authHeader) { - token = authHeader.split("Bearer ").pop() - log("receive http header token", token) - } else if (req.query.access_token) { - if (Array.isArray(req.query.access_token)) { - token = req.query.access_token[0].toString(); + websocketServer = new websocket.Server({port}); + console.log(`llonebot websocket service started 0.0.0.0:${port}`); + + websocketServer.on("connection", (ws, req) => { + const url = req.url.split("?").shift(); + log("receive ws connect", url) + let clientToken: string = "" + const authHeader = req.headers['authorization']; + if (authHeader) { + clientToken = authHeader.split("Bearer ").pop() + log("receive ws header token", clientToken); } else { - token = req.query.access_token.toString(); + const parsedUrl = urlParse.parse(req.url, true); + const urlToken = parsedUrl.query.access_token; + if (urlToken) { + if (Array.isArray(urlToken)) { + clientToken = urlToken[0] + } else { + clientToken = urlToken + } + log("receive ws url token", clientToken); + } + } + if (token && clientToken != token) { + ws.send(JSON.stringify(OB11WebsocketResponse.res(null, "failed", 1403, "token验证失败"))) + return ws.close() } - log("receive http url token", token) - } - if (accessToken) { - if (token != accessToken) { - return res.status(403).send(JSON.stringify({message: 'token verify failed!'})); + if (url == "/api" || url == "/api/" || url == "/") { + ws.on("message", async (msg) => { + let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}} + let echo = "" + log("收到正向Websocket消息", msg.toString()) + try { + receiveData = JSON.parse(msg.toString()) + echo = receiveData.echo + } catch (e) { + return wsReply(ws, OB11WebsocketResponse.error("json解析失败,请检查数据格式", 1400, echo)) + } + const action: BaseAction = actionMap.get(receiveData.action); + if (!action) { + return wsReply(ws, OB11WebsocketResponse.error("不支持的api " + receiveData.action, 1404, echo)) + } + try { + let handleResult = await action.websocketHandle(receiveData.params, echo); + wsReply(ws, handleResult) + } catch (e) { + wsReply(ws, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo)) + } + }) } - } - }catch (e) { - log("receive http failed", e.stack) + if (url == "/event" || url == "/event/" || url == "/") { + registerEventSender(ws); + + log("event上报ws客户端已连接") + + try { + wsReply(ws, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)) + } catch (e) { + log("发送生命周期失败", e) + } + + ws.on("close", () => { + log("event上报ws客户端已断开") + unregisterEventSender(ws); + }) + } + }) } - next(); -}; - -export function setToken(token: string) { - accessToken = token + initReverseWebsocket(); } -export function startHTTPServer(port: number) { - if (httpServer) { - httpServer.close(); - } - expressAPP.get('/', (req: Request, res: Response) => { - res.send('LLOneBot已启动'); - }) +function initReverseWebsocket() { + const config = getConfigUtil().getConfig(); + if (config.ob11.enableWsReverse) { + console.log("Prepare to connect all reverse websockets..."); + for (const url of config.ob11.wsHosts) { + new Promise(() => { + try { + let wsClient = new ReconnectingWebsocket(url); + registerEventSender(wsClient); - httpServer = expressAPP.listen(port, "0.0.0.0", () => { - console.log(`LLOneBot http server started 0.0.0.0:${port}`); - }); + wsClient.onopen = function () { + wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT)); + } + + wsClient.onclose = function () { + unregisterEventSender(wsClient); + } + + wsClient.onmessage = async function (msg) { + let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}} + let echo = "" + log("收到反向Websocket消息", msg.toString()) + try { + receiveData = JSON.parse(msg.toString()) + echo = receiveData.echo + } catch (e) { + return wsReply(wsClient, OB11WebsocketResponse.error("json解析失败,请检查数据格式", 1400, echo)) + } + const action: BaseAction = actionMap.get(receiveData.action); + if (!action) { + return wsReply(wsClient, OB11WebsocketResponse.error("不支持的api " + receiveData.action, 1404, echo)) + } + try { + let handleResult = await action.websocketHandle(receiveData.params, echo); + wsReply(wsClient, handleResult) + } catch (e) { + wsReply(wsClient, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo)) + } + } + } catch (e) { + log(e.stack); + } + }).then(); + } + } } -let wsEventClients: websocket.WebSocket[] = []; -type RouterHandler = (payload: any) => Promise> -let routers: Record = {}; - -function wsReply(wsClient: websocket.WebSocket, data: OB11Return | PostMsgType) { +export function wsReply(wsClient: websocket.WebSocket | ReconnectingWebsocket, data: OB11WebsocketResponse | PostMsgType) { try { - wsClient.send(JSON.stringify(data)) + let packet = Object.assign({ + echo: "" + }, data); + if (!packet.echo) { + packet.echo = ""; + } + + wsClient.send(JSON.stringify(packet)) log("ws 消息上报", data) } catch (e) { log("websocket 回复失败", e) } } -export function startWSServer(port: number) { - if (wsServer) { - wsServer.close((err) => { - log("ws server close failed!", err) - }) - } - wsServer = new websocket.Server({port}) - wsServer.on("connection", (ws, req) => { - const url = req.url.split("?").shift(); - log("receive ws connect", url) - let token: string = "" - const authHeader = req.headers['authorization']; - if (authHeader) { - token = authHeader.split("Bearer ").pop() - log("receive ws header token", token); - } else { - const parsedUrl = urlParse.parse(req.url, true); - const urlToken = parsedUrl.query.access_token; - if (urlToken) { - if (Array.isArray(urlToken)) { - token = urlToken[0] - } else { - token = urlToken - } - log("receive ws url token", token); - } - - } - if (accessToken) { - if (token != accessToken) { - ws.send(JSON.stringify(OB11Response.res(null, 1403, "token验证失败"))) - return ws.close() - } - } - if (url == "/api" || url == "/api/" || url == "/") { - ws.on("message", async (msg) => { - - let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}} - let echo = "" - log("收到ws消息", msg.toString()) - try { - receiveData = JSON.parse(msg.toString()) - echo = receiveData.echo - } catch (e) { - return wsReply(ws, {...OB11Response.error("json解析失败,请检查数据格式"), echo}) - } - const handle: RouterHandler | undefined = routers[receiveData.action] - if (!handle) { - let handleResult = OB11Response.error("不支持的api " + receiveData.action, 1404) - handleResult.echo = echo - return wsReply(ws, handleResult) - } - try { - let handleResult = await handle(receiveData.params) - if (echo){ - handleResult.echo = echo - } - wsReply(ws, handleResult) - } catch (e) { - wsReply(ws, OB11Response.error(`api处理出错:${e}`)) - } - }) - } - if (url == "/event" || url == "/event/" || url == "/") { - log("event上报ws客户端已连接") - wsEventClients.push(ws) - try { - wsReply(ws, OB11Constructor.lifeCycleEvent()) - }catch (e){ - log("发送生命周期失败", e) - } - // 心跳 - let wsHeart = setInterval(()=>{ - if (wsEventClients.find(c => c == ws)){ - wsReply(ws, OB11Constructor.heartEvent()) - } - }, heartInterval) - ws.on("close", () => { - clearInterval(wsHeart); - log("event上报ws客户端已断开") - wsEventClients = wsEventClients.filter((c) => c != ws) - }) - } - }) -} - -type PostMsgType = OB11Message | OB11MetaEvent | OB11NoticeEvent +export type PostMsgType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent export function postMsg(msg: PostMsgType) { - const {reportSelfMessage} = getConfigUtil().getConfig() + const config = getConfigUtil().getConfig(); // 判断msg是否是event - if (!reportSelfMessage) { + if (!config.reportSelfMessage) { if ((msg as OB11Message).user_id.toString() == selfInfo.uin) { return } } - for (const host of getConfigUtil().getConfig().hosts) { + for (const host of config.ob11.httpHosts) { fetch(host, { method: "POST", headers: { @@ -208,41 +193,7 @@ export function postMsg(msg: PostMsgType) { log(`新消息事件HTTP上报失败: ${host} ` + err + JSON.stringify(msg)); }); } - for (const wsClient of wsEventClients) { - log("新消息事件ws上报", msg) - new Promise((resolve, reject) => { - wsReply(wsClient, msg); - }).then(); - } + + log("新消息事件ws上报", msg); + callEvent(msg); } - - -function registerRouter(action: string, handle: (payload: any) => Promise) { - let url = action.toString() - if (!action.startsWith("/")) { - url = "/" + action - } - - async function _handle(res: Response, payload: any) { - log("receive post data", url, payload) - try { - const result = await handle(payload) - res.send(result) - } catch (e) { - log(e.stack); - res.send(OB11Response.error(e.stack.toString())) - } - } - - expressAPP.post(url, expressAuthorize, (req: Request, res: Response) => { - _handle(res, req.body || {}).then() - }); - expressAPP.get(url, expressAuthorize, (req: Request, res: Response) => { - _handle(res, req.query as any || {}).then() - }); - routers[action] = handle -} - -for (const action of actionHandlers) { - registerRouter(action.actionName, (payload) => action.handle(payload)) -} \ No newline at end of file diff --git a/src/onebot11/server/http.ts b/src/onebot11/server/http.ts new file mode 100644 index 0000000..b7408a2 --- /dev/null +++ b/src/onebot11/server/http.ts @@ -0,0 +1,26 @@ +import {Response} from "express"; +import {getConfigUtil} from "../../common/utils"; +import {OB11Response} from "../action/utils"; +import {HttpServerBase} from "../../server/http"; +import {actionHandlers} from "../action"; + +class OB11HTTPServer extends HttpServerBase { + name = "OneBot V11 server" + handleFailed(res: Response, payload: any, e: any) { + res.send(OB11Response.error(e.stack.toString(), 200)) + } + + protected listen(port: number) { + if (getConfigUtil().getConfig().ob11.enableHttp) { + super.listen(port); + } + } +} + +export const ob11HTTPServer = new OB11HTTPServer(); + +for (const action of actionHandlers) { + for(const method of ["post", "get"]){ + ob11HTTPServer.registerRouter(method, action.actionName, (res, payload) => action.handle(payload)) + } +} diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index ad9c9c4..f7db754 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -1,4 +1,4 @@ -import { AtType, RawMessage } from "../ntqqapi/types"; +import {AtType, RawMessage} from "../ntqqapi/types"; export interface OB11User { user_id: number; @@ -72,14 +72,14 @@ export interface OB11Message { } export interface OB11Return { - status: number + status: string retcode: number data: DataType message: string, - echo?: string } -export interface OB11SendMsgReturn extends OB11Return<{ message_id: string }> { +export interface OB11WebsocketReturn extends OB11Return{ + echo: string } export enum OB11MessageDataType { diff --git a/src/onebot11/utils.ts b/src/onebot11/utils.ts index e6e1278..8050e68 100644 --- a/src/onebot11/utils.ts +++ b/src/onebot11/utils.ts @@ -1,7 +1,7 @@ -import { CONFIG_DIR, isGIF } from "../common/utils"; +import {CONFIG_DIR, isGIF} from "../common/utils"; import * as path from 'path'; -import { NTQQApi } from '../ntqqapi/ntcall'; -import { OB11MessageData } from "./types"; +import {OB11MessageData} from "./types"; + const fs = require("fs").promises; export async function uri2local(fileName: string, uri: string){ diff --git a/src/preload.ts b/src/preload.ts index 2f3d27e..4fe23c2 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -20,5 +20,4 @@ const llonebot = { export type LLOneBot = typeof llonebot; // 在window对象下导出只读对象 -contextBridge.exposeInMainWorld("llonebot", llonebot); -; \ No newline at end of file +contextBridge.exposeInMainWorld("llonebot", llonebot); \ No newline at end of file diff --git a/src/renderer.ts b/src/renderer.ts index b379192..312bc23 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -5,47 +5,103 @@ async function onSettingWindowCreated(view: Element) { window.llonebot.log("setting window created"); let config = await window.llonebot.getConfig() + const httpClass = "http"; + const httpPostClass = "http-post"; + const wsClass = "ws"; + const reverseWSClass = "reverse-ws"; - function creatHostEleStr(host: string) { + function createHttpHostEleStr(host: string) { let eleStr = ` - -

事件上报地址(http)

- +

HTTP事件上报地址(http)

+
- ` return eleStr } - let hostsEleStr = "" - for (const host of config.hosts) { - hostsEleStr += creatHostEleStr(host); + function createWsHostEleStr(host: string) { + let eleStr = ` + +

事件上报地址(反向websocket)

+ +
+ ` + return eleStr } + + let httpHostsEleStr = "" + for (const host of config.ob11.httpHosts) { + httpHostsEleStr += createHttpHostEleStr(host); + } + + let wsHostsEleStr = "" + for (const host of config.ob11.wsHosts) { + wsHostsEleStr += createWsHostEleStr(host); + } + let html = `
- + +
+
启用HTTP服务
+
+ +
+ HTTP监听端口 - + - - 正向ws监听端口 - + +
+
启用HTTP事件上报
+
+
+
+
+ +
+
+ ${httpHostsEleStr} +
+
+ +
+
启用正向Websocket协议
+
+ +
+ + 正向Websocket监听端口 + + + + +
+
启用反向Websocket协议
+
+ +
+
+
+ +
+
+ ${wsHostsEleStr} +
+
Access Token -
- -
-
- ${hostsEleStr} -
@@ -74,7 +130,7 @@ async function onSettingWindowCreated(view: Element) {
日志
-
日志目录:${window.LiteLoader.plugins["LLOneBot"].path.data}
+
目录:${window.LiteLoader.plugins["LLOneBot"].path.data}
@@ -100,30 +156,57 @@ async function onSettingWindowCreated(view: Element) { const doc = parser.parseFromString(html, "text/html"); - function addHostEle(initValue: string = "") { - let addressDoc = parser.parseFromString(creatHostEleStr(initValue), "text/html"); - let addressEle = addressDoc.querySelector("setting-item") - let hostItemsEle = document.getElementById("hostItems"); + function addHostEle(type: string, initValue: string = "") { + let addressEle, hostItemsEle; + if (type === "ws") { + let addressDoc = parser.parseFromString(createWsHostEleStr(initValue), "text/html"); + addressEle = addressDoc.querySelector("setting-item") + hostItemsEle = document.getElementById("wsHostItems"); + } else { + let addressDoc = parser.parseFromString(createHttpHostEleStr(initValue), "text/html"); + addressEle = addressDoc.querySelector("setting-item") + hostItemsEle = document.getElementById("httpHostItems"); + } + hostItemsEle.appendChild(addressEle); } - doc.getElementById("addHost").addEventListener("click", () => addHostEle()) + doc.getElementById("addHttpHost").addEventListener("click", () => addHostEle("http")) + doc.getElementById("addWsHost").addEventListener("click", () => addHostEle("ws")) - function switchClick(eleId: string, configKey: string) { + function switchClick(eleId: string, configKey: string, _config=null) { + if (!_config){ + _config = config + } doc.getElementById(eleId)?.addEventListener("click", (e) => { const switchEle = e.target as HTMLInputElement - if (config[configKey]) { - config[configKey] = false + if (_config[configKey]) { + _config[configKey] = false switchEle.removeAttribute("is-active") } else { - config[configKey] = true + _config[configKey] = true switchEle.setAttribute("is-active", "") } + // 妈蛋,手动操作DOM越写越麻烦,要不用vue算了 + const keyClassMap = { + "enableHttp": httpClass, + "enableHttpPost": httpPostClass, + "enableWs": wsClass, + "enableWsReverse": reverseWSClass, + } + for (let e of document.getElementsByClassName(keyClassMap[configKey])) { + e["style"].display = _config[configKey] ? "" : "none" + } + window.llonebot.setConfig(config) }) } + switchClick("http", "enableHttp", config.ob11); + switchClick("httpPost", "enableHttpPost", config.ob11); + switchClick("websocket", "enableWs", config.ob11); + switchClick("websocketReverse", "enableWsReverse", config.ob11); switchClick("debug", "debug"); switchClick("switchBase64", "enableBase64"); switchClick("reportSelfMessage", "reportSelfMessage"); @@ -131,27 +214,35 @@ async function onSettingWindowCreated(view: Element) { doc.getElementById("save")?.addEventListener("click", () => { - const portEle: HTMLInputElement = document.getElementById("port") as HTMLInputElement - const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement - const hostEles: HTMLCollectionOf = document.getElementsByClassName("host") as HTMLCollectionOf; + const httpPortEle: HTMLInputElement = document.getElementById("httpPort") as HTMLInputElement; + const httpHostEles: HTMLCollectionOf = document.getElementsByClassName("httpHost") as HTMLCollectionOf; + const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement; + const wsHostEles: HTMLCollectionOf = document.getElementsByClassName("wsHost") as HTMLCollectionOf; const tokenEle = document.getElementById("token") as HTMLInputElement; - // const port = doc.querySelector("input[type=number]")?.value - // const host = doc.querySelector("input[type=text]")?.value - // 获取端口和host - const port = portEle.value - const wsPort = wsPortEle.value - const token = tokenEle.value - let hosts: string[] = []; - for (const hostEle of hostEles) { - if (hostEle.value) { - hosts.push(hostEle.value.trim()); - } + // 获取端口和host + const httpPort = httpPortEle.value + let httpHosts: string[] = []; + + for (const hostEle of httpHostEles) { + const value = hostEle.value.trim(); + value && httpHosts.push(value); } - config.port = parseInt(port); - config.wsPort = parseInt(wsPort); - config.hosts = hosts; - config.token = token.trim(); + + const wsPort = wsPortEle.value; + const token = tokenEle.value.trim(); + let wsHosts: string[] = []; + + for (const hostEle of wsHostEles) { + const value = hostEle.value.trim(); + value && wsHosts.push(value); + } + + config.ob11.httpPort = parseInt(httpPort); + config.ob11.httpHosts = httpHosts; + config.ob11.wsPort = parseInt(wsPort); + config.ob11.wsHosts = wsHosts; + config.token = token; window.llonebot.setConfig(config); alert("保存成功"); }) @@ -161,7 +252,6 @@ async function onSettingWindowCreated(view: Element) { view.appendChild(node); }); - } diff --git a/src/server/base.ts b/src/server/base.ts new file mode 100644 index 0000000..7291840 --- /dev/null +++ b/src/server/base.ts @@ -0,0 +1,6 @@ + + +export abstract class ServerBase{ + abstract start: () => void + abstract restart: ()=>void +} \ No newline at end of file diff --git a/src/server/http.ts b/src/server/http.ts new file mode 100644 index 0000000..6163140 --- /dev/null +++ b/src/server/http.ts @@ -0,0 +1,106 @@ +import express, {Express, Request, Response} from "express"; +import {getConfigUtil, log} from "../common/utils"; +import http from "http"; + +const JSONbig = require('json-bigint')({storeAsString: true}); + +type RegisterHandler = (res: Response, payload: any) => Promise + +export abstract class HttpServerBase { + name: string = "LLOneBot"; + private readonly expressAPP: Express; + private server: http.Server = null; + + constructor() { + this.expressAPP = express(); + this.expressAPP.use(express.urlencoded({extended: true, limit: "500mb"})); + this.expressAPP.use((req, res, next) => { + let data = ''; + req.on('data', chunk => { + data += chunk.toString(); + }); + req.on('end', () => { + if (data) { + try { + // log("receive raw", data) + req.body = JSONbig.parse(data); + } catch (e) { + return next(e); + } + } + next(); + }); + }); + } + + authorize(req: Request, res: Response, next: () => void) { + let serverToken = getConfigUtil().getConfig().token; + let clientToken = "" + const authHeader = req.get("authorization") + if (authHeader) { + clientToken = authHeader.split("Bearer ").pop() + log("receive http header token", clientToken) + } else if (req.query.access_token) { + if (Array.isArray(req.query.access_token)) { + clientToken = req.query.access_token[0].toString(); + } else { + clientToken = req.query.access_token.toString(); + } + log("receive http url token", clientToken) + } + + if (serverToken && clientToken != serverToken) { + return res.status(403).send(JSON.stringify({message: 'token verify failed!'})); + } + next(); + }; + + start(port: number) { + this.expressAPP.get('/', (req: Request, res: Response) => { + res.send(`${this.name}已启动`); + }) + this.listen(port); + } + + stop() { + if (this.server){ + this.server.close() + this.server = null; + } + } + + restart(port: number){ + this.stop() + this.start(port) + } + + abstract handleFailed(res: Response, payload: any, err: any): void + + registerRouter(method: string, url: string, handler: RegisterHandler) { + if (!url.startsWith("/")) { + url = "/" + url + } + const methodFunc = this.expressAPP[method] + if (!methodFunc){ + const err = `${this.name} register router failed,${method} not exist`; + log(err); + throw err; + } + this.expressAPP[method](url, this.authorize, async (req: Request, res: Response) => { + const payload = req.body || req.query || {} + try{ + res.send(await handler(res, payload)) + }catch (e) { + this.handleFailed(res, payload, e.stack.toString()) + } + }); + } + + protected listen(port: number) { + this.server = this.expressAPP.listen(port, "0.0.0.0", () => { + const info = `${this.name} started 0.0.0.0:${port}` + console.log(info); + log(info); + }); + } +} \ No newline at end of file