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 {
@CommandDescription('Echo command from decorators!')
@CommandUsage('Command usage')
@CommandExample('Command example')
testEchoCommand(argv: Argv, content: string) {
testEchoCommand(@PutArgv() argv: Argv, @PutArg(0) content: string) {
return content;
}
}
......@@ -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)
* `@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 使用一组装饰器进行描述指令的行为。这些装饰器
* `@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 不支持的指令类型。
#### 指令参数
指令参数也使用一组装饰器对指令参数进行注入。下列装饰器应对提供者方法参数进行操作。
* `@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 交互
您可以使用装饰器与 Koishi 的 Service 系统进行交互。
......@@ -459,3 +478,13 @@ export class AppModule {}
* `plugin`: Koishi 插件。
* `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",
"version": "1.2.2",
"version": "1.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "koishi-nestjs",
"version": "1.2.2",
"version": "1.3.0",
"license": "MIT",
"dependencies": {
"@koa/router": "^10.1.1",
......
{
"name": "koishi-nestjs",
"version": "1.2.3",
"version": "1.3.0",
"description": "Koishi.js as Nest.js Module",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
......
......@@ -2,6 +2,7 @@ import { CustomDecorator, Inject, SetMetadata } from '@nestjs/common';
import {
KOISHI_CONTEXT,
KoishiCommandDefinition,
KoishiCommandPutDef,
KoishiDoRegister,
KoishiOnContextScope,
KoishiServiceProvideSym,
......@@ -10,14 +11,15 @@ import {
} from './utility/koishi.constants';
import {
CommandDefinitionFun,
ContextFunction,
CommandPutConfig,
CommandPutConfigMap,
DoRegisterConfig,
EventName,
EventNameAndPrepend,
GenerateMappingStruct,
OnContextFunction,
Selection,
} from './koishi.interfaces';
import { Argv, Command, Context } from 'koishi';
import { Argv, Command, Context, FieldCollector, Session } from 'koishi';
import {
ContextScopeTypes,
getContextProvideToken,
......@@ -66,33 +68,55 @@ export const SetExtraMetadata = <K = string, V = any>(
// Register methods
export const UseMiddleware = (prepend?: boolean): MethodDecorator =>
SetMetadata<string, DoRegisterConfig<boolean>>(KoishiDoRegister, {
type: 'middleware',
data: prepend,
});
SetMetadata<string, DoRegisterConfig<'middleware'>>(
KoishiDoRegister,
GenerateMappingStruct('middleware', prepend),
);
export const UseEvent = (name: EventName, prepend?: boolean): MethodDecorator =>
SetMetadata<string, DoRegisterConfig<EventNameAndPrepend>>(KoishiDoRegister, {
type: 'onevent',
data: { name, prepend },
});
SetMetadata<string, DoRegisterConfig<'onevent'>>(
KoishiDoRegister,
GenerateMappingStruct('onevent', { name, prepend }),
);
export const UsePlugin = (): MethodDecorator =>
SetMetadata<string, DoRegisterConfig>(KoishiDoRegister, {
type: 'plugin',
});
SetMetadata<string, DoRegisterConfig<'plugin'>>(
KoishiDoRegister,
GenerateMappingStruct('plugin'),
);
export function UseCommand<D extends string>(
def: D,
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 {
return SetMetadata<
string,
DoRegisterConfig<
ContextFunction<Command<never, never, Argv.ArgumentType<D>>>
>
>(KoishiDoRegister, {
const desc = typeof args[0] === 'string' ? (args.shift() as string) : '';
const config = args[0] as Command.Config;
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<string, DoRegisterConfig<'command'>>(
KoishiDoRegister,
{
type: 'command',
data: (ctx) => ctx.command(def, config),
});
data: {
def,
desc,
config,
putOptions,
},
},
);
return metadataDec(obj, key, des);
};
}
// Context scopes
......@@ -154,6 +178,54 @@ export const CommandOption = (
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));
// 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
export function WireContextService(name?: string): PropertyDecorator {
......
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 = [
'user',
......@@ -74,18 +84,66 @@ 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 DoRegisterConfigDataMap {
middleware: boolean; // prepend
onevent: EventNameAndPrepend;
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;
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;
......@@ -7,7 +7,7 @@ import {
ModulesContainer,
Reflector,
} 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 {
KoishiCommandDefinition,
......@@ -19,10 +19,9 @@ import {
} from '../utility/koishi.constants';
import {
CommandDefinitionFun,
ContextFunction,
CommandPutConfig,
DoRegisterConfig,
EventName,
EventNameAndPrepend,
KoishiModulePlugin,
OnContextFunction,
} from '../koishi.interfaces';
......@@ -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(
ctx: Context,
instance: Record<string, any>,
......@@ -84,15 +156,14 @@ export class KoishiMetascanService {
}
switch (regData.type) {
case 'middleware':
const { data: midPrepend } = regData as DoRegisterConfig<'middleware'>;
baseContext.middleware(
(session, next) => methodFun.call(instance, session, next),
regData.data,
midPrepend,
);
break;
case 'onevent':
const {
data: eventData,
} = regData as DoRegisterConfig<EventNameAndPrepend>;
const { data: eventData } = regData as DoRegisterConfig<'onevent'>;
const eventName = eventData.name;
baseContext.on(eventData.name, (...args: any[]) =>
methodFun.call(instance, ...args),
......@@ -116,10 +187,12 @@ export class KoishiMetascanService {
pluginCtx.plugin(pluginDesc.plugin, pluginDesc.options);
break;
case 'command':
const { data: commandData } = regData as DoRegisterConfig<
ContextFunction<Command>
>;
let command = commandData(baseContext);
const { data: commandData } = regData as DoRegisterConfig<'command'>;
let command = baseContext.command(
commandData.def,
commandData.desc,
commandData.config,
);
const commandDefs: CommandDefinitionFun[] = this.reflector.get(
KoishiCommandDefinition,
methodFun,
......@@ -129,9 +202,21 @@ export class KoishiMetascanService {
command = commandDef(command) || command;
}
}
if (!commandData.putOptions) {
command.action((argv: Argv, ...args: any[]) =>
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;
default:
throw new Error(`Unknown operaton type ${regData.type}`);
......
......@@ -6,6 +6,7 @@ export const KOISHI_CONTEXT = 'KOISHI_CONTEXT';
export const KoishiOnContextScope = 'KoishiOnContextScope';
export const KoishiDoRegister = 'KoishiDoRegister';
export const KoishiCommandDefinition = 'KoishiCommandDefinition';
export const KoishiCommandPutDef = Symbol('KoishiCommandPutDef');
export const KoishiServiceWireProperty = Symbol('KoishiServiceWireProperty');
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