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_friend_list
- [x] get_msg
- [x] get_version_info
- [x] can_send_image
- [x] can_send_record
## 示例

View File

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

View File

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

View File

@ -86,4 +86,7 @@ export function getStrangerByUin(uin: string) {
return uidMaps[key];
}
}
}
}
export const version = "v3.2.0"
export const heartInterval = 15000 // 毫秒

View File

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

View File

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

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 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()
]

View File

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

View File

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

View File

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

View File

@ -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<OB11Return<any>>
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 {
wsClient.send(JSON.stringify(data))
log("ws 消息上报", data)
} catch (e) {
log("websocket 回复失败", e)
}
@ -64,31 +97,66 @@ function wsReply(wsClient: websocket.WebSocket, data: OB11Return<any> | 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<any>)
}
}
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

View File

@ -89,7 +89,8 @@ export interface OB11Return<DataType> {
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
}

View File

@ -36,6 +36,10 @@ async function onSettingWindowCreated(view: Element) {
<setting-text>ws监听端口</setting-text>
<input id="wsPort" type="number" value="${config.wsPort}"/>
</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>
<button id="addHost" class="q-button">HTTP上报地址</button>
</div>
@ -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<HTMLInputElement> = document.getElementsByClassName("host") as HTMLCollectionOf<HTMLInputElement>;
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("保存成功");
})