Commit 1a3e9d87 authored by nanahira's avatar nanahira

adapt ws

parent 2e062606
/dist
\ No newline at end of file
...@@ -92,8 +92,21 @@ Koishi-Nest 的配置项和 Koishi 配置项一致,参照 [Koishi 文档](http ...@@ -92,8 +92,21 @@ Koishi-Nest 的配置项和 Koishi 配置项一致,参照 [Koishi 文档](http
插件的使用可以参考 [Koishi 文档](https://koishi.js.org/v4/guide/plugin/plugin.html)`moduleSelection` 的使用见本文 **复用性** 部分。 插件的使用可以参考 [Koishi 文档](https://koishi.js.org/v4/guide/plugin/plugin.html)`moduleSelection` 的使用见本文 **复用性** 部分。
* `useWs`: `boolean` 默认 `false` 。是否启用 WebSocket 。**异步配置该项应写入异步配置项中。** 详见本文的 **WebSocket** 部分。
* `isGlobal`: `boolean` 默认 `true` 。指示 Koishi-Nest 模块是否应被注册为全局模块,建议开启。当安装了其他模块的情况下,需要将 Koishi-Nest 注册为全局模块使得其他模块可以正常注入 Koishi-Nest 作为依赖项。 **异步配置该项应写入异步配置项中。** 关于全局模块请参考 [Nest.js 文档](https://docs.nestjs.cn/8/modules?id=%e5%85%a8%e5%b1%80%e6%a8%a1%e5%9d%97) * `isGlobal`: `boolean` 默认 `true` 。指示 Koishi-Nest 模块是否应被注册为全局模块,建议开启。当安装了其他模块的情况下,需要将 Koishi-Nest 注册为全局模块使得其他模块可以正常注入 Koishi-Nest 作为依赖项。 **异步配置该项应写入异步配置项中。** 关于全局模块请参考 [Nest.js 文档](https://docs.nestjs.cn/8/modules?id=%e5%85%a8%e5%b1%80%e6%a8%a1%e5%9d%97)
## WebSocket
Koishi-Nest 针对 Koishi 的 WebSocket 功能进行了针对 Nest.js 的适配。
若要使用 Koishi 的与 WebSocket 服务器相关的插件或功能,需要在 Koishi-Nest 配置项中,把 `useWs` 设置为 `true` ,并在 `main.ts` 修改下列部分。
```ts
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new KoishiWsAdapter(app));
```
## 注入 Koishi 实例 ## 注入 Koishi 实例
可以直接注入 Koishi 实例或上下文进行注册操作。这种情况下,建议让 Nest 提供者实现 `OnModuleInit` 接口,并在该事件方法中进行 Koishi 指令注册操作。 可以直接注入 Koishi 实例或上下文进行注册操作。这种情况下,建议让 Nest 提供者实现 `OnModuleInit` 接口,并在该事件方法中进行 Koishi 指令注册操作。
......
...@@ -3,3 +3,4 @@ export * from './src/koishi.interfaces'; ...@@ -3,3 +3,4 @@ export * from './src/koishi.interfaces';
export * from './src/koishi.service'; export * from './src/koishi.service';
export * from './src/koishi.module'; export * from './src/koishi.module';
export * from './src/koishi-context.factory'; export * from './src/koishi-context.factory';
export * from './src/koishi.ws-adapter';
This diff is collapsed.
...@@ -32,15 +32,14 @@ ...@@ -32,15 +32,14 @@
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^8.0.0", "@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0", "@nestjs/core": "^8.0.0",
"koishi": "^4.0.0-alpha.11", "koishi": "^4.0.0-alpha.12",
"proxy-agent": "^5.0.0", "reflect-metadata": "^0.1.13",
"reflect-metadata": "^0.1.13" "rxjs": "^7.4.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/common": "^8.0.9", "@nestjs/common": "^8.0.9",
"@nestjs/core": "^8.0.9", "@nestjs/core": "^8.0.9",
"@types/koa": "^2.13.4", "@types/koa": "^2.13.4",
"@types/koa__router": "^8.0.8",
"@types/koa-bodyparser": "^4.3.3", "@types/koa-bodyparser": "^4.3.3",
"@types/lodash": "^4.14.175", "@types/lodash": "^4.14.175",
"@types/node": "^16.10.2", "@types/node": "^16.10.2",
...@@ -50,16 +49,19 @@ ...@@ -50,16 +49,19 @@
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1", "eslint-plugin-prettier": "^3.4.1",
"express": "^4.17.1", "express": "^4.17.1",
"koishi": "^4.0.0-alpha.11", "koishi": "^4.0.0-alpha.12",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"proxy-agent": "^5.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.4.0",
"typescript": "^4.4.3" "typescript": "^4.4.3"
}, },
"dependencies": { "dependencies": {
"@koa/router": "^10.1.1", "@nestjs/platform-ws": "^8.1.2",
"@nestjs/websockets": "^8.1.2",
"@types/ws": "^8.2.0",
"koa": "^2.13.3", "koa": "^2.13.3",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",
"lodash": "^4.17.21" "lodash": "^4.17.21",
"ws": "^8.2.3"
} }
} }
import { Injectable } from '@nestjs/common';
import { AbstractHttpAdapter, HttpAdapterHost, ModuleRef } from '@nestjs/core';
import { AbstractWsAdapter } from '@nestjs/websockets';
@Injectable()
export class KoishiHttpDiscoveryService {
constructor(private readonly moduleRef: ModuleRef) {}
getHttpAdapter(): AbstractHttpAdapter {
const apdaterHost = this.moduleRef.get(HttpAdapterHost, { strict: false });
if (apdaterHost) {
return apdaterHost.httpAdapter;
} else {
return null;
}
}
}
...@@ -52,11 +52,14 @@ export interface KoishiModuleSelection extends ContextSelector { ...@@ -52,11 +52,14 @@ export interface KoishiModuleSelection extends ContextSelector {
module: Type<any>; module: Type<any>;
} }
export interface WhetherGlobalOption { export interface KoishiModuleTopOptions {
isGlobal?: boolean; isGlobal?: boolean;
useWs?: boolean;
} }
export interface KoishiModuleOptions extends App.Config, WhetherGlobalOption { export interface KoishiModuleOptions
extends App.Config,
KoishiModuleTopOptions {
usePlugins?: KoishiModulePlugin<Plugin>[]; usePlugins?: KoishiModulePlugin<Plugin>[];
loggerPrefix?: string; loggerPrefix?: string;
loggerColor?: number; loggerColor?: number;
...@@ -69,7 +72,7 @@ export interface KoishiModuleOptionsFactory { ...@@ -69,7 +72,7 @@ export interface KoishiModuleOptionsFactory {
export interface KoishiModuleAsyncOptions export interface KoishiModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'>, extends Pick<ModuleMetadata, 'imports'>,
WhetherGlobalOption { KoishiModuleTopOptions {
useExisting?: Type<KoishiModuleOptionsFactory>; useExisting?: Type<KoishiModuleOptionsFactory>;
useClass?: Type<KoishiModuleOptionsFactory>; useClass?: Type<KoishiModuleOptionsFactory>;
useFactory?: ( useFactory?: (
......
...@@ -26,6 +26,8 @@ import { Context } from 'koishi'; ...@@ -26,6 +26,8 @@ import { Context } from 'koishi';
import { defaultContextContainer } from './koishi-context.factory'; import { defaultContextContainer } from './koishi-context.factory';
import { KoishiInjectionService } from './providers/koishi-injection.service'; import { KoishiInjectionService } from './providers/koishi-injection.service';
import { KoishiContextService } from './providers/koishi-context.service'; import { KoishiContextService } from './providers/koishi-context.service';
import { KoishiHttpDiscoveryService } from './koishi-http-discovery/koishi-http-discovery.service';
import { KoishiWebsocketGateway } from './providers/koishi-websocket.gateway';
const koishiContextProvider: Provider<Context> = { const koishiContextProvider: Provider<Context> = {
provide: KOISHI_CONTEXT, provide: KOISHI_CONTEXT,
...@@ -44,6 +46,7 @@ const koishiContextProvider: Provider<Context> = { ...@@ -44,6 +46,7 @@ const koishiContextProvider: Provider<Context> = {
koishiContextProvider, koishiContextProvider,
KoishiContextService, KoishiContextService,
KoishiInjectionService, KoishiInjectionService,
KoishiHttpDiscoveryService,
], ],
exports: [KoishiService, koishiContextProvider], exports: [KoishiService, koishiContextProvider],
}) })
...@@ -58,6 +61,7 @@ export class KoishiModule implements NestModule { ...@@ -58,6 +61,7 @@ export class KoishiModule implements NestModule {
providers: [ providers: [
{ provide: KOISHI_MODULE_OPTIONS, useValue: options }, { provide: KOISHI_MODULE_OPTIONS, useValue: options },
...defaultContextContainer.contextsToProvide, ...defaultContextContainer.contextsToProvide,
...(options.useWs ? [KoishiWebsocketGateway] : []),
], ],
exports: defaultContextContainer.contextsToProvide, exports: defaultContextContainer.contextsToProvide,
global: options.isGlobal != null ? options.isGlobal : true, global: options.isGlobal != null ? options.isGlobal : true,
...@@ -72,6 +76,7 @@ export class KoishiModule implements NestModule { ...@@ -72,6 +76,7 @@ export class KoishiModule implements NestModule {
...this.createAsyncProviders(options), ...this.createAsyncProviders(options),
...defaultContextContainer.contextsToProvide, ...defaultContextContainer.contextsToProvide,
...(options.extraProviders || []), ...(options.extraProviders || []),
...(options.useWs ? [KoishiWebsocketGateway] : []),
], ],
exports: defaultContextContainer.contextsToProvide, exports: defaultContextContainer.contextsToProvide,
global: options.isGlobal != null ? options.isGlobal : true, global: options.isGlobal != null ? options.isGlobal : true,
......
import { App } from 'koishi'; import { App, Router } from 'koishi';
import { import {
Inject, Inject,
Injectable, Injectable,
...@@ -9,38 +9,40 @@ import { ...@@ -9,38 +9,40 @@ import {
import { KoishiModuleOptions } from './koishi.interfaces'; import { KoishiModuleOptions } from './koishi.interfaces';
import { Server } from 'http'; import { Server } from 'http';
import Koa from 'koa'; import Koa from 'koa';
import KoaRouter from '@koa/router';
import KoaBodyParser from 'koa-bodyparser'; import KoaBodyParser from 'koa-bodyparser';
import { KoishiMetascanService } from './providers/koishi-metascan.service'; import { KoishiMetascanService } from './providers/koishi-metascan.service';
import { applySelector } from './utility/koishi.utility'; import { applySelector } from './utility/koishi.utility';
import { KOISHI_MODULE_OPTIONS } from './utility/koishi.constants'; import { KOISHI_MODULE_OPTIONS } from './utility/koishi.constants';
import { KoishiLoggerService } from './providers/koishi-logger.service'; import { KoishiLoggerService } from './providers/koishi-logger.service';
import { KoishiHttpDiscoveryService } from './koishi-http-discovery/koishi-http-discovery.service';
@Injectable() @Injectable()
export class KoishiService export class KoishiService
extends App extends App
implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy { implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy
{
constructor( constructor(
@Inject(KOISHI_MODULE_OPTIONS) @Inject(KOISHI_MODULE_OPTIONS)
private readonly koishiModuleOptions: KoishiModuleOptions, private readonly koishiModuleOptions: KoishiModuleOptions,
private readonly metascan: KoishiMetascanService, private readonly metascan: KoishiMetascanService,
private readonly httpDiscovery: KoishiHttpDiscoveryService,
private readonly koishiLogger: KoishiLoggerService, private readonly koishiLogger: KoishiLoggerService,
) { ) {
super({ super({
...koishiModuleOptions, ...koishiModuleOptions,
port: 0, port: 0,
}); });
this.router = new KoaRouter(); this.router = new Router();
this._nestKoaTmpInstance.use(KoaBodyParser());
this._nestKoaTmpInstance.use(this.router.routes()); this._nestKoaTmpInstance.use(this.router.routes());
this._nestKoaTmpInstance.use(this.router.allowedMethods()); this._nestKoaTmpInstance.use(this.router.allowedMethods());
this._nestKoaTmpInstance.use(KoaBodyParser());
this.options.port = 1; this.options.port = 1;
} }
readonly _nestKoaTmpInstance = new Koa(); readonly _nestKoaTmpInstance = new Koa();
private async setHttpServer() { private async setHttpServer() {
const httpAdapter = this.metascan.getHttpAdapter(); const httpAdapter = this.httpDiscovery.getHttpAdapter();
if (!httpAdapter) { if (!httpAdapter) {
return; return;
} }
......
import { WsAdapter } from '@nestjs/platform-ws';
import { INestApplicationContext } from '@nestjs/common';
import * as http from 'http';
export class KoishiWsAdapter extends WsAdapter {
constructor(appOrHttpServer?: INestApplicationContext | any) {
super(appOrHttpServer);
}
override ensureHttpServerExists(
port: number,
httpServer = http.createServer(),
) {
if (this.httpServersRegistry.has(port)) {
return;
}
this.httpServersRegistry.set(port, httpServer);
httpServer.on('upgrade', (request, socket, head) => {
const baseUrl = 'ws://' + request.headers.host + '/';
const pathname = new URL(request.url, baseUrl).pathname;
const wsServersCollection = this.wsServersRegistry.get(port);
let isRequestDelegated = false;
let fallbackWsServer: any;
for (const wsServer of wsServersCollection) {
if (pathname === wsServer.path) {
wsServer.handleUpgrade(request, socket, head, (ws: unknown) => {
wsServer.emit('connection', ws, request);
});
isRequestDelegated = true;
break;
}
if (wsServer.path === '__koishi_fallback') {
fallbackWsServer = wsServer;
}
}
if (!isRequestDelegated && fallbackWsServer) {
const wsServer = fallbackWsServer;
wsServer.handleUpgrade(request, socket, head, (ws: unknown) => {
wsServer.emit('connection', ws, request);
});
isRequestDelegated = true;
}
if (!isRequestDelegated) {
socket.destroy();
}
});
return httpServer;
}
}
...@@ -40,15 +40,6 @@ export class KoishiMetascanService { ...@@ -40,15 +40,6 @@ export class KoishiMetascanService {
private readonly ctxService: KoishiContextService, private readonly ctxService: KoishiContextService,
) {} ) {}
getHttpAdapter(): AbstractHttpAdapter {
const apdaterHost = this.moduleRef.get(HttpAdapterHost, { strict: false });
if (apdaterHost) {
return apdaterHost.httpAdapter;
} else {
return null;
}
}
private preRegisterCommandActionArg(config: CommandPutConfig, cmd: Command) { private preRegisterCommandActionArg(config: CommandPutConfig, cmd: Command) {
if (!config) { if (!config) {
return; return;
......
import {
OnGatewayConnection,
OnGatewayInit,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { KoishiService } from '../koishi.service';
import type WebSocket from 'ws';
import type { Server } from 'ws';
import { IncomingMessage } from 'http';
@WebSocketGateway()
export class KoishiWebsocketGateway
implements OnGatewayInit, OnGatewayConnection {
constructor(private readonly koishi: KoishiService) {}
@WebSocketServer()
wsServer: Server;
afterInit(server: any): any {
// console.log('Init ws server', server, server === this.wsServer);
this.wsServer.path = '__koishi_fallback';
this.koishi._wsServer = this.wsServer;
}
handleConnection(socket: WebSocket, request: IncomingMessage) {
// console.log(socket);
for (const manager of this.koishi.router.wsStack) {
if (manager.accept(socket, request)) return;
}
socket.close();
}
}
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