diff --git a/lib/decorators/core/index.ts b/lib/decorators/core/index.ts index baddc8e..a982c9d 100644 --- a/lib/decorators/core/index.ts +++ b/lib/decorators/core/index.ts @@ -1,3 +1,4 @@ export * from './update.decorator'; export * from './scene.decorator'; +export * from './wizard.decorator'; export * from './inject-bot.decorator'; diff --git a/lib/decorators/core/scene.decorator.ts b/lib/decorators/core/scene.decorator.ts index 391c842..0e89f26 100644 --- a/lib/decorators/core/scene.decorator.ts +++ b/lib/decorators/core/scene.decorator.ts @@ -1,8 +1,14 @@ import { SetMetadata } from '@nestjs/common'; +import { SceneOptions } from 'telegraf/typings/scenes/base'; +import { SceneMetadata } from '../../interfaces'; import { SCENE_METADATA } from '../../telegraf.constants'; -/** - * TODO - */ -export const Scene = (id: string): ClassDecorator => - SetMetadata(SCENE_METADATA, id); +export const Scene = ( + sceneId: string, + options?: SceneOptions, +): ClassDecorator => + SetMetadata(SCENE_METADATA, { + sceneId, + type: 'base', + options, + }); diff --git a/lib/decorators/core/wizard.decorator.ts b/lib/decorators/core/wizard.decorator.ts new file mode 100644 index 0000000..bcf6957 --- /dev/null +++ b/lib/decorators/core/wizard.decorator.ts @@ -0,0 +1,14 @@ +import { SetMetadata } from '@nestjs/common'; +import { SceneOptions } from 'telegraf/typings/scenes/base'; +import { SceneMetadata } from '../../interfaces'; +import { SCENE_METADATA } from '../../telegraf.constants'; + +export const Wizard = ( + sceneId: string, + options?: SceneOptions, +): ClassDecorator => + SetMetadata(SCENE_METADATA, { + sceneId, + type: 'wizard', + options, + }); diff --git a/lib/decorators/scene/index.ts b/lib/decorators/scene/index.ts index b1d61b2..99d89d9 100644 --- a/lib/decorators/scene/index.ts +++ b/lib/decorators/scene/index.ts @@ -1,2 +1,3 @@ export * from './scene-enter.decorator'; export * from './scene-leave.decorator'; +export * from './wizard-step.decorator'; diff --git a/lib/decorators/scene/wizard-step.decorator.ts b/lib/decorators/scene/wizard-step.decorator.ts new file mode 100644 index 0000000..7cd8b28 --- /dev/null +++ b/lib/decorators/scene/wizard-step.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; +import { WizardStepMetadata } from '../../interfaces'; +import { WIZARD_STEP_METADATA } from '../../telegraf.constants'; + +export const WizardStep = (step: number) => + SetMetadata(WIZARD_STEP_METADATA, { step }); diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index ef12fd5..b664a3d 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -1,2 +1,4 @@ export * from './telegraf-options.interface'; export * from './listener-metadata.interface'; +export * from './scene-metadata.interface'; +export * from './telegraf-exception-filter.interface'; diff --git a/lib/interfaces/scene-metadata.interface.ts b/lib/interfaces/scene-metadata.interface.ts new file mode 100644 index 0000000..589cedd --- /dev/null +++ b/lib/interfaces/scene-metadata.interface.ts @@ -0,0 +1,11 @@ +import { SceneOptions } from 'telegraf/typings/scenes/base'; + +export interface SceneMetadata { + sceneId: string; + type: 'base' | 'wizard'; + options?: SceneOptions; +} + +export interface WizardStepMetadata { + step: number; +} diff --git a/lib/services/listeners-explorer.service.ts b/lib/services/listeners-explorer.service.ts index 6a5dbc8..a2348b8 100644 --- a/lib/services/listeners-explorer.service.ts +++ b/lib/services/listeners-explorer.service.ts @@ -17,12 +17,13 @@ import { import { BaseExplorerService } from './base-explorer.service'; import { TelegrafParamsFactory } from '../factories/telegraf-params-factory'; import { TelegrafContextType } from '../execution-context'; -import { TelegrafModuleOptions } from '../interfaces'; +import { ListenerMetadata, TelegrafModuleOptions } from '../interfaces'; @Injectable() export class ListenersExplorerService extends BaseExplorerService - implements OnModuleInit { + implements OnModuleInit +{ private readonly telegrafParamsFactory = new TelegrafParamsFactory(); private bot: Telegraf; @@ -74,13 +75,20 @@ export class ListenersExplorerService this.filterScenes(wrapper), ); scenes.forEach((wrapper) => { - const sceneId = this.metadataAccessor.getSceneMetadata( + const { sceneId, type, options } = this.metadataAccessor.getSceneMetadata( wrapper.instance.constructor, ); - const scene = new Scenes.BaseScene(sceneId); + const scene = + type === 'base' + ? new Scenes.BaseScene(sceneId, options || ({} as any)) + : new Scenes.WizardScene(sceneId, options || ({} as any)); this.stage.register(scene); - this.registerListeners(scene, wrapper); + if (type === 'base') { + this.registerListeners(scene, wrapper); + } else { + this.registerWizardListeners(scene as Scenes.WizardScene, wrapper); + } }); } @@ -115,15 +123,72 @@ export class ListenersExplorerService ); } + private registerWizardListeners( + wizard: Scenes.WizardScene, + wrapper: InstanceWrapper, + ): void { + const { instance } = wrapper; + const prototype = Object.getPrototypeOf(instance); + + type WizardMetadata = { step: number; methodName: string }; + const wizardSteps: WizardMetadata[] = []; + const basicListeners = []; + + this.metadataScanner.scanFromPrototype( + instance, + prototype, + (methodName) => { + const methodRef = prototype[methodName]; + const metadata = this.metadataAccessor.getWizardStepMetadata(methodRef); + if (!metadata) { + basicListeners.push(methodName); + return undefined; + } + wizardSteps.push({ step: metadata.step, methodName }); + }, + ); + + for (const methodName of basicListeners) { + this.registerIfListener(wizard, instance, prototype, methodName); + } + + const group = wizardSteps + .sort((a, b) => a.step - b.step) + .reduce<{ [key: number]: WizardMetadata[] }>( + (prev, cur) => ({ + ...prev, + [cur.step]: [...(prev[cur.step] || []), cur], + }), + {}, + ); + + const steps = Object.values(group).map((stepsMetadata) => { + const composer = new Composer(); + stepsMetadata.forEach((stepMethod) => { + this.registerIfListener( + composer, + instance, + prototype, + stepMethod.methodName, + [{ method: 'use', args: [] }], + ); + }); + return composer.middleware(); + }); + + wizard.steps = steps; + } + private registerIfListener( composer: Composer, instance: any, prototype: any, methodName: string, + defaultMetadata?: ListenerMetadata[], ): void { const methodRef = prototype[methodName]; - const metadata = this.metadataAccessor.getListenerMetadata(methodRef); - if (!metadata) { + const metadata = this.metadataAccessor.getListenerMetadata(methodRef) || defaultMetadata; + if (!metadata || metadata.length < 1) { return undefined; } @@ -133,22 +198,22 @@ export class ListenersExplorerService methodName, ); - const { method, args } = metadata; + for (const { method, args } of metadata) { + /* Basic callback */ + // composer[method](...args, listenerCallbackFn); - /* Basic callback */ - // composer[method](...args, listenerCallbackFn); - - /* Complex callback return value handing */ - composer[method]( - ...args, - async (ctx: Context, next: Function): Promise => { - const result = await listenerCallbackFn(ctx, next); - if (result) { - await ctx.reply(String(result)); - } - // TODO-Possible-Feature: Add more supported return types - }, - ); + /* Complex callback return value handing */ + composer[method]( + ...args, + async (ctx: Context, next: Function): Promise => { + const result = await listenerCallbackFn(ctx, next); + if (result) { + await ctx.reply(String(result)); + } + // TODO-Possible-Feature: Add more supported return types + }, + ); + } } createContextCallback>( diff --git a/lib/services/metadata-accessor.service.ts b/lib/services/metadata-accessor.service.ts index 75fb79c..5020773 100644 --- a/lib/services/metadata-accessor.service.ts +++ b/lib/services/metadata-accessor.service.ts @@ -2,10 +2,15 @@ import { Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { SCENE_METADATA, - LISTENER_METADATA, + LISTENERS_METADATA, UPDATE_METADATA, + WIZARD_STEP_METADATA, } from '../telegraf.constants'; -import { ListenerMetadata } from '../interfaces'; +import { + ListenerMetadata, + SceneMetadata, + WizardStepMetadata, +} from '../interfaces'; @Injectable() export class MetadataAccessorService { @@ -21,11 +26,15 @@ export class MetadataAccessorService { return !!this.reflector.get(SCENE_METADATA, target); } - getListenerMetadata(target: Function): ListenerMetadata | undefined { - return this.reflector.get(LISTENER_METADATA, target); + getListenerMetadata(target: Function): ListenerMetadata[] | undefined { + return this.reflector.get(LISTENERS_METADATA, target); } - getSceneMetadata(target: Function): string | undefined { + getSceneMetadata(target: Function): SceneMetadata | undefined { return this.reflector.get(SCENE_METADATA, target); } + + getWizardStepMetadata(target: Function): WizardStepMetadata | undefined { + return this.reflector.get(WIZARD_STEP_METADATA, target); + } } diff --git a/lib/telegraf.constants.ts b/lib/telegraf.constants.ts index 56e78f2..26e4cd1 100644 --- a/lib/telegraf.constants.ts +++ b/lib/telegraf.constants.ts @@ -6,7 +6,8 @@ export const DEFAULT_BOT_NAME = 'DEFAULT_BOT_NAME'; export const UPDATE_METADATA = 'UPDATE_METADATA'; export const SCENE_METADATA = 'SCENE_METADATA'; -export const LISTENER_METADATA = 'LISTENER_METADATA'; +export const LISTENERS_METADATA = 'LISTENERS_METADATA'; +export const WIZARD_STEP_METADATA = 'WIZARD_STEP_METADATA'; export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA; diff --git a/lib/utils/create-listener-decorator.util.ts b/lib/utils/create-listener-decorator.util.ts index c423ff3..1b3cb9a 100644 --- a/lib/utils/create-listener-decorator.util.ts +++ b/lib/utils/create-listener-decorator.util.ts @@ -1,17 +1,29 @@ -import { SetMetadata } from '@nestjs/common'; import { Composer } from 'telegraf'; import { ComposerMethodArgs, OnlyFunctionPropertyNames } from '../types'; -import { LISTENER_METADATA } from '../telegraf.constants'; +import { LISTENERS_METADATA } from '../telegraf.constants'; import { ListenerMetadata } from '../interfaces'; export function createListenerDecorator< TComposer extends Composer, - TMethod extends OnlyFunctionPropertyNames = OnlyFunctionPropertyNames + TMethod extends OnlyFunctionPropertyNames = OnlyFunctionPropertyNames, >(method: TMethod) { return (...args: ComposerMethodArgs): MethodDecorator => { - return SetMetadata(LISTENER_METADATA, { - method, - args, - } as ListenerMetadata); + return ( + _target: any, + _key?: string | symbol, + descriptor?: TypedPropertyDescriptor, + ) => { + const metadata = [ + { + method, + args, + } as ListenerMetadata, + ]; + + const previousValue = Reflect.getMetadata(LISTENERS_METADATA, descriptor.value) || []; + const value = [...previousValue, ...metadata]; + Reflect.defineMetadata(LISTENERS_METADATA, value, descriptor.value); + return descriptor; + }; }; }