Commit 7af29461 authored by nanahira's avatar nanahira

interceptor in context

parent 489d798f
......@@ -89,6 +89,8 @@ Koishi-Nest 的配置项和 Koishi 配置项一致,参照 [Koishi 文档](http
* `module` Nest 模块名。
* 上下文选择器见本文 **上下文选择器** 部分。
* `globalInterceptors` 全局命令拦截器,详见 **命令拦截器** 部分。
插件的使用可以参考 [Koishi 文档](https://koishi.js.org/v4/guide/plugin/plugin.html)`moduleSelection` 的使用见本文 **复用性** 部分。
......@@ -333,6 +335,59 @@ Koishi-Nest 使用一组装饰器进行描述指令的行为。这些装饰器
* `@PutUserName(useDatabase: boolean = true)` 注入当前用户的用户名。
* `useDatabase` 是否尝试从数据库获取用户名。
### 指令拦截器
和 Koishi 中的 [`command.before`](https://koishi.js.org/v4/guide/message/command.html#%E4%BD%BF%E7%94%A8%E6%A3%80%E6%9F%A5%E5%99%A8) 对应,Koishi-Nest 提供了**指令拦截器**,便于在指令运行之前进行一些操作,如参数检查,记录日志等。
#### 定义
指令拦截器需要实现 `KoishiCommandInterceptor` 接口,提供 `intercept` 方法。该方法的参数与 `command.before` 的回调函数一致。
> 不要将指令拦截器与 Nest.js 的拦截器混淆。
下面是一个指令拦截器的例子。
```ts
import { KoishiCommandInterceptor } from "koishi-nestjs";
export class MyCommandInterceptor implements KoishiCommandInterceptor {
intercept(argv: Argv, arg1: string) {
if(arg1.startsWith('foo')) {
return 'Intercepted!';
}
}
}
```
#### 注册
要注册拦截器,只需要在指令对应的提供者方法或提供者本人使用 `@CommandInterceptors` 装饰器即可。也可以指定多个拦截器。
其中,在注册过拦截器的提供者类中,使用 `@InjectContext()` 或类似方法注入的上下文对象,也会应用拦截器。
> 这些上下文内安装的 Koishi 插件不会应用拦截器。
```ts
import { InjectContext } from 'koishi-nestjs';
import { Context } from 'koishi';
@Injectable()
// 可以在提供者类中指定上下文选择器,等价于 `ctx.guild('111111111')`
@CommandInterceptors(MyGlobalInterceptor)
export class AppService {
// 这里的 Koishi 上下文注册的任何指令也会应用拦截器
constructor(@InjectContext() private ctx: Context) {}
@UseCommand('my-echo <content:string>')
@CommandInterceptors(MyInterceptor1, MyInterceptor2) // 可以指定多个拦截器。
testEchoCommand(@PutArgv() argv: Argv, @PutArg(0) content: string) {
return content;
}
}
```
也可以在 Koishi-Nest 启动配置中,使用 `globalInterceptors` 方法注册拦截器。
## 上下文 Service 交互
......
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { KoishiCommandInterceptorRegistration } from '../utility/koishi.interfaces';
import { Command } from 'koishi';
@Injectable()
export class KoishiInterceptorManagerService {
constructor(private readonly moduleRef: ModuleRef) {}
getInterceptor(interceptorDef: KoishiCommandInterceptorRegistration) {
if (typeof interceptorDef !== 'object') {
return this.moduleRef.get(interceptorDef, { strict: false });
}
return interceptorDef;
}
addInterceptor(
command: Command,
interceptorDef: KoishiCommandInterceptorRegistration,
) {
const interceptor = this.getInterceptor(interceptorDef);
command.before((...params) => interceptor.intercept(...params));
}
addInterceptors(
command: Command,
interceptorDefs: KoishiCommandInterceptorRegistration[],
) {
if (!interceptorDefs) {
return;
}
interceptorDefs.forEach((interceptorDef) =>
this.addInterceptor(command, interceptorDef),
);
}
}
......@@ -27,6 +27,7 @@ import { KoishiContextService } from './providers/koishi-context.service';
import { KoishiHttpDiscoveryService } from './koishi-http-discovery/koishi-http-discovery.service';
import { KoishiWebsocketGateway } from './providers/koishi-websocket.gateway';
import { KoishiMetadataFetcherService } from './koishi-metadata-fetcher/koishi-metadata-fetcher.service';
import { KoishiInterceptorManagerService } from './koishi-interceptor-manager/koishi-interceptor-manager.service';
const koishiContextProvider: Provider<Context> = {
provide: KOISHI_CONTEXT,
......@@ -47,6 +48,7 @@ const koishiContextProvider: Provider<Context> = {
KoishiInjectionService,
KoishiHttpDiscoveryService,
KoishiMetadataFetcherService,
KoishiInterceptorManagerService,
],
exports: [KoishiService, koishiContextProvider],
})
......
import { App, Router } from 'koishi';
import { App, Command, Context, Router } from 'koishi';
import {
Inject,
Injectable,
......@@ -6,7 +6,10 @@ import {
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { KoishiModuleOptions } from './utility/koishi.interfaces';
import {
KoishiCommandInterceptorRegistration,
KoishiModuleOptions,
} from './utility/koishi.interfaces';
import { Server } from 'http';
import Koa from 'koa';
import KoaBodyParser from 'koa-bodyparser';
......@@ -15,6 +18,7 @@ import { applySelector } from './utility/koishi.utility';
import { KOISHI_MODULE_OPTIONS } from './utility/koishi.constants';
import { KoishiLoggerService } from './providers/koishi-logger.service';
import { KoishiHttpDiscoveryService } from './koishi-http-discovery/koishi-http-discovery.service';
import { Filter, ReplacedContext } from './utility/replaced-context';
@Injectable()
export class KoishiService
......@@ -71,4 +75,48 @@ export class KoishiService
async onModuleDestroy() {
await this.stop();
}
addInterceptors(
command: Command,
interceptorDefs: KoishiCommandInterceptorRegistration[],
) {
return this.metascan.addInterceptors(command, interceptorDefs);
}
private cloneContext(
filter: Filter,
interceptors: KoishiCommandInterceptorRegistration[] = [],
): Context {
return new ReplacedContext(filter, this, null, [
...(this.koishiModuleOptions.globalInterceptors || []),
...interceptors,
]);
}
withInterceptors(interceptors: KoishiCommandInterceptorRegistration[]) {
return this.cloneContext(() => true, interceptors);
}
any() {
return this.cloneContext(() => true);
}
never() {
return this.cloneContext(() => false);
}
union(arg: Filter | Context) {
const filter = typeof arg === 'function' ? arg : arg.filter;
return this.cloneContext((s) => this.filter(s) || filter(s));
}
intersect(arg: Filter | Context) {
const filter = typeof arg === 'function' ? arg : arg.filter;
return this.cloneContext((s) => this.filter(s) && filter(s));
}
except(arg: Filter | Context) {
const filter = typeof arg === 'function' ? arg : arg.filter;
return this.cloneContext((s) => this.filter(s) && !filter(s));
}
}
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { KoishiService } from '../koishi.service';
import { KoishiContextService } from './koishi-context.service';
import { ModulesContainer } from '@nestjs/core';
import { KoishiMetadataFetcherService } from '../koishi-metadata-fetcher/koishi-metadata-fetcher.service';
import {
KOISHI_MODULE_OPTIONS,
KoishiCommandInterceptorDef,
} from '../utility/koishi.constants';
import { KoishiModuleOptions } from '../utility/koishi.interfaces';
import { Context } from 'koishi';
@Injectable()
export class KoishiInjectionService {
constructor(
private readonly koishi: KoishiService,
private readonly ctxService: KoishiContextService,
private readonly metaFetcher: KoishiMetadataFetcherService,
private moduleContainer: ModulesContainer,
@Inject(KOISHI_MODULE_OPTIONS)
private readonly koishiModuleOptions: KoishiModuleOptions,
) {}
getInjectContext(inquerier: string | any) {
let ctx = this.koishi.any();
const token =
typeof inquerier === 'string' ? inquerier : inquerier.constructor;
const interceptors = this.metaFetcher.getMetadataArray(
KoishiCommandInterceptorDef,
token,
);
let ctx: Context = this.koishi.withInterceptors(interceptors);
for (const module of this.moduleContainer.values()) {
if (module.hasProvider(token) || module.controllers.has(token)) {
ctx = this.ctxService.getModuleCtx(ctx, module);
......
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { MetadataScanner, ModuleRef, ModulesContainer } from '@nestjs/core';
import { Argv, Command, Context, User } from 'koishi';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import {
KOISHI_MODULE_OPTIONS,
KoishiCommandDefinition,
KoishiCommandInterceptorDef,
KoishiDoRegister,
......@@ -16,6 +17,7 @@ import {
DoRegisterConfig,
EventName,
KoishiCommandInterceptorRegistration,
KoishiModuleOptions,
KoishiModulePlugin,
} from '../utility/koishi.interfaces';
import { applySelector } from '../utility/koishi.utility';
......@@ -23,6 +25,7 @@ import _ from 'lodash';
import { KoishiContextService } from './koishi-context.service';
import { Module } from '@nestjs/core/injector/module';
import { KoishiMetadataFetcherService } from '../koishi-metadata-fetcher/koishi-metadata-fetcher.service';
import { KoishiInterceptorManagerService } from '../koishi-interceptor-manager/koishi-interceptor-manager.service';
@Injectable()
export class KoishiMetascanService {
......@@ -32,6 +35,9 @@ export class KoishiMetascanService {
private readonly moduleContainer: ModulesContainer,
private readonly ctxService: KoishiContextService,
private readonly metaFetcher: KoishiMetadataFetcherService,
@Inject(KOISHI_MODULE_OPTIONS)
private readonly koishiModuleOptions: KoishiModuleOptions,
private readonly intercepterManager: KoishiInterceptorManagerService,
) {}
private preRegisterCommandActionArg(config: CommandPutConfig, cmd: Command) {
......@@ -107,19 +113,11 @@ export class KoishiMetascanService {
}
}
private getInterceptor(interceptorDef: KoishiCommandInterceptorRegistration) {
if (typeof interceptorDef !== 'object') {
return this.moduleRef.get(interceptorDef, { strict: false });
}
return interceptorDef;
}
private addInterceptor(
addInterceptors(
command: Command,
interceptorDef: KoishiCommandInterceptorRegistration,
interceptorDefs: KoishiCommandInterceptorRegistration[],
) {
const interceptor = this.getInterceptor(interceptorDef);
command.before((...params) => interceptor.intercept(...params));
return this.intercepterManager.addInterceptors(command, interceptorDefs);
}
private async handleInstanceRegistration(
......@@ -187,14 +185,14 @@ export class KoishiMetascanService {
for (const commandDef of commandDefs) {
command = commandDef(command) || command;
}
const interceptorDefs = this.metaFetcher.getPropertyMetadataArray(
KoishiCommandInterceptorDef,
instance,
methodKey,
const interceptorDefs = _.uniq(
this.metaFetcher.getPropertyMetadataArray(
KoishiCommandInterceptorDef,
instance,
methodKey,
),
);
for (const interceptorDef of interceptorDefs) {
this.addInterceptor(command, interceptorDef);
}
this.addInterceptors(command, interceptorDefs);
if (!commandData.putOptions) {
command.action((argv: Argv, ...args: any[]) =>
methodFun.call(instance, argv, ...args),
......
......@@ -67,6 +67,7 @@ export interface KoishiModuleOptions
loggerPrefix?: string;
loggerColor?: number;
moduleSelection?: KoishiModuleSelection[];
globalInterceptors?: KoishiCommandInterceptorRegistration[];
}
export interface KoishiModuleOptionsFactory {
......
import { Argv, Command, Context, Plugin, Session } from 'koishi';
import { KoishiService } from '../koishi.service';
import { KoishiCommandInterceptorRegistration } from './koishi.interfaces';
export type Filter = (session: Session) => boolean;
export class ReplacedContext extends Context {
constructor(
private _filter: Filter,
private _app: KoishiService,
private __plugin: Plugin = null,
public _interceptors: KoishiCommandInterceptorRegistration[] = [],
) {
super(_filter, _app, __plugin);
}
private cloneContext(
filter: Filter,
interceptors: KoishiCommandInterceptorRegistration[] = [],
): Context {
return new ReplacedContext(filter, this._app, this.__plugin, [
...this._interceptors,
...interceptors,
]);
}
withInterceptors(interceptors: KoishiCommandInterceptorRegistration[]) {
return this.cloneContext(this.filter, interceptors);
}
any() {
return this.cloneContext(() => true);
}
never() {
return this.cloneContext(() => false);
}
union(arg: Filter | Context) {
const filter = typeof arg === 'function' ? arg : arg.filter;
return this.cloneContext((s) => this.filter(s) || filter(s));
}
intersect(arg: Filter | Context) {
const filter = typeof arg === 'function' ? arg : arg.filter;
return this.cloneContext((s) => this.filter(s) && filter(s));
}
except(arg: Filter | Context) {
const filter = typeof arg === 'function' ? arg : arg.filter;
return this.cloneContext((s) => this.filter(s) && !filter(s));
}
command<D extends string>(
def: D,
config?: Command.Config,
): Command<never, never, Argv.ArgumentType<D>>;
command<D extends string>(
def: D,
desc: string,
config?: Command.Config,
): Command<never, never, Argv.ArgumentType<D>>;
command(def: string, ...args: [Command.Config?] | [string, Command.Config?]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const cmd = super.command(def, ...args);
this._app.addInterceptors(cmd, this._interceptors);
return cmd;
}
}
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