diff --git a/lib/decorators/core/index.ts b/lib/decorators/core/index.ts index 7d609ad..baddc8e 100644 --- a/lib/decorators/core/index.ts +++ b/lib/decorators/core/index.ts @@ -1,2 +1,3 @@ export * from './update.decorator'; +export * from './scene.decorator'; export * from './inject-bot.decorator'; diff --git a/lib/decorators/core/inject-bot.decorator.ts b/lib/decorators/core/inject-bot.decorator.ts index f697bd7..d010fbe 100644 --- a/lib/decorators/core/inject-bot.decorator.ts +++ b/lib/decorators/core/inject-bot.decorator.ts @@ -1,4 +1,4 @@ import { Inject } from '@nestjs/common'; -import { TelegrafProvider } from '../../telegraf.provider'; +import { Telegraf } from 'telegraf'; -export const InjectBot = (): ParameterDecorator => Inject(TelegrafProvider); +export const InjectBot = (): ParameterDecorator => Inject(Telegraf); diff --git a/lib/decorators/core/scene.decorator.ts b/lib/decorators/core/scene.decorator.ts new file mode 100644 index 0000000..391c842 --- /dev/null +++ b/lib/decorators/core/scene.decorator.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; +import { SCENE_METADATA } from '../../telegraf.constants'; + +/** + * TODO + */ +export const Scene = (id: string): ClassDecorator => + SetMetadata(SCENE_METADATA, id); diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index f6f8104..31fa986 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -1,2 +1,3 @@ export * from './core'; export * from './listeners'; +export * from './scene'; diff --git a/lib/decorators/scene/index.ts b/lib/decorators/scene/index.ts new file mode 100644 index 0000000..b1d61b2 --- /dev/null +++ b/lib/decorators/scene/index.ts @@ -0,0 +1,2 @@ +export * from './scene-enter.decorator'; +export * from './scene-leave.decorator'; diff --git a/lib/decorators/scene/scene-enter.decorator.ts b/lib/decorators/scene/scene-enter.decorator.ts new file mode 100644 index 0000000..7307e48 --- /dev/null +++ b/lib/decorators/scene/scene-enter.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; +import { SCENE_LISTENER_METADATA } from '../../telegraf.constants'; +import { SceneEventType } from '../../enums/scene-event-type.enum'; + +export const SceneEnter = (): MethodDecorator => + SetMetadata(SCENE_LISTENER_METADATA, SceneEventType.Enter); diff --git a/lib/decorators/scene/scene-leave.decorator.ts b/lib/decorators/scene/scene-leave.decorator.ts new file mode 100644 index 0000000..09eb509 --- /dev/null +++ b/lib/decorators/scene/scene-leave.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; +import { SCENE_LISTENER_METADATA } from '../../telegraf.constants'; +import { SceneEventType } from '../../enums/scene-event-type.enum'; + +export const SceneLeave = (): MethodDecorator => + SetMetadata(SCENE_LISTENER_METADATA, SceneEventType.Leave); diff --git a/lib/enums/scene-event-type.enum.ts b/lib/enums/scene-event-type.enum.ts new file mode 100644 index 0000000..ebd37d2 --- /dev/null +++ b/lib/enums/scene-event-type.enum.ts @@ -0,0 +1,4 @@ +export enum SceneEventType { + Enter = 'enter', + Leave = 'leave', +} diff --git a/lib/index.ts b/lib/index.ts index bca10ec..a27fc83 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,3 @@ export * from './decorators'; export * from './interfaces'; export * from './telegraf.module'; -export * from './telegraf.provider'; diff --git a/lib/interfaces/context.interface.ts b/lib/interfaces/context.interface.ts deleted file mode 100644 index 97001b3..0000000 --- a/lib/interfaces/context.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Context as TelegrafContext } from 'telegraf'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Context extends TelegrafContext {} diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index 3b25926..23e7d9f 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -1,2 +1 @@ -export * from './context.interface'; export * from './telegraf-options.interface'; diff --git a/lib/telegraf-scene.explorer.ts b/lib/telegraf-scene.explorer.ts new file mode 100644 index 0000000..74baad9 --- /dev/null +++ b/lib/telegraf-scene.explorer.ts @@ -0,0 +1,74 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { DiscoveryService } from '@nestjs/core'; +import { MetadataScanner } from '@nestjs/core/metadata-scanner'; +import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; +import { BaseScene as Scene, Stage, Telegraf } from 'telegraf'; +import { TelegrafMetadataAccessor } from './telegraf.metadata-accessor'; + +@Injectable() +export class TelegrafSceneExplorer implements OnModuleInit { + constructor( + @Inject(Telegraf) + private readonly telegraf: Telegraf, + private readonly discoveryService: DiscoveryService, + private readonly metadataAccessor: TelegrafMetadataAccessor, + private readonly metadataScanner: MetadataScanner, + ) {} + + onModuleInit(): void { + this.explore(); + } + + private explore(): void { + const sceneClasses = this.filterSceneClasses(); + const stage = new Stage(); + + sceneClasses.forEach((wrapper) => { + const { instance } = wrapper; + + const sceneId = this.metadataAccessor.getSceneMetadata( + instance.constructor, + ); + const scene = new Scene(sceneId); + stage.register(scene); + + const prototype = Object.getPrototypeOf(instance); + this.metadataScanner.scanFromPrototype( + instance, + prototype, + (methodKey: string) => + this.registerIfListener(scene, instance, methodKey), + ); + + stage.register(scene); + }); + + this.telegraf.use(stage.middleware()); + } + + private filterSceneClasses(): InstanceWrapper[] { + return this.discoveryService + .getProviders() + .filter((wrapper) => wrapper.instance) + .filter((wrapper) => + this.metadataAccessor.isScene(wrapper.instance.constructor), + ); + } + + private registerIfListener( + scene: Scene, + instance: Record, + methodKey: string, + ): void { + const methodRef = instance[methodKey]; + const middlewareFn = methodRef.bind(instance); + + const listenerMetadata = this.metadataAccessor.getListenerMetadata( + methodRef, + ); + if (!listenerMetadata) return; + + const { method, args } = listenerMetadata; + (scene[method] as any)(...args, middlewareFn); + } +} diff --git a/lib/telegraf.explorer.ts b/lib/telegraf-update.explorer.ts similarity index 77% rename from lib/telegraf.explorer.ts rename to lib/telegraf-update.explorer.ts index f52d601..4e4640c 100644 --- a/lib/telegraf.explorer.ts +++ b/lib/telegraf-update.explorer.ts @@ -1,15 +1,15 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { DiscoveryService } from '@nestjs/core'; import { MetadataScanner } from '@nestjs/core/metadata-scanner'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; -import { Composer } from 'telegraf'; +import { Telegraf } from 'telegraf'; import { TelegrafMetadataAccessor } from './telegraf.metadata-accessor'; -import { TelegrafProvider } from './telegraf.provider'; @Injectable() -export class TelegrafExplorer implements OnModuleInit { +export class TelegrafUpdateExplorer implements OnModuleInit { constructor( - private readonly telegraf: TelegrafProvider, + @Inject(Telegraf) + private readonly telegraf: Telegraf, private readonly discoveryService: DiscoveryService, private readonly metadataAccessor: TelegrafMetadataAccessor, private readonly metadataScanner: MetadataScanner, @@ -19,7 +19,7 @@ export class TelegrafExplorer implements OnModuleInit { this.explore(); } - explore(): void { + private explore(): void { const updateClasses = this.filterUpdateClasses(); updateClasses.forEach((wrapper) => { @@ -56,10 +56,8 @@ export class TelegrafExplorer implements OnModuleInit { if (!listenerMetadata) return; const { method, args } = listenerMetadata; - const composerMiddlewareFn = Composer[method](...args, middlewareFn); - - console.log('composerMiddlewareFn', composerMiddlewareFn); - - this.telegraf.use(composerMiddlewareFn); + // NOTE: Use "any" to disable "Expected at least 1 arguments, but got 1 or more." error. + // Use telegraf instance for non-scene listeners + (this.telegraf[method] as any)(...args, middlewareFn); } } diff --git a/lib/telegraf.constants.ts b/lib/telegraf.constants.ts index 1790594..e7ce73e 100644 --- a/lib/telegraf.constants.ts +++ b/lib/telegraf.constants.ts @@ -1,6 +1,8 @@ +export const STAGE_MIDDLEWARE = 'StageMiddleware'; export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS'; export const UPDATE_METADATA = 'UPDATE_METADATA'; export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA'; export const SCENE_METADATA = 'SCENE_METADATA'; +export const SCENE_LISTENER_METADATA = 'SCENE_LISTENER_METADATA'; diff --git a/lib/telegraf.metadata-accessor.ts b/lib/telegraf.metadata-accessor.ts index c2ec1ef..19e58b5 100644 --- a/lib/telegraf.metadata-accessor.ts +++ b/lib/telegraf.metadata-accessor.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { + SCENE_METADATA, UPDATE_LISTENER_METADATA, UPDATE_METADATA, } from './telegraf.constants'; @@ -14,7 +15,15 @@ export class TelegrafMetadataAccessor { return !!this.reflector.get(UPDATE_METADATA, target); } + isScene(target: Function): boolean { + return !!this.reflector.get(SCENE_METADATA, target); + } + getListenerMetadata(target: Function): ListenerMetadata | undefined { return this.reflector.get(UPDATE_LISTENER_METADATA, target); } + + getSceneMetadata(target: Function): string | undefined { + return this.reflector.get(SCENE_METADATA, target); + } } diff --git a/lib/telegraf.module.ts b/lib/telegraf.module.ts index f0e82a2..f40ca9e 100644 --- a/lib/telegraf.module.ts +++ b/lib/telegraf.module.ts @@ -1,22 +1,53 @@ -import { DiscoveryModule } from '@nestjs/core'; -import { Module, DynamicModule, Provider } from '@nestjs/common'; +import { DiscoveryModule, ModuleRef } from '@nestjs/core'; +import { + DynamicModule, + Inject, + Module, + OnApplicationBootstrap, + OnApplicationShutdown, + Provider, +} from '@nestjs/common'; +import { Telegraf } from 'telegraf'; import { - TelegrafModuleOptions, TelegrafModuleAsyncOptions, + TelegrafModuleOptions, TelegrafOptionsFactory, } from './interfaces'; import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants'; import { TelegrafMetadataAccessor } from './telegraf.metadata-accessor'; -import { TelegrafExplorer } from './telegraf.explorer'; -import { TelegrafProvider } from './telegraf.provider'; +import { TelegrafUpdateExplorer } from './telegraf-update.explorer'; +import { TelegrafSceneExplorer } from './telegraf-scene.explorer'; +import { createProviders, TelegrafProvider } from './telegraf.providers'; @Module({ imports: [DiscoveryModule], - providers: [TelegrafMetadataAccessor, TelegrafExplorer], + providers: [ + TelegrafMetadataAccessor, + TelegrafSceneExplorer, + TelegrafUpdateExplorer, + ], }) -export class TelegrafModule { +export class TelegrafModule + implements OnApplicationBootstrap, OnApplicationShutdown { + constructor( + @Inject(TELEGRAF_MODULE_OPTIONS) + private readonly options: TelegrafModuleOptions, + private readonly moduleRef: ModuleRef, + ) {} + + async onApplicationBootstrap(): Promise { + const { launchOptions } = this.options; + const telegraf = this.moduleRef.get(Telegraf); + await telegraf.launch(launchOptions); + } + + async onApplicationShutdown(): Promise { + const telegraf = this.moduleRef.get(Telegraf); + await telegraf.stop(); + } + public static forRoot(options: TelegrafModuleOptions): DynamicModule { - const providers = [...this.createProviders(options), TelegrafProvider]; + const providers = [...createProviders(options), TelegrafProvider]; return { module: TelegrafModule, @@ -25,15 +56,6 @@ export class TelegrafModule { }; } - private static createProviders(options: TelegrafModuleOptions): Provider[] { - return [ - { - provide: TELEGRAF_MODULE_OPTIONS, - useValue: options, - }, - ]; - } - public static forRootAsync( options: TelegrafModuleAsyncOptions, ): DynamicModule { diff --git a/lib/telegraf.provider.ts b/lib/telegraf.provider.ts deleted file mode 100644 index 0602704..0000000 --- a/lib/telegraf.provider.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - Injectable, - Inject, - OnApplicationBootstrap, - Logger, - OnApplicationShutdown, -} from '@nestjs/common'; -import { Telegraf } from 'telegraf'; -import { Context, TelegrafModuleOptions } from './interfaces'; -import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants'; - -@Injectable() -export class TelegrafProvider - extends Telegraf - implements OnApplicationBootstrap, OnApplicationShutdown { - private logger = new Logger('Telegraf'); - private readonly launchOptions; - - constructor(@Inject(TELEGRAF_MODULE_OPTIONS) options: TelegrafModuleOptions) { - super(options.token, options.options); - this.launchOptions = options.launchOptions; - } - - async onApplicationBootstrap(): Promise { - this.catch(async (err, ctx) => { - this.logger.error( - `Encountered an error for ${ctx.updateType} update type`, - err as string, - ); - }); - - await this.launch(this.launchOptions); - } - - async onApplicationShutdown(): Promise { - await this.stop(); - } -} diff --git a/lib/telegraf.providers.ts b/lib/telegraf.providers.ts new file mode 100644 index 0000000..b7ac54a --- /dev/null +++ b/lib/telegraf.providers.ts @@ -0,0 +1,22 @@ +import { Provider } from '@nestjs/common'; +import { session, Telegraf } from 'telegraf'; +import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants'; +import { TelegrafModuleOptions } from './interfaces'; + +export const TelegrafProvider = { + provide: Telegraf, + inject: [TELEGRAF_MODULE_OPTIONS], + useFactory: (options: TelegrafModuleOptions) => { + const telegraf = new Telegraf(options.token, options.options); + return telegraf; + }, +}; + +export function createProviders(options: TelegrafModuleOptions): Provider[] { + return [ + { + provide: TELEGRAF_MODULE_OPTIONS, + useValue: options, + }, + ]; +} diff --git a/sample/app.constants.ts b/sample/app.constants.ts new file mode 100644 index 0000000..b9cfdda --- /dev/null +++ b/sample/app.constants.ts @@ -0,0 +1 @@ +export const HELLO_SCENE_ID = 'HELLO_SCENE_ID'; diff --git a/sample/app.module.ts b/sample/app.module.ts index 19b71ac..8d4f95d 100644 --- a/sample/app.module.ts +++ b/sample/app.module.ts @@ -2,13 +2,14 @@ import { Module } from '@nestjs/common'; import { TelegrafModule } from '../lib'; import { EchoService } from './echo.service'; import { AppUpdate } from './app.update'; +import { HelloScene } from './scenes/hello.scene'; @Module({ imports: [ TelegrafModule.forRoot({ - token: '1467731595:AAHCvH65H9VQYKF9jE-E8c2rXsQBVAYseg8', + token: '1467731595:AAHCvH65H9VQYKF9jE-E8c2rXsQBVAYseg8', // Don't steal >:( }), ], - providers: [EchoService, AppUpdate], + providers: [EchoService, AppUpdate, HelloScene], }) export class AppModule {} diff --git a/sample/app.update.ts b/sample/app.update.ts index dab2a76..402e9fb 100644 --- a/sample/app.update.ts +++ b/sample/app.update.ts @@ -1,13 +1,14 @@ -import { Telegraf } from 'telegraf'; -import { Help, InjectBot, On, Start, Update } from '../lib/decorators'; -import { Context } from '../lib/interfaces'; +import { SceneContext, Telegraf } from 'telegraf'; +import { Command, Help, InjectBot, On, Start, Update } from '../lib'; import { EchoService } from './echo.service'; +import { HELLO_SCENE_ID } from './app.constants'; +import { Context } from './interfaces/context.interface'; @Update() export class AppUpdate { constructor( @InjectBot() - private readonly bot: Telegraf, + private readonly bot: Telegraf, private readonly echoService: EchoService, ) {} @@ -22,6 +23,11 @@ export class AppUpdate { await ctx.reply('Send me any text'); } + @Command('scene') + async onSceneCommand(ctx: Context): Promise { + await ctx.scene.enter(HELLO_SCENE_ID); + } + @On('message') async onMessage(ctx: Context): Promise { console.log('New message received'); diff --git a/sample/interfaces/context.interface.ts b/sample/interfaces/context.interface.ts new file mode 100644 index 0000000..fcefe97 --- /dev/null +++ b/sample/interfaces/context.interface.ts @@ -0,0 +1,4 @@ +import { SceneContext } from 'telegraf'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface Context extends SceneContext {} diff --git a/sample/scenes/hello.scene.ts b/sample/scenes/hello.scene.ts new file mode 100644 index 0000000..beab49a --- /dev/null +++ b/sample/scenes/hello.scene.ts @@ -0,0 +1,29 @@ +import { HELLO_SCENE_ID } from '../app.constants'; +import { Context } from '../interfaces/context.interface'; +import { Scene, SceneEnter, SceneLeave, Command } from '../../lib'; + +@Scene(HELLO_SCENE_ID) +export class HelloScene { + @SceneEnter() + async onSceneEnter(ctx: Context): Promise { + console.log('Enter to scene'); + await ctx.reply('Welcome on scene ✋'); + } + + @SceneLeave() + async onSceneLeave(): Promise { + console.log('Leave from scene'); + await ctx.reply('Bye Bye 👋'); + } + + @Command('hello') + async onHelloCommand(ctx: Context): Promise { + console.log('Use say hello'); + await ctx.reply('Hi'); + } + + @Command('leave') + async onLeaveCommand(ctx: Context): Promise { + await ctx.scene.leave(); + } +}