From a4cb8df4340845faa008ab12d1c910e42dda0a09 Mon Sep 17 00:00:00 2001 From: "xTCry [Vladislav Kh]" Date: Sun, 1 Aug 2021 07:31:09 +0300 Subject: [PATCH 1/2] feat(listeners): support for chaining method listeners --- lib/services/listeners-explorer.service.ts | 35 +++++++++++---------- lib/services/metadata-accessor.service.ts | 6 ++-- lib/telegraf.constants.ts | 2 +- lib/utils/create-listener-decorator.util.ts | 26 ++++++++++----- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/lib/services/listeners-explorer.service.ts b/lib/services/listeners-explorer.service.ts index 6a5dbc8..e4c145e 100644 --- a/lib/services/listeners-explorer.service.ts +++ b/lib/services/listeners-explorer.service.ts @@ -22,7 +22,8 @@ import { TelegrafModuleOptions } from '../interfaces'; @Injectable() export class ListenersExplorerService extends BaseExplorerService - implements OnModuleInit { + implements OnModuleInit +{ private readonly telegrafParamsFactory = new TelegrafParamsFactory(); private bot: Telegraf; @@ -123,7 +124,7 @@ export class ListenersExplorerService ): void { const methodRef = prototype[methodName]; const metadata = this.metadataAccessor.getListenerMetadata(methodRef); - if (!metadata) { + if (!metadata || metadata.length < 1) { return undefined; } @@ -133,22 +134,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..2e5b343 100644 --- a/lib/services/metadata-accessor.service.ts +++ b/lib/services/metadata-accessor.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { SCENE_METADATA, - LISTENER_METADATA, + LISTENERS_METADATA, UPDATE_METADATA, } from '../telegraf.constants'; import { ListenerMetadata } from '../interfaces'; @@ -21,8 +21,8 @@ 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 { diff --git a/lib/telegraf.constants.ts b/lib/telegraf.constants.ts index 56e78f2..70c6a90 100644 --- a/lib/telegraf.constants.ts +++ b/lib/telegraf.constants.ts @@ -6,7 +6,7 @@ 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 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; + }; }; } From 623ce16327c8edc9148a3ab47201c4e67df033ff Mon Sep 17 00:00:00 2001 From: "xTCry [Vladislav Kh]" Date: Tue, 3 Aug 2021 17:03:16 +0300 Subject: [PATCH 2/2] feat(scene): added wizard scene support --- lib/decorators/core/index.ts | 1 + lib/decorators/core/scene.decorator.ts | 16 ++-- lib/decorators/core/wizard.decorator.ts | 14 ++++ lib/decorators/scene/index.ts | 1 + lib/decorators/scene/wizard-step.decorator.ts | 6 ++ lib/interfaces/index.ts | 2 + lib/interfaces/scene-metadata.interface.ts | 11 +++ lib/services/listeners-explorer.service.ts | 74 +++++++++++++++++-- lib/services/metadata-accessor.service.ts | 13 +++- lib/telegraf.constants.ts | 1 + 10 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 lib/decorators/core/wizard.decorator.ts create mode 100644 lib/decorators/scene/wizard-step.decorator.ts create mode 100644 lib/interfaces/scene-metadata.interface.ts 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 e4c145e..a2348b8 100644 --- a/lib/services/listeners-explorer.service.ts +++ b/lib/services/listeners-explorer.service.ts @@ -17,7 +17,7 @@ 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 @@ -75,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); + } }); } @@ -116,14 +123,71 @@ 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); + const metadata = this.metadataAccessor.getListenerMetadata(methodRef) || defaultMetadata; if (!metadata || metadata.length < 1) { return undefined; } diff --git a/lib/services/metadata-accessor.service.ts b/lib/services/metadata-accessor.service.ts index 2e5b343..5020773 100644 --- a/lib/services/metadata-accessor.service.ts +++ b/lib/services/metadata-accessor.service.ts @@ -4,8 +4,13 @@ import { SCENE_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 { @@ -25,7 +30,11 @@ export class MetadataAccessorService { 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 70c6a90..26e4cd1 100644 --- a/lib/telegraf.constants.ts +++ b/lib/telegraf.constants.ts @@ -7,6 +7,7 @@ export const DEFAULT_BOT_NAME = 'DEFAULT_BOT_NAME'; export const UPDATE_METADATA = 'UPDATE_METADATA'; export const SCENE_METADATA = 'SCENE_METADATA'; export const LISTENERS_METADATA = 'LISTENERS_METADATA'; +export const WIZARD_STEP_METADATA = 'WIZARD_STEP_METADATA'; export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA;