!feat(): add scene support & many refactor

This commit is contained in:
Arthur 2020-12-29 22:41:06 +03:00
parent b3dc258c70
commit 8c72790226
23 changed files with 232 additions and 80 deletions

View File

@ -1,2 +1,3 @@
export * from './update.decorator';
export * from './scene.decorator';
export * from './inject-bot.decorator';

View File

@ -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);

View File

@ -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);

View File

@ -1,2 +1,3 @@
export * from './core';
export * from './listeners';
export * from './scene';

View File

@ -0,0 +1,2 @@
export * from './scene-enter.decorator';
export * from './scene-leave.decorator';

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,4 @@
export enum SceneEventType {
Enter = 'enter',
Leave = 'leave',
}

View File

@ -1,4 +1,3 @@
export * from './decorators';
export * from './interfaces';
export * from './telegraf.module';
export * from './telegraf.provider';

View File

@ -1,4 +0,0 @@
import { Context as TelegrafContext } from 'telegraf';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Context extends TelegrafContext {}

View File

@ -1,2 +1 @@
export * from './context.interface';
export * from './telegraf-options.interface';

View File

@ -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<string, Function>,
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);
}
}

View File

@ -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);
}
}

View File

@ -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';

View File

@ -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);
}
}

View File

@ -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<void> {
const { launchOptions } = this.options;
const telegraf = this.moduleRef.get(Telegraf);
await telegraf.launch(launchOptions);
}
async onApplicationShutdown(): Promise<void> {
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 {

View File

@ -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<Context>
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<void> {
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<void> {
await this.stop();
}
}

22
lib/telegraf.providers.ts Normal file
View File

@ -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,
},
];
}

1
sample/app.constants.ts Normal file
View File

@ -0,0 +1 @@
export const HELLO_SCENE_ID = 'HELLO_SCENE_ID';

View File

@ -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 {}

View File

@ -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<Context>,
private readonly bot: Telegraf<SceneContext>,
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<void> {
await ctx.scene.enter(HELLO_SCENE_ID);
}
@On('message')
async onMessage(ctx: Context): Promise<void> {
console.log('New message received');

View File

@ -0,0 +1,4 @@
import { SceneContext } from 'telegraf';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Context extends SceneContext {}

View File

@ -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<void> {
console.log('Enter to scene');
await ctx.reply('Welcome on scene ✋');
}
@SceneLeave()
async onSceneLeave(): Promise<void> {
console.log('Leave from scene');
await ctx.reply('Bye Bye 👋');
}
@Command('hello')
async onHelloCommand(ctx: Context): Promise<void> {
console.log('Use say hello');
await ctx.reply('Hi');
}
@Command('leave')
async onLeaveCommand(ctx: Context): Promise<void> {
await ctx.scene.leave();
}
}