mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-12 07:50:25 +00:00
NapCatQQ
This commit is contained in:
32
src/onebot11/server/http.ts
Normal file
32
src/onebot11/server/http.ts
Normal 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);
|
||||
185
src/onebot11/server/postOB11Event.ts
Normal file
185
src/onebot11/server/postOB11Event.ts
Normal 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);
|
||||
}
|
||||
143
src/onebot11/server/ws/ReverseWebsocket.ts
Normal file
143
src/onebot11/server/ws/ReverseWebsocket.ts
Normal 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();
|
||||
|
||||
76
src/onebot11/server/ws/WebsocketServer.ts
Normal file
76
src/onebot11/server/ws/WebsocketServer.ts
Normal 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();
|
||||
|
||||
19
src/onebot11/server/ws/reply.ts
Normal file
19
src/onebot11/server/ws/reply.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user