From ccb2db01065256376848f4cc4a56cda6d2f61d6d Mon Sep 17 00:00:00 2001 From: Alexander Bukhalo Date: Sat, 2 Jan 2021 01:27:01 +0300 Subject: [PATCH] feat(): multiple instances support --- lib/decorators/inject-bot.decorator.ts | 5 +- lib/index.ts | 6 +- lib/interfaces/index.ts | 1 + lib/interfaces/telegraf-options.interface.ts | 5 + lib/interfaces/update-metadata.interface.ts | 6 + lib/services/base-explorer.service.ts | 59 ++++++++ lib/services/index.ts | 2 + .../metadata-accessor.service.ts} | 6 +- .../updates-explorer.service.ts} | 138 ++++++++++-------- lib/telegraf-core.module.ts | 109 +++++++++++--- lib/telegraf.constants.ts | 3 +- lib/telegraf.provider.ts | 37 ----- lib/utils/get-bot-token.util.ts | 7 + lib/utils/index.ts | 1 + 14 files changed, 263 insertions(+), 122 deletions(-) create mode 100644 lib/interfaces/update-metadata.interface.ts create mode 100644 lib/services/base-explorer.service.ts create mode 100644 lib/services/index.ts rename lib/{telegraf-metadata.accessor.ts => services/metadata-accessor.service.ts} (97%) rename lib/{telegraf.explorer.ts => services/updates-explorer.service.ts} (64%) delete mode 100644 lib/telegraf.provider.ts create mode 100644 lib/utils/get-bot-token.util.ts create mode 100644 lib/utils/index.ts diff --git a/lib/decorators/inject-bot.decorator.ts b/lib/decorators/inject-bot.decorator.ts index 84f1c50..47808e3 100644 --- a/lib/decorators/inject-bot.decorator.ts +++ b/lib/decorators/inject-bot.decorator.ts @@ -1,4 +1,5 @@ import { Inject } from '@nestjs/common'; -import { TELEGRAF_PROVIDER } from '../telegraf.constants'; +import { getBotToken } from '../utils'; -export const InjectBot = (): ParameterDecorator => Inject(TELEGRAF_PROVIDER); +export const InjectBot = (name?: string): ParameterDecorator => + Inject(getBotToken(name)); diff --git a/lib/index.ts b/lib/index.ts index 39b657d..819279a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -9,4 +9,8 @@ export * as Extra from 'telegraf/extra'; export * from './decorators'; export * from './interfaces'; export * from './telegraf.module'; -export * from './telegraf.provider'; + +/** + * Backward compatibility with versions < 1.4.0 + */ +export { Telegraf as TelegrafProvider } from 'telegraf'; diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index 3b25926..a403036 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -1,2 +1,3 @@ export * from './context.interface'; export * from './telegraf-options.interface'; +export * from './update-metadata.interface'; diff --git a/lib/interfaces/telegraf-options.interface.ts b/lib/interfaces/telegraf-options.interface.ts index 13c729d..60e96f2 100644 --- a/lib/interfaces/telegraf-options.interface.ts +++ b/lib/interfaces/telegraf-options.interface.ts @@ -4,6 +4,7 @@ import { LaunchPollingOptions, LaunchWebhookOptions, } from 'telegraf/typings/telegraf'; +import { Middleware } from 'telegraf/typings/composer'; export interface TelegrafModuleOptions { token: string; @@ -12,6 +13,9 @@ export interface TelegrafModuleOptions { polling?: LaunchPollingOptions; webhook?: LaunchWebhookOptions; }; + botName?: string; + include?: Function[]; + middlewares?: ReadonlyArray>; } export interface TelegrafOptionsFactory { @@ -20,6 +24,7 @@ export interface TelegrafOptionsFactory { export interface TelegrafModuleAsyncOptions extends Pick { + botName?: string; useExisting?: Type; useClass?: Type; useFactory?: ( diff --git a/lib/interfaces/update-metadata.interface.ts b/lib/interfaces/update-metadata.interface.ts new file mode 100644 index 0000000..4c2ba0e --- /dev/null +++ b/lib/interfaces/update-metadata.interface.ts @@ -0,0 +1,6 @@ +export interface UpdateMetadata { + name: string; + type: string; + methodName: string; + callback?: Function | Record; +} diff --git a/lib/services/base-explorer.service.ts b/lib/services/base-explorer.service.ts new file mode 100644 index 0000000..eff7c82 --- /dev/null +++ b/lib/services/base-explorer.service.ts @@ -0,0 +1,59 @@ +import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; +import { Module } from '@nestjs/core/injector/module'; +import { flattenDeep, groupBy, identity, isEmpty, mapValues } from 'lodash'; +import { UpdateMetadata } from '../interfaces'; + +export class BaseExplorerService { + getModules( + modulesContainer: Map, + include: Function[], + ): Module[] { + if (!include || isEmpty(include)) { + return [...modulesContainer.values()]; + } + const whitelisted = this.includeWhitelisted(modulesContainer, include); + return whitelisted; + } + + includeWhitelisted( + modulesContainer: Map, + include: Function[], + ): Module[] { + const modules = [...modulesContainer.values()]; + return modules.filter(({ metatype }) => + include.some((item) => item === metatype), + ); + } + + flatMap( + modules: Module[], + callback: (instance: InstanceWrapper, moduleRef: Module) => T | T[], + ): T[] { + const invokeMap = () => { + return modules.map((moduleRef) => { + const providers = [...moduleRef.providers.values()]; + return providers.map((wrapper) => callback(wrapper, moduleRef)); + }); + }; + return flattenDeep(invokeMap()).filter(identity); + } + + groupMetadata(resolvers: UpdateMetadata[]) { + const groupByType = groupBy( + resolvers, + (metadata: UpdateMetadata) => metadata.type, + ); + const groupedMetadata = mapValues( + groupByType, + (resolversArr: UpdateMetadata[]) => + resolversArr.reduce( + (prev, curr) => ({ + ...prev, + [curr.name]: curr.callback, + }), + {}, + ), + ); + return groupedMetadata; + } +} diff --git a/lib/services/index.ts b/lib/services/index.ts new file mode 100644 index 0000000..9514e05 --- /dev/null +++ b/lib/services/index.ts @@ -0,0 +1,2 @@ +export * from './updates-explorer.service'; +export * from './metadata-accessor.service'; diff --git a/lib/telegraf-metadata.accessor.ts b/lib/services/metadata-accessor.service.ts similarity index 97% rename from lib/telegraf-metadata.accessor.ts rename to lib/services/metadata-accessor.service.ts index b204d60..a4c0ac7 100644 --- a/lib/telegraf-metadata.accessor.ts +++ b/lib/services/metadata-accessor.service.ts @@ -12,11 +12,11 @@ import { OnOptions, PhoneOptions, UpdateHookOptions, -} from './decorators'; -import { DECORATORS } from './telegraf.constants'; +} from '../decorators'; +import { DECORATORS } from '../telegraf.constants'; @Injectable() -export class TelegrafMetadataAccessor { +export class MetadataAccessorService { constructor(private readonly reflector: Reflector) {} isUpdate(target: Type | Function): boolean { diff --git a/lib/telegraf.explorer.ts b/lib/services/updates-explorer.service.ts similarity index 64% rename from lib/telegraf.explorer.ts rename to lib/services/updates-explorer.service.ts index 4ddefd6..31d4325 100644 --- a/lib/telegraf.explorer.ts +++ b/lib/services/updates-explorer.service.ts @@ -1,10 +1,12 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { DiscoveryService, ModuleRef } from '@nestjs/core'; +import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { DiscoveryService, ModuleRef, ModulesContainer } from '@nestjs/core'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { MetadataScanner } from '@nestjs/core/metadata-scanner'; -import { TelegrafMetadataAccessor } from './telegraf-metadata.accessor'; -import { TelegrafProvider } from './telegraf.provider'; -import { TELEGRAF_PROVIDER } from './telegraf.constants'; +import { MetadataAccessorService } from './metadata-accessor.service'; +import { + TELEGRAF_BOT_NAME, + TELEGRAF_MODULE_OPTIONS, +} from '../telegraf.constants'; import { ActionOptions, CashtagOptions, @@ -17,65 +19,88 @@ import { OnOptions, PhoneOptions, UpdateHookOptions, -} from './decorators'; +} from '../decorators'; +import { Telegraf } from 'telegraf'; +import { TelegrafModuleOptions } from '../interfaces'; +import { BaseExplorerService } from './base-explorer.service'; +import { Module } from '@nestjs/core/injector/module'; @Injectable() -export class TelegrafExplorer implements OnModuleInit { +export class UpdatesExplorerService + extends BaseExplorerService + implements OnModuleInit { + private readonly logger = new Logger(UpdatesExplorerService.name); + constructor( + @Inject(TELEGRAF_BOT_NAME) + private readonly botName: string, + @Inject(TELEGRAF_MODULE_OPTIONS) + private readonly telegrafModuleOptions: TelegrafModuleOptions, private readonly moduleRef: ModuleRef, private readonly discoveryService: DiscoveryService, - private readonly metadataAccessor: TelegrafMetadataAccessor, + private readonly metadataAccessor: MetadataAccessorService, private readonly metadataScanner: MetadataScanner, - ) {} + private readonly modulesContainer: ModulesContainer, + ) { + super(); + } - private telegraf: TelegrafProvider; + private bot: Telegraf; - onModuleInit() { - this.telegraf = this.moduleRef.get(TELEGRAF_PROVIDER, { + onModuleInit(): void { + this.logger.debug(this.botName); + this.bot = this.moduleRef.get>(this.botName, { strict: false, }); this.explore(); } explore() { - /** - * Update providers section is only for decorators under Update decorator - */ - const updateProviders: InstanceWrapper[] = this.discoveryService - .getProviders() - .filter((wrapper: InstanceWrapper) => + const modules = this.getModules( + this.modulesContainer, + this.telegrafModuleOptions.include || [], + ); + const updates = this.flatMap(modules, (instance, moduleRef) => + this.applyUpdates(instance, moduleRef), + ); + } + + private applyUpdates(wrapper: InstanceWrapper, moduleRef: Module) { + const { instance } = wrapper; + if (!instance) { + return undefined; + } + const prototype = Object.getPrototypeOf(instance); + + const providers: InstanceWrapper[] = this.discoveryService.getProviders(); + const updateProviders: InstanceWrapper[] = providers.filter( + (wrapper: InstanceWrapper) => this.metadataAccessor.isUpdate(wrapper.metatype), - ); + ); updateProviders.forEach((wrapper: InstanceWrapper) => { const { instance } = wrapper; - - this.metadataScanner.scanFromPrototype( - instance, - Object.getPrototypeOf(instance), - (key: string) => { - if (this.metadataAccessor.isUpdateHook(instance[key])) { - const metadata = this.metadataAccessor.getUpdateHookMetadata( - instance[key], - ); - this.handleUpdateHook(instance, key, metadata); - } - }, - ); + if (!instance) { + return undefined; + } + this.metadataScanner.scanFromPrototype(instance, prototype, (name) => { + if (this.metadataAccessor.isUpdateHook(instance[name])) { + const metadata = this.metadataAccessor.getUpdateHookMetadata( + instance[name], + ); + this.handleUpdateHook(instance, name, metadata); + } + }); }); - const providers: InstanceWrapper[] = this.discoveryService.getProviders(); - providers.forEach((wrapper: InstanceWrapper) => { const { instance } = wrapper; - if (!instance) { - return; + return undefined; } - this.metadataScanner.scanFromPrototype( instance, - Object.getPrototypeOf(instance), + prototype, (key: string) => { if (this.metadataAccessor.isTelegrafUse(instance[key])) { this.handleTelegrafUse(instance, key); @@ -146,19 +171,19 @@ export class TelegrafExplorer implements OnModuleInit { } handleUpdateHook(instance: object, key: string, metadata: UpdateHookOptions) { - this.telegraf.on(metadata.updateType, instance[key].bind(instance)); + this.bot.on(metadata.updateType, instance[key].bind(instance)); } handleTelegrafUse(instance: object, key: string) { - this.telegraf.use(instance[key].bind(instance)); + this.bot.use(instance[key].bind(instance)); } handleTelegrafOn(instance: object, key: string, metadata: OnOptions) { - this.telegraf.on(metadata.updateTypes, instance[key].bind(instance)); + this.bot.on(metadata.updateTypes, instance[key].bind(instance)); } handleTelegrafHears(instance: object, key: string, metadata: HearsOptions) { - this.telegraf.hears(metadata.triggers, instance[key].bind(instance)); + this.bot.hears(metadata.triggers, instance[key].bind(instance)); } handleTelegrafCommand( @@ -166,25 +191,25 @@ export class TelegrafExplorer implements OnModuleInit { key: string, metadata: CommandOptions, ) { - this.telegraf.command(metadata.commands, instance[key].bind(instance)); + this.bot.command(metadata.commands, instance[key].bind(instance)); } handleTelegrafStart(instance: object, key: string) { - this.telegraf.start(instance[key].bind(instance)); + this.bot.start(instance[key].bind(instance)); } handleTelegrafHelp(instance: object, key: string) { - this.telegraf.help(instance[key].bind(instance)); + this.bot.help(instance[key].bind(instance)); } handleTelegrafSettings(instance: object, key: string) { // @ts-ignore - this.telegraf.settings(instance[key].bind(instance)); + this.bot.settings(instance[key].bind(instance)); } handleTelegrafEntity(instance: object, key: string, metadata: EntityOptions) { // @ts-ignore - this.telegraf.entity(metadata.entity, instance[key].bind(instance)); + this.bot.entity(metadata.entity, instance[key].bind(instance)); } handleTelegrafMention( @@ -193,12 +218,12 @@ export class TelegrafExplorer implements OnModuleInit { metadata: MentionOptions, ) { // @ts-ignore - this.telegraf.mention(metadata.username, instance[key].bind(instance)); + this.bot.mention(metadata.username, instance[key].bind(instance)); } handleTelegrafPhone(instance: object, key: string, metadata: PhoneOptions) { // @ts-ignore - this.telegraf.phone(metadata.phone, instance[key].bind(instance)); + this.bot.phone(metadata.phone, instance[key].bind(instance)); } handleTelegrafHashtag( @@ -207,7 +232,7 @@ export class TelegrafExplorer implements OnModuleInit { metadata: HashtagOptions, ) { // @ts-ignore - this.telegraf.hashtag(metadata.hashtag, instance[key].bind(instance)); + this.bot.hashtag(metadata.hashtag, instance[key].bind(instance)); } handleTelegrafCashtag( @@ -216,11 +241,11 @@ export class TelegrafExplorer implements OnModuleInit { metadata: CashtagOptions, ) { // @ts-ignore - this.telegraf.cashtag(metadata.cashtag, instance[key].bind(instance)); + this.bot.cashtag(metadata.cashtag, instance[key].bind(instance)); } handleTelegrafAction(instance: object, key: string, metadata: ActionOptions) { - this.telegraf.action(metadata.triggers, instance[key].bind(instance)); + this.bot.action(metadata.triggers, instance[key].bind(instance)); } handleTelegrafInlineQuery( @@ -230,16 +255,13 @@ export class TelegrafExplorer implements OnModuleInit { ) { if (metadata.triggers) { // @ts-ignore - this.telegraf.inlineQuery( - metadata.triggers, - instance[key].bind(instance), - ); + this.bot.inlineQuery(metadata.triggers, instance[key].bind(instance)); } else { - this.telegraf.on(metadata.updateType, instance[key].bind(instance)); + this.bot.on(metadata.updateType, instance[key].bind(instance)); } } handleTelegrafGameQuery(instance: object, key: string) { - this.telegraf.gameQuery(instance[key].bind(instance)); + this.bot.gameQuery(instance[key].bind(instance)); } } diff --git a/lib/telegraf-core.module.ts b/lib/telegraf-core.module.ts index 9be69ed..7cf3703 100644 --- a/lib/telegraf-core.module.ts +++ b/lib/telegraf-core.module.ts @@ -1,56 +1,120 @@ -import { DiscoveryModule } from '@nestjs/core'; -import { Module, DynamicModule, Provider, Type } from '@nestjs/common'; +import { DiscoveryModule, ModuleRef } from '@nestjs/core'; +import { + Module, + DynamicModule, + Provider, + Type, + Global, + Inject, + OnApplicationShutdown, + Logger, +} from '@nestjs/common'; import { TelegrafModuleOptions, TelegrafModuleAsyncOptions, TelegrafOptionsFactory, } from './interfaces'; import { + TELEGRAF_BOT_NAME, TELEGRAF_MODULE_OPTIONS, - TELEGRAF_PROVIDER, } from './telegraf.constants'; -import { TelegrafMetadataAccessor } from './telegraf-metadata.accessor'; -import { TelegrafExplorer } from './telegraf.explorer'; -import { TelegrafProvider } from './telegraf.provider'; +import { MetadataAccessorService, UpdatesExplorerService } from './services'; +import { getBotToken } from './utils'; +import { Telegraf } from 'telegraf'; +import { defer } from 'rxjs'; +@Global() @Module({ imports: [DiscoveryModule], - providers: [TelegrafMetadataAccessor, TelegrafExplorer], + providers: [UpdatesExplorerService, MetadataAccessorService], }) -export class TelegrafCoreModule { +export class TelegrafCoreModule implements OnApplicationShutdown { + private readonly logger = new Logger(TelegrafCoreModule.name); + + constructor( + @Inject(TELEGRAF_BOT_NAME) private readonly botName: string, + private readonly moduleRef: ModuleRef, + ) { + this.logger.debug(botName); + } + public static forRoot(options: TelegrafModuleOptions): DynamicModule { - const telegrafProvider = { - provide: TELEGRAF_PROVIDER, - useClass: TelegrafProvider, - inject: [TELEGRAF_MODULE_OPTIONS], + const telegrafBotName = getBotToken(options.botName); + + const telegrafBotProvider = { + provide: telegrafBotName, + useFactory: async (): Promise => + await defer(async () => { + const bot = new Telegraf(options.token); + this.applyBotMiddlewares(bot, options.middlewares); + await bot.launch(options.launchOptions); + return bot; + }).toPromise(), }; + return { module: TelegrafCoreModule, providers: [ - { provide: TELEGRAF_MODULE_OPTIONS, useValue: options }, - telegrafProvider, + { + provide: TELEGRAF_MODULE_OPTIONS, + useValue: options, + }, + { + provide: TELEGRAF_BOT_NAME, + useValue: telegrafBotName, + }, + telegrafBotProvider, ], - exports: [telegrafProvider], + exports: [telegrafBotProvider], }; } public static forRootAsync( options: TelegrafModuleAsyncOptions, ): DynamicModule { - const telegrafProvider = { - provide: TELEGRAF_PROVIDER, - useClass: TelegrafProvider, + const telegrafBotName = getBotToken(options.botName); + + const telegrafBotProvider = { + provide: telegrafBotName, + useFactory: async ( + telegrafModuleOptions: TelegrafModuleOptions, + ): Promise => { + const { botName, ...telegrafOptions } = telegrafModuleOptions; + + return await defer(async () => { + const bot = new Telegraf(telegrafOptions.token); + this.applyBotMiddlewares(bot, telegrafOptions.middlewares); + await bot.launch(telegrafOptions.launchOptions); + return bot; + }).toPromise(); + }, inject: [TELEGRAF_MODULE_OPTIONS], }; + const asyncProviders = this.createAsyncProviders(options); return { module: TelegrafCoreModule, imports: options.imports, - providers: [...asyncProviders, telegrafProvider], - exports: [telegrafProvider], + providers: [ + ...asyncProviders, + { + provide: TELEGRAF_BOT_NAME, + useValue: telegrafBotName, + }, + telegrafBotProvider, + ], + exports: [telegrafBotProvider], }; } + private static applyBotMiddlewares(bot, middlewares) { + if (middlewares) { + middlewares.forEach((middleware) => { + bot.use(middleware); + }); + } + } + private static createAsyncProviders( options: TelegrafModuleAsyncOptions, ): Provider[] { @@ -88,4 +152,9 @@ export class TelegrafCoreModule { inject, }; } + + async onApplicationShutdown(): Promise { + const bot = this.moduleRef.get(this.botName); + bot && (await bot.stop()); + } } diff --git a/lib/telegraf.constants.ts b/lib/telegraf.constants.ts index 1affe84..54270ef 100644 --- a/lib/telegraf.constants.ts +++ b/lib/telegraf.constants.ts @@ -1,5 +1,6 @@ export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS'; -export const TELEGRAF_PROVIDER = 'TelegrafProvider'; +export const TELEGRAF_BOT_NAME = 'TELEGRAF_BOT_NAME'; +export const DEFAULT_BOT_NAME = 'DEFAULT_BOT_NAME'; export const DECORATORS_PREFIX = 'TELEGRAF'; export const DECORATORS = { diff --git a/lib/telegraf.provider.ts b/lib/telegraf.provider.ts deleted file mode 100644 index 58be5b1..0000000 --- a/lib/telegraf.provider.ts +++ /dev/null @@ -1,37 +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() { - this.catch((err, ctx: Context) => { - this.logger.error( - `Encountered an error for ${ctx.updateType} update type`, - err, - ); - }); - - await this.launch(this.launchOptions); - } - - async onApplicationShutdown() { - await this.stop(); - } -} diff --git a/lib/utils/get-bot-token.util.ts b/lib/utils/get-bot-token.util.ts new file mode 100644 index 0000000..a8458e8 --- /dev/null +++ b/lib/utils/get-bot-token.util.ts @@ -0,0 +1,7 @@ +import { DEFAULT_BOT_NAME } from '../telegraf.constants'; + +export function getBotToken(name?: string) { + return name && name !== DEFAULT_BOT_NAME + ? `${name}_BOT_NAME` + : DEFAULT_BOT_NAME; +} diff --git a/lib/utils/index.ts b/lib/utils/index.ts new file mode 100644 index 0000000..9dc7d1d --- /dev/null +++ b/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './get-bot-token.util';