mirror of
				https://github.com/Maks1mS/nestjs-telegraf.git
				synced 2025-11-04 07:51:22 +03:00 
			
		
		
		
	Merge pull request #469 from xTCry/features
Added wizard scene decorators & support multiple method listeners
This commit is contained in:
		@@ -1,3 +1,4 @@
 | 
				
			|||||||
export * from './update.decorator';
 | 
					export * from './update.decorator';
 | 
				
			||||||
export * from './scene.decorator';
 | 
					export * from './scene.decorator';
 | 
				
			||||||
 | 
					export * from './wizard.decorator';
 | 
				
			||||||
export * from './inject-bot.decorator';
 | 
					export * from './inject-bot.decorator';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,14 @@
 | 
				
			|||||||
import { SetMetadata } from '@nestjs/common';
 | 
					import { SetMetadata } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { SceneOptions } from 'telegraf/typings/scenes/base';
 | 
				
			||||||
 | 
					import { SceneMetadata } from '../../interfaces';
 | 
				
			||||||
import { SCENE_METADATA } from '../../telegraf.constants';
 | 
					import { SCENE_METADATA } from '../../telegraf.constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					export const Scene = (
 | 
				
			||||||
 * TODO
 | 
					  sceneId: string,
 | 
				
			||||||
 */
 | 
					  options?: SceneOptions<any>,
 | 
				
			||||||
export const Scene = (id: string): ClassDecorator =>
 | 
					): ClassDecorator =>
 | 
				
			||||||
  SetMetadata(SCENE_METADATA, id);
 | 
					  SetMetadata<string, SceneMetadata>(SCENE_METADATA, {
 | 
				
			||||||
 | 
					    sceneId,
 | 
				
			||||||
 | 
					    type: 'base',
 | 
				
			||||||
 | 
					    options,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								lib/decorators/core/wizard.decorator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								lib/decorators/core/wizard.decorator.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<any>,
 | 
				
			||||||
 | 
					): ClassDecorator =>
 | 
				
			||||||
 | 
					  SetMetadata<string, SceneMetadata>(SCENE_METADATA, {
 | 
				
			||||||
 | 
					    sceneId,
 | 
				
			||||||
 | 
					    type: 'wizard',
 | 
				
			||||||
 | 
					    options,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
@@ -1,2 +1,3 @@
 | 
				
			|||||||
export * from './scene-enter.decorator';
 | 
					export * from './scene-enter.decorator';
 | 
				
			||||||
export * from './scene-leave.decorator';
 | 
					export * from './scene-leave.decorator';
 | 
				
			||||||
 | 
					export * from './wizard-step.decorator';
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								lib/decorators/scene/wizard-step.decorator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								lib/decorators/scene/wizard-step.decorator.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<string, WizardStepMetadata>(WIZARD_STEP_METADATA, { step });
 | 
				
			||||||
@@ -1,2 +1,4 @@
 | 
				
			|||||||
export * from './telegraf-options.interface';
 | 
					export * from './telegraf-options.interface';
 | 
				
			||||||
export * from './listener-metadata.interface';
 | 
					export * from './listener-metadata.interface';
 | 
				
			||||||
 | 
					export * from './scene-metadata.interface';
 | 
				
			||||||
 | 
					export * from './telegraf-exception-filter.interface';
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								lib/interfaces/scene-metadata.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/interfaces/scene-metadata.interface.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { SceneOptions } from 'telegraf/typings/scenes/base';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SceneMetadata {
 | 
				
			||||||
 | 
					  sceneId: string;
 | 
				
			||||||
 | 
					  type: 'base' | 'wizard';
 | 
				
			||||||
 | 
					  options?: SceneOptions<any>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface WizardStepMetadata {
 | 
				
			||||||
 | 
					  step: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -17,12 +17,13 @@ import {
 | 
				
			|||||||
import { BaseExplorerService } from './base-explorer.service';
 | 
					import { BaseExplorerService } from './base-explorer.service';
 | 
				
			||||||
import { TelegrafParamsFactory } from '../factories/telegraf-params-factory';
 | 
					import { TelegrafParamsFactory } from '../factories/telegraf-params-factory';
 | 
				
			||||||
import { TelegrafContextType } from '../execution-context';
 | 
					import { TelegrafContextType } from '../execution-context';
 | 
				
			||||||
import { TelegrafModuleOptions } from '../interfaces';
 | 
					import { ListenerMetadata, TelegrafModuleOptions } from '../interfaces';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class ListenersExplorerService
 | 
					export class ListenersExplorerService
 | 
				
			||||||
  extends BaseExplorerService
 | 
					  extends BaseExplorerService
 | 
				
			||||||
  implements OnModuleInit {
 | 
					  implements OnModuleInit
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
  private readonly telegrafParamsFactory = new TelegrafParamsFactory();
 | 
					  private readonly telegrafParamsFactory = new TelegrafParamsFactory();
 | 
				
			||||||
  private bot: Telegraf<any>;
 | 
					  private bot: Telegraf<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,13 +75,20 @@ export class ListenersExplorerService
 | 
				
			|||||||
      this.filterScenes(wrapper),
 | 
					      this.filterScenes(wrapper),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    scenes.forEach((wrapper) => {
 | 
					    scenes.forEach((wrapper) => {
 | 
				
			||||||
      const sceneId = this.metadataAccessor.getSceneMetadata(
 | 
					      const { sceneId, type, options } = this.metadataAccessor.getSceneMetadata(
 | 
				
			||||||
        wrapper.instance.constructor,
 | 
					        wrapper.instance.constructor,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      const scene = new Scenes.BaseScene<any>(sceneId);
 | 
					      const scene =
 | 
				
			||||||
 | 
					        type === 'base'
 | 
				
			||||||
 | 
					          ? new Scenes.BaseScene<any>(sceneId, options || ({} as any))
 | 
				
			||||||
 | 
					          : new Scenes.WizardScene<any>(sceneId, options || ({} as any));
 | 
				
			||||||
      this.stage.register(scene);
 | 
					      this.stage.register(scene);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.registerListeners(scene, wrapper);
 | 
					      if (type === 'base') {
 | 
				
			||||||
 | 
					        this.registerListeners(scene, wrapper);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.registerWizardListeners(scene as Scenes.WizardScene<any>, wrapper);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -115,15 +123,72 @@ export class ListenersExplorerService
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private registerWizardListeners(
 | 
				
			||||||
 | 
					    wizard: Scenes.WizardScene<any>,
 | 
				
			||||||
 | 
					    wrapper: InstanceWrapper<unknown>,
 | 
				
			||||||
 | 
					  ): 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(
 | 
					  private registerIfListener(
 | 
				
			||||||
    composer: Composer<any>,
 | 
					    composer: Composer<any>,
 | 
				
			||||||
    instance: any,
 | 
					    instance: any,
 | 
				
			||||||
    prototype: any,
 | 
					    prototype: any,
 | 
				
			||||||
    methodName: string,
 | 
					    methodName: string,
 | 
				
			||||||
 | 
					    defaultMetadata?: ListenerMetadata[],
 | 
				
			||||||
  ): void {
 | 
					  ): void {
 | 
				
			||||||
    const methodRef = prototype[methodName];
 | 
					    const methodRef = prototype[methodName];
 | 
				
			||||||
    const metadata = this.metadataAccessor.getListenerMetadata(methodRef);
 | 
					    const metadata = this.metadataAccessor.getListenerMetadata(methodRef) || defaultMetadata;
 | 
				
			||||||
    if (!metadata) {
 | 
					    if (!metadata || metadata.length < 1) {
 | 
				
			||||||
      return undefined;
 | 
					      return undefined;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -133,22 +198,22 @@ export class ListenersExplorerService
 | 
				
			|||||||
      methodName,
 | 
					      methodName,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { method, args } = metadata;
 | 
					    for (const { method, args } of metadata) {
 | 
				
			||||||
 | 
					      /* Basic callback */
 | 
				
			||||||
 | 
					      // composer[method](...args, listenerCallbackFn);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Basic callback */
 | 
					      /* Complex callback return value handing */
 | 
				
			||||||
    // composer[method](...args, listenerCallbackFn);
 | 
					      composer[method](
 | 
				
			||||||
 | 
					        ...args,
 | 
				
			||||||
    /* Complex callback return value handing */
 | 
					        async (ctx: Context, next: Function): Promise<void> => {
 | 
				
			||||||
    composer[method](
 | 
					          const result = await listenerCallbackFn(ctx, next);
 | 
				
			||||||
      ...args,
 | 
					          if (result) {
 | 
				
			||||||
      async (ctx: Context, next: Function): Promise<void> => {
 | 
					            await ctx.reply(String(result));
 | 
				
			||||||
        const result = await listenerCallbackFn(ctx, next);
 | 
					          }
 | 
				
			||||||
        if (result) {
 | 
					          // TODO-Possible-Feature: Add more supported return types
 | 
				
			||||||
          await ctx.reply(String(result));
 | 
					        },
 | 
				
			||||||
        }
 | 
					      );
 | 
				
			||||||
        // TODO-Possible-Feature: Add more supported return types
 | 
					    }
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createContextCallback<T extends Record<string, unknown>>(
 | 
					  createContextCallback<T extends Record<string, unknown>>(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,15 @@ import { Injectable } from '@nestjs/common';
 | 
				
			|||||||
import { Reflector } from '@nestjs/core';
 | 
					import { Reflector } from '@nestjs/core';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  SCENE_METADATA,
 | 
					  SCENE_METADATA,
 | 
				
			||||||
  LISTENER_METADATA,
 | 
					  LISTENERS_METADATA,
 | 
				
			||||||
  UPDATE_METADATA,
 | 
					  UPDATE_METADATA,
 | 
				
			||||||
 | 
					  WIZARD_STEP_METADATA,
 | 
				
			||||||
} from '../telegraf.constants';
 | 
					} from '../telegraf.constants';
 | 
				
			||||||
import { ListenerMetadata } from '../interfaces';
 | 
					import {
 | 
				
			||||||
 | 
					  ListenerMetadata,
 | 
				
			||||||
 | 
					  SceneMetadata,
 | 
				
			||||||
 | 
					  WizardStepMetadata,
 | 
				
			||||||
 | 
					} from '../interfaces';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class MetadataAccessorService {
 | 
					export class MetadataAccessorService {
 | 
				
			||||||
@@ -21,11 +26,15 @@ export class MetadataAccessorService {
 | 
				
			|||||||
    return !!this.reflector.get(SCENE_METADATA, target);
 | 
					    return !!this.reflector.get(SCENE_METADATA, target);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getListenerMetadata(target: Function): ListenerMetadata | undefined {
 | 
					  getListenerMetadata(target: Function): ListenerMetadata[] | undefined {
 | 
				
			||||||
    return this.reflector.get(LISTENER_METADATA, target);
 | 
					    return this.reflector.get(LISTENERS_METADATA, target);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getSceneMetadata(target: Function): string | undefined {
 | 
					  getSceneMetadata(target: Function): SceneMetadata | undefined {
 | 
				
			||||||
    return this.reflector.get(SCENE_METADATA, target);
 | 
					    return this.reflector.get(SCENE_METADATA, target);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getWizardStepMetadata(target: Function): WizardStepMetadata | undefined {
 | 
				
			||||||
 | 
					    return this.reflector.get(WIZARD_STEP_METADATA, target);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,8 @@ export const DEFAULT_BOT_NAME = 'DEFAULT_BOT_NAME';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const UPDATE_METADATA = 'UPDATE_METADATA';
 | 
					export const UPDATE_METADATA = 'UPDATE_METADATA';
 | 
				
			||||||
export const SCENE_METADATA = 'SCENE_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;
 | 
					export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,29 @@
 | 
				
			|||||||
import { SetMetadata } from '@nestjs/common';
 | 
					 | 
				
			||||||
import { Composer } from 'telegraf';
 | 
					import { Composer } from 'telegraf';
 | 
				
			||||||
import { ComposerMethodArgs, OnlyFunctionPropertyNames } from '../types';
 | 
					import { ComposerMethodArgs, OnlyFunctionPropertyNames } from '../types';
 | 
				
			||||||
import { LISTENER_METADATA } from '../telegraf.constants';
 | 
					import { LISTENERS_METADATA } from '../telegraf.constants';
 | 
				
			||||||
import { ListenerMetadata } from '../interfaces';
 | 
					import { ListenerMetadata } from '../interfaces';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createListenerDecorator<
 | 
					export function createListenerDecorator<
 | 
				
			||||||
  TComposer extends Composer<never>,
 | 
					  TComposer extends Composer<never>,
 | 
				
			||||||
  TMethod extends OnlyFunctionPropertyNames<TComposer> = OnlyFunctionPropertyNames<TComposer>
 | 
					  TMethod extends OnlyFunctionPropertyNames<TComposer> = OnlyFunctionPropertyNames<TComposer>,
 | 
				
			||||||
>(method: TMethod) {
 | 
					>(method: TMethod) {
 | 
				
			||||||
  return (...args: ComposerMethodArgs<TComposer, TMethod>): MethodDecorator => {
 | 
					  return (...args: ComposerMethodArgs<TComposer, TMethod>): MethodDecorator => {
 | 
				
			||||||
    return SetMetadata(LISTENER_METADATA, {
 | 
					    return (
 | 
				
			||||||
      method,
 | 
					      _target: any,
 | 
				
			||||||
      args,
 | 
					      _key?: string | symbol,
 | 
				
			||||||
    } as ListenerMetadata);
 | 
					      descriptor?: TypedPropertyDescriptor<any>,
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					      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;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user