Commit e0d42ac3 authored by nanahira's avatar nanahira

refactor with koishi-decorators

parent 714309db
This diff is collapsed.
......@@ -14,11 +14,11 @@ import { Server } from 'http';
import Koa from 'koa';
import KoaBodyParser from 'koa-bodyparser';
import { KoishiMetascanService } from './providers/koishi-metascan.service';
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';
import { applySelector } from 'koishi-decorators';
// eslint-disable-next-line @typescript-eslint/no-empty-function
Router.prepare = () => {};
......@@ -70,17 +70,17 @@ export class KoishiService
async onModuleInit() {
await this.setHttpServer();
await this.metascan.preRegisterContext(this.any());
this.metascan.preRegisterContext(this.any());
if (this.koishiModuleOptions.usePlugins) {
for (const pluginDesc of this.koishiModuleOptions.usePlugins) {
const ctx = applySelector(this, pluginDesc);
ctx.plugin(pluginDesc.plugin, pluginDesc.options);
for (const pluginDef of this.koishiModuleOptions.usePlugins) {
const ctx = applySelector(this, pluginDef);
ctx.plugin(pluginDef.plugin, pluginDef.options);
}
}
}
async onApplicationBootstrap() {
await this.metascan.registerContext(this.any());
this.metascan.registerContext(this.any());
return this.start();
}
......
import { Inject, Injectable, Type } from '@nestjs/common';
import {
KOISHI_MODULE_OPTIONS,
KoishiOnContextScope,
} from '../utility/koishi.constants';
import { KOISHI_MODULE_OPTIONS } from '../utility/koishi.constants';
import {
KoishiModuleOptions,
KoishiModuleSelection,
} from '../utility/koishi.interfaces';
import { applySelector } from '../utility/koishi.utility';
import { applySelector, Registrar } from 'koishi-decorators';
import { Context } from 'koishi';
import { Module } from '@nestjs/core/injector/module';
import { KoishiMetadataFetcherService } from '../koishi-metadata-fetcher/koishi-metadata-fetcher.service';
import _ from 'lodash';
@Injectable()
export class KoishiContextService {
moduleSelections = new Map<Type<any>, KoishiModuleSelection>();
constructor(
@Inject(KOISHI_MODULE_OPTIONS) options: KoishiModuleOptions,
private readonly metaFetcher: KoishiMetadataFetcherService,
) {
constructor(@Inject(KOISHI_MODULE_OPTIONS) options: KoishiModuleOptions) {
if (options.moduleSelection) {
for (const selection of options.moduleSelection) {
this.moduleSelections.set(selection.module, selection);
......@@ -37,13 +29,8 @@ export class KoishiContextService {
}
getProviderCtx(ctx: Context, ...instances: any[]) {
const contextFilters = _.flatten(
instances.map((instance) =>
this.metaFetcher.getMetadataArray(KoishiOnContextScope, instance),
),
);
for (const filter of contextFilters) {
ctx = filter(ctx) || ctx;
for (const instance of instances) {
ctx = new Registrar(instance).getScopeContext(ctx);
}
return ctx;
}
......
This diff is collapsed.
......@@ -10,7 +10,8 @@ import { IncomingMessage } from 'http';
@WebSocketGateway()
export class KoishiWebsocketGateway
implements OnGatewayInit, OnGatewayConnection {
implements OnGatewayInit, OnGatewayConnection
{
constructor(private readonly koishi: KoishiService) {}
@WebSocketServer()
......
// Injections
import {
CommandDefinitionFun,
DoRegisterConfig,
KoishiCommandInterceptorRegistration,
OnContextFunction,
} from './koishi.interfaces';
import { KoishiCommandInterceptorRegistration } from './koishi.interfaces';
import { Context } from 'koishi';
export const KOISHI_MODULE_OPTIONS = 'KOISHI_MODULE_OPTIONS';
export const KOISHI_CONTEXT = 'KOISHI_CONTEXT';
// metadatas
export const KoishiOnContextScope = 'KoishiOnContextScope';
export const KoishiDoRegister = 'KoishiDoRegister';
export const KoishiCommandDefinition = 'KoishiCommandDefinition';
export const KoishiCommandPutDef = 'KoishiCommandPutDef';
export const KoishiCommandInterceptorDef = 'KoishiCommandInterceptorDef';
export const KoishiServiceWireProperty = 'KoishiServiceWireProperty';
......@@ -24,12 +15,8 @@ export const KoishiServiceProvideSym = 'KoishiServiceProvideSym';
// metadata map
export interface MetadataArrayMap {
KoishiOnContextScope: OnContextFunction;
KoishiCommandDefinition: CommandDefinitionFun;
KoishiServiceProvideSym: keyof Context.Services;
KoishiCommandInterceptorDef: KoishiCommandInterceptorRegistration;
}
export interface MetadataMap {
KoishiDoRegister: DoRegisterConfig;
}
export interface MetadataMap {}
import { CustomDecorator, Inject } from '@nestjs/common';
import {
KOISHI_CONTEXT,
KoishiCommandDefinition,
KoishiCommandInterceptorDef,
KoishiCommandPutDef,
KoishiDoRegister,
KoishiOnContextScope,
KoishiServiceProvideSym,
KoishiServiceWireKeys,
KoishiServiceWireProperty,
MetadataArrayMap,
} from './koishi.constants';
import {
BeforeEventName,
CommandConfigExtended,
CommandDefinitionFun,
CommandPutConfig,
CommandPutConfigMap,
EventName,
GenerateMappingStruct,
KoishiCommandInterceptorRegistration,
MetadataArrayValue,
MetadataArrayValueMap,
MetadataGenericMap,
MetadataKey,
OnContextFunction,
} from './koishi.interfaces';
import {
Argv,
Command,
Context,
FieldCollector,
Selection,
Session,
} from 'koishi';
import { Context } from 'koishi';
import {
ContextScopeTypes,
getContextProvideToken,
......@@ -96,6 +77,8 @@ export const AppendMetadata = <K extends keyof MetadataArrayMap>(
): CustomDecorator<K> =>
TransformMetadata<K, MetadataArrayValueMap>(metadataKey, (arr) => {
const newArr = arr || [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
newArr.push(metadataValue);
return newArr;
});
......@@ -108,186 +91,10 @@ export const ConcatMetadata = <K extends keyof MetadataArrayValueMap>(
((arr || []) as any[]).concat(metadataValue),
);
// Register methods
export const UseMiddleware = (prepend?: boolean): MethodDecorator =>
SetMetadata(KoishiDoRegister, GenerateMappingStruct('middleware', prepend));
export const UseEvent = (name: EventName, prepend?: boolean): MethodDecorator =>
SetMetadata(
KoishiDoRegister,
GenerateMappingStruct('onevent', { name, prepend }),
);
export const BeforeEvent = (
name: BeforeEventName,
prepend?: boolean,
): MethodDecorator =>
SetMetadata(
KoishiDoRegister,
GenerateMappingStruct('beforeEvent', { name, prepend }),
);
export const UsePlugin = (): MethodDecorator =>
SetMetadata(KoishiDoRegister, GenerateMappingStruct('plugin'));
export function UseCommand<D extends string>(
def: D,
config?: CommandConfigExtended,
): MethodDecorator;
export function UseCommand<D extends string>(
def: D,
desc: string,
config?: CommandConfigExtended,
): MethodDecorator;
export function UseCommand(
def: string,
...args: [CommandConfigExtended?] | [string, CommandConfigExtended?]
): MethodDecorator {
const desc = typeof args[0] === 'string' ? (args.shift() as string) : '';
const config = args[0] as CommandConfigExtended;
return (obj, key: string, des) => {
const putOptions: CommandPutConfig<keyof CommandPutConfigMap>[] =
Reflect.getMetadata(KoishiCommandPutDef, obj.constructor, key) ||
undefined;
// console.log(Reflect.getMetadata('design:paramtypes', obj, key));
const metadataDec = SetMetadata(KoishiDoRegister, {
type: 'command',
data: {
def,
desc,
config,
putOptions,
},
});
return metadataDec(obj, key, des);
};
}
// Context scopes
export const OnContext = (
ctxFun: OnContextFunction,
): MethodDecorator & ClassDecorator =>
AppendMetadata(KoishiOnContextScope, ctxFun);
export const OnAnywhere = () => OnContext((ctx) => ctx.any());
export const OnNowhere = () => OnContext((ctx) => ctx.never());
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 & ClassDecorator =>
AppendMetadata(KoishiCommandDefinition, def);
export const CommandUse = <T extends Command, R extends any[]>(
callback: (command: Command, ...args: R) => T,
...args: R
) => CommandDef((cmd) => callback(cmd, ...args));
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: Command.Usage) =>
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));
export const CommandUserFields = (fields: FieldCollector<'user'>) =>
CommandDef((cmd) => cmd.userFields(fields));
export const CommandChannelFields = (fields: FieldCollector<'channel'>) =>
CommandDef((cmd) => cmd.channelFields(fields));
export const CommandBefore = (callback: Command.Action, append = false) =>
CommandDef((cmd) => cmd.before(callback, append));
export const CommandAction = (callback: Command.Action, prepend = false) =>
CommandDef((cmd) => cmd.action(callback, prepend));
// Command put config
function PutCommandParam<T extends keyof CommandPutConfigMap>(
type: T,
data?: CommandPutConfigMap[T],
): ParameterDecorator {
return (obj, key: string, index) => {
const objClass = obj.constructor;
const list: CommandPutConfig<T>[] =
Reflect.getMetadata(KoishiCommandPutDef, objClass, key) || [];
list[index] = GenerateMappingStruct(type, data);
Reflect.defineMetadata(KoishiCommandPutDef, list, objClass, key);
};
}
export const PutArgv = (field?: keyof Argv) =>
field ? PutCommandParam('argvField', field) : PutCommandParam('argv');
export const PutSession = (field?: keyof Session) =>
field ? PutCommandParam('sessionField', field) : PutArgv('session');
export const PutArg = (i: number) => PutCommandParam('arg', i);
export const PutArgs = () => PutCommandParam('args');
export const PutOption = (
name: string,
desc: string,
config: Argv.OptionConfig = {},
) => PutCommandParam('option', { name, desc, config });
export const PutUser = (field: FieldCollector<'user'>) =>
PutCommandParam('user', field);
export const PutChannel = (field: FieldCollector<'channel'>) =>
PutCommandParam('channel', field);
export const PutUserName = (useDatabase = true) =>
PutCommandParam('username', useDatabase);
// Export all koishi-decorator decorators
export const PutUserId = () => PutSession('userId');
export const PutGuildId = () => PutSession('guildId');
export const PutGuildName = () => PutSession('guildName');
export const PutChannelId = () => PutSession('channelId');
export const PutChannelName = () => PutSession('channelName');
export const PutSelfId = () => PutSession('selfId');
export const PutBot = () => PutSession('bot');
export const PutNext = () => PutArgv('next');
export * from 'koishi-decorators/dist/src/decorators';
export { PluginDef } from 'koishi-decorators';
// Service
......
import { ModuleMetadata, Provider, Type } from '@nestjs/common';
import {
App,
Argv,
Channel,
Command,
Context,
EventMap,
FieldCollector,
Modules,
Plugin,
Selection,
Session,
User,
} from 'koishi';
import { App, Channel, Command, User } from 'koishi';
import { MetadataArrayMap, MetadataMap } from './koishi.constants';
export interface ContextSelector {
select?: Selection;
useSelector?: OnContextFunction;
}
export type KoishiPluginOptions<T extends Plugin> = boolean | Plugin.Config<T>;
export interface KoishiModulePluginExact<T extends Plugin>
extends ContextSelector {
plugin: T;
options?: boolean | KoishiPluginOptions<T>;
}
export interface KoishiModulePluginName extends ContextSelector {
plugin: string;
options?: any;
}
export type KoishiModulePlugin<T extends Plugin = any> =
| KoishiModulePluginExact<T>
| KoishiModulePluginName;
export function PluginDef(name: string, options?: any, select?: Selection);
export function PluginDef<T extends Plugin>(
plugin: T,
options?: KoishiPluginOptions<T>,
select?: Selection,
);
export function PluginDef<T extends Plugin>(
plugin: T,
options?: KoishiPluginOptions<T>,
select?: Selection,
): KoishiModulePlugin<T> {
return { plugin, options, select };
}
import { ContextSelector, PluginDefinition } from 'koishi-decorators';
export interface KoishiModuleSelection extends ContextSelector {
module: Type<any>;
......@@ -63,7 +15,7 @@ export interface KoishiModuleTopOptions {
export interface KoishiModuleOptions
extends App.Config,
KoishiModuleTopOptions {
usePlugins?: KoishiModulePlugin<any>[];
usePlugins?: PluginDefinition<any>[];
loggerPrefix?: string;
loggerColor?: number;
moduleSelection?: KoishiModuleSelection[];
......@@ -86,91 +38,6 @@ export interface KoishiModuleAsyncOptions
extraProviders?: Provider[];
}
export interface CommonEventNameAndPrepend<T extends keyof any> {
name: T;
prepend?: boolean;
}
export type EventName = keyof EventMap;
export type EventNameAndPrepend = CommonEventNameAndPrepend<EventName>;
type OmitSubstring<
S extends string,
T extends string,
> = S extends `${infer L}${T}${infer R}` ? `${L}${R}` : never;
export type BeforeEventName = OmitSubstring<EventName & string, 'before-'>;
export type BeforeEventNameAndPrepend =
CommonEventNameAndPrepend<BeforeEventName>;
export type ContextFunction<T> = (ctx: Context) => T;
export type OnContextFunction = ContextFunction<Context>;
export interface DoRegisterConfigDataMap {
middleware: boolean; // prepend
onevent: EventNameAndPrepend;
beforeEvent: BeforeEventNameAndPrepend;
plugin: never;
command: CommandRegisterConfig;
}
export interface MappingStruct<
T extends Record<string | number | symbol, any>,
K extends keyof T,
> {
type: K;
data?: T[K];
}
export function GenerateMappingStruct<
T extends Record<string | number | symbol, any>,
K extends keyof T,
>(type: K, data?: T[K]): MappingStruct<T, K> {
return {
type,
data,
};
}
export type DoRegisterConfig<
K extends keyof DoRegisterConfigDataMap = keyof DoRegisterConfigDataMap,
> = MappingStruct<DoRegisterConfigDataMap, K>;
// Command stuff
export interface CommandRegisterConfig<D extends string = string> {
def: D;
desc?: string;
config?: CommandConfigExtended;
putOptions?: CommandPutConfig<keyof CommandPutConfigMap>[];
}
export interface CommandConfigExtended extends Command.Config {
empty?: boolean;
}
export interface CommandOptionConfig {
name: string;
desc: string;
config?: Argv.OptionConfig;
}
export interface CommandPutConfigMap {
args: never;
arg: number;
argv: never;
argvField: keyof Argv;
option: CommandOptionConfig;
user: FieldCollector<'user'>;
channel: FieldCollector<'channel'>;
username: boolean;
sessionField: keyof Session;
}
export type CommandPutConfig<
K extends keyof CommandPutConfigMap = keyof CommandPutConfigMap,
> = MappingStruct<CommandPutConfigMap, K>;
export type CommandDefinitionFun = (cmd: Command) => Command;
// metadata map
export type MetadataArrayValueMap = {
[K in keyof MetadataArrayMap]: MetadataArrayMap[K][];
......
import { Context } from 'koishi';
import { ContextSelector } from './koishi.interfaces';
export function applySelector(
ctx: Context,
selector: ContextSelector,
): Context {
if (!selector) {
return ctx;
}
let targetCtx = ctx;
if (selector.select) {
targetCtx = targetCtx.select(selector.select);
}
if (selector.useSelector) {
targetCtx = selector.useSelector(targetCtx) || targetCtx;
}
return targetCtx;
}
import { Test } from '@nestjs/testing';
import { KoishiModule } from '../src/koishi.module';
import { KoishiService } from '../src/koishi.service';
import { INestApplication } from '@nestjs/common';
import { INestApplication, Injectable } from '@nestjs/common';
import { KoishiWsAdapter } from '../src/koishi.ws-adapter';
import http from 'http';
import request from 'supertest';
import {
CommandUsage,
OnGuild,
OnPlatform,
PutOption,
UseCommand,
} from 'koishi-decorators';
import { Session } from 'koishi';
@OnPlatform('discord')
@Injectable()
class KoishiTestService {
@OnGuild('1111111111')
@UseCommand('echo', 'hi')
@CommandUsage('foo')
async onEcho(@PutOption('content', '-c <content:string>') content: string) {
return `bot: ${content}`;
}
}
describe('HttpServer', () => {
let app: INestApplication;
......@@ -17,6 +36,7 @@ describe('HttpServer', () => {
useWs: true,
}),
],
providers: [KoishiTestService],
}).compile();
app = moduleFixture.createNestApplication();
app.useWebSocketAdapter(new KoishiWsAdapter(app));
......@@ -37,6 +57,7 @@ describe('HttpServer', () => {
expect(koishiApp._httpServer).toBeInstanceOf(http.Server);
expect(app.getHttpServer()).toEqual(koishiApp._httpServer);
});
it('should response to koishi routes', () => {
koishiApp.router.get('/ping', (ctx) => {
ctx.status = 233;
......@@ -44,4 +65,33 @@ describe('HttpServer', () => {
});
return request(app.getHttpServer()).get('/ping').expect(233).expect('pong');
});
it('should register command', () => {
const command = koishiApp.command('echo');
expect(command._usage).toBe('foo');
expect(command._options.content.name).toBe('content');
expect(command.execute({ options: { content: 'hello' } })).resolves.toBe(
'bot: hello',
);
const correctSession = {
guildId: '1111111111',
platform: 'discord',
} as Session;
const wrongSession1 = {
guildId: '2222222222',
platform: 'discord',
} as Session;
const wrongSession2 = {
guildId: '1111111111',
platform: 'telegram',
} as Session;
const methodCtx = command.context;
expect(methodCtx.filter(correctSession)).toBe(true);
expect(methodCtx.filter(wrongSession1)).toBe(false);
expect(methodCtx.filter(wrongSession2)).toBe(false);
});
});
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