Commit a524dd8d authored by nanahira's avatar nanahira

adapt Koishi 4.8 and migrate to satori-decorators

parent 40c85300
import 'reflect-metadata';
export * from './src/register';
export * from './src/decorators';
export * from 'cordis-decorators';
export * from './src/cosmotype-exports';
export * from './src/base-plugin';
export * from './src/plugin-operators';
export * from './src/plugin-factory';
export * from './src/def/interfaces';
export * from 'schemastery-gen';
export * from 'koishi-decorators/dist/src/def/select';
export * from './src/decorators';
This diff is collapsed.
......@@ -44,18 +44,13 @@
"rimraf": "^3.0.2",
"supertest": "^6.1.6",
"ts-jest": "^27.0.7",
"typescript": "^4.6.4",
"ws": "^8.2.3"
"typescript": "^4.6.4"
},
"dependencies": {
"@types/koa": "^2.13.4",
"@types/koa__router": "^8.0.11",
"koishi-decorators": "^2.1.7",
"lodash": "^4.17.21",
"minato-decorators": "^2.0.7",
"reflect-metadata": "^0.1.13",
"schemastery-gen": "^3.1.14",
"typed-reflector": "^1.0.10"
"minato-decorators": "^2.0.8",
"rxjs": "^7.5.6",
"satori-decorators": "^1.0.7"
},
"jest": {
"moduleFileExtensions": [
......@@ -75,7 +70,7 @@
"testEnvironment": "node"
},
"peerDependencies": {
"koishi": "^4.7.5",
"koishi": "^4.8.1",
"schemastery": "^3.4.3"
}
}
import { Context } from 'koishi';
import { ClassType } from 'schemastery-gen';
import { Fork, InjectConfig, PluginSchema } from './decorators';
import { ExactClassPluginConfig, PluginClass, TypeFromClass } from './def';
import { LifecycleEvents } from './register';
export type PartialDeep<T> = T extends
| string
| number
| bigint
| boolean
| null
| undefined
| symbol
| Date
// eslint-disable-next-line @typescript-eslint/ban-types
| Function
? T | undefined
: // Arrays, Sets and Maps and their readonly counterparts have their items made
// deeply partial, but their own instances are left untouched
T extends Array<infer ArrayType>
? Array<PartialDeep<ArrayType>>
: T extends ReadonlyArray<infer ArrayType>
? ReadonlyArray<ArrayType>
: T extends Set<infer SetType>
? Set<PartialDeep<SetType>>
: T extends ReadonlySet<infer SetType>
? ReadonlySet<SetType>
: T extends Map<infer KeyType, infer ValueType>
? Map<PartialDeep<KeyType>, PartialDeep<ValueType>>
: T extends ReadonlyMap<infer KeyType, infer ValueType>
? ReadonlyMap<PartialDeep<KeyType>, PartialDeep<ValueType>>
: // ...and finally, all other objects.
{
[K in keyof T]?: PartialDeep<T[K]>;
};
export class BasePlugin<C, PC = PartialDeep<C>> {
constructor(public ctx: Context, config: PC) {}
@InjectConfig()
config: C;
}
export function StarterPlugin<C>(config: ClassType<C>) {
const plugin = class StarterPluginBase extends BasePlugin<C> {};
PluginSchema(config)(plugin);
return plugin;
}
export function ParentPlugin<PC extends PluginClass>(child: PC) {
const plugin = class ParentPluginBase
extends StarterPlugin<ExactClassPluginConfig<PC>>(child['Config'])
implements LifecycleEvents
{
onFork(instance: TypeFromClass<PC>): void | Promise<void> {}
onForkDisconnect(instance: TypeFromClass<PC>): void | Promise<void> {}
};
Fork(child)(plugin);
return plugin;
}
export function ParentPluginSet<PC extends PluginClass>(child: PC) {
return class ParentPluginSet extends ParentPlugin(child) {
instances = new Set<TypeFromClass<PC>>();
onFork(instance: TypeFromClass<PC>) {
this.instances.add(instance);
}
onForkDisconnect(instance: TypeFromClass<PC>) {
this.instances.delete(instance);
}
};
}
export function ParentPluginMap<PC extends PluginClass, R>(
child: PC,
classifyFunction: (instance: TypeFromClass<PC>) => R,
) {
return class ParentPluginMapBase extends ParentPlugin(child) {
instances = new Map<R, TypeFromClass<PC>>();
onFork(instance: TypeFromClass<PC>) {
this.instances.set(classifyFunction(instance), instance);
}
onForkDisconnect(instance: TypeFromClass<PC>) {
this.instances.delete(classifyFunction(instance));
}
};
}
import { ModelDecorators } from 'minato-decorators';
import { Tables } from 'koishi';
const decorators = new ModelDecorators<Tables>();
export const DefineModel = decorators.DefineModel;
export const ModelField = decorators.ModelField;
export const Primary = decorators.Primary;
export const PrimaryGenerated = decorators.PrimaryGenerated;
export const Foreign = decorators.Foreign;
export const Unique = decorators.Unique;
export const ChildModel = decorators.ChildModel;
export const {
DefineModel,
ModelField,
Primary,
PrimaryGenerated,
Foreign,
Unique,
ChildModel,
} = new ModelDecorators<Tables>();
import 'reflect-metadata';
import { App, Context, Flatten, Keys, Schema, Tables } from 'koishi';
import { Metadata } from './meta/metadata.decorators';
import {
Condition,
KoishiAddUsingList,
KoishiServiceInjectSym,
KoishiServiceInjectSymKeys,
KoishiServiceProvideSym,
KoishiSystemInjectSym,
KoishiSystemInjectSymKeys,
PluginClass,
ProvideOptions,
ServiceName,
SystemInjectFun,
} from './def';
import {
CallbackLayer,
TopLevelAction,
Selection,
selectContext,
} from 'koishi-decorators';
import { ModelClassType, ModelRegistrar } from 'minato-decorators';
import { ClassType } from 'schemastery-gen';
// Export all koishi-decorator decorators
export * from 'koishi-decorators/dist/src/decorators';
export * from 'koishi-decorators/dist/src/http-decorators';
export { PluginDef } from 'koishi-decorators';
// Service API
export function Inject(
name?: ServiceName,
addUsing?: boolean,
): PropertyDecorator;
export function Inject(addUsing?: boolean): PropertyDecorator;
export function Inject(
...args: [(ServiceName | boolean)?, boolean?]
): PropertyDecorator {
let name: ServiceName;
let addUsing = false;
if (args.length === 1) {
if (typeof args[0] === 'boolean') {
addUsing = args[0];
} else {
name = args[0];
}
} else if (args.length >= 2) {
name = args[0] as ServiceName;
addUsing = args[1];
}
return (obj, key) => {
if (!name) {
const functionType = Reflect.getMetadata('design:type', obj, key);
let dec: PropertyDecorator;
if (functionType === Context) {
dec = InjectContext();
} else if (functionType === App) {
dec = InjectApp();
}
if (dec) {
return dec(obj, key);
}
}
const serviceName = name || (key as ServiceName);
if (addUsing) {
Metadata.appendUnique(KoishiAddUsingList, serviceName)(obj.constructor);
}
const dec = Metadata.set(
KoishiServiceInjectSym,
serviceName,
KoishiServiceInjectSymKeys,
);
return dec(obj, key);
};
}
export function Provide(
name: ServiceName,
options?: ProvideOptions,
): ClassDecorator {
Context.service(name, options);
return Metadata.append(KoishiServiceProvideSym, {
...options,
serviceName: name,
});
}
const InjectSystem = (fun: SystemInjectFun) =>
Metadata.set(KoishiSystemInjectSym, fun, KoishiSystemInjectSymKeys);
export const InjectContext = (select?: Selection) =>
InjectSystem((obj) => {
if (select) {
return selectContext(obj.__ctx, select);
} else {
return obj.__ctx;
}
});
export const InjectApp = () => InjectSystem((obj) => obj.__ctx.app);
export const InjectConfig = () => InjectSystem((obj) => obj.__config);
export const InjectLogger = (name?: string) =>
InjectSystem((obj) => obj.__ctx.logger(name || obj.constructor.name));
export const InjectParent = () => InjectSystem((obj) => obj.__ctx.__parent);
export const Caller = () =>
InjectSystem((obj) => {
const targetCtx: Context = obj[Context.current] || obj.__ctx;
return targetCtx;
});
export function UsingService(
...services: ServiceName[]
): ClassDecorator & MethodDecorator {
return (obj, key?, des?) => {
for (const service of services) {
if (!key) {
// fallback to KoishiAddUsingList
Metadata.appendUnique(KoishiAddUsingList, service)(obj);
} else {
const dec = CallbackLayer((ctx, cb) => {
ctx.plugin({
name: `${ctx.state.runtime.uid}_${key.toString()}`,
using: services,
apply: cb,
});
});
dec(obj, key, des);
}
}
};
}
export const PluginSchema = (schema: Schema | ClassType<any>) =>
Metadata.set('KoishiPredefineSchema', schema);
export const PluginName = (name: string) =>
Metadata.set('KoishiPredefineName', name);
export const If = <T>(
func: Condition<boolean, T, [Record<string, any>]>,
): MethodDecorator =>
Metadata.append('KoishiControl', { type: 'if', condition: func });
export const For = <T>(
func: Condition<Iterable<Record<string, any>>, T, [Record<string, any>]>,
): MethodDecorator =>
Metadata.append('KoishiControl', { type: 'for', condition: func });
export const UseModel = (...models: ModelClassType[]): ClassDecorator =>
TopLevelAction((ctx) => {
const registrar = new ModelRegistrar(ctx.model);
models.forEach((m) => registrar.registerModel(m));
});
export const MixinModel = <K extends Keys<Tables>>(
tableName: K,
classDict: {
[F in Keys<Tables[K]>]?: ModelClassType<Flatten<Tables[K][F]>>;
},
): ClassDecorator =>
TopLevelAction((ctx) => {
const registrar = new ModelRegistrar(ctx.model);
registrar.mixinModel(tableName, classDict);
});
export const Fork = (forkPlugin: PluginClass) =>
Metadata.set('KoishiFork', forkPlugin);
export const Reusable = (value = true) => Metadata.set('KoishiReusable', value);
import { koishiRegistrar } from '../registrar';
import {
CommandConfigExtended,
CommandLocaleDef,
CommandOptionConfigWithDescription,
CommandReturnType,
} from '../def';
import {
adaptLocaleDict,
applyOptionToCommand,
registerTemplate,
} from '../utility/utility';
import { Argv, Command, Dict, FieldCollector, Session, User } from 'koishi';
import { applyNativeTypeToArg } from '../utility/native-type-mapping';
import { TypedMethodDecorator } from 'satori-decorators';
export * from 'satori-decorators/dist/src/decorators/common';
const methodDecorators = koishiRegistrar.methodDecorators();
export const { UseEvent, UseBeforeEvent, UseMiddleware } = methodDecorators;
export function UseCommand<D extends string>(
def: D,
config?: CommandConfigExtended,
): TypedMethodDecorator<(...args: any[]) => CommandReturnType>;
export function UseCommand<D extends string>(
def: D,
desc: string,
config?: CommandConfigExtended,
): TypedMethodDecorator<(...args: any[]) => CommandReturnType>;
export function UseCommand(
...args:
| [string, CommandConfigExtended?]
| [string, string, CommandConfigExtended?]
): TypedMethodDecorator<(...args: any[]) => CommandReturnType> {
return methodDecorators.UseCommand(...args);
}
export const CommandLocale = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, locale: string, def: CommandLocaleDef) => {
ctx.i18n.define(locale, `commands.${cmd.name}`, def);
return cmd;
},
);
export const CommandDescription = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, desc: string | Dict<string>) => {
for (const localData of Object.entries(adaptLocaleDict(desc))) {
const [locale, text] = localData;
ctx.i18n.define(locale, `commands.${cmd.name}.description`, text);
}
return cmd;
},
);
export const CommandAlias = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, ...names: string[]) => cmd.alias(...names),
);
export const CommandShortcut = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, name: string | RegExp, config: Command.Shortcut = {}) =>
cmd.shortcut(name, config),
);
export const CommandUsage = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, text: Command.Usage) => cmd.usage(text),
);
export const CommandExample = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, text: string) => cmd.example(text),
);
export const CommandOption = koishiRegistrar.decorateCommandTransformer(
(
ctx,
cmd,
name: string,
desc: string,
config: CommandOptionConfigWithDescription = {},
) => applyOptionToCommand(ctx, cmd, { name, desc, config }),
);
export const CommandUserFields = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, fields: FieldCollector<'user'>) => cmd.userFields(fields),
);
export const CommandChannelFields = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, fields: FieldCollector<'channel'>) => cmd.channelFields(fields),
);
export const CommandBefore = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, callback: Command.Action, append = false) =>
cmd.before(callback, append),
);
export const CommandAction = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, callback: Command.Action, prepend = false) =>
cmd.action(callback, prepend),
);
export const CommandTemplate = koishiRegistrar.decorateCommandTransformer(
(ctx, cmd, name: string, text: string | Dict<string>) => {
registerTemplate({ name, text: adaptLocaleDict(text) }, ctx, cmd);
return cmd;
},
);
export const PutValue = koishiRegistrar.decorateCommandPut((data, v: any) => v);
export const PutArgv = koishiRegistrar.decorateCommandPut(
(data, field?: keyof Argv) => (field ? data.argv[field] : data.argv),
);
export const PutSession = koishiRegistrar.decorateCommandPut(
(data, field?: keyof Session) =>
field ? data.argv.session[field] : data.argv.session,
);
export const PutContext = koishiRegistrar.decorateCommandPut(
(data) => data.ctx,
);
export const PutCommand = koishiRegistrar.decorateCommandPut(
(data) => data.command,
);
export const PutArg = koishiRegistrar.decorateCommandPut(
(data, index: number, decl?: Argv.Declaration) => data.args[index],
(data, index, decl) => {
const cmd = data.command;
let arg = cmd._arguments[index];
if (!arg) {
arg = {};
cmd._arguments[index] = arg;
}
applyNativeTypeToArg(arg, data.nativeType);
if (decl) {
Object.assign(arg, decl);
}
},
);
export const PutArgs = koishiRegistrar.decorateCommandPut((data) => data.args);
export const PutOption = koishiRegistrar.decorateCommandPut(
(
data,
name: string,
desc: string,
config?: CommandOptionConfigWithDescription,
) => data.argv.options[name],
(data, name, desc, config) =>
applyOptionToCommand(
data.ctx,
data.command,
{
name,
desc,
config: config || {},
},
data.nativeType,
),
);
export const PutUser = koishiRegistrar.decorateCommandPut(
(data, field: FieldCollector<'user'>) => data.argv.session.user,
(data, field) => data.command.userFields(field),
);
export const PutChannel = koishiRegistrar.decorateCommandPut(
(data, field: FieldCollector<'channel'>) => data.argv.session.channel,
(data, field) => data.command.channelFields(field),
);
export const PutGuild = koishiRegistrar.decorateCommandPut(
(data, field: FieldCollector<'channel'>) => data.argv.session.guild,
(data, field) => data.command.channelFields(field),
);
export const PutUserName = koishiRegistrar.decorateCommandPut(
(data, useDatabase?: boolean) => {
const { argv } = data;
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 ||
argv.session.userId
);
},
(data, useDatabase) => {
if (useDatabase !== false) {
data.command.userFields(['name']);
}
},
);
export const PutUserId = koishiRegistrar.decorateCommandPut(
(data) => data.argv.session.userId,
);
export const PutChannelName = koishiRegistrar.decorateCommandPut(
(data) => data.argv.session.channelName,
);
export const PutChannelId = koishiRegistrar.decorateCommandPut(
(data) => data.argv.session.channelId,
);
export const PutGuildName = koishiRegistrar.decorateCommandPut(
(data) => data.argv.session.guildName,
);
export const PutGuildId = koishiRegistrar.decorateCommandPut(
(data) => data.argv.session.guildId,
);
export const PutBot = koishiRegistrar.decorateCommandPut(
(data) => data.argv.session.bot,
);
export const PutSelfId = koishiRegistrar.decorateCommandPut(
(data) => data.argv.session.selfId,
);
export const PutNext = koishiRegistrar.decorateCommandPut(
(data) => data.argv.next,
);
export const PutRenderer = koishiRegistrar.decorateCommandPut(
(data, path?: string) =>
path
? // eslint-disable-next-line @typescript-eslint/ban-types
(params: object) => data.argv.session.text(path, params)
: // eslint-disable-next-line @typescript-eslint/ban-types
(path: string, params: object) => data.argv.session.text(path, params),
);
export const PutCommonRenderer = PutRenderer;
export const PutTemplate = koishiRegistrar.decorateCommandPut(
// eslint-disable-next-line @typescript-eslint/ban-types
(data, name: string, text: string | Dict<string>) => (params: object) =>
data.argv.session.text(`.${name}`, params),
(data, name, text) =>
registerTemplate(
{
name,
text: adaptLocaleDict(text),
},
data.ctx,
data.command,
),
);
export const PutObject = koishiRegistrar.decorateCommandPut(
(data) => {
const targetClass = data.nativeType as { new (): any };
if (!targetClass) {
return;
}
const instance = new targetClass();
const keys = koishiRegistrar.reflector.getArray(
'KoishiCommandPutObjectKeys',
instance,
);
for (const key of keys) {
const meta = koishiRegistrar.reflector.get(
'KoishiCommandPutObject',
instance,
key,
);
if (meta) {
const propertyNativeType = Reflect.getMetadata(
'design:type',
targetClass.prototype,
key,
);
instance[key] = meta.run(data.view, {
...data,
nativeType: propertyNativeType,
});
}
}
return instance;
},
(data) => {
const targetClass = data.nativeType as { new (): any };
if (!targetClass) {
return;
}
const keys = koishiRegistrar.reflector.getArray(
'KoishiCommandPutObjectKeys',
targetClass,
);
for (const key of keys) {
const meta = koishiRegistrar.reflector.get(
'KoishiCommandPutObject',
targetClass,
key,
)?.info?.pre;
if (meta) {
const propertyNativeType = Reflect.getMetadata(
'design:type',
targetClass.prototype,
key,
);
meta.run(data.view, {
...data,
nativeType: propertyNativeType,
});
}
}
},
);
export * from 'satori-decorators/dist/src/decorators/http';
export * from './common';
export * from './http';
export * from './plugin';
import { koishiRegistrar } from '../registrar';
import { ModelClassType, ModelRegistrar } from 'minato-decorators';
import { Flatten, Keys, Tables } from 'koishi';
export * from 'satori-decorators/dist/src/decorators/plugin';
export const { DefinePlugin } = koishiRegistrar.pluginDecorators();
export const UseModel = koishiRegistrar.decorateTopLevelAction(
(ctx, obj, ...models: ModelClassType[]) => {
const registrar = new ModelRegistrar(ctx.model);
models.forEach((m) => registrar.registerModel(m));
},
);
export const MixinModel = <K extends Keys<Tables>>(
tableName: K,
classDict: {
[F in Keys<Tables[K]>]?: ModelClassType<Flatten<Tables[K][F]>>;
},
): ClassDecorator =>
koishiRegistrar.decorateTopLevelAction((ctx, obj) => {
const registrar = new ModelRegistrar(ctx.model);
registrar.mixinModel(tableName, classDict);
})();
// metadatas
import { Schema } from 'koishi';
import {
ControlType,
PluginClass,
ProvideDefinition,
ServiceName,
SystemInjectFun,
} from './interfaces';
import { ClassType } from 'schemastery-gen';
export const KoishiServiceInjectSym = 'KoishiServiceInjectSym';
export const KoishiServiceInjectSymKeys = 'KoishiServiceInjectSymKeys';
export const KoishiServiceProvideSym = 'KoishiServiceProvideSym';
export const KoishiSystemInjectSym = 'KoishiSystemInjectSym';
export const KoishiSystemInjectSymKeys = 'KoishiSystemInjectSymKeys';
export const KoishiAddUsingList = 'KoishiAddUsingList';
// metadata map
export interface MetadataArrayMap {
KoishiServiceProvideSym: ProvideDefinition;
KoishiServiceInjectSymKeys: string;
KoishiSystemInjectSymKeys: string;
KoishiAddUsingList: ServiceName;
KoishiControl: ControlType;
}
export interface MetadataMap {
KoishiServiceInjectSym: ServiceName;
KoishiSystemInjectSym: SystemInjectFun;
KoishiPredefineSchema: Schema | ClassType<any>;
KoishiPredefineName: string;
KoishiFork: PluginClass;
KoishiReusable: boolean;
}
export const ThirdEyeSym = Symbol('ThirdEyeSym');
export * from './interfaces';
export * from './constants';
import { Context, Dict } from 'koishi';
import { PluginMeta } from '../register';
import { Selection } from 'koishi-decorators';
import { Argv, Command, Context, Dict, I18n } from 'koishi';
import { CanBeObserved } from '../utility/rxjs-session';
export * from 'koishi-decorators/dist/src/def/interfaces';
type CommandReg<A extends any[] = any[], R = any> = (
ctx: Context,
command: Command,
...args: A
) => R;
export type CommandTransformer<A extends any[] = any[]> = CommandReg<
A,
Command
>;
export interface CommandPutData {
ctx: Context;
command: Command;
// eslint-disable-next-line @typescript-eslint/ban-types
nativeType: Function;
view: any;
}
// Command stuff
export interface CommandPutRuntime extends CommandPutData {
args: any[];
argv: Argv;
}
export type SystemInjectFun = <T = any>(
obj: PluginMeta<T>,
cl: PluginClass,
export type CommandPutPre<A extends any[] = any[]> = (
data: CommandPutData,
...args: A
) => any;
export type ServiceName = keyof Context | string;
export type CommandPut<A extends any[] = any[]> = (
data: CommandPutRuntime,
...args: A
) => any;
export interface ProvideOptions extends Context.ServiceOptions {
immediate?: boolean;
export interface CommandConfigExtended extends Command.Config {
empty?: boolean;
}
export interface ProvideDefinition extends ProvideOptions {
serviceName: ServiceName;
export interface CommandOptionConfigWithDescription extends Argv.OptionConfig {
description?: string | Dict<string>;
}
export type Condition<R, T = any, Ext extends any[] = []> = (
o: T,
...ext: Ext
) => R;
export interface Instances<T> {
instances: T[];
export interface CommandLocaleDef extends I18n.Store {
description?: string;
options?: Dict<string>;
usage?: string;
examples?: string;
messages?: I18n.Store;
}
export type TypeFromClass<T> = T extends { new (...args: any[]): infer U }
? U
: never;
// eslint-disable-next-line @typescript-eslint/ban-types
export type Renderer<T = any> = (params?: T) => string;
// eslint-disable-next-line @typescript-eslint/ban-types
export type CRenderer = (path: string, params?: object) => string;
export type ParamsFromClass<T> = T extends { new (...args: infer U): any }
? U
: never;
export type PluginClass<C = any, P = any> = new (ctx: Context, config: C) => P;
export type ClassPluginConfig<P extends PluginClass> = P extends PluginClass<
infer C
>
? C
: never;
export type ExactClassPluginConfig<P extends PluginClass> =
P extends PluginClass<any, { config: infer IC }> ? IC : ClassPluginConfig<P>;
export interface TemplateConfig {
name: string;
text: Dict<string>;
}
export type MapPluginToConfig<M extends Dict<PluginClass>> = {
[K in keyof M]: ClassPluginConfig<M[K]>;
};
export interface CommandArgDef {
index: number;
decl?: Argv.Declaration;
}
export type MapPluginToConfigWithSelection<M extends Dict<PluginClass>> = {
[K in keyof M]: ClassPluginConfig<M[K]> & Selection;
};
export interface CommandRegisterConfig<D extends string = string> {
def: D;
desc?: string;
config?: CommandConfigExtended;
// putOptions?: CommandPut.Config[];
// eslint-disable-next-line @typescript-eslint/ban-types
paramTypes: Function[];
}
export interface ControlTypeMap {
if: boolean;
for: Iterable<Record<string, any>>;
export interface CommandConfigExtended extends Command.Config {
empty?: boolean;
}
export interface ControlType<
T extends keyof ControlTypeMap = keyof ControlTypeMap,
> {
type: T;
condition: Condition<ControlTypeMap[T], any, [Record<string, any>]>;
export interface CommandOptionConfig {
name: string;
desc: string;
config?: CommandOptionConfigWithDescription;
}
export type Prop<T> = T;
export type CommandReturnType = CanBeObserved<string | void>;
import 'reflect-metadata';
import { Reflector } from 'typed-reflector';
import { MetadataArrayMap, MetadataMap } from '../def';
export const reflector = new Reflector<MetadataMap, MetadataArrayMap>();
import 'reflect-metadata';
import { MetadataSetter } from 'typed-reflector';
import { MetadataArrayMap, MetadataMap } from '../def';
export const Metadata = new MetadataSetter<MetadataMap, MetadataArrayMap>();
import { Context } from 'koishi';
import { AnyClass, ClassType, Mixin } from 'schemastery-gen';
import { PluginSchema } from './decorators';
import { PartialDeep } from './base-plugin';
import { PluginClass } from './def';
export function CreatePluginFactory<C, IC, P extends { config: IC }>(
basePlugin: PluginClass<C, P>,
baseConfig: ClassType<IC>,
): <S>(specificConfig?: ClassType<S>) => new (
ctx: Context,
config: PartialDeep<S> & C,
) => P & {
config: IC & S;
};
export function CreatePluginFactory(
basePlugin: PluginClass,
baseConfig: AnyClass,
) {
return (specificConfig: AnyClass) => {
const plugin = class specificPlugin extends basePlugin {};
const config = specificConfig
? Mixin(specificConfig, baseConfig)
: baseConfig;
PluginSchema(config)(plugin);
return plugin;
};
}
export * from './map-plugin';
export * from './multi-plugin';
export * from './merge-plugin';
import { Dict } from 'koishi';
import { MapPluginToConfigWithSelection, PluginClass } from '../def';
import { ClassType, SchemaProperty } from 'schemastery-gen';
import { CreatePluginFactory } from '../plugin-factory';
import { MappingPluginBase } from './mapping-base';
import { getPluginSchema } from '../utility/get-schema';
function MappedConfig<M extends Dict<PluginClass>>(
dict: M,
): ClassType<MapPluginToConfigWithSelection<M>> {
const PropertySchema = class SpecificPropertySchema {} as ClassType<
MapPluginToConfigWithSelection<M>
>;
for (const [key, plugin] of Object.entries(dict)) {
SchemaProperty({
type: getPluginSchema(plugin),
})(PropertySchema.prototype, key);
}
return PropertySchema;
}
export function MapPlugin<M extends Dict<PluginClass>, OuterConfig>(
dict: M,
outerConfig?: ClassType<OuterConfig>,
) {
const basePlugin = class SpecificMapPlugin extends MappingPluginBase<
M,
MapPluginToConfigWithSelection<M>,
Partial<MapPluginToConfigWithSelection<M>>
> {
_getDict() {
return dict;
}
_getPluginConfig(key: keyof M): any {
return this.config[key];
}
};
const schema = MappedConfig(dict);
const factory = CreatePluginFactory(basePlugin, schema);
return factory(outerConfig);
}
import { Dict } from 'koishi';
import { PluginClass } from '../../def';
import { BasePlugin, PartialDeep } from '../../base-plugin';
import { LifecycleEvents } from '../../register';
import { ClonePlugin } from '../../utility/clone-plugin';
import { Apply, selectContext, UseEvent } from 'koishi-decorators';
export class MappingPluginBase<
M extends Dict<PluginClass>,
C,
PC = PartialDeep<C>,
>
extends BasePlugin<C, PC>
implements LifecycleEvents
{
_getDict(): M {
throw new Error('not implemented');
}
_instanceMap = new Map<string, PluginClass>();
getInstance<K extends keyof M>(key: K): M[K] {
return this._instanceMap?.get(key as string) as M[K];
}
_getPluginConfig(key: keyof M): any {
return {};
}
@Apply()
_registerInstances() {
const dict = this._getDict();
for (const [key, plugin] of Object.entries(dict)) {
const config = this._getPluginConfig(key);
if (config == null) continue;
const ctx =
typeof config === 'object' ? selectContext(this.ctx, config) : this.ctx;
const clonedPlugin = ClonePlugin(
plugin,
`${this.constructor.name}_${plugin.name}_dict_${key}`,
(o) => this._instanceMap.set(key, o),
);
ctx.plugin(clonedPlugin, config);
}
}
onApply() {}
@UseEvent('dispose')
_onThingsDispose() {
delete this._instanceMap;
}
}
import { MapPluginToConfig, PluginClass } from '../def';
import { Dict } from 'koishi';
import { ClassType, Mixin } from 'schemastery-gen';
import { MappingPluginBase } from './mapping-base';
import { CreatePluginFactory } from '../plugin-factory';
import _ from 'lodash';
import { getPluginSchema } from '../utility/get-schema';
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
x: infer R,
) => any
? R
: never;
type MergePluginConfig<M extends Dict<PluginClass>> = UnionToIntersection<
MapPluginToConfig<M>[keyof M]
>;
export function MergePlugin<M extends Dict<PluginClass>, OuterConfig>(
dict: M,
outerConfig?: ClassType<OuterConfig>,
) {
const basePlugin = class SpecificMapPlugin extends MappingPluginBase<
M,
MergePluginConfig<M>,
MergePluginConfig<M>
> {
_getDict() {
return dict;
}
_getPluginConfig(key: keyof M): any {
return this.config;
}
};
const schemas = _.compact(
Object.values(dict).map((plugin) => getPluginSchema(plugin)),
);
const factory = CreatePluginFactory(basePlugin, Mixin(...schemas));
return factory(outerConfig);
}
import { ClonePlugin } from '../utility/clone-plugin';
import { selectContext, Selection } from 'koishi-decorators';
import { BasePlugin } from '../base-plugin';
import {
ClassPluginConfig,
Instances,
PluginClass,
ServiceName,
TypeFromClass,
} from '../def';
import { ClassType } from 'schemastery-gen';
import { ToInstancesConfig } from '../utility/to-instance-config';
import Schema from 'schemastery';
import { UsingService } from '../decorators';
import { Apply, UseEvent } from 'koishi-decorators';
import { CreatePluginFactory } from '../plugin-factory';
import { LifecycleEvents } from '../register';
export class MultiInstancePluginFramework<InnerPlugin extends PluginClass>
extends BasePlugin<
Instances<ClassPluginConfig<InnerPlugin>>,
Instances<ClassPluginConfig<InnerPlugin> & Selection>
>
implements LifecycleEvents
{
instances: TypeFromClass<InnerPlugin>[] = [];
_getInnerPlugin(): InnerPlugin {
throw new Error(`Not implemented`);
}
@Apply()
_registerInstances() {
const innerPlugin = this._getInnerPlugin();
for (let i = 0; i < this.config.instances.length; i++) {
const clonedInnerPlugin = ClonePlugin(
innerPlugin,
`${this.constructor.name}_${innerPlugin.name}_instance_${i}`,
(instance) => this.instances.push(instance),
);
const instanceConfig = this.config.instances[i];
const instanceContext =
typeof instanceConfig === 'object'
? selectContext(this.ctx, instanceConfig)
: this.ctx;
instanceContext.plugin(clonedInnerPlugin, instanceConfig);
}
}
@UseEvent('dispose')
_onThingsDispose() {
delete this.instances;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onApply() {}
}
export function MultiInstancePlugin<
InnerPlugin extends PluginClass,
OuterConfig,
>(innerPlugin: InnerPlugin, outerConfig?: ClassType<OuterConfig>) {
const basePlugin = class SpecificMultiInstancePlugin extends MultiInstancePluginFramework<InnerPlugin> {
_getInnerPlugin() {
return innerPlugin;
}
};
const schema = ToInstancesConfig(
(innerPlugin['Config'] ||
innerPlugin['schema'] ||
Schema.any()) as ClassType<ClassPluginConfig<InnerPlugin>>,
);
const factory = CreatePluginFactory(basePlugin, schema);
const plugin = factory(outerConfig);
if (innerPlugin['using']) {
UsingService(...(innerPlugin['using'] as ServiceName[]))(plugin);
}
return plugin;
}
This diff is collapsed.
import { RegisterMeta, SatoriRegistrar } from 'satori-decorators';
import { BeforeEventMap, Command, Context, Next, Session } from 'koishi';
import { CanBeObserved, sessionRxToPromise } from './utility/rxjs-session';
import {
CommandConfigExtended,
CommandPut,
CommandPutPre,
CommandReturnType,
CommandTransformer,
} from './def';
type PutMeta = RegisterMeta<CommandPut, { pre?: RegisterMeta<CommandPutPre> }>;
declare module 'satori-decorators' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Registrar {
interface MetadataMap {
KoishiCommandPutObject: PutMeta;
}
interface MetadataArrayMap {
KoishiCommandTransformer: RegisterMeta<CommandTransformer>;
KoishiCommandPut: PutMeta;
KoishiCommandPutObjectKeys: string;
}
}
}
export class KoishiRegistrar extends SatoriRegistrar<Context> {
decorateCommandTransformer<A extends any[]>(
transformer: CommandTransformer<A>,
) {
return (...args: A): ClassDecorator & MethodDecorator =>
this.metadata.append(
'KoishiCommandTransformer',
new RegisterMeta(transformer, args),
);
}
decorateCommandPut<A extends any[]>(
put: CommandPut<A>,
pre?: CommandPutPre<A>,
) {
return (...args: A): ParameterDecorator & PropertyDecorator =>
(obj, key, i?) => {
const meta = new RegisterMeta(put, args, {
pre: pre && new RegisterMeta(pre, args),
});
if (typeof i === 'number') {
this.metadata.param('KoishiCommandPut', meta)(obj, key, i);
} else {
this.metadata.set(
'KoishiCommandPutObject',
meta,
'KoishiCommandPutObjectKeys',
)(obj, key, i);
}
};
}
override methodDecorators() {
return {
...super.methodDecorators(),
UseMiddleware: this.decorateMethod(
'middleware',
(
{ ctx },
fun: (session: Session, next: Next) => CanBeObserved<string>,
prepend?: boolean,
) =>
ctx.middleware(
(s, next) => sessionRxToPromise(s, fun(s, next)),
prepend,
),
),
UseBeforeEvent: this.decorateMethod(
'before',
(
{ ctx },
fun: BeforeEventMap[keyof BeforeEventMap],
event: keyof BeforeEventMap,
append?: boolean,
) => ctx.before(event, fun, append),
),
UseCommand: this.decorateMethod(
'command',
(
info,
fun: (...args: any[]) => CommandReturnType,
...args:
| [string, CommandConfigExtended?]
| [string, string, CommandConfigExtended?]
) => {
const { ctx, view, obj, key } = info;
const def = args.shift() as string;
const desc =
typeof args[0] === 'string' ? (args.shift() as string) : '';
const config = args[0] as CommandConfigExtended;
const transformers = this.reflector.getProperty(
'KoishiCommandTransformer',
obj,
key,
);
let command: Command = ctx.command(def, desc, config);
for (const transformer of transformers) {
command = transformer.run(view, ctx, command) || command;
}
if (config?.empty) {
return command;
}
// eslint-disable-next-line @typescript-eslint/ban-types
const paramTypes: Function[] = Reflect.getMetadata(
'design:paramtypes',
obj,
key,
);
const putOptions = this.reflector.getArray(
'KoishiCommandPut',
obj,
key,
);
for (let i = 0; i < putOptions.length; i++) {
const prePutOption = putOptions[i]?.info?.pre;
if (!prePutOption) {
continue;
}
const nativeType = paramTypes[i];
prePutOption.run(view, { ctx, command, nativeType, view });
}
command.action(async (argv, ...args) => {
const params = putOptions.map((option, i) =>
option?.run(view, {
ctx,
command,
nativeType: paramTypes[i],
argv,
args,
view,
}),
);
return sessionRxToPromise(
argv.session,
fun(...params) as CanBeObserved<string>,
);
});
return command;
},
),
};
}
}
export const koishiRegistrar = new KoishiRegistrar(Context);
export const StarterPlugin = koishiRegistrar.starterPluginFactory();
import { TypeFromClass } from '../def';
export function ClonePlugin<P extends { new (...args: any[]): any }>(
target: P,
name: string,
callback?: (instance: TypeFromClass<P>) => void,
): P {
const clonedPlugin = class extends target {
constructor(...args: any[]) {
super(...args);
if (callback) {
callback(this as any);
}
}
};
for (const property of ['Config', 'schema', 'using']) {
Object.defineProperty(clonedPlugin, property, {
enumerable: true,
configurable: true,
writable: true,
value: target[property],
});
}
Object.defineProperty(clonedPlugin, 'name', {
enumerable: true,
configurable: true,
writable: true,
value: name,
});
return clonedPlugin;
}
import { reflector } from '../meta/meta-fetch';
import { DefinePlugin } from '../register';
export function getFork(obj: any) {
const fork = reflector.get('KoishiFork', obj);
if (!fork) {
return;
}
return DefinePlugin()(fork);
}
import { PluginClass } from '../def';
import { BasePlugin } from '../base-plugin';
import { ClassType } from 'schemastery-gen';
import { reflector } from '../meta/meta-fetch';
export function getPluginSchema<P extends PluginClass>(
plugin: P,
): ClassType<
P extends BasePlugin<any, infer PC>
? PC
: P extends PluginClass<infer C>
? C
: never
> {
return (
plugin['Config'] ||
plugin['schema'] ||
reflector.get('KoishiPredefineSchema', plugin)
);
}
import { Argv } from 'koishi';
// eslint-disable-next-line @typescript-eslint/ban-types
export const nativeTypeMapping = new Map<Function, Argv.Type>();
nativeTypeMapping.set(String, 'string');
nativeTypeMapping.set(Number, 'number');
nativeTypeMapping.set(Boolean, 'boolean');
nativeTypeMapping.set(Date, 'date');
export function applyNativeTypeToArg(
arg: Argv.Declaration,
// eslint-disable-next-line @typescript-eslint/ban-types
nativeType: Function,
) {
if (arg.type || !nativeType) {
return;
}
if (nativeTypeMapping.has(nativeType)) {
arg.type = nativeTypeMapping.get(nativeType);
}
}
import { Awaitable, Session } from 'koishi';
import { isObservable, Observable } from 'rxjs';
export type CanBeObserved<T> = Awaitable<T | Observable<T>>;
export async function sessionRxToPromise(
session: Session,
obs: CanBeObserved<string>,
) {
const obsAwaited = await obs;
if (!isObservable(obsAwaited)) {
return obsAwaited;
}
return new Promise<string>((resolve, reject) => {
let lastValue: string;
obsAwaited.subscribe({
next: async (value) => {
if (lastValue && session.send) {
await session.send(lastValue);
}
lastValue = value;
},
error: async (error) => {
if (lastValue && session.send) {
await session.send(lastValue);
}
reject(error);
},
complete: () => {
resolve(lastValue);
},
});
});
}
import { SchemaClass, SchemaProperty } from 'schemastery-gen';
import { Instances, TypeFromClass } from '../def';
export function ToInstancesConfig<Inner extends new (...args: any[]) => any>(
instanceConfig: Inner,
): new () => Instances<TypeFromClass<Inner>> {
const instanceConfigClass = class InstancesConfig {
instances: TypeFromClass<Inner>[];
};
SchemaProperty({
type: SchemaClass(instanceConfig),
default: [],
array: true,
})(instanceConfigClass.prototype, 'instances');
return instanceConfigClass;
}
import { Command, Context, Dict } from 'koishi';
import { CommandOptionConfig, TemplateConfig } from '../def';
import { applyNativeTypeToArg } from './native-type-mapping';
export function adaptLocaleDict(value: string | Dict<string>): Dict<string> {
if (typeof value === 'string') {
return {
'': value,
};
}
return value;
}
export const registerTemplate = (
templateConfig: TemplateConfig,
ctx: Context,
command?: Command,
) => {
const key =
(command ? `commands.${command.name}.messages.` : '') + templateConfig.name;
for (const [locale, text] of Object.entries(templateConfig.text)) {
ctx.i18n.define(locale, key, text);
}
};
export function applyOptionToCommand(
ctx: Context,
cmd: Command,
def: CommandOptionConfig,
// eslint-disable-next-line @typescript-eslint/ban-types
nativeType?: Function,
) {
const { name, config } = def;
const { desc } = def;
if (config?.description) {
const desc = adaptLocaleDict(config.description);
for (const [locale, text] of Object.entries(desc)) {
ctx.i18n.define(locale, `commands.${cmd.name}.options.${name}`, text);
}
}
const clonedConfig = { ...(config || {}) };
delete clonedConfig.description;
cmd = cmd.option(name, desc, clonedConfig);
const option = cmd._options[name];
applyNativeTypeToArg(option, nativeType);
return cmd;
}
import { DefinePlugin } from '../src/register';
import { BasePlugin } from '../src/base-plugin';
import { Caller, Provide } from '../src/decorators';
import { App } from 'koishi';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Context {
interface Services {
callerTester: CallerTester;
}
}
}
@Provide('callerTester')
@DefinePlugin()
class CallerTester extends BasePlugin<any> {
@Caller()
caller: string;
}
describe('Caller', () => {
let app: App;
beforeEach(async () => {
app = new App();
app.plugin(CallerTester);
await app.start();
});
it('should put caller with correct values', async () => {
const ctx1 = app.any();
const ctx2 = app.any();
const caller1 = ctx1.callerTester.caller;
const caller2 = ctx2.callerTester.caller;
expect(caller1).toEqual(ctx1);
expect(caller2).toEqual(ctx2);
expect(app.callerTester.caller).toEqual(app);
});
});
import { CommandExample, CommandUsage, UseCommand } from '../src/decorators';
import { BasePlugin } from '../src/base-plugin';
import { DefinePlugin } from '../src/register';
import {
CommandExample,
CommandUsage,
DefinePlugin,
PutArg,
PutObject,
PutOption,
UseCommand,
} from '../src/decorators';
import { App, Command } from 'koishi';
import { StarterPlugin } from '../src/registrar';
class Sound {
@PutArg(0)
sound: string;
@PutOption('volume', '-v <volume>')
volume: number;
getMessage() {
return `${this.sound} in ${this.volume}`;
}
}
@CommandUsage('乒乓球真好玩!') // 会适用于 ping 和 pang 两个指令
@DefinePlugin()
class MyPlugin extends BasePlugin<any> {
class MyPlugin extends StarterPlugin() {
@UseCommand('ping', 'Ping!')
@CommandExample('枰!')
onPing() {
return 'pong';
onPing(@PutOption('sound', '-s <sound>') sound: string) {
return `pong ${sound}`;
}
@UseCommand('pang', 'Pang!')
@CommandExample('乓!')
onPang() {
return 'peng';
onPang(@PutArg(0) sound: string) {
return `pong ${sound}`;
}
@UseCommand('peng', 'Peng!')
onPeng(@PutObject() sound: Sound) {
return sound.getMessage();
}
}
describe('Top level command def', () => {
describe('Command', () => {
let app: App;
let pingCommand: Command;
let pangCommand: Command;
......@@ -39,4 +63,26 @@ describe('Top level command def', () => {
expect(pingCommand._examples[0]).toEqual('枰!');
expect(pangCommand._examples[0]).toEqual('乓!');
});
it('Should infer option types', () => {
expect(pingCommand._options.sound.type).toEqual('string');
});
it('should call command', async () => {
expect(await pingCommand.execute({ options: { sound: '' } })).toBe(
'pong 枰',
);
expect(await pangCommand.execute({ args: [''] })).toBe('pong 乓');
});
it('should resolve put object', async () => {
const pengCommand = app.command('peng');
expect(pengCommand._options.volume.type).toBe('number');
expect(
await pengCommand.execute({
args: ['吵死了!'],
options: { volume: 20 },
}),
).toBe('吵死了! in 20');
});
});
import { RegisterSchema, SchemaProperty } from 'schemastery-gen';
import { DefinePlugin } from '../src/register';
import { ParentPluginMap, StarterPlugin } from '../src/base-plugin';
import { Fork, InjectParent, Provide, Reusable } from '../src/decorators';
import { Apply, UseCommand } from 'koishi-decorators';
import { App } from 'koishi';
import { Prop } from '../src/def';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Context {
interface Services {
forkTest: MyPlugin;
}
}
}
@RegisterSchema()
class Config {
@SchemaProperty()
name: string;
getName() {
return this.name;
}
}
@DefinePlugin()
class ChildPlugin extends StarterPlugin(Config) {
@InjectParent()
parent: Prop<MyPlugin>;
@UseCommand('parent{{name}}')
async onParentCommand() {
return this.parent.config.getName();
}
@UseCommand('child{{name}}')
async onSelfCommand() {
return this.config.getName();
}
@Apply()
increase() {
this.parent.loadCount++;
// console.log('fork loaded: ', this.parent.loadCount);
}
}
@Provide('forkTest', { immediate: true })
@DefinePlugin()
class MyPlugin extends ParentPluginMap(ChildPlugin, (p) => p.config.getName()) {
loadCount = 0;
isParent = true;
@Apply()
onLoad() {
// console.log('load', this.config);
}
}
@Reusable()
@DefinePlugin()
class MyReusablePlugin extends StarterPlugin(Config) {
@Apply()
onLoad() {
this.ctx.app['count']++;
}
}
describe('Fork', () => {
let app: App;
beforeEach(async () => {
app = new App();
await app.start();
app['count'] = 0;
});
it('should fork a plugin', async () => {
// console.log('before 1');
app.plugin(MyPlugin, { name: 'a' });
// console.log('after 1: ' + app.forkTest.loadCount);
const myPlugin = app.forkTest;
expect(app.forkTest.config.getName()).toEqual('a');
expect(app.forkTest.instances.get('a').config.getName()).toEqual('a');
// console.log(myPlugin.instances.get('a').parent);
// console.log(myPlugin);
expect(myPlugin.instances.get('a').parent === myPlugin).toBe(true);
expect(app.forkTest.instances.get('b')).toBeUndefined();
expect(app.forkTest.loadCount).toBe(1);
// console.log('before 2: ' + app.forkTest.loadCount);
app.plugin(MyPlugin, { name: 'b' });
// console.log('after 2: ' + app.forkTest.loadCount);
expect(app.forkTest.instances.get('b').config.getName()).toEqual('b');
// console.log(myPlugin.instances.get('b').parent);
// console.log(myPlugin);
expect(myPlugin.instances.get('b').parent === myPlugin).toBe(true);
expect(app.forkTest.loadCount).toBe(2);
// console.log('before 3: ' + app.forkTest.loadCount);
app.plugin(MyPlugin, { name: 'c' });
// console.log('after 3: ' + app.forkTest.loadCount);
expect(app.forkTest.instances.get('c').config.getName()).toEqual('c');
// console.log(myPlugin.instances.get('c').parent);
// console.log(myPlugin);
expect(myPlugin.instances.get('c').parent === myPlugin).toBe(true);
expect(app.forkTest.loadCount).toBe(3);
const commandChildA = app.command('childa');
const commandChildB = app.command('childb');
const commandParentA = app.command('parenta');
const commandParentB = app.command('parentb');
expect(await commandChildA.execute({})).toEqual('a');
expect(await commandChildB.execute({})).toEqual('b');
expect(await commandParentA.execute({})).toEqual('a');
expect(await commandParentB.execute({})).toEqual('a');
});
it('it should work on reusable', async () => {
expect(app['count']).toBe(0);
app.plugin(MyReusablePlugin, { name: 'a' });
expect(app['count']).toBe(1);
app.plugin(MyReusablePlugin, { name: 'b' });
expect(app['count']).toBe(2);
});
});
import { App } from 'koishi';
import { DefinePlugin } from '../src/register';
import { Get } from '../src/decorators';
import request from 'supertest';
import { KoaContext } from 'koishi-decorators';
@DefinePlugin()
class MyPlugin {
@Get('ping')
async ping(ctx: KoaContext) {
ctx.status = 233;
ctx.body = 'pong';
}
}
describe('Http Routes', () => {
let app: App;
beforeEach(() => {
app = new App();
});
it('should be able to get a route', async () => {
app.plugin(MyPlugin);
await app.start();
return request(app._httpServer).get('/ping').expect(233).expect('pong');
});
});
import { DefinePlugin } from '../src/register';
import { PutValue, UseCommand } from 'koishi-decorators';
import { For, If } from '../src/decorators';
import { App } from 'koishi';
import { BasePlugin } from '../src/base-plugin';
@DefinePlugin()
class MyPlugin extends BasePlugin<{ foo: boolean; bar: boolean }> {
@If<MyPlugin>((o) => o.config.foo)
@UseCommand('foo')
foo() {
return 'foo';
}
@If<MyPlugin>((o) => o.config.bar)
@UseCommand('bar')
bar() {
return 'bar';
}
}
@DefinePlugin()
class MyPlugin2 extends BasePlugin<{
prefix: string;
commands: { name: string; return: string }[];
matrix: { commands: { name: string; return: string }[] }[];
}> {
@For<MyPlugin2>(({ config }) => config.commands)
@If<MyPlugin2>((_, def) => def.name !== 'badthing')
@UseCommand('{{name}}')
onCommand(
@PutValue('{{return}}') returnValue: string,
@PutValue('{{prefix}}') prefix: string,
) {
return prefix + returnValue;
}
@For<MyPlugin2>(({ config }) => config.matrix)
@For<MyPlugin2>((_, matrix) => matrix.commands)
@If<MyPlugin2>((_, def) => def.name !== 'badthing')
@UseCommand('{{name}}')
onMatrix(
@PutValue('{{return}}') returnValue: string,
@PutValue('{{prefix}}') prefix: string,
) {
return prefix + returnValue;
}
}
describe('It should register conditionally', () => {
it('registers command on condition', async () => {
const app = new App();
app.plugin(MyPlugin, { foo: true, bar: false });
await app.start();
const commandFoo = app.command('foo');
const commandBar = app.command('bar');
expect(await commandFoo.execute({})).toBe('foo');
expect(await commandBar.execute({})).toBeFalsy();
});
it('iterates commands on condition', async () => {
const app = new App();
app.plugin(MyPlugin2, {
commands: [
{ name: 'foo', return: 'bar' },
{ name: 'bar', return: 'baz' },
{ name: 'badthing', return: 'bad' },
],
matrix: [
{
commands: [
{ name: 'foo1', return: 'bar1' },
{ name: 'bar1', return: 'baz1' },
{ name: 'badthing', return: 'bad' },
],
},
{
commands: [
{ name: 'foo2', return: 'bar2' },
{ name: 'bar2', return: 'baz2' },
{ name: 'badthing', return: 'bad' },
],
},
],
prefix: '> ',
});
await app.start();
const commandFoo = app.command('foo');
const commandBar = app.command('bar');
expect(await commandFoo.execute({})).toBe('> bar');
expect(await commandBar.execute({})).toBe('> baz');
const commandFoo1 = app.command('foo1');
const commandBar1 = app.command('bar1');
expect(await commandFoo1.execute({})).toBe('> bar1');
expect(await commandBar1.execute({})).toBe('> baz1');
const commandFoo2 = app.command('foo2');
const commandBar2 = app.command('bar2');
expect(await commandFoo2.execute({})).toBe('> bar2');
expect(await commandBar2.execute({})).toBe('> baz2');
expect(await app.command('badthing').execute({})).toBeFalsy();
});
});
import { Fork, RegisterSchema, SchemaProperty, StarterPlugin } from '..';
import { Assets, Bot, Cache } from 'koishi';
import { Inject, PluginName, UsingService } from '../src/decorators';
import { DefinePlugin } from '../src/register';
import { ServiceName } from '../src/def';
@RegisterSchema()
class Config {
@SchemaProperty()
foo: string;
}
describe('InjectUsing', () => {
@DefinePlugin()
@UsingService('foo')
class MyFork extends StarterPlugin(Config) {}
@PluginName('foo-plugin')
@UsingService('router')
@DefinePlugin({ using: ['database'], schema: Config })
@UsingService('http')
@Fork(MyFork)
class MyPlugin extends StarterPlugin(Config) {
@Inject(true)
cache: Cache;
@Inject('assets', true)
assets: Assets;
@Inject('bots')
bots: Bot[];
}
it('Should include injected using services', () => {
expect(MyPlugin.name).toBe('foo-plugin');
expect(MyPlugin['Config']).toEqual(Config);
const usingList = (MyPlugin as any).using as ServiceName[];
expect(usingList).toBeInstanceOf(Array);
expect(usingList.length).toEqual(6);
expect(usingList.includes('database')).toEqual(true);
expect(usingList.includes('assets')).toEqual(true);
expect(usingList.includes('cache')).toEqual(true);
expect(usingList.includes('router')).toEqual(true);
expect(usingList.includes('bots')).toEqual(false);
expect(usingList.includes('http')).toEqual(true);
expect(usingList.includes('foo')).toEqual(true);
});
});
import { App } from 'koishi';
import { DefinePlugin } from '../src/register';
import { UsePlugin } from '../src/decorators';
import PluginOnebot from '@koishijs/plugin-adapter-onebot';
import { BasePlugin } from '../src/base-plugin';
import { PluginDef } from 'koishi-decorators';
@DefinePlugin()
class MyPlugin extends BasePlugin<any> {
@UsePlugin()
loadOnebot() {
return PluginDef(PluginOnebot, {
bots: [{ protocol: 'wsreverse', selfId: '11111' }],
});
}
}
@DefinePlugin()
class MyLazyPlugin extends BasePlugin<any> {
@UsePlugin()
async loadOnebot() {
await new Promise((resolve) => setTimeout(resolve, 1000));
this.ctx.app['_flag'] = 2;
return PluginDef((ctx) => (ctx.app['_flag1'] = 1));
}
}
describe('Inner plugin', () => {
let app: App;
it('should load inner plugin properly', async () => {
app = new App();
await app.start();
app.plugin(MyPlugin);
expect(app.bots.length).toBe(1);
//expect(app.cache).toBeDefined();
});
it('should wait for plugin to load', async () => {
app = new App();
app['_flag'] = 1;
app['_flag1'] = 0;
app.plugin(MyLazyPlugin);
expect(app['_flag']).toBe(1);
expect(app['_flag1']).toBe(0);
await app.start();
expect(app['_flag']).toBe(2);
expect(app['_flag1']).toBe(1);
});
});
import { SchemaProperty } from 'schemastery-gen';
import { StarterPlugin } from '../src/base-plugin';
import { DefinePlugin } from '../src/register';
import { UseCommand } from 'koishi-decorators';
import { MapPlugin } from '../src/plugin-operators';
import { App } from 'koishi';
import { MergePlugin } from '../src/plugin-operators/merge-plugin';
class DressConfig {
@SchemaProperty()
color: string;
}
@DefinePlugin()
class DressPlugin extends StarterPlugin(DressConfig) {
@UseCommand('dressColor')
dressColor() {
return this.config.color;
}
}
class SkirtConfig {
@SchemaProperty({ default: 'S' })
size: string;
}
@DefinePlugin()
class SkirtPlugin extends StarterPlugin(SkirtConfig) {
@UseCommand('skirtSize')
skirtSize() {
return this.config.size;
}
}
class WearingConfig {
@SchemaProperty()
strip: string;
}
@DefinePlugin()
class WearingPlugin extends MapPlugin(
{ dress: DressPlugin, skirt: SkirtPlugin },
WearingConfig,
) {
@UseCommand('wearingStrip')
wearingStrip() {
return this.config.strip;
}
}
@DefinePlugin()
class MergedWearingPlugin extends MergePlugin(
{ dress: DressPlugin, skirt: SkirtPlugin },
WearingConfig,
) {
@UseCommand('wearingStrip')
wearingStrip() {
return this.config.strip;
}
}
describe('register map plugin instance', () => {
it('should work on each level', async () => {
const app = new App();
app.plugin(WearingPlugin, {
dress: { color: 'red' },
skirt: { size: 'XL' },
strip: 'pink',
});
await app.start();
expect(await app.command('dressColor').execute({})).toBe('red');
expect(await app.command('skirtSize').execute({})).toBe('XL');
expect(await app.command('wearingStrip').execute({})).toBe('pink');
});
it('should partial register', async () => {
const app = new App();
app.plugin(WearingPlugin, {
dress: { color: 'red' },
strip: 'pink',
});
await app.start();
expect(await app.command('dressColor').execute({})).toBe('red');
expect(await app.command('wearingStrip').execute({})).toBe('pink');
expect(await app.command('skirtSize').execute({})).toBe('S');
});
it('should work on merge plugin', async () => {
const app = new App();
app.plugin(MergedWearingPlugin, {
color: 'red',
size: 'XL',
strip: 'pink',
});
await app.start();
expect(await app.command('dressColor').execute({})).toBe('red');
expect(await app.command('skirtSize').execute({})).toBe('XL');
expect(await app.command('wearingStrip').execute({})).toBe('pink');
});
});
import { DefinePlugin } from '../src/register';
import { RegisterSchema, SchemaProperty } from 'schemastery-gen';
import { BasePlugin } from '../src/base-plugin';
import { UseCommand } from 'koishi-decorators';
import { MultiInstancePlugin } from '../src/plugin-operators';
import { App, Schema } from 'koishi';
class MessageConfig {
@SchemaProperty()
msg: string;
getMsg() {
return this.msg;
}
}
@RegisterSchema()
class InnerMessageConfig extends MessageConfig {}
@RegisterSchema()
class OuterMessageConfig extends MessageConfig {}
@DefinePlugin({ schema: InnerMessageConfig })
class Inner extends BasePlugin<InnerMessageConfig> {
@UseCommand('message')
async onMessage() {
return this.config.getMsg();
}
}
@DefinePlugin({ schema: Schema.object({ msg: Schema.string() }) })
class Inner2 extends BasePlugin<InnerMessageConfig> {
@UseCommand('message')
async onMessage() {
return this.config.msg;
}
}
@DefinePlugin()
class Outer extends MultiInstancePlugin(Inner, OuterMessageConfig) {
@UseCommand('message2')
async onMessage() {
return this.config.getMsg();
}
@UseCommand('message3')
async onInnerMessage() {
return this.instances[0].config.getMsg();
}
}
@DefinePlugin()
class Outer2 extends MultiInstancePlugin(Inner2, OuterMessageConfig) {
@UseCommand('message2')
async onMessage() {
return this.config.getMsg();
}
@UseCommand('message3')
async onInnerMessage() {
return this.instances[0].config.msg;
}
}
describe('register multi plugin instance', () => {
it('should work on schemastery-gen', async () => {
const app = new App();
app.plugin(Outer, { msg: 'hello', instances: [{ msg: 'world' }] });
await app.start();
const innerCommand = app.command('message');
const outerCommand = app.command('message2');
const innerInnerCommand = app.command('message3');
expect(await innerCommand.execute({})).toBe('world');
expect(await outerCommand.execute({})).toBe('hello');
expect(await innerInnerCommand.execute({})).toBe('world');
});
it('should work on common schemastery', async () => {
const app = new App();
app.plugin(Outer2, { msg: 'hello', instances: [{ msg: 'world' }] });
await app.start();
const innerCommand = app.command('message');
const outerCommand = app.command('message2');
const innerInnerCommand = app.command('message3');
expect(await innerCommand.execute({})).toBe('world');
expect(await outerCommand.execute({})).toBe('hello');
expect(await innerInnerCommand.execute({})).toBe('world');
});
});
import { App, Context, Logger } from 'koishi';
import {
DefinePlugin,
OnApply,
OnConnect,
OnDisconnect,
} from '../src/register';
import {
Inject,
InjectContext,
InjectLogger,
Provide,
} from '../src/decorators';
import { BasePlugin } from '../src/base-plugin';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Context {
interface Services {
immediateDependency: ImmediateDependency;
nonImmediateDependency: NonImmediateDependency;
myPlugin: TestingBase;
}
}
}
@Provide('immediateDependency', { immediate: true })
@DefinePlugin()
class ImmediateDependency {}
@Provide('nonImmediateDependency')
@DefinePlugin()
class NonImmediateDependency {}
@Provide('myPlugin', { immediate: true })
@DefinePlugin()
class TestingBase
extends BasePlugin<any>
implements OnConnect, OnDisconnect, OnApply
{
@InjectLogger()
logger: Logger;
onApply() {
this.applied = true;
}
onConnect() {
this.connected = true;
}
onDisconnect() {
this.disconnected = true;
}
applied = false;
connected = false;
disconnected = false;
}
class MyPlugin extends TestingBase {}
class MyPlugin2 extends TestingBase {
@Inject(true)
immediateDependency: ImmediateDependency;
}
class MyPlugin3 extends TestingBase {
@Inject(true)
nonImmediateDependency: NonImmediateDependency;
}
async function RunApplyTest(app: App, plugin: any) {
app.plugin(plugin);
await app.start();
const myPlugin = app.myPlugin;
expect(myPlugin.applied).toBe(true);
expect(myPlugin.connected).toBe(true);
expect(myPlugin.disconnected).toBe(false);
app.dispose(plugin);
expect(myPlugin.disconnected).toBe(true);
expect(app.immediateDependency).toBeDefined();
expect(app.nonImmediateDependency).toBeDefined();
await app.stop();
}
describe('Apply and Connect in koishi-thirdeye', () => {
let app: App;
beforeEach(() => {
app = new App();
app.plugin(ImmediateDependency);
app.plugin(NonImmediateDependency);
});
it('should be applied and connected', async () => {
await RunApplyTest(app, MyPlugin);
});
it('should be applied and connected with immediate dependency', async () => {
await RunApplyTest(app, MyPlugin2);
});
it('should be applied and connected with non-immediate dependency', async () => {
await RunApplyTest(app, MyPlugin3);
});
it('should name logger correctly', () => {
app.plugin(MyPlugin);
const myPlugin = app.myPlugin;
expect(myPlugin.logger.name).toBe('MyPlugin');
});
});
import { SchemaProperty } from 'schemastery-gen';
import { UseCommand } from 'koishi-decorators';
import { CreatePluginFactory } from '../src/plugin-factory';
import { App } from 'koishi';
import { DefinePlugin } from '../src/register';
import { BasePlugin } from '../src/base-plugin';
class MessageConfig {
@SchemaProperty()
msg: string;
getMsg() {
return this.msg;
}
}
class Base extends BasePlugin<MessageConfig> {
@UseCommand('message')
async onMessage() {
return this.config.getMsg();
}
}
const Factory = CreatePluginFactory(Base, MessageConfig);
class SpecificConfig {
@SchemaProperty()
msg2: string;
getMsg2() {
return this.msg2;
}
}
@DefinePlugin()
class SpecificPlugin extends Factory(SpecificConfig) {
@UseCommand('message2')
async onMessage2() {
return this.config.getMsg2();
}
}
describe('plugin factory', () => {
it('should register SpecificPlugin', async () => {
const app = new App();
app.plugin(SpecificPlugin, { msg: 'hello', msg2: 'world' });
await app.start();
const innerCommand = app.command('message');
const outerCommand = app.command('message2');
expect(await innerCommand.execute({})).toBe('hello');
expect(await outerCommand.execute({})).toBe('world');
});
});
import { App, Session } from 'koishi';
import { OnGuild, OnPlatform, UseCommand } from 'koishi-decorators';
import { DefinePlugin } from '../src/register';
@OnPlatform('discord')
@DefinePlugin()
class MyClass {
@OnGuild('1111111111')
@UseCommand('foo')
foo() {}
}
describe('Scope', () => {
let app: App;
beforeEach(async () => {
app = new App();
await app.start();
});
it('should check scope', () => {
const correctSession = {
guildId: '1111111111',
platform: 'discord',
} as Session;
const wrongSession1 = {
guildId: '2222222222',
platform: 'discord',
} as Session;
const wrongSession2 = {
guildId: '1111111111',
platform: 'telegram',
} as Session;
app.plugin(MyClass);
const methodCtx = app.command('foo').ctx;
expect(methodCtx.filter(correctSession)).toBe(true);
expect(methodCtx.filter(wrongSession1)).toBe(false);
expect(methodCtx.filter(wrongSession2)).toBe(false);
});
});
import { App, Context } from 'koishi';
import { DefinePlugin } from '../src/register';
import { Inject, Provide, UseEvent, UsingService } from '../src/decorators';
import { BasePlugin } from '../src/base-plugin';
import { ServiceName } from '../src/def';
declare module 'koishi' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Context {
interface Services {
myProvider: MyProvider;
myEagerProvider: MyEagerProvider;
myConsumer: MyConsumer;
myUsingConsumer: MyUsingConsumer;
myPartialConsumer: MyPartialConsumer;
dummyProvider: any;
}
}
interface EventMap {
'pang'(message: string): Promise<string>;
'pong'(message: string): Promise<string>;
}
}
@Provide('myProvider')
@DefinePlugin()
class MyProvider extends BasePlugin<any> {
ping() {
return 'pong';
}
dispose() {
return this.ctx.dispose();
}
}
@Provide('myEagerProvider', { immediate: true })
@DefinePlugin()
class MyEagerProvider extends BasePlugin<any> {
ping() {
return 'pong eager';
}
dispose() {
return this.ctx.dispose();
}
}
@Provide('myConsumer', { immediate: true })
@DefinePlugin()
class MyConsumer {
@Inject()
myProvider: MyProvider;
@Inject()
myEagerProvider: MyEagerProvider;
pongResult: string;
eagerPongResult: string;
@UseEvent('internal/service')
async onService(name: ServiceName) {
if (name === 'myProvider') {
this.pongResult = this.myProvider.ping();
} else if (name === 'myEagerProvider') {
this.eagerPongResult = this.myEagerProvider.ping();
}
}
}
@Provide('myUsingConsumer', { immediate: true })
@DefinePlugin()
class MyUsingConsumer {
@Inject(true)
myProvider: MyProvider;
@Inject(true)
myEagerProvider: MyEagerProvider;
pongResult: string;
eagerPongResult: string;
@UseEvent('internal/service')
async onService(name: ServiceName) {
if (name === 'myProvider') {
this.pongResult = this.myProvider.ping();
} else if (name === 'myEagerProvider') {
this.eagerPongResult = this.myEagerProvider.ping();
}
}
emitResult: string;
}
@Provide('myPartialConsumer', { immediate: true })
@DefinePlugin()
class MyPartialConsumer {
@Inject()
dummyProvider: number;
pongResult: string;
@UsingService('dummyProvider')
@UseEvent('pang')
async onPang(content: string) {
const msg = `pang: ${content}`;
console.log(msg);
return msg;
}
@UseEvent('pong')
async onPong(content: string) {
const msg = `pong: ${content}`;
console.log(msg);
return msg;
}
}
describe('On service', () => {
let app: App;
it('Should call service', async () => {
app = new App();
app.plugin(MyProvider);
app.plugin(MyEagerProvider);
app.plugin(MyConsumer);
expect(app.myEagerProvider).toBeDefined();
expect(app.myEagerProvider.ping()).toBe('pong eager');
expect(app.myProvider).toBeUndefined();
await app.start();
expect(app.myConsumer).toBeDefined();
expect(app.myProvider).toBeDefined();
// expect(app.myConsumer.eagerPongResult).toBe('pong eager');
expect(app.myConsumer.pongResult).toBe('pong');
});
it('Should call service with using', async () => {
app = new App();
app.plugin(MyUsingConsumer);
expect(app.myUsingConsumer).toBeUndefined();
app.plugin(MyProvider);
expect(app.myUsingConsumer).toBeUndefined();
app.plugin(MyEagerProvider);
expect(app.myUsingConsumer).toBeUndefined();
await app.start();
expect(app.myUsingConsumer).toBeDefined();
expect(app.myProvider).toBeDefined();
expect(app.myProvider.ping()).toBe('pong');
expect(app.myEagerProvider).toBeDefined();
expect(app.myEagerProvider.ping()).toBe('pong eager');
//expect(app.myUsingConsumer.eagerPongResult).toBe('pong eager');
//expect(app.myUsingConsumer.pongResult).toBe('pong');
});
it('Should handle partial using deps', async () => {
Context.service('dummyProvider');
app = new App();
await app.start();
app.plugin(MyPartialConsumer);
expect(app.myPartialConsumer).toBeDefined();
expect(await app.waterfall('pang', 'hello')).toEqual('hello');
expect(await app.waterfall('pong', 'hello')).toEqual('pong: hello');
app.dummyProvider = { foo: 'bar' };
expect(await app.waterfall('pang', 'hello')).toEqual('pang: hello');
expect(await app.waterfall('pong', 'hello')).toEqual('pong: hello');
app.dummyProvider = undefined;
expect(await app.waterfall('pang', 'hi')).toEqual('hi');
expect(await app.waterfall('pong', 'hi')).toEqual('pong: hi');
app.dummyProvider = { foo: 'baz' };
expect(await app.waterfall('pang', 'hi')).toEqual('pang: hi');
expect(await app.waterfall('pong', 'hi')).toEqual('pong: hi');
});
});
import { MixinModel, UseModel } from '../src/decorators';
import { DefinePlugin, MixinModel, UseModel } from '../src/decorators';
import { App } from 'koishi';
import { BasePlugin } from '../src/base-plugin';
import { DefinePlugin } from '../src/register';
import {
ChildModel,
DefineModel,
......@@ -10,6 +8,7 @@ import {
PrimaryGenerated,
Unique,
} from '../src/cosmotype-exports';
import { StarterPlugin } from '../src/registrar';
declare module 'koishi' {
interface Tables {
......@@ -79,7 +78,7 @@ class Wearing {
@MixinModel('user', { shirt: Wearing })
@UseModel(Dress)
@DefinePlugin()
class MyPlugin extends BasePlugin<any> {}
class MyPlugin extends StarterPlugin() {}
describe('Test of model', () => {
it('should register model', async () => {
......
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