From 4f9682289cd4e5db0abc99a25bf28dd1e41f4dfd Mon Sep 17 00:00:00 2001 From: linyuchen Date: Fri, 16 Feb 2024 21:32:37 +0800 Subject: [PATCH] feat: api /get_version_info feat: api /can_send_image feat: api /can_send_record feat: ws heart & lifecycle --- README.md | 3 + manifest.json | 2 +- src/common/config.ts | 23 +++++- src/common/data.ts | 5 +- src/common/types.ts | 1 + src/main/main.ts | 6 +- src/onebot11/actions/CanSendImage.ts | 10 +++ src/onebot11/actions/CanSendRecord.ts | 16 ++++ src/onebot11/actions/GetStatus.ts | 12 +++ src/onebot11/actions/GetVersionInfo.ts | 15 ++++ src/onebot11/actions/index.ts | 8 +- src/onebot11/actions/types.ts | 8 +- src/onebot11/actions/utils.ts | 9 ++- src/onebot11/constructor.ts | 29 ++++++- src/onebot11/server.ts | 100 ++++++++++++++++++++++--- src/onebot11/types.ts | 33 +++++++- src/renderer.ts | 10 ++- 17 files changed, 265 insertions(+), 25 deletions(-) create mode 100644 src/onebot11/actions/CanSendImage.ts create mode 100644 src/onebot11/actions/CanSendRecord.ts create mode 100644 src/onebot11/actions/GetStatus.ts create mode 100644 src/onebot11/actions/GetVersionInfo.ts diff --git a/README.md b/README.md index 20bc524..81c6140 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ LiteLoaderQQNT的OneBot11协议插件 - [x] get_group_member_info - [x] get_friend_list - [x] get_msg +- [x] get_version_info +- [x] can_send_image +- [x] can_send_record ## 示例 diff --git a/manifest.json b/manifest.json index b1ac6bf..31d8846 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "LLOneBot", "slug": "LLOneBot", "description": "LiteLoaderQQNT的OneBotApi", - "version": "3.1.2", + "version": "3.2.0", "thumbnail": "./icon.png", "authors": [{ "name": "linyuchen", diff --git a/src/common/config.ts b/src/common/config.ts index 4fafec8..a751771 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -10,17 +10,36 @@ export class ConfigUtil { } getConfig(): Config { + let defaultConfig: Config = { + port: 3000, + wsPort: 3001, + hosts: [], + token: "", + enableBase64: false, + debug: false, + log: false, + reportSelfMessage: false + } if (!fs.existsSync(this.configPath)) { - return {port: 3000, hosts: ["http://192.168.1.2:5000/"], wsPort: 3001} + return defaultConfig } else { const data = fs.readFileSync(this.configPath, "utf-8"); - let jsonData = JSON.parse(data); + let jsonData: Config = defaultConfig; + try { + jsonData = JSON.parse(data) + } + catch (e){ + + } if (!jsonData.hosts) { jsonData.hosts = [] } if (!jsonData.wsPort){ jsonData.wsPort = 3001 } + if (!jsonData.token){ + jsonData.token = "" + } return jsonData; } } diff --git a/src/common/data.ts b/src/common/data.ts index bbd23e1..2af3461 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -86,4 +86,7 @@ export function getStrangerByUin(uin: string) { return uidMaps[key]; } } -} \ No newline at end of file +} + +export const version = "v3.2.0" +export const heartInterval = 15000 // 毫秒 \ No newline at end of file diff --git a/src/common/types.ts b/src/common/types.ts index 1583420..bb98810 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -2,6 +2,7 @@ export interface Config { port: number wsPort: number hosts: string[] + token?: string enableBase64?: boolean debug?: boolean reportSelfMessage?: boolean diff --git a/src/main/main.ts b/src/main/main.ts index 7e56efd..945c14b 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, startHTTPServer, startWSServer } from "../onebot11/server"; +import { postMsg, setToken, startHTTPServer, startWSServer } 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"; @@ -45,6 +45,9 @@ function onLoad() { if (arg.wsPort != oldConfig.wsPort){ startWSServer(arg.wsPort) } + if (arg.token != oldConfig.token){ + setToken(arg.token); + } }) ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { @@ -99,6 +102,7 @@ function onLoad() { const config = getConfigUtil().getConfig() startHTTPServer(config.port) startWSServer(config.wsPort) + setToken(config.token) log("LLOneBot start") } diff --git a/src/onebot11/actions/CanSendImage.ts b/src/onebot11/actions/CanSendImage.ts new file mode 100644 index 0000000..f8c6b2c --- /dev/null +++ b/src/onebot11/actions/CanSendImage.ts @@ -0,0 +1,10 @@ +import { ActionName } from "./types"; +import CanSendRecord from "./CanSendRecord"; + +interface ReturnType{ + yes: boolean +} + +export default class CanSendImage extends CanSendRecord{ + actionName = ActionName.CanSendImage +} \ No newline at end of file diff --git a/src/onebot11/actions/CanSendRecord.ts b/src/onebot11/actions/CanSendRecord.ts new file mode 100644 index 0000000..4d94d17 --- /dev/null +++ b/src/onebot11/actions/CanSendRecord.ts @@ -0,0 +1,16 @@ +import BaseAction from "./BaseAction"; +import { ActionName } from "./types"; + +interface ReturnType{ + yes: boolean +} + +export default class CanSendRecord extends BaseAction{ + actionName = ActionName.CanSendRecord + + protected async _handle(payload): Promise{ + return { + yes: true + } + } +} \ No newline at end of file diff --git a/src/onebot11/actions/GetStatus.ts b/src/onebot11/actions/GetStatus.ts new file mode 100644 index 0000000..1e388af --- /dev/null +++ b/src/onebot11/actions/GetStatus.ts @@ -0,0 +1,12 @@ +import BaseAction from "./BaseAction"; +import {OB11Status} from "../types"; + + +export default class GetStatus extends BaseAction { + protected async _handle(payload: any): Promise { + return { + online: null, + good: true + } + } +} \ No newline at end of file diff --git a/src/onebot11/actions/GetVersionInfo.ts b/src/onebot11/actions/GetVersionInfo.ts new file mode 100644 index 0000000..5fadb22 --- /dev/null +++ b/src/onebot11/actions/GetVersionInfo.ts @@ -0,0 +1,15 @@ +import BaseAction from "./BaseAction"; +import { OB11Version } from "../types"; +import {version} from "../../common/data"; +import { ActionName } from "./types"; + +export default class GetVersionInfo extends BaseAction{ + actionName = ActionName.GetVersionInfo + protected async _handle(payload: any): Promise { + return { + app_name: "LLOneBot", + protocol_version: "v11", + app_version: version + } + } +} \ No newline at end of file diff --git a/src/onebot11/actions/index.ts b/src/onebot11/actions/index.ts index 42fcda6..ae75b78 100644 --- a/src/onebot11/actions/index.ts +++ b/src/onebot11/actions/index.ts @@ -9,6 +9,9 @@ import SendGroupMsg from './SendGroupMsg' import SendPrivateMsg from './SendPrivateMsg' import SendMsg from './SendMsg' import DeleteMsg from "./DeleteMsg"; +import GetVersionInfo from "./GetVersionInfo"; +import CanSendRecord from "./CanSendRecord"; +import CanSendImage from "./CanSendImage"; export const actionHandlers = [ new GetMsg(), @@ -16,5 +19,8 @@ export const actionHandlers = [ new GetFriendList(), new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(), new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), - new DeleteMsg() + new DeleteMsg(), + new GetVersionInfo(), + new CanSendRecord(), + new CanSendImage() ] \ No newline at end of file diff --git a/src/onebot11/actions/types.ts b/src/onebot11/actions/types.ts index fdfeeb5..39f0b25 100644 --- a/src/onebot11/actions/types.ts +++ b/src/onebot11/actions/types.ts @@ -1,3 +1,5 @@ +import GetVersionInfo from "./GetVersionInfo"; + export type BaseCheckResult = ValidCheckResult | InvalidCheckResult export interface ValidCheckResult { @@ -22,5 +24,9 @@ export enum ActionName{ SendMsg = "send_msg", SendGroupMsg = "send_group_msg", SendPrivateMsg = "send_private_msg", - DeleteMsg = "delete_msg" + DeleteMsg = "delete_msg", + GetVersionInfo = "get_version_info", + GetStatus = "get_status", + CanSendRecord = "can_send_record", + CanSendImage = "can_send_image", } \ No newline at end of file diff --git a/src/onebot11/actions/utils.ts b/src/onebot11/actions/utils.ts index 8120d7f..9edfc90 100644 --- a/src/onebot11/actions/utils.ts +++ b/src/onebot11/actions/utils.ts @@ -1,18 +1,19 @@ import { OB11Return } from '../types'; export class OB11Response { - static res(data: T, status: number = 0, message: string = ""): OB11Return { + static res(data: T, status: number = 0, message: string = "", echo=""): OB11Return { return { status: status, retcode: status, data: data, - message: message + message: message, + echo, } } static ok(data: T) { return OB11Response.res(data) } - static error(err: string) { - return OB11Response.res(null, -1, err) + 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 d4e7d3f..d8afb2e 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -4,10 +4,10 @@ import { OB11Message, OB11Group, OB11GroupMember, - OB11User + OB11User, OB11LifeCycleEvent, OB11HeartEvent } from "./types"; import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/types'; -import { getFriend, getGroupMember, getHistoryMsgBySeq, msgHistory, selfInfo } from '../common/data'; +import { getFriend, getGroupMember, getHistoryMsgBySeq, heartInterval, msgHistory, selfInfo } from '../common/data'; import { file2base64, getConfigUtil, log } from "../common/utils"; import { NTQQApi } from "../ntqqapi/ntcall"; @@ -41,6 +41,7 @@ export class OB11Constructor { const member = await getGroupMember(msg.peerUin, msg.senderUin); if (member) { resMsg.sender.role = OB11Constructor.groupMemberRole(member.role); + resMsg.sender.nickname = member.nick } } else if (msg.chatType == ChatType.friend) { resMsg.sub_type = "friend" @@ -185,4 +186,28 @@ export class OB11Constructor { static groups(groups: Group[]): OB11Group[] { return groups.map(OB11Constructor.group) } + + static lifeCycleEvent(): OB11LifeCycleEvent { + return { + time: Math.floor(Date.now() / 1000), + self_id: parseInt(selfInfo.uin), + post_type: "meta_event", + meta_event_type: "lifecycle", + sub_type: "connect" + } + } + + static heartEvent(): OB11HeartEvent { + return { + time: Math.floor(Date.now() / 1000), + self_id: parseInt(selfInfo.uin), + post_type: "meta_event", + meta_event_type: "heartbeat", + status: { + online: true, + good: true + }, + interval: heartInterval + } + } } \ No newline at end of file diff --git a/src/onebot11/server.ts b/src/onebot11/server.ts index 9c0e93d..b69aab7 100644 --- a/src/onebot11/server.ts +++ b/src/onebot11/server.ts @@ -1,17 +1,20 @@ 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 { selfInfo } from "../common/data"; -import { OB11Message, OB11Return, OB11MessageData } from './types'; +import { heartInterval, selfInfo } from "../common/data"; +import { OB11Message, OB11Return, OB11MessageData, OB11LifeCycleEvent, OB11MetaEvent } 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"; let wsServer: websocket.Server = null; +let accessToken = "" const JSONbig = require('json-bigint')({storeAsString: true}); const expressAPP = express(); @@ -36,6 +39,35 @@ expressAPP.use((req, res, next) => { }); }); +const expressAuthorize = (req: Request, res: Response, next: () => void) => { + 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(); + } else { + token = req.query.access_token.toString(); + } + log("receive http url token", token) + } + + if (accessToken) { + if (token != accessToken) { + return res.status(403).send(JSON.stringify({message: 'token verify failed!'})); + } + } + + + next(); + +}; + +export function setToken(token: string) { + accessToken = token +} export function startHTTPServer(port: number) { if (httpServer) { @@ -54,9 +86,10 @@ let wsEventClients: websocket.WebSocket[] = []; type RouterHandler = (payload: any) => Promise> let routers: Record = {}; -function wsReply(wsClient: websocket.WebSocket, data: OB11Return | OB11Message) { +function wsReply(wsClient: websocket.WebSocket, data: OB11Return | OB11Message | OB11MetaEvent) { try { wsClient.send(JSON.stringify(data)) + log("ws 消息上报", data) } catch (e) { log("websocket 回复失败", e) } @@ -64,31 +97,66 @@ function wsReply(wsClient: websocket.WebSocket, data: OB11Return | OB11Mess export function startWSServer(port: number) { if (wsServer) { - wsServer.close((err)=>{ + wsServer.close((err) => { log("ws server close failed!", err) }) } wsServer = new websocket.Server({port}) wsServer.on("connection", (ws, req) => { const url = req.url; + log("received 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(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() + } + } + // const queryParams = querystring.parse(parsedUrl.query); + // let token = req // ws.send('Welcome to the LLOneBot WebSocket server! url:' + url); + if (url == "/api" || url == "/api/" || url == "/") { ws.on("message", async (msg) => { - let receiveData: { action: ActionName, params: any } = {action: null, params: {}} + 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解析失败,请检查数据格式")) + return wsReply(ws, {...OB11Response.error("json解析失败,请检查数据格式"), echo}) } const handle: RouterHandler | undefined = routers[receiveData.action] if (!handle) { - return wsReply(ws, OB11Response.error("不支持的api " + receiveData.action)) + let handleResult = OB11Response.error("不支持的api " + receiveData.action, 1404) + handleResult.echo = echo + return wsReply(ws, handleResult) } try { - const handleResult = await handle(receiveData.params) + let handleResult = await handle(receiveData.params) + if (echo){ + handleResult.echo = echo + } wsReply(ws, handleResult) } catch (e) { wsReply(ws, OB11Response.error(`api处理出错:${e}`)) @@ -98,7 +166,19 @@ export function startWSServer(port: number) { 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) }) @@ -154,10 +234,10 @@ function registerRouter(action: string, handle: (payload: any) => Promise) } } - expressAPP.post(url, (req: Request, res: Response) => { + expressAPP.post(url, expressAuthorize, (req: Request, res: Response) => { _handle(res, req.body).then() }); - expressAPP.get(url, (req: Request, res: Response) => { + expressAPP.get(url, expressAuthorize, (req: Request, res: Response) => { _handle(res, req.query as any).then() }); routers[action] = handle diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index ee4f4aa..611ad3f 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -89,7 +89,8 @@ export interface OB11Return { status: number retcode: number data: DataType - message: string + message: string, + echo?: string } export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{} @@ -139,4 +140,34 @@ export interface OB11PostSendMsg { user_id: string, group_id?: string, message: OB11MessageData[] | string | OB11MessageData; +} + +export interface OB11Version { + app_name: "LLOneBot" + app_version: string + protocol_version: "v11" +} + + +export interface OB11MetaEvent { + time: number + self_id: number + post_type: "meta_event" + meta_event_type: "lifecycle" | "heartbeat" +} + +export interface OB11LifeCycleEvent extends OB11MetaEvent{ + meta_event_type: "lifecycle" + sub_type: "enable" | "disable" | "connect" +} + +export interface OB11Status { + online: boolean | null, + good: boolean +} + +export interface OB11HeartEvent extends OB11MetaEvent{ + meta_event_type: "heartbeat" + status: OB11Status + interval: number } \ No newline at end of file diff --git a/src/renderer.ts b/src/renderer.ts index abe1b2c..55319d9 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -36,6 +36,10 @@ async function onSettingWindowCreated(view: Element) { 正向ws监听端口 + + Access Token + +
@@ -130,20 +134,24 @@ async function onSettingWindowCreated(view: Element) { 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 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); + hosts.push(hostEle.value.trim()); } } config.port = parseInt(port); config.wsPort = parseInt(wsPort); config.hosts = hosts; + config.token = token.trim(); window.llonebot.setConfig(config); alert("保存成功"); })