This commit is contained in:
linyuchen
2024-04-15 00:09:08 +08:00
parent 0ef3e38d70
commit 356aba762c
218 changed files with 8465 additions and 5 deletions

View File

@@ -0,0 +1,32 @@
import { Response } from 'express';
import { OB11Response } from '../action/OB11Response';
import { HttpServerBase } from '@/common/server/http';
import { actionHandlers } from '../action';
import { ob11Config } from '@/onebot11/config';
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 (ob11Config.enableHttp) {
super.listen(port);
}
}
}
export const ob11HTTPServer = new OB11HTTPServer();
setTimeout(() => {
for (const action of actionHandlers) {
for (const method of ['post', 'get']) {
ob11HTTPServer.registerRouter(method, action.actionName, (res, payload) => {
// @ts-expect-error wait fix
return action.handle(payload);
});
}
}
}, 0);

View File

@@ -0,0 +1,185 @@
import { OB11Message, OB11MessageAt, OB11MessageData } from '../types';
import { OB11BaseMetaEvent } from '../event/meta/OB11BaseMetaEvent';
import { OB11BaseNoticeEvent } from '../event/notice/OB11BaseNoticeEvent';
import { WebSocket as WebSocketClass } from 'ws';
import { wsReply } from './ws/reply';
import { log } from '@/common/utils/log';
import { ob11Config } from '@/onebot11/config';
import crypto from 'crypto';
import { ChatType, Group, GroupRequestOperateTypes, Peer } from '@/core/qqnt/entities';
import { convertMessage2List, createSendElements, sendMsg } from '../action/msg/SendMsg';
import { OB11FriendRequestEvent } from '../event/request/OB11FriendRequest';
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import { isNull } from '@/common/utils/helper';
import { dbUtil } from '@/common/utils/db';
import { friendRequests, getGroup, groupNotifies, selfInfo } from '@/common/data';
import { NTQQFriendApi, NTQQGroupApi, NTQQMsgApi } from '@/core/qqnt/apis';
export type PostEventType = OB11Message | OB11BaseMetaEvent | OB11BaseNoticeEvent
interface QuickActionPrivateMessage {
reply?: string;
auto_escape?: boolean;
}
interface QuickActionGroupMessage extends QuickActionPrivateMessage {
// 回复群消息
at_sender?: boolean;
delete?: boolean;
kick?: boolean;
ban?: boolean;
ban_duration?: number;
//
}
interface QuickActionFriendRequest {
approve?: boolean;
remark?: string;
}
interface QuickActionGroupRequest {
approve?: boolean;
reason?: string;
}
type QuickAction =
QuickActionPrivateMessage
& QuickActionGroupMessage
& QuickActionFriendRequest
& QuickActionGroupRequest
const eventWSList: WebSocketClass[] = [];
export function registerWsEventSender(ws: WebSocketClass) {
eventWSList.push(ws);
}
export function unregisterWsEventSender(ws: WebSocketClass) {
const index = eventWSList.indexOf(ws);
if (index !== -1) {
eventWSList.splice(index, 1);
}
}
export function postWsEvent(event: PostEventType) {
for (const ws of eventWSList) {
new Promise(() => {
wsReply(ws, event);
}).then();
}
}
export function postOB11Event(msg: PostEventType, reportSelf = false) {
const config = ob11Config;
// 判断msg是否是event
if (!config.reportSelfMessage && !reportSelf) {
if (msg.post_type === 'message' && (msg as OB11Message).user_id.toString() == selfInfo.uin) {
return;
}
}
if (config.enableHttpPost) {
const msgStr = JSON.stringify(msg);
const hmac = crypto.createHmac('sha1', ob11Config.httpSecret);
hmac.update(msgStr);
const sig = hmac.digest('hex');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-self-id': selfInfo.uin
};
if (config.httpSecret) {
headers['x-signature'] = 'sha1=' + sig;
}
for (const host of config.httpPostUrls) {
fetch(host, {
method: 'POST',
headers,
body: msgStr
}).then(async (res) => {
log(`新消息事件HTTP上报成功: ${host} `, msgStr);
// todo: 处理不够优雅应该使用高级泛型进行QuickAction类型识别
let resJson: QuickAction;
try {
resJson = await res.json();
log('新消息事件HTTP上报返回快速操作: ', JSON.stringify(resJson));
} catch (e) {
log('新消息事件HTTP上报没有返回快速操作不需要处理');
return;
}
if (msg.post_type === 'message') {
msg = msg as OB11Message;
const rawMessage = await dbUtil.getMsgByShortId(msg.message_id);
resJson = resJson as QuickActionPrivateMessage | QuickActionGroupMessage;
const reply = resJson.reply;
const peer: Peer = {
chatType: ChatType.friend,
peerUid: msg.user_id.toString()
};
if (msg.message_type == 'private') {
if (msg.sub_type === 'group') {
peer.chatType = ChatType.temp;
}
} else {
peer.chatType = ChatType.group;
peer.peerUid = msg.group_id!.toString();
}
if (reply) {
let group: Group | undefined;
let replyMessage: OB11MessageData[] = [];
if (msg.message_type == 'group') {
group = await getGroup(msg.group_id!.toString());
if ((resJson as QuickActionGroupMessage).at_sender) {
replyMessage.push({
type: 'at',
data: {
qq: msg.user_id.toString()
}
} as OB11MessageAt);
}
}
replyMessage = replyMessage.concat(convertMessage2List(reply, resJson.auto_escape));
const { sendElements, deleteAfterSentFiles } = await createSendElements(replyMessage, group);
sendMsg(peer, sendElements, deleteAfterSentFiles, false).then();
} else if (resJson.delete) {
NTQQMsgApi.recallMsg(peer, [rawMessage!.msgId]).then();
} else if (resJson.kick) {
NTQQGroupApi.kickMember(peer.peerUid, [rawMessage!.senderUid]).then();
} else if (resJson.ban) {
NTQQGroupApi.banMember(peer.peerUid, [{
uid: rawMessage!.senderUid,
timeStamp: resJson.ban_duration || 60 * 30
}],).then();
}
} else if (msg.post_type === 'request') {
if ((msg as OB11FriendRequestEvent).request_type === 'friend') {
resJson = resJson as QuickActionFriendRequest;
if (!isNull(resJson.approve)) {
// todo: set remark
const flag = (msg as OB11FriendRequestEvent).flag;
// const [friendUid, seq] = flag.split('|');
const request = friendRequests[flag];
NTQQFriendApi.handleFriendRequest(
request,
!!resJson.approve,
).then();
}
} else if ((msg as OB11GroupRequestEvent).request_type === 'group') {
resJson = resJson as QuickActionGroupRequest;
if (!isNull(resJson.approve)) {
const flag = (msg as OB11GroupRequestEvent).flag;
const request = groupNotifies[flag];
// const [groupCode, seq] = flag.split('|');
NTQQGroupApi.handleGroupRequest(request,
resJson.approve ? GroupRequestOperateTypes.approve : GroupRequestOperateTypes.reject
).then();
}
}
}
}, (err: any) => {
log(`新消息事件HTTP上报失败: ${host} `, err, msg);
});
}
}
postWsEvent(msg);
}

View File

@@ -0,0 +1,143 @@
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent';
import { ActionName } from '../../action/types';
import { OB11Response } from '../../action/OB11Response';
import BaseAction from '../../action/BaseAction';
import { actionMap } from '../../action';
import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../postOB11Event';
import { wsReply } from './reply';
import { WebSocket as WebSocketClass } from 'ws';
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent';
import { log } from '../../../common/utils/log';
import { ob11Config } from '@/onebot11/config';
import { napCatCore } from '@/core';
import { selfInfo } from '@/common/data';
export const rwsList: ReverseWebsocket[] = [];
export class ReverseWebsocket {
public websocket: WebSocketClass | undefined;
public url: string;
private running: boolean = false;
public constructor(url: string) {
this.url = url;
this.running = true;
this.connect();
}
public stop() {
this.running = false;
this.websocket!.close();
}
public onopen() {
wsReply(this.websocket!, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT));
}
public async onmessage(msg: string) {
let receiveData: { action: ActionName | undefined, params: any, echo?: any } = { action: undefined, params: {} };
let echo = null;
try {
receiveData = JSON.parse(msg.toString());
echo = receiveData.echo;
log('收到反向Websocket消息', receiveData);
} catch (e) {
return wsReply(this.websocket!, OB11Response.error('json解析失败请检查数据格式', 1400, echo));
}
const action: BaseAction<any, any> | undefined = actionMap.get(receiveData.action!);
if (!action) {
return wsReply(this.websocket!, OB11Response.error('不支持的api ' + receiveData.action, 1404, echo));
}
try {
const handleResult = await action.websocketHandle(receiveData.params, echo);
wsReply(this.websocket!, handleResult);
} catch (e) {
wsReply(this.websocket!, OB11Response.error(`api处理出错:${e}`, 1200, echo));
}
}
public onclose = () => {
log('反向ws断开', this.url);
unregisterWsEventSender(this.websocket!);
if (this.running) {
this.reconnect();
}
};
public send(msg: string) {
if (this.websocket && this.websocket.readyState == WebSocket.OPEN) {
this.websocket.send(msg);
}
}
private reconnect() {
setTimeout(() => {
this.connect();
}, 3000); // TODO: 重连间隔在配置文件中实现
}
private connect() {
const { token, heartInterval } = ob11Config;
this.websocket = new WebSocketClass(this.url, {
handshakeTimeout: 2000,
perMessageDeflate: false,
headers: {
'X-Self-ID': selfInfo.uin,
'Authorization': `Bearer ${token}`,
'x-client-role': 'Universal', // koishi-adapter-onebot 需要这个字段
}
});
registerWsEventSender(this.websocket);
log('Trying to connect to the websocket server: ' + this.url);
this.websocket.on('open', () => {
log('Connected to the websocket server: ' + this.url);
this.onopen();
});
this.websocket.on('message', async (data) => {
await this.onmessage(data.toString());
});
this.websocket.on('error', log);
const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(!!selfInfo.online, true, heartInterval));
}, heartInterval); // 心跳包
this.websocket.on('close', () => {
clearInterval(wsClientInterval);
log('The websocket connection: ' + this.url + ' closed, trying reconnecting...');
this.onclose();
});
}
}
class OB11ReverseWebsockets {
start() {
for (const url of ob11Config.wsReverseUrls) {
log('开始连接反向ws', url);
new Promise(() => {
try {
rwsList.push(new ReverseWebsocket(url));
} catch (e: any) {
log(e.stack);
}
}).then();
}
}
stop() {
for (const rws of rwsList) {
rws.stop();
}
}
restart() {
this.stop();
this.start();
}
}
export const ob11ReverseWebsockets = new OB11ReverseWebsockets();

View File

@@ -0,0 +1,76 @@
import { WebSocket } from 'ws';
import { actionMap } from '../../action';
import { OB11Response } from '../../action/OB11Response';
import { postWsEvent, registerWsEventSender, unregisterWsEventSender } from '../postOB11Event';
import { ActionName } from '../../action/types';
import BaseAction from '../../action/BaseAction';
import { LifeCycleSubType, OB11LifeCycleEvent } from '../../event/meta/OB11LifeCycleEvent';
import { OB11HeartbeatEvent } from '../../event/meta/OB11HeartbeatEvent';
import { WebsocketServerBase } from '@/common/server/websocket';
import { IncomingMessage } from 'node:http';
import { wsReply } from './reply';
import { napCatCore } from '@/core';
import { log } from '../../../common/utils/log';
import { ob11Config } from '@/onebot11/config';
import { selfInfo } from '@/common/data';
const heartbeatRunning = false;
class OB11WebsocketServer extends WebsocketServerBase {
authorizeFailed(wsClient: WebSocket) {
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
}
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) {
const action: BaseAction<any, any> | undefined = actionMap.get(actionName);
if (!action) {
return wsReply(wsClient, OB11Response.error('不支持的api ' + actionName, 1404, echo));
}
try {
const handleResult = await action.websocketHandle(params, echo);
wsReply(wsClient, handleResult);
} catch (e: any) {
wsReply(wsClient, OB11Response.error(`api处理出错:${e.stack}`, 1200, echo));
}
}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
if (url == '/api' || url == '/api/' || url == '/') {
wsClient.on('message', async (msg) => {
let receiveData: { action: ActionName, params: any, echo?: any } = { action: '', params: {} };
let echo = null;
try {
receiveData = JSON.parse(msg.toString());
echo = receiveData.echo;
log('收到正向Websocket消息', receiveData);
} catch (e) {
return wsReply(wsClient, OB11Response.error('json解析失败请检查数据格式', 1400, echo));
}
this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then();
});
}
if (url == '/event' || url == '/event/' || url == '/') {
registerWsEventSender(wsClient);
log('event上报ws客户端已连接');
try {
wsReply(wsClient, new OB11LifeCycleEvent(LifeCycleSubType.CONNECT));
} catch (e) {
log('发送生命周期失败', e);
}
const { heartInterval } = ob11Config;
const wsClientInterval = setInterval(() => {
postWsEvent(new OB11HeartbeatEvent(!!selfInfo.online, true, heartInterval));
}, heartInterval); // 心跳包
wsClient.on('close', () => {
log('event上报ws客户端已断开');
clearInterval(wsClientInterval);
unregisterWsEventSender(wsClient);
});
}
}
}
export const ob11WebsocketServer = new OB11WebsocketServer();

View File

@@ -0,0 +1,19 @@
import { WebSocket as WebSocketClass } from 'ws';
import { OB11Response } from '../../action/OB11Response';
import { PostEventType } from '../postOB11Event';
import { log } from '../../../common/utils/log';
import { isNull } from '../../../common/utils/helper';
export function wsReply(wsClient: WebSocketClass, data: OB11Response | PostEventType) {
try {
const packet = Object.assign({}, data);
if (isNull(packet['echo'])) {
delete packet['echo'];
}
wsClient.send(JSON.stringify(packet));
log('ws 消息上报', wsClient.url || '', data);
} catch (e: any) {
log('websocket 回复失败', e.stack, data);
}
}