Commit 4dc247fa authored by nanahira's avatar nanahira

finish

parent f8ba79a2
...@@ -3,4 +3,8 @@ bots: ...@@ -3,4 +3,8 @@ bots:
endpoint: 'ws://localhost:6700' endpoint: 'ws://localhost:6700'
selfId: '1111111111' selfId: '1111111111'
token: 'token' token: 'token'
routes: [] routes:
- name: default
botId: '3221204940'
token: 'oONw7YpqUdYuc'
balancePolicy: hash
...@@ -5,21 +5,8 @@ import { KoishiModule } from 'koishi-nestjs'; ...@@ -5,21 +5,8 @@ import { KoishiModule } from 'koishi-nestjs';
import { BotLoaderService } from './bot-loader/bot-loader.service'; import { BotLoaderService } from './bot-loader/bot-loader.service';
import { RouteService } from './route/route.service'; import { RouteService } from './route/route.service';
import { OnebotGateway } from './onebot.gateway'; import { OnebotGateway } from './onebot.gateway';
import { Adapter, Session } from 'koishi';
import { MessageService } from './message/message.service'; import { MessageService } from './message/message.service';
import { ReverseWsService } from './reverse-ws/reverse-ws.service';
declare module 'koishi' {
interface EventMap {
dispatch: (session: Session) => void;
}
}
const originalDispatch = Adapter.prototype.dispatch;
Adapter.prototype.dispatch = function (this: Adapter, session: Session) {
if (!this.ctx.app.isActive) return;
originalDispatch.call(this, session);
this.ctx.emit(session, 'dispatch', session);
};
@Module({ @Module({
imports: [ imports: [
...@@ -33,6 +20,12 @@ Adapter.prototype.dispatch = function (this: Adapter, session: Session) { ...@@ -33,6 +20,12 @@ Adapter.prototype.dispatch = function (this: Adapter, session: Session) {
useWs: true, useWs: true,
}), }),
], ],
providers: [BotLoaderService, RouteService, OnebotGateway, MessageService], providers: [
BotLoaderService,
RouteService,
OnebotGateway,
MessageService,
ReverseWsService,
],
}) })
export class AppModule {} export class AppModule {}
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { Adapter, Context, Session } from 'koishi';
declare module 'koishi' {
interface EventMap {
dispatch: (session: Session) => void;
}
}
const originalDispatch = Adapter.prototype.dispatch;
Adapter.prototype.dispatch = function (this: Adapter, session: Session) {
if (!this.ctx.app.isActive) return;
originalDispatch.call(this, session);
this.ctx.emit(session, 'dispatch', session);
};
import * as PluginOnebot from '@koishijs/plugin-adapter-onebot'; import * as PluginOnebot from '@koishijs/plugin-adapter-onebot';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { InjectContext, PluginDef, UsePlugin } from 'koishi-nestjs'; import { InjectContext, PluginDef, UsePlugin } from 'koishi-nestjs';
import { BotConfig } from '@koishijs/plugin-adapter-onebot/lib/bot'; import { BotConfig } from '@koishijs/plugin-adapter-onebot/lib/bot';
import { Context } from 'koishi';
@Injectable() @Injectable()
export class BotLoaderService implements OnModuleInit { export class BotLoaderService implements OnModuleInit {
...@@ -15,6 +29,9 @@ export class BotLoaderService implements OnModuleInit { ...@@ -15,6 +29,9 @@ export class BotLoaderService implements OnModuleInit {
@UsePlugin() @UsePlugin()
loadBots() { loadBots() {
const bots = this.config.get<BotConfig[]>('bots'); const bots = this.config.get<BotConfig[]>('bots');
for (const bot of bots) {
bot.selfId = bot.selfId.toString();
}
return PluginDef(PluginOnebot, { bots }); return PluginDef(PluginOnebot, { bots });
} }
......
...@@ -16,7 +16,7 @@ export class MessageService extends ConsoleLogger { ...@@ -16,7 +16,7 @@ export class MessageService extends ConsoleLogger {
registerWsEvent(client: WebSocket, route: Route) { registerWsEvent(client: WebSocket, route: Route) {
client.on('message', async (data) => { client.on('message', async (data) => {
if (typeof data !== 'string') { if (typeof data !== 'string') {
this.warn(`Got non-string.`); this.warn(`Got non-string from ${route.name}.`);
client.send( client.send(
JSON.stringify({ JSON.stringify({
retcode: 1400, retcode: 1400,
...@@ -24,7 +24,7 @@ export class MessageService extends ConsoleLogger { ...@@ -24,7 +24,7 @@ export class MessageService extends ConsoleLogger {
data: null, data: null,
error: { error: {
code: 1404, code: 1404,
message: `Got non-string`, message: `Got non-string from ${route.name}.`,
}, },
}), }),
); );
...@@ -35,7 +35,7 @@ export class MessageService extends ConsoleLogger { ...@@ -35,7 +35,7 @@ export class MessageService extends ConsoleLogger {
const message = JSON.stringify(await this.onWsEvent(route, parsedData)); const message = JSON.stringify(await this.onWsEvent(route, parsedData));
client.send(message); client.send(message);
} catch (e) { } catch (e) {
this.warn(`Got bad JSON ${data}`); this.warn(`Got bad JSON ${data} from ${route.name}.`);
client.send( client.send(
JSON.stringify({ JSON.stringify({
retcode: 1400, retcode: 1400,
...@@ -43,7 +43,7 @@ export class MessageService extends ConsoleLogger { ...@@ -43,7 +43,7 @@ export class MessageService extends ConsoleLogger {
data: null, data: null,
error: { error: {
code: 1404, code: 1404,
message: `Got bad JSON.`, message: `Got bad JSON from ${route.name}.`,
}, },
}), }),
); );
...@@ -58,33 +58,34 @@ export class MessageService extends ConsoleLogger { ...@@ -58,33 +58,34 @@ export class MessageService extends ConsoleLogger {
(b) => b.selfId === route.botId && b.platform === 'onebot', (b) => b.selfId === route.botId && b.platform === 'onebot',
) as OneBotBot; ) as OneBotBot;
if (!bot) { if (!bot) {
this.error(`Bot ${route.botId} not found`); this.error(`Bot ${route.botId} from ${route.name} not found.`);
return { return {
retcode: 1404, retcode: 1404,
status: 'failed', status: 'failed',
data: null, data: null,
error: { error: {
code: 1404, code: 1404,
message: `Bot ${route.botId} not found.`, message: `Bot ${route.botId} from ${route.name} not found.`,
}, },
echo: data?.echo, echo: data?.echo,
}; };
} }
try { try {
const result = await bot.internal._request(data.action, data.params); const result = await bot.internal._request(data.action, data.params);
// console.log(result);
return { return {
...result, ...result,
echo: data?.echo, echo: data?.echo,
}; };
} catch (e) { } catch (e) {
this.error(`Bot ${route.botId} timed out.`); this.error(`Bot ${route.botId} from ${route.name} timed out.`);
return { return {
retcode: 1404, retcode: 1404,
status: 'failed', status: 'failed',
data: null, data: null,
error: { error: {
code: 1404, code: 1404,
message: `Bot ${route.botId} timed out.`, message: `Bot ${route.botId} from ${route.name} timed out.`,
}, },
echo: data?.echo, echo: data?.echo,
}; };
......
...@@ -56,8 +56,9 @@ export class OnebotGateway ...@@ -56,8 +56,9 @@ export class OnebotGateway
ip: (request.socket.address() as AddressInfo).address, ip: (request.socket.address() as AddressInfo).address,
}; };
this.clientRouteMap.set(client, clientInfo); this.clientRouteMap.set(client, clientInfo);
route.addConnection(client);
this.messageService.registerWsEvent(client, route); this.messageService.registerWsEvent(client, route);
this.warn( this.log(
`Client ${clientInfo.ip} of route ${clientInfo.routeName} connected.`, `Client ${clientInfo.ip} of route ${clientInfo.routeName} connected.`,
); );
} }
......
import { Test, TestingModule } from '@nestjs/testing';
import { ReverseWsService } from './reverse-ws.service';
describe('ReverseWsService', () => {
let service: ReverseWsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ReverseWsService],
}).compile();
service = module.get<ReverseWsService>(ReverseWsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { MessageService } from '../message/message.service';
import { ReverseWsConfig, Route } from '../route/Route';
import { OutgoingHttpHeaders } from 'http';
import WebSocket from 'ws';
@Injectable()
export class ReverseWsService extends ConsoleLogger {
constructor(private meesageService: MessageService) {
super('reverse-ws');
}
initializeReverseWs(route: Route, revConfig: ReverseWsConfig) {
const headers: OutgoingHttpHeaders = {
'X-Self-ID': route.botId,
'X-Client-Role': 'Universal',
'User-Agent': 'OneBot',
};
if (revConfig.token) {
headers['Authorization'] = `Bearer ${revConfig.token}`;
}
const ws = new WebSocket(revConfig.url, { headers });
ws.on('error', (err) =>
this.warn(`Socket from ${route.name} error: ${err.toString()}`),
);
ws.on('open', () => {
this.log(`Route ${route.name} connected to ${revConfig.url}.`);
route.addConnection(ws);
this.meesageService.registerWsEvent(ws, route);
});
ws.on('close', (code, msg) => {
route.removeConnection(ws);
const interval = revConfig.reconnectInterval || 5000;
this.log(
`Route ${route.name} disconnected from ${revConfig.url}: ${code}: ${msg}. Will retry after ${interval} ms.`,
);
setTimeout(() => this.initializeReverseWs(route, revConfig), interval);
});
}
}
...@@ -5,29 +5,37 @@ import { Random, remove } from 'koishi'; ...@@ -5,29 +5,37 @@ import { Random, remove } from 'koishi';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { OneBotBot } from '@koishijs/plugin-adapter-onebot/lib/bot'; import { OneBotBot } from '@koishijs/plugin-adapter-onebot/lib/bot';
export type HashPolicy = 'broadcast' | 'random' | 'round-robin' | 'hash'; export type BalancePolicy = 'broadcast' | 'random' | 'round-robin' | 'hash';
export interface ReverseWsConfig {
url: string;
token?: string;
reconnectInterval?: number;
}
export interface RouteConfig { export interface RouteConfig {
name: string; name: string;
botId: string; botId: string;
token?: string; token?: string;
select?: Selection; select?: Selection;
hashPolicy?: HashPolicy; balancePolicy?: BalancePolicy;
heartbeat?: number; heartbeat?: number;
reverseWs?: ReverseWsConfig[];
} }
export class Route implements RouteConfig { export class Route implements RouteConfig {
connections: WebSocket[] = []; private connections: WebSocket[] = [];
private roundCount = 0; private roundCount = 0;
ctx: Context; ctx: Context;
name: string; name: string;
botId: string; botId: string;
token?: string; token?: string;
select?: Selection; select?: Selection;
hashPolicy?: HashPolicy; balancePolicy?: BalancePolicy;
heartbeat?: number; heartbeat?: number;
constructor(routeConfig: RouteConfig, ctx: Context) { constructor(routeConfig: RouteConfig, ctx: Context) {
Object.assign(this, routeConfig); Object.assign(this, routeConfig);
this.hashPolicy ||= 'hash'; this.balancePolicy ||= 'hash';
this.botId = this.botId.toString();
this.ctx = this.getFilteredContext(ctx); this.ctx = this.getFilteredContext(ctx);
if (this.heartbeat) { if (this.heartbeat) {
setInterval(() => { setInterval(() => {
...@@ -41,23 +49,47 @@ export class Route implements RouteConfig { ...@@ -41,23 +49,47 @@ export class Route implements RouteConfig {
}, this.heartbeat); }, this.heartbeat);
} }
} }
send(data: any, sess: Session) { send(data: any, sess: Session, allConns = this.connections) {
const message = JSON.stringify(data); const message = JSON.stringify(data);
for (const conn of this.getRelatedConnections(sess)) { const conns = this.getRelatedConnections(sess, allConns);
conn.send(message, (err) => {}); for (const conn of conns) {
conn.send(message, (err) => {
if (err) {
this.ctx
.logger(`route-${this.name}`)
.error(`Failed to send data: ${err.message}`);
if (allConns.length > 1 && conns.length === 1) {
this.ctx
.logger(`route-${this.name}`)
.warn(`Retrying another connection.`);
this.send(
data,
sess,
allConns.filter((c) => c !== conn),
);
}
}
});
} }
} }
broadcast(data: any) { broadcast(data: any) {
const message = JSON.stringify(data); const message = JSON.stringify(data);
for (const conn of this.connections) { for (const conn of this.connections) {
conn.send(message, (err) => {}); conn.send(message, (err) => {
if (err) {
this.ctx
.logger(`route-${this.name}`)
.error(`Failed to broadcast data: ${err.message}`);
}
});
} }
} }
getFilteredContext(ctx: Context) { getFilteredContext(ctx: Context) {
const idCtx = ctx.self(this.botId);
if (!this.select) { if (!this.select) {
return ctx; return idCtx;
} }
return ctx.select(this.select); return idCtx.select(this.select);
} }
private getSequenceFromSession(sess: Session) { private getSequenceFromSession(sess: Session) {
const hash = createHash('md5'); const hash = createHash('md5');
...@@ -68,24 +100,30 @@ export class Route implements RouteConfig { ...@@ -68,24 +100,30 @@ export class Route implements RouteConfig {
} }
return parseInt(hash.digest('hex'), 16) % 4294967295; return parseInt(hash.digest('hex'), 16) % 4294967295;
} }
getRelatedConnections(sess: Session): WebSocket[] { getRelatedConnections(
switch (this.hashPolicy) { sess: Session,
allConns = this.connections,
): WebSocket[] {
if (allConns.length <= 1) {
return allConns;
}
switch (this.balancePolicy) {
case 'broadcast': case 'broadcast':
return this.connections; return allConns;
case 'round-robin': case 'round-robin':
const index = this.roundCount++ % this.connections.length; const index = this.roundCount++ % allConns.length;
return [this.connections[index]]; return [allConns[index]];
case 'random': case 'random':
return [Random.pick(this.connections)]; return [Random.pick(allConns)];
case 'hash': case 'hash':
return [ return [allConns[this.getSequenceFromSession(sess) % allConns.length]];
this.connections[ default:
this.getSequenceFromSession(sess) % this.connections.length this.ctx
], .logger(`route-${this.name}`)
]; .error(`Unknown policy ${this.balancePolicy}`);
}
return []; return [];
} }
}
addConnection(conn: WebSocket) { addConnection(conn: WebSocket) {
this.connections.push(conn); this.connections.push(conn);
return conn; return conn;
......
...@@ -29,10 +29,6 @@ export class RouteService ...@@ -29,10 +29,6 @@ export class RouteService
return this.routes.get(name); return this.routes.get(name);
} }
getRoutesFromBot(botId: string) {
return Array.from(this.routes.values()).filter((r) => r.botId === botId);
}
onApplicationBootstrap() { onApplicationBootstrap() {
for (const route of this.routes.values()) { for (const route of this.routes.values()) {
route.ctx.on('dispatch', (session) => this.onOnebotEvent(session, route)); route.ctx.on('dispatch', (session) => this.onOnebotEvent(session, route));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment