Commit b8ac1588 authored by nanahira's avatar nanahira

add so many decorators

parent 951f83ef
Pipeline #6139 passed with stages
in 1 minute and 10 seconds
......@@ -142,3 +142,44 @@ export class AppService {
constructor(@InjectContextGuild() private ctx: Context) {}
}
```
## Register Koishi events with decorators
```ts
@Injectable()
// You may define a context scope here
@OnGuild('111111111')
export class AppService {
// equal to `ctx.on('message', (session) => { })`
@UseEvent('message')
async onMessage(session: Session.Payload<'message'>) {
console.log(`event ${session.userId}: ${session.content}`);
}
// You may also define a context scope at a specific function
@OnPlatform('onebot')
// equal to `ctx.middleware((session, next) => { })`
@UseMiddleware(true)
async onMiddleware(session: Session.Payload<'message'>, next: NextFunction) {
console.log(`middleware ${session.userId}: ${session.content}`);
next();
}
// Plugins could be registered asynchronously
@UsePlugin()
async installPlugin() {
const config = await someAsyncThings();
return PluginDef(PluginCommon, config);
}
// Define command
@UseCommand('my-echo <content:string>')
@CommandDescription('Echo command from decorators!')
@CommandUsage('Command usage')
@CommandExample('Command example')
testEchoCommand(session: Session, content: string) {
return content;
}
}
```
{
"name": "koishi-nestjs",
"version": "1.0.22",
"version": "1.0.23",
"description": "Koishi.js as Nest.js Module",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
......
import { Injectable } from '@nestjs/common';
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
import { Argv, Command, Context } from 'koishi';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import {
KoishiCommandDefinition,
KoishiDoRegister,
KoishiOnContextScope,
} from './koishi.constants';
import {
CommandDefinitionFun,
ContextFunction,
DoRegisterConfig,
EventNameAndPrepend,
KoishiModulePlugin,
OnContextFunction,
} from './koishi.interfaces';
@Injectable()
export class KoishiMetascanService {
constructor(
private readonly discoveryService: DiscoveryService,
private readonly metadataScanner: MetadataScanner,
private readonly reflector: Reflector,
) {}
private async handleInstance(
ctx: Context,
instance: Record<string, any>,
methodKey: string,
) {
const methodFun: (...args: any[]) => any = instance[methodKey];
const regData: DoRegisterConfig = this.reflector.get(
KoishiDoRegister,
methodFun,
);
if (!regData) {
return;
}
let baseContext = ctx;
const instanceContextFilters: OnContextFunction[] = this.reflector.get(
KoishiOnContextScope,
instance.constructor,
);
if (instanceContextFilters) {
for (const filter of instanceContextFilters) {
baseContext = filter(baseContext);
}
}
const methodContextFilters: OnContextFunction[] = this.reflector.get(
KoishiOnContextScope,
methodFun,
);
if (methodContextFilters) {
for (const filter of methodContextFilters) {
baseContext = filter(baseContext);
}
}
console.log(regData);
switch (regData.type) {
case 'middleware':
baseContext.middleware(
(session, next) => methodFun.call(instance, session, next),
regData.data,
);
break;
case 'onevent':
const {
data: eventData,
} = regData as DoRegisterConfig<EventNameAndPrepend>;
baseContext.on(eventData.name, (...args: any[]) =>
methodFun.call(instance, ...args),
);
break;
case 'plugin':
const pluginDesc: KoishiModulePlugin<any> = await methodFun.call(
instance,
);
const pluginCtx = pluginDesc.select
? baseContext.select(pluginDesc.select)
: baseContext.any();
pluginCtx.plugin(pluginDesc.plugin, pluginDesc.options);
break;
case 'command':
const { data: commandData } = regData as DoRegisterConfig<
ContextFunction<Command>
>;
let command = commandData(
baseContext,
).action((argv: Argv, ...args: any[]) =>
methodFun.call(instance, argv, ...args),
);
const commandDefs: CommandDefinitionFun[] = this.reflector.get(
KoishiCommandDefinition,
methodFun,
);
if (commandDefs) {
for (const commandDef of commandDefs) {
command = commandDef(command);
}
}
break;
default:
throw new Error(`Unknown operaton type ${regData.type}`);
}
}
async registerContext(ctx: Context) {
const providers = this.discoveryService.getProviders();
const controllers = this.discoveryService.getControllers();
await Promise.all(
[...providers, ...controllers]
.filter((wrapper) => wrapper.isDependencyTreeStatic())
.filter((wrapper) => wrapper.instance)
.map((wrapper: InstanceWrapper) => {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);
return this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodKey: string) =>
this.handleInstance(ctx, instance, methodKey),
);
}),
);
}
}
// Injections
export const KOISHI_MODULE_OPTIONS = 'KOISHI_MODULE_OPTIONS';
export const KOISHI_CONTEXT = 'KOISHI_CONTEXT';
export const KOISHI_CONTEXT_PRIVATE = 'KOISHI_CONTEXT_PRIVATE';
export const KOISHI_CONTEXT_CHANNEL = 'KOISHI_CONTEXT_CHANNEL';
export const KOISHI_CONTEXT_GUILD = 'KOISHI_CONTEXT_GUILD';
// metadatas
export const KoishiOnContextScope = 'KoishiOnContextScope';
export const KoishiDoRegister = 'KoishiDoRegister';
export const KoishiCommandDefinition = 'KoishiCommandDefinition';
import { Inject } from '@nestjs/common';
import { CustomDecorator, Inject, SetMetadata } from '@nestjs/common';
import {
KOISHI_CONTEXT,
KOISHI_CONTEXT_CHANNEL,
KOISHI_CONTEXT_GUILD,
KOISHI_CONTEXT_PRIVATE,
KoishiCommandDefinition,
KoishiDoRegister,
KoishiOnContextScope,
} from './koishi.constants';
import {
CommandConfigWIthDescription,
CommandDefinitionFun,
ContextFunction,
DoRegisterConfig,
EventName,
EventNameAndPrepend,
OnContextFunction,
Selection,
} from './koishi.interfaces';
import { Context, Command, Argv } from 'koishi';
// Injections
export const InjectContext = () => Inject(KOISHI_CONTEXT);
export const InjectContextPrivate = () => Inject(KOISHI_CONTEXT_PRIVATE);
export const InjectContextChannel = () => Inject(KOISHI_CONTEXT_CHANNEL);
export const InjectContextGuild = () => Inject(KOISHI_CONTEXT_GUILD);
export const SetExtraMetadata = <K = string, V = any>(
metadataKey: K,
metadataValue: V,
): CustomDecorator<K> => {
const decoratorFactory = (target: any, key?: any, descriptor?: any) => {
const currentMetadata: any[] =
Reflect.getMetadata(
metadataKey,
descriptor ? descriptor.value : target,
) || [];
currentMetadata.push(metadataValue);
if (descriptor) {
Reflect.defineMetadata(metadataKey, currentMetadata, descriptor.value);
return descriptor;
}
Reflect.defineMetadata(metadataKey, currentMetadata, target);
return target;
};
decoratorFactory.KEY = metadataKey;
return decoratorFactory;
};
// Register methods
export const UseMiddleware = (prepend?: boolean): MethodDecorator =>
SetMetadata<string, DoRegisterConfig<boolean>>(KoishiDoRegister, {
type: 'middleware',
data: prepend,
});
export const UseEvent = (name: EventName, prepend?: boolean): MethodDecorator =>
SetMetadata<string, DoRegisterConfig<EventNameAndPrepend>>(KoishiDoRegister, {
type: 'onevent',
data: { name, prepend },
});
export const UsePlugin = (): MethodDecorator =>
SetMetadata<string, DoRegisterConfig>(KoishiDoRegister, {
type: 'plugin',
});
export function UseCommand<D extends string>(
def: D,
config?: Command.Config,
): MethodDecorator {
return SetMetadata<
string,
DoRegisterConfig<
ContextFunction<Command<never, never, Argv.ArgumentType<D>>>
>
>(KoishiDoRegister, {
type: 'command',
data: (ctx) => ctx.command(def, config),
});
}
// Context scopes
export const OnContext = (
ctxFun: OnContextFunction,
): MethodDecorator & ClassDecorator =>
SetExtraMetadata<string, OnContextFunction>(KoishiOnContextScope, ctxFun);
export const OnUser = (...values: string[]) =>
OnContext((ctx) => ctx.user(...values));
export const OnSelf = (...values: string[]) =>
OnContext((ctx) => ctx.self(...values));
export const OnGuild = (...values: string[]) =>
OnContext((ctx) => ctx.guild(...values));
export const OnChannel = (...values: string[]) =>
OnContext((ctx) => ctx.channel(...values));
export const OnPlatform = (...values: string[]) =>
OnContext((ctx) => ctx.platform(...values));
export const OnPrivate = (...values: string[]) =>
OnContext((ctx) => ctx.private(...values));
export const OnSelection = (selection: Selection) =>
OnContext((ctx) => ctx.select(selection));
// Command definition
export const CommandDef = (def: CommandDefinitionFun): MethodDecorator =>
SetExtraMetadata<string, CommandDefinitionFun>(KoishiCommandDefinition, def);
export const CommandDescription = (desc: string) =>
CommandDef((cmd) => {
cmd.description = desc;
return cmd;
});
export const CommandAlias = (...names: string[]) =>
CommandDef((cmd) => cmd.alias(...names));
export const CommandShortcut = (
name: string | RegExp,
config: Command.Shortcut = {},
) => CommandDef((cmd) => cmd.shortcut(name, config));
export const CommandUsage = (text: string) =>
CommandDef((cmd) => cmd.usage(text));
export const CommandExample = (text: string) =>
CommandDef((cmd) => cmd.example(text));
export const CommandOption = (
name: string,
desc: string,
config: Argv.OptionConfig = {},
) => CommandDef((cmd) => cmd.option(name, desc, config));
import { ModuleMetadata, Provider, Type } from '@nestjs/common';
import { App, MaybeArray, Plugin } from 'koishi';
import { App, Command, Context, EventMap, MaybeArray, Plugin } from 'koishi';
import { AbstractHttpAdapter } from '@nestjs/core';
const selectors = [
......@@ -55,3 +55,24 @@ export interface KoishiModuleAsyncOptions
inject?: any[];
extraProviders?: Provider[];
}
export type EventName = keyof EventMap;
export interface EventNameAndPrepend {
name: EventName;
prepend?: boolean;
}
export type Promisify<T> = T extends Promise<unknown> ? T : Promise<T>;
export type ContextFunction<T> = (ctx: Context) => T;
export type OnContextFunction = ContextFunction<Context>;
export type DoRegisterType = 'middleware' | 'command' | 'onevent' | 'plugin';
export interface DoRegisterConfig<T = any> {
type: DoRegisterType;
data?: T;
}
export interface CommandConfigWIthDescription extends Command.Config {
desc?: string;
}
export type CommandDefinitionFun = (cmd: Command) => Command;
......@@ -22,6 +22,8 @@ import { KoishiMiddleware } from './koishi.middleware';
import { createServer } from 'http';
import { AddressInfo } from 'net';
import { KoishiLoggerService } from './koishi-logger.service';
import { KoishiMetascanService } from './koishi-metascan.service';
import { DiscoveryModule, DiscoveryService } from '@nestjs/core';
const koishiContextProvider: Provider = {
provide: KOISHI_CONTEXT,
......@@ -48,12 +50,21 @@ const koishiContextProviderPrivate: Provider = {
};
@Module({
imports: [DiscoveryModule],
providers: [
{
provide: KoishiService,
inject: [KOISHI_MODULE_OPTIONS, KoishiLoggerService],
useFactory: async (options: KoishiModuleOptions) => {
const koishi = new KoishiService(options);
inject: [
KOISHI_MODULE_OPTIONS,
KoishiMetascanService,
KoishiLoggerService,
DiscoveryService,
],
useFactory: async (
options: KoishiModuleOptions,
metascan: KoishiMetascanService,
) => {
const koishi = new KoishiService(options, metascan);
koishi._nestKoaTmpServer = createServer(
koishi._nestKoaTmpInstance.callback(),
);
......@@ -65,6 +76,7 @@ const koishiContextProviderPrivate: Provider = {
},
},
KoishiLoggerService,
KoishiMetascanService,
koishiContextProvider,
koishiContextProviderChannel,
koishiContextProviderGuild,
......
......@@ -10,12 +10,16 @@ import { Server } from 'http';
import Koa from 'koa';
import KoaRouter from '@koa/router';
import KoaBodyParser from 'koa-bodyparser';
import { KoishiMetascanService } from './koishi-metascan.service';
@Injectable()
export class KoishiService
extends App
implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy {
constructor(private readonly koishiModuleOptions: KoishiModuleOptions) {
constructor(
private readonly koishiModuleOptions: KoishiModuleOptions,
private readonly metascan: KoishiMetascanService,
) {
super({
...koishiModuleOptions,
port: 0,
......@@ -54,7 +58,8 @@ export class KoishiService
}
}
onApplicationBootstrap() {
async onApplicationBootstrap() {
await this.metascan.registerContext(this.any());
return this.start();
}
......
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