feat: api /get_version_info

feat: api /can_send_image
feat: api /can_send_record
feat: ws heart & lifecycle
This commit is contained in:
linyuchen 2024-02-16 21:32:37 +08:00
parent 963aad1510
commit 4f9682289c
17 changed files with 265 additions and 25 deletions

View File

@ -57,6 +57,9 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] get_group_member_info - [x] get_group_member_info
- [x] get_friend_list - [x] get_friend_list
- [x] get_msg - [x] get_msg
- [x] get_version_info
- [x] can_send_image
- [x] can_send_record
## 示例 ## 示例

View File

@ -4,7 +4,7 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi", "description": "LiteLoaderQQNT的OneBotApi",
"version": "3.1.2", "version": "3.2.0",
"thumbnail": "./icon.png", "thumbnail": "./icon.png",
"authors": [{ "authors": [{
"name": "linyuchen", "name": "linyuchen",

View File

@ -10,17 +10,36 @@ export class ConfigUtil {
} }
getConfig(): Config { getConfig(): Config {
let defaultConfig: Config = {
port: 3000,
wsPort: 3001,
hosts: [],
token: "",
enableBase64: false,
debug: false,
log: false,
reportSelfMessage: false
}
if (!fs.existsSync(this.configPath)) { if (!fs.existsSync(this.configPath)) {
return {port: 3000, hosts: ["http://192.168.1.2:5000/"], wsPort: 3001} return defaultConfig
} else { } else {
const data = fs.readFileSync(this.configPath, "utf-8"); 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) { if (!jsonData.hosts) {
jsonData.hosts = [] jsonData.hosts = []
} }
if (!jsonData.wsPort){ if (!jsonData.wsPort){
jsonData.wsPort = 3001 jsonData.wsPort = 3001
} }
if (!jsonData.token){
jsonData.token = ""
}
return jsonData; return jsonData;
} }
} }

View File

@ -87,3 +87,6 @@ export function getStrangerByUin(uin: string) {
} }
} }
} }
export const version = "v3.2.0"
export const heartInterval = 15000 // 毫秒

View File

@ -2,6 +2,7 @@ export interface Config {
port: number port: number
wsPort: number wsPort: number
hosts: string[] hosts: string[]
token?: string
enableBase64?: boolean enableBase64?: boolean
debug?: boolean debug?: boolean
reportSelfMessage?: boolean reportSelfMessage?: boolean

View File

@ -10,7 +10,7 @@ import {
CHANNEL_LOG, CHANNEL_LOG,
CHANNEL_SET_CONFIG, CHANNEL_SET_CONFIG,
} from "../common/channels"; } 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 { CONFIG_DIR, getConfigUtil, log } from "../common/utils";
import { addHistoryMsg, msgHistory, selfInfo } from "../common/data"; import { addHistoryMsg, msgHistory, selfInfo } from "../common/data";
import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook"; import { hookNTQQApiReceive, ReceiveCmd, registerReceiveHook } from "../ntqqapi/hook";
@ -45,6 +45,9 @@ function onLoad() {
if (arg.wsPort != oldConfig.wsPort){ if (arg.wsPort != oldConfig.wsPort){
startWSServer(arg.wsPort) startWSServer(arg.wsPort)
} }
if (arg.token != oldConfig.token){
setToken(arg.token);
}
}) })
ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => { ipcMain.on(CHANNEL_LOG, (event: any, arg: any) => {
@ -99,6 +102,7 @@ function onLoad() {
const config = getConfigUtil().getConfig() const config = getConfigUtil().getConfig()
startHTTPServer(config.port) startHTTPServer(config.port)
startWSServer(config.wsPort) startWSServer(config.wsPort)
setToken(config.token)
log("LLOneBot start") log("LLOneBot start")
} }

View File

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

View File

@ -0,0 +1,16 @@
import BaseAction from "./BaseAction";
import { ActionName } from "./types";
interface ReturnType{
yes: boolean
}
export default class CanSendRecord extends BaseAction<any, ReturnType>{
actionName = ActionName.CanSendRecord
protected async _handle(payload): Promise<ReturnType>{
return {
yes: true
}
}
}

View File

@ -0,0 +1,12 @@
import BaseAction from "./BaseAction";
import {OB11Status} from "../types";
export default class GetStatus extends BaseAction<any, OB11Status> {
protected async _handle(payload: any): Promise<OB11Status> {
return {
online: null,
good: true
}
}
}

View File

@ -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<any, OB11Version>{
actionName = ActionName.GetVersionInfo
protected async _handle(payload: any): Promise<OB11Version> {
return {
app_name: "LLOneBot",
protocol_version: "v11",
app_version: version
}
}
}

View File

@ -9,6 +9,9 @@ import SendGroupMsg from './SendGroupMsg'
import SendPrivateMsg from './SendPrivateMsg' import SendPrivateMsg from './SendPrivateMsg'
import SendMsg from './SendMsg' import SendMsg from './SendMsg'
import DeleteMsg from "./DeleteMsg"; import DeleteMsg from "./DeleteMsg";
import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage";
export const actionHandlers = [ export const actionHandlers = [
new GetMsg(), new GetMsg(),
@ -16,5 +19,8 @@ export const actionHandlers = [
new GetFriendList(), new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(), new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(), new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg() new DeleteMsg(),
new GetVersionInfo(),
new CanSendRecord(),
new CanSendImage()
] ]

View File

@ -1,3 +1,5 @@
import GetVersionInfo from "./GetVersionInfo";
export type BaseCheckResult = ValidCheckResult | InvalidCheckResult export type BaseCheckResult = ValidCheckResult | InvalidCheckResult
export interface ValidCheckResult { export interface ValidCheckResult {
@ -22,5 +24,9 @@ export enum ActionName{
SendMsg = "send_msg", SendMsg = "send_msg",
SendGroupMsg = "send_group_msg", SendGroupMsg = "send_group_msg",
SendPrivateMsg = "send_private_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",
} }

View File

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

View File

@ -4,10 +4,10 @@ import {
OB11Message, OB11Message,
OB11Group, OB11Group,
OB11GroupMember, OB11GroupMember,
OB11User OB11User, OB11LifeCycleEvent, OB11HeartEvent
} from "./types"; } from "./types";
import { AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User } from '../ntqqapi/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 { file2base64, getConfigUtil, log } from "../common/utils";
import { NTQQApi } from "../ntqqapi/ntcall"; import { NTQQApi } from "../ntqqapi/ntcall";
@ -41,6 +41,7 @@ export class OB11Constructor {
const member = await getGroupMember(msg.peerUin, msg.senderUin); const member = await getGroupMember(msg.peerUin, msg.senderUin);
if (member) { if (member) {
resMsg.sender.role = OB11Constructor.groupMemberRole(member.role); resMsg.sender.role = OB11Constructor.groupMemberRole(member.role);
resMsg.sender.nickname = member.nick
} }
} else if (msg.chatType == ChatType.friend) { } else if (msg.chatType == ChatType.friend) {
resMsg.sub_type = "friend" resMsg.sub_type = "friend"
@ -185,4 +186,28 @@ export class OB11Constructor {
static groups(groups: Group[]): OB11Group[] { static groups(groups: Group[]): OB11Group[] {
return groups.map(OB11Constructor.group) 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
}
}
} }

View File

@ -1,17 +1,20 @@
import * as http from "http"; import * as http from "http";
import * as websocket from "ws"; import * as websocket from "ws";
import urlParse from "url";
import express from "express"; import express from "express";
import { Request } from 'express'; import { Request } from 'express';
import { Response } from 'express'; import { Response } from 'express';
import { getConfigUtil, log } from "../common/utils"; import { getConfigUtil, log } from "../common/utils";
import { selfInfo } from "../common/data"; import { heartInterval, selfInfo } from "../common/data";
import { OB11Message, OB11Return, OB11MessageData } from './types'; import { OB11Message, OB11Return, OB11MessageData, OB11LifeCycleEvent, OB11MetaEvent } from './types';
import { actionHandlers } from "./actions"; import { actionHandlers } from "./actions";
import { OB11Response } from "./actions/utils"; import { OB11Response } from "./actions/utils";
import { ActionName } from "./actions/types"; import { ActionName } from "./actions/types";
import BaseAction from "./actions/BaseAction"; import BaseAction from "./actions/BaseAction";
import { OB11Constructor } from "./constructor";
let wsServer: websocket.Server = null; let wsServer: websocket.Server = null;
let accessToken = ""
const JSONbig = require('json-bigint')({storeAsString: true}); const JSONbig = require('json-bigint')({storeAsString: true});
const expressAPP = express(); 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) { export function startHTTPServer(port: number) {
if (httpServer) { if (httpServer) {
@ -54,9 +86,10 @@ let wsEventClients: websocket.WebSocket[] = [];
type RouterHandler = (payload: any) => Promise<OB11Return<any>> type RouterHandler = (payload: any) => Promise<OB11Return<any>>
let routers: Record<string, RouterHandler> = {}; let routers: Record<string, RouterHandler> = {};
function wsReply(wsClient: websocket.WebSocket, data: OB11Return<any> | OB11Message) { function wsReply(wsClient: websocket.WebSocket, data: OB11Return<any> | OB11Message | OB11MetaEvent) {
try { try {
wsClient.send(JSON.stringify(data)) wsClient.send(JSON.stringify(data))
log("ws 消息上报", data)
} catch (e) { } catch (e) {
log("websocket 回复失败", e) log("websocket 回复失败", e)
} }
@ -64,31 +97,66 @@ function wsReply(wsClient: websocket.WebSocket, data: OB11Return<any> | OB11Mess
export function startWSServer(port: number) { export function startWSServer(port: number) {
if (wsServer) { if (wsServer) {
wsServer.close((err)=>{ wsServer.close((err) => {
log("ws server close failed!", err) log("ws server close failed!", err)
}) })
} }
wsServer = new websocket.Server({port}) wsServer = new websocket.Server({port})
wsServer.on("connection", (ws, req) => { wsServer.on("connection", (ws, req) => {
const url = req.url; 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); // ws.send('Welcome to the LLOneBot WebSocket server! url:' + url);
if (url == "/api" || url == "/api/" || url == "/") { if (url == "/api" || url == "/api/" || url == "/") {
ws.on("message", async (msg) => { 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()) log("收到ws消息", msg.toString())
try { try {
receiveData = JSON.parse(msg.toString()) receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
} catch (e) { } catch (e) {
return wsReply(ws, OB11Response.error("json解析失败请检查数据格式")) return wsReply(ws, {...OB11Response.error("json解析失败请检查数据格式"), echo})
} }
const handle: RouterHandler | undefined = routers[receiveData.action] const handle: RouterHandler | undefined = routers[receiveData.action]
if (!handle) { 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 { try {
const handleResult = await handle(receiveData.params) let handleResult = await handle(receiveData.params)
if (echo){
handleResult.echo = echo
}
wsReply(ws, handleResult) wsReply(ws, handleResult)
} catch (e) { } catch (e) {
wsReply(ws, OB11Response.error(`api处理出错:${e}`)) wsReply(ws, OB11Response.error(`api处理出错:${e}`))
@ -98,7 +166,19 @@ export function startWSServer(port: number) {
if (url == "/event" || url == "/event/" || url == "/") { if (url == "/event" || url == "/event/" || url == "/") {
log("event上报ws客户端已连接") log("event上报ws客户端已连接")
wsEventClients.push(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", () => { ws.on("close", () => {
clearInterval(wsHeart);
log("event上报ws客户端已断开") log("event上报ws客户端已断开")
wsEventClients = wsEventClients.filter((c) => c != ws) wsEventClients = wsEventClients.filter((c) => c != ws)
}) })
@ -154,10 +234,10 @@ function registerRouter(action: string, handle: (payload: any) => Promise<any>)
} }
} }
expressAPP.post(url, (req: Request, res: Response) => { expressAPP.post(url, expressAuthorize, (req: Request, res: Response) => {
_handle(res, req.body).then() _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() _handle(res, req.query as any).then()
}); });
routers[action] = handle routers[action] = handle

View File

@ -89,7 +89,8 @@ export interface OB11Return<DataType> {
status: number status: number
retcode: number retcode: number
data: DataType data: DataType
message: string message: string,
echo?: string
} }
export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{} export interface OB11SendMsgReturn extends OB11Return<{message_id: string}>{}
@ -140,3 +141,33 @@ export interface OB11PostSendMsg {
group_id?: string, group_id?: string,
message: OB11MessageData[] | string | OB11MessageData; 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
}

View File

@ -36,6 +36,10 @@ async function onSettingWindowCreated(view: Element) {
<setting-text>ws监听端口</setting-text> <setting-text>ws监听端口</setting-text>
<input id="wsPort" type="number" value="${config.wsPort}"/> <input id="wsPort" type="number" value="${config.wsPort}"/>
</setting-item> </setting-item>
<setting-item class="vertical-list-item" data-direction="row">
<setting-text>Access Token</setting-text>
<input id="token" type="text" placeholder="可为空" value="${config.token}"/>
</setting-item>
<div> <div>
<button id="addHost" class="q-button">HTTP上报地址</button> <button id="addHost" class="q-button">HTTP上报地址</button>
</div> </div>
@ -130,20 +134,24 @@ async function onSettingWindowCreated(view: Element) {
const portEle: HTMLInputElement = document.getElementById("port") as HTMLInputElement const portEle: HTMLInputElement = document.getElementById("port") as HTMLInputElement
const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement const wsPortEle: HTMLInputElement = document.getElementById("wsPort") as HTMLInputElement
const hostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("host") as HTMLCollectionOf<HTMLInputElement>; const hostEles: HTMLCollectionOf<HTMLInputElement> = document.getElementsByClassName("host") as HTMLCollectionOf<HTMLInputElement>;
const tokenEle = document.getElementById("token") as HTMLInputElement;
// const port = doc.querySelector("input[type=number]")?.value // const port = doc.querySelector("input[type=number]")?.value
// const host = doc.querySelector("input[type=text]")?.value // const host = doc.querySelector("input[type=text]")?.value
// 获取端口和host // 获取端口和host
const port = portEle.value const port = portEle.value
const wsPort = wsPortEle.value const wsPort = wsPortEle.value
const token = tokenEle.value
let hosts: string[] = []; let hosts: string[] = [];
for (const hostEle of hostEles) { for (const hostEle of hostEles) {
if (hostEle.value) { if (hostEle.value) {
hosts.push(hostEle.value); hosts.push(hostEle.value.trim());
} }
} }
config.port = parseInt(port); config.port = parseInt(port);
config.wsPort = parseInt(wsPort); config.wsPort = parseInt(wsPort);
config.hosts = hosts; config.hosts = hosts;
config.token = token.trim();
window.llonebot.setConfig(config); window.llonebot.setConfig(config);
alert("保存成功"); alert("保存成功");
}) })