Commit f6ba250a authored by nanahira's avatar nanahira

Merge branch 'command-option'

parents 908560ea fbc1def7
Pipeline #6411 passed with stages
in 32 seconds
...@@ -238,7 +238,7 @@ export class AppService { ...@@ -238,7 +238,7 @@ export class AppService {
@CommandDescription('Echo command from decorators!') @CommandDescription('Echo command from decorators!')
@CommandUsage('Command usage') @CommandUsage('Command usage')
@CommandExample('Command example') @CommandExample('Command example')
testEchoCommand(argv: Argv, content: string) { testEchoCommand(@PutArgv() argv: Argv, @PutArg(0) content: string) {
return content; return content;
} }
} }
...@@ -282,7 +282,7 @@ export class AppService { ...@@ -282,7 +282,7 @@ export class AppService {
* `@UsePlugin()` 使用该方法注册插件。在 Koishi 实例注册时该方法会自动被调用。该方法需要返回插件定义,可以使用 `PluginDef(plugin, options, select)` 方法生成。 [参考](https://koishi.js.org/v4/guide/plugin/plugin.html#%E5%AE%89%E8%A3%85%E6%8F%92%E4%BB%B6) * `@UsePlugin()` 使用该方法注册插件。在 Koishi 实例注册时该方法会自动被调用。该方法需要返回插件定义,可以使用 `PluginDef(plugin, options, select)` 方法生成。 [参考](https://koishi.js.org/v4/guide/plugin/plugin.html#%E5%AE%89%E8%A3%85%E6%8F%92%E4%BB%B6)
* `@UseCommand(def: string, config?: Command.Config)` 注册指令。指令系统可以参考 [Koishi 文档](https://koishi.js.org/guide/command.html) 。指令回调参数位置和类型和 Koishi 指令一致。 * `@UseCommand(def: string, desc?: string, config?: Command.Config)` 注册指令。指令系统可以参考 [Koishi 文档](https://koishi.js.org/guide/command.html) 。指令回调参数位置和类型和 Koishi 指令一致。
#### 指令描述装饰器 #### 指令描述装饰器
...@@ -298,10 +298,29 @@ Koishi-Nest 使用一组装饰器进行描述指令的行为。这些装饰器 ...@@ -298,10 +298,29 @@ Koishi-Nest 使用一组装饰器进行描述指令的行为。这些装饰器
* `@CommandShortcut(def: string, config?: Command.Shortcut)` 指令快捷方式。等价于 `cmd.shortcut(def, config)` * `@CommandShortcut(def: string, config?: Command.Shortcut)` 指令快捷方式。等价于 `cmd.shortcut(def, config)`
* `@CommandOption(name: string, desc: string, config: Argv.OptionConfig = {})` 指令选项。等价于 `cmd.option(name, desc, config)`
* `@CommandDef((cmd: Command) => Command)` 手动定义指令信息,用于 Koishi-Nest 不支持的指令类型。 * `@CommandDef((cmd: Command) => Command)` 手动定义指令信息,用于 Koishi-Nest 不支持的指令类型。
#### 指令参数
指令参数也使用一组装饰器对指令参数进行注入。下列装饰器应对提供者方法参数进行操作。
* `@PutArgv()` 注入 `Argv` 对象。
* `@PutSession(field?: keyof Session)` 注入 `Session` 对象,或 `Session` 对象的指定字段。
* `@PutArg(index: number)` 注入指令的第 n 个参数。
* `@PutOption(name: string, desc: string, config: Argv.OptionConfig = {})` 给指令添加选项并注入到该参数。等价于 `cmd.option(name, desc, config)`
* `@PutUser(fields: string[])` 添加一部分字段用于观测,并将 User 对象注入到该参数。
* `@PutChannel(fields: string[])` 添加一部分字段用于观测,并将 Channel 对象注入到该参数。
关于 Koishi 的观察者概念详见 [Koishi 文档](https://koishi.js.org/v4/guide/database/observer.html#%E8%A7%82%E5%AF%9F%E8%80%85%E5%AF%B9%E8%B1%A1)
* `@PutUserName(useDatabase: boolean = true)` 注入当前用户的用户名。
* `useDatabase` 是否尝试从数据库获取用户名。
## 上下文 Service 交互 ## 上下文 Service 交互
您可以使用装饰器与 Koishi 的 Service 系统进行交互。 您可以使用装饰器与 Koishi 的 Service 系统进行交互。
...@@ -459,3 +478,13 @@ export class AppModule {} ...@@ -459,3 +478,13 @@ export class AppModule {}
* `plugin`: Koishi 插件。 * `plugin`: Koishi 插件。
* `options`: Koishi 插件配置。等同于 `ctx.plugin(plugin, options)` * `options`: Koishi 插件配置。等同于 `ctx.plugin(plugin, options)`
* 上下文选择器见本文 **上下文选择器** 部分。 * 上下文选择器见本文 **上下文选择器** 部分。
## 更新历史
### 1.3
* `@UseCommand` 现在定义和 Koishi 的指令定义,即 `ctx.command(name, desc, config)``ctx.command(name, config)` 一致了。
* 增加了 `@CommandUserFields` 对应 `cmd.userFields` 以及 `@CommandChannelFields` 对应 `cmd.channelFields`
* 增加了用于注入 Koishi 指令调用信息的提供者方法参数装饰器。详见 **指令参数** 部分。
\ No newline at end of file
{ {
"name": "koishi-nestjs", "name": "koishi-nestjs",
"version": "1.2.2", "version": "1.3.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "koishi-nestjs", "name": "koishi-nestjs",
"version": "1.2.2", "version": "1.3.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@koa/router": "^10.1.1", "@koa/router": "^10.1.1",
......
{ {
"name": "koishi-nestjs", "name": "koishi-nestjs",
"version": "1.2.3", "version": "1.3.0",
"description": "Koishi.js as Nest.js Module", "description": "Koishi.js as Nest.js Module",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
......
...@@ -2,6 +2,7 @@ import { CustomDecorator, Inject, SetMetadata } from '@nestjs/common'; ...@@ -2,6 +2,7 @@ import { CustomDecorator, Inject, SetMetadata } from '@nestjs/common';
import { import {
KOISHI_CONTEXT, KOISHI_CONTEXT,
KoishiCommandDefinition, KoishiCommandDefinition,
KoishiCommandPutDef,
KoishiDoRegister, KoishiDoRegister,
KoishiOnContextScope, KoishiOnContextScope,
KoishiServiceProvideSym, KoishiServiceProvideSym,
...@@ -10,14 +11,15 @@ import { ...@@ -10,14 +11,15 @@ import {
} from './utility/koishi.constants'; } from './utility/koishi.constants';
import { import {
CommandDefinitionFun, CommandDefinitionFun,
ContextFunction, CommandPutConfig,
CommandPutConfigMap,
DoRegisterConfig, DoRegisterConfig,
EventName, EventName,
EventNameAndPrepend, GenerateMappingStruct,
OnContextFunction, OnContextFunction,
Selection, Selection,
} from './koishi.interfaces'; } from './koishi.interfaces';
import { Argv, Command, Context } from 'koishi'; import { Argv, Command, Context, FieldCollector, Session } from 'koishi';
import { import {
ContextScopeTypes, ContextScopeTypes,
getContextProvideToken, getContextProvideToken,
...@@ -66,33 +68,55 @@ export const SetExtraMetadata = <K = string, V = any>( ...@@ -66,33 +68,55 @@ export const SetExtraMetadata = <K = string, V = any>(
// Register methods // Register methods
export const UseMiddleware = (prepend?: boolean): MethodDecorator => export const UseMiddleware = (prepend?: boolean): MethodDecorator =>
SetMetadata<string, DoRegisterConfig<boolean>>(KoishiDoRegister, { SetMetadata<string, DoRegisterConfig<'middleware'>>(
type: 'middleware', KoishiDoRegister,
data: prepend, GenerateMappingStruct('middleware', prepend),
}); );
export const UseEvent = (name: EventName, prepend?: boolean): MethodDecorator => export const UseEvent = (name: EventName, prepend?: boolean): MethodDecorator =>
SetMetadata<string, DoRegisterConfig<EventNameAndPrepend>>(KoishiDoRegister, { SetMetadata<string, DoRegisterConfig<'onevent'>>(
type: 'onevent', KoishiDoRegister,
data: { name, prepend }, GenerateMappingStruct('onevent', { name, prepend }),
}); );
export const UsePlugin = (): MethodDecorator => export const UsePlugin = (): MethodDecorator =>
SetMetadata<string, DoRegisterConfig>(KoishiDoRegister, { SetMetadata<string, DoRegisterConfig<'plugin'>>(
type: 'plugin', KoishiDoRegister,
}); GenerateMappingStruct('plugin'),
);
export function UseCommand<D extends string>( export function UseCommand<D extends string>(
def: D, def: D,
config?: Command.Config, config?: Command.Config,
): MethodDecorator;
export function UseCommand<D extends string>(
def: D,
desc: string,
config?: Command.Config,
): MethodDecorator;
export function UseCommand(
def: string,
...args: [Command.Config?] | [string, Command.Config?]
): MethodDecorator { ): MethodDecorator {
return SetMetadata< const desc = typeof args[0] === 'string' ? (args.shift() as string) : '';
string, const config = args[0] as Command.Config;
DoRegisterConfig< return (obj, key: string, des) => {
ContextFunction<Command<never, never, Argv.ArgumentType<D>>> const putOptions: CommandPutConfig<keyof CommandPutConfigMap>[] =
> Reflect.getMetadata(KoishiCommandPutDef, obj.constructor, key) ||
>(KoishiDoRegister, { undefined;
// console.log(Reflect.getMetadata('design:paramtypes', obj, key));
const metadataDec = SetMetadata<string, DoRegisterConfig<'command'>>(
KoishiDoRegister,
{
type: 'command', type: 'command',
data: (ctx) => ctx.command(def, config), data: {
}); def,
desc,
config,
putOptions,
},
},
);
return metadataDec(obj, key, des);
};
} }
// Context scopes // Context scopes
...@@ -154,6 +178,54 @@ export const CommandOption = ( ...@@ -154,6 +178,54 @@ export const CommandOption = (
config: Argv.OptionConfig = {}, config: Argv.OptionConfig = {},
) => CommandDef((cmd) => cmd.option(name, desc, config)); ) => 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));
// 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 = () => PutCommandParam('argv');
export const PutSession = (field?: keyof Session) =>
field ? PutCommandParam('sessionField', field) : PutCommandParam('session');
export const PutArg = (i: number) => PutCommandParam('arg', i);
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 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');
// Service // Service
export function WireContextService(name?: string): PropertyDecorator { export function WireContextService(name?: string): PropertyDecorator {
......
import { ModuleMetadata, Provider, Type } from '@nestjs/common'; import { ModuleMetadata, Provider, Type } from '@nestjs/common';
import { App, Command, Context, EventMap, MaybeArray, Plugin } from 'koishi'; import {
App,
Argv,
Command,
Context,
EventMap,
FieldCollector,
MaybeArray,
Plugin,
Session,
} from 'koishi';
const selectors = [ const selectors = [
'user', 'user',
...@@ -74,18 +84,66 @@ export interface EventNameAndPrepend { ...@@ -74,18 +84,66 @@ export interface EventNameAndPrepend {
name: EventName; name: EventName;
prepend?: boolean; prepend?: boolean;
} }
export type Promisify<T> = T extends Promise<unknown> ? T : Promise<T>;
export type ContextFunction<T> = (ctx: Context) => T; export type ContextFunction<T> = (ctx: Context) => T;
export type OnContextFunction = ContextFunction<Context>; export type OnContextFunction = ContextFunction<Context>;
export type DoRegisterType = 'middleware' | 'command' | 'onevent' | 'plugin'; export interface DoRegisterConfigDataMap {
export interface DoRegisterConfig<T = any> { middleware: boolean; // prepend
type: DoRegisterType; onevent: EventNameAndPrepend;
data?: T; plugin: never;
command: CommandRegisterConfig;
} }
export interface CommandConfigWIthDescription extends Command.Config { 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; desc?: string;
config?: Command.Config;
putOptions?: CommandPutConfig<keyof CommandPutConfigMap>[];
} }
export interface CommandOptionConfig {
name: string;
desc: string;
config?: Argv.OptionConfig;
}
export interface CommandPutConfigMap {
arg: number;
argv: never;
session: never;
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; export type CommandDefinitionFun = (cmd: Command) => Command;
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
ModulesContainer, ModulesContainer,
Reflector, Reflector,
} from '@nestjs/core'; } from '@nestjs/core';
import { Argv, Command, Context } from 'koishi'; import { Argv, Command, Context, User } from 'koishi';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { import {
KoishiCommandDefinition, KoishiCommandDefinition,
...@@ -19,10 +19,9 @@ import { ...@@ -19,10 +19,9 @@ import {
} from '../utility/koishi.constants'; } from '../utility/koishi.constants';
import { import {
CommandDefinitionFun, CommandDefinitionFun,
ContextFunction, CommandPutConfig,
DoRegisterConfig, DoRegisterConfig,
EventName, EventName,
EventNameAndPrepend,
KoishiModulePlugin, KoishiModulePlugin,
OnContextFunction, OnContextFunction,
} from '../koishi.interfaces'; } from '../koishi.interfaces';
...@@ -50,6 +49,79 @@ export class KoishiMetascanService { ...@@ -50,6 +49,79 @@ export class KoishiMetascanService {
} }
} }
private preRegisterCommandActionArg(config: CommandPutConfig, cmd: Command) {
if (!config) {
return;
}
switch (config.type) {
case 'option':
const { data: optionData } = config as CommandPutConfig<'option'>;
cmd.option(optionData.name, optionData.desc, optionData.config);
break;
case 'user':
const { data: userFields } = config as CommandPutConfig<'user'>;
if (userFields) {
cmd.userFields(userFields);
}
break;
case 'channel':
const { data: channelFields } = config as CommandPutConfig<'channel'>;
if (channelFields) {
cmd.channelFields(channelFields);
}
break;
case 'username':
const { data: useDatabase } = config as CommandPutConfig<'username'>;
if (useDatabase) {
cmd.userFields(['name']);
}
break;
default:
break;
}
return;
}
private getCommandActionArg(
config: CommandPutConfig,
argv: Argv,
args: any[],
) {
if (!config) {
return;
}
switch (config.type) {
case 'arg':
const { data: index } = config as CommandPutConfig<'arg'>;
return args[index];
case 'argv':
return argv;
case 'session':
return argv.session;
case 'option':
const { data: optionData } = config as CommandPutConfig<'option'>;
return argv.options[optionData.name];
case 'user':
return argv.session.user;
case 'channel':
return argv.session.channel;
case 'username':
const { data: useDatabase } = config as CommandPutConfig<'username'>;
if (useDatabase) {
const user = argv.session.user as User.Observed<'name'>;
if (user?.name) {
return user?.name;
}
}
return argv.session.author?.nickname || argv.session.author?.username;
case 'sessionField':
const { data: field } = config as CommandPutConfig<'sessionField'>;
return argv.session[field];
default:
return;
}
}
private async handleInstanceRegistration( private async handleInstanceRegistration(
ctx: Context, ctx: Context,
instance: Record<string, any>, instance: Record<string, any>,
...@@ -84,15 +156,14 @@ export class KoishiMetascanService { ...@@ -84,15 +156,14 @@ export class KoishiMetascanService {
} }
switch (regData.type) { switch (regData.type) {
case 'middleware': case 'middleware':
const { data: midPrepend } = regData as DoRegisterConfig<'middleware'>;
baseContext.middleware( baseContext.middleware(
(session, next) => methodFun.call(instance, session, next), (session, next) => methodFun.call(instance, session, next),
regData.data, midPrepend,
); );
break; break;
case 'onevent': case 'onevent':
const { const { data: eventData } = regData as DoRegisterConfig<'onevent'>;
data: eventData,
} = regData as DoRegisterConfig<EventNameAndPrepend>;
const eventName = eventData.name; const eventName = eventData.name;
baseContext.on(eventData.name, (...args: any[]) => baseContext.on(eventData.name, (...args: any[]) =>
methodFun.call(instance, ...args), methodFun.call(instance, ...args),
...@@ -116,10 +187,12 @@ export class KoishiMetascanService { ...@@ -116,10 +187,12 @@ export class KoishiMetascanService {
pluginCtx.plugin(pluginDesc.plugin, pluginDesc.options); pluginCtx.plugin(pluginDesc.plugin, pluginDesc.options);
break; break;
case 'command': case 'command':
const { data: commandData } = regData as DoRegisterConfig< const { data: commandData } = regData as DoRegisterConfig<'command'>;
ContextFunction<Command> let command = baseContext.command(
>; commandData.def,
let command = commandData(baseContext); commandData.desc,
commandData.config,
);
const commandDefs: CommandDefinitionFun[] = this.reflector.get( const commandDefs: CommandDefinitionFun[] = this.reflector.get(
KoishiCommandDefinition, KoishiCommandDefinition,
methodFun, methodFun,
...@@ -129,9 +202,21 @@ export class KoishiMetascanService { ...@@ -129,9 +202,21 @@ export class KoishiMetascanService {
command = commandDef(command) || command; command = commandDef(command) || command;
} }
} }
if (!commandData.putOptions) {
command.action((argv: Argv, ...args: any[]) => command.action((argv: Argv, ...args: any[]) =>
methodFun.call(instance, argv, ...args), methodFun.call(instance, argv, ...args),
); );
} else {
for (const _optionToRegister of commandData.putOptions) {
this.preRegisterCommandActionArg(_optionToRegister, command);
}
command.action((argv: Argv, ...args: any[]) => {
const params = commandData.putOptions.map((o) =>
this.getCommandActionArg(o, argv, args),
);
return methodFun.call(instance, ...params);
});
}
break; break;
default: default:
throw new Error(`Unknown operaton type ${regData.type}`); throw new Error(`Unknown operaton type ${regData.type}`);
......
...@@ -6,6 +6,7 @@ export const KOISHI_CONTEXT = 'KOISHI_CONTEXT'; ...@@ -6,6 +6,7 @@ export const KOISHI_CONTEXT = 'KOISHI_CONTEXT';
export const KoishiOnContextScope = 'KoishiOnContextScope'; export const KoishiOnContextScope = 'KoishiOnContextScope';
export const KoishiDoRegister = 'KoishiDoRegister'; export const KoishiDoRegister = 'KoishiDoRegister';
export const KoishiCommandDefinition = 'KoishiCommandDefinition'; export const KoishiCommandDefinition = 'KoishiCommandDefinition';
export const KoishiCommandPutDef = Symbol('KoishiCommandPutDef');
export const KoishiServiceWireProperty = Symbol('KoishiServiceWireProperty'); export const KoishiServiceWireProperty = Symbol('KoishiServiceWireProperty');
export const KoishiServiceWireKeys = Symbol('KoishiServiceWireKeys'); export const KoishiServiceWireKeys = Symbol('KoishiServiceWireKeys');
......
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