feat(): multiple instances support

This commit is contained in:
Alexander Bukhalo 2021-01-02 01:27:01 +03:00
parent b1a6e50f8f
commit ccb2db0106
14 changed files with 263 additions and 122 deletions

View File

@ -1,4 +1,5 @@
import { Inject } from '@nestjs/common'; 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));

View File

@ -9,4 +9,8 @@ export * as Extra from 'telegraf/extra';
export * from './decorators'; export * from './decorators';
export * from './interfaces'; export * from './interfaces';
export * from './telegraf.module'; export * from './telegraf.module';
export * from './telegraf.provider';
/**
* Backward compatibility with versions < 1.4.0
*/
export { Telegraf as TelegrafProvider } from 'telegraf';

View File

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

View File

@ -4,6 +4,7 @@ import {
LaunchPollingOptions, LaunchPollingOptions,
LaunchWebhookOptions, LaunchWebhookOptions,
} from 'telegraf/typings/telegraf'; } from 'telegraf/typings/telegraf';
import { Middleware } from 'telegraf/typings/composer';
export interface TelegrafModuleOptions { export interface TelegrafModuleOptions {
token: string; token: string;
@ -12,6 +13,9 @@ export interface TelegrafModuleOptions {
polling?: LaunchPollingOptions; polling?: LaunchPollingOptions;
webhook?: LaunchWebhookOptions; webhook?: LaunchWebhookOptions;
}; };
botName?: string;
include?: Function[];
middlewares?: ReadonlyArray<Middleware<any>>;
} }
export interface TelegrafOptionsFactory { export interface TelegrafOptionsFactory {
@ -20,6 +24,7 @@ export interface TelegrafOptionsFactory {
export interface TelegrafModuleAsyncOptions export interface TelegrafModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> { extends Pick<ModuleMetadata, 'imports'> {
botName?: string;
useExisting?: Type<TelegrafOptionsFactory>; useExisting?: Type<TelegrafOptionsFactory>;
useClass?: Type<TelegrafOptionsFactory>; useClass?: Type<TelegrafOptionsFactory>;
useFactory?: ( useFactory?: (

View File

@ -0,0 +1,6 @@
export interface UpdateMetadata {
name: string;
type: string;
methodName: string;
callback?: Function | Record<string, any>;
}

View File

@ -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<string, Module>,
include: Function[],
): Module[] {
if (!include || isEmpty(include)) {
return [...modulesContainer.values()];
}
const whitelisted = this.includeWhitelisted(modulesContainer, include);
return whitelisted;
}
includeWhitelisted(
modulesContainer: Map<string, Module>,
include: Function[],
): Module[] {
const modules = [...modulesContainer.values()];
return modules.filter(({ metatype }) =>
include.some((item) => item === metatype),
);
}
flatMap<T = UpdateMetadata>(
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;
}
}

2
lib/services/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './updates-explorer.service';
export * from './metadata-accessor.service';

View File

@ -12,11 +12,11 @@ import {
OnOptions, OnOptions,
PhoneOptions, PhoneOptions,
UpdateHookOptions, UpdateHookOptions,
} from './decorators'; } from '../decorators';
import { DECORATORS } from './telegraf.constants'; import { DECORATORS } from '../telegraf.constants';
@Injectable() @Injectable()
export class TelegrafMetadataAccessor { export class MetadataAccessorService {
constructor(private readonly reflector: Reflector) {} constructor(private readonly reflector: Reflector) {}
isUpdate(target: Type<any> | Function): boolean { isUpdate(target: Type<any> | Function): boolean {

View File

@ -1,10 +1,12 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, ModuleRef } from '@nestjs/core'; import { DiscoveryService, ModuleRef, ModulesContainer } from '@nestjs/core';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { MetadataScanner } from '@nestjs/core/metadata-scanner'; import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { TelegrafMetadataAccessor } from './telegraf-metadata.accessor'; import { MetadataAccessorService } from './metadata-accessor.service';
import { TelegrafProvider } from './telegraf.provider'; import {
import { TELEGRAF_PROVIDER } from './telegraf.constants'; TELEGRAF_BOT_NAME,
TELEGRAF_MODULE_OPTIONS,
} from '../telegraf.constants';
import { import {
ActionOptions, ActionOptions,
CashtagOptions, CashtagOptions,
@ -17,65 +19,88 @@ import {
OnOptions, OnOptions,
PhoneOptions, PhoneOptions,
UpdateHookOptions, 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() @Injectable()
export class TelegrafExplorer implements OnModuleInit { export class UpdatesExplorerService
extends BaseExplorerService
implements OnModuleInit {
private readonly logger = new Logger(UpdatesExplorerService.name);
constructor( constructor(
@Inject(TELEGRAF_BOT_NAME)
private readonly botName: string,
@Inject(TELEGRAF_MODULE_OPTIONS)
private readonly telegrafModuleOptions: TelegrafModuleOptions,
private readonly moduleRef: ModuleRef, private readonly moduleRef: ModuleRef,
private readonly discoveryService: DiscoveryService, private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: TelegrafMetadataAccessor, private readonly metadataAccessor: MetadataAccessorService,
private readonly metadataScanner: MetadataScanner, private readonly metadataScanner: MetadataScanner,
) {} private readonly modulesContainer: ModulesContainer,
) {
super();
}
private telegraf: TelegrafProvider; private bot: Telegraf<any>;
onModuleInit() { onModuleInit(): void {
this.telegraf = this.moduleRef.get<TelegrafProvider>(TELEGRAF_PROVIDER, { this.logger.debug(this.botName);
this.bot = this.moduleRef.get<Telegraf<any>>(this.botName, {
strict: false, strict: false,
}); });
this.explore(); this.explore();
} }
explore() { explore() {
/** const modules = this.getModules(
* Update providers section is only for decorators under Update decorator this.modulesContainer,
*/ this.telegrafModuleOptions.include || [],
const updateProviders: InstanceWrapper[] = this.discoveryService );
.getProviders() const updates = this.flatMap(modules, (instance, moduleRef) =>
.filter((wrapper: InstanceWrapper) => 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), this.metadataAccessor.isUpdate(wrapper.metatype),
); );
updateProviders.forEach((wrapper: InstanceWrapper) => { updateProviders.forEach((wrapper: InstanceWrapper) => {
const { instance } = wrapper; const { instance } = wrapper;
if (!instance) {
this.metadataScanner.scanFromPrototype( return undefined;
instance, }
Object.getPrototypeOf(instance), this.metadataScanner.scanFromPrototype(instance, prototype, (name) => {
(key: string) => { if (this.metadataAccessor.isUpdateHook(instance[name])) {
if (this.metadataAccessor.isUpdateHook(instance[key])) { const metadata = this.metadataAccessor.getUpdateHookMetadata(
const metadata = this.metadataAccessor.getUpdateHookMetadata( instance[name],
instance[key], );
); this.handleUpdateHook(instance, name, metadata);
this.handleUpdateHook(instance, key, metadata); }
} });
},
);
}); });
const providers: InstanceWrapper[] = this.discoveryService.getProviders();
providers.forEach((wrapper: InstanceWrapper) => { providers.forEach((wrapper: InstanceWrapper) => {
const { instance } = wrapper; const { instance } = wrapper;
if (!instance) { if (!instance) {
return; return undefined;
} }
this.metadataScanner.scanFromPrototype( this.metadataScanner.scanFromPrototype(
instance, instance,
Object.getPrototypeOf(instance), prototype,
(key: string) => { (key: string) => {
if (this.metadataAccessor.isTelegrafUse(instance[key])) { if (this.metadataAccessor.isTelegrafUse(instance[key])) {
this.handleTelegrafUse(instance, key); this.handleTelegrafUse(instance, key);
@ -146,19 +171,19 @@ export class TelegrafExplorer implements OnModuleInit {
} }
handleUpdateHook(instance: object, key: string, metadata: UpdateHookOptions) { 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) { 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) { 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) { 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( handleTelegrafCommand(
@ -166,25 +191,25 @@ export class TelegrafExplorer implements OnModuleInit {
key: string, key: string,
metadata: CommandOptions, 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) { handleTelegrafStart(instance: object, key: string) {
this.telegraf.start(instance[key].bind(instance)); this.bot.start(instance[key].bind(instance));
} }
handleTelegrafHelp(instance: object, key: string) { handleTelegrafHelp(instance: object, key: string) {
this.telegraf.help(instance[key].bind(instance)); this.bot.help(instance[key].bind(instance));
} }
handleTelegrafSettings(instance: object, key: string) { handleTelegrafSettings(instance: object, key: string) {
// @ts-ignore // @ts-ignore
this.telegraf.settings(instance[key].bind(instance)); this.bot.settings(instance[key].bind(instance));
} }
handleTelegrafEntity(instance: object, key: string, metadata: EntityOptions) { handleTelegrafEntity(instance: object, key: string, metadata: EntityOptions) {
// @ts-ignore // @ts-ignore
this.telegraf.entity(metadata.entity, instance[key].bind(instance)); this.bot.entity(metadata.entity, instance[key].bind(instance));
} }
handleTelegrafMention( handleTelegrafMention(
@ -193,12 +218,12 @@ export class TelegrafExplorer implements OnModuleInit {
metadata: MentionOptions, metadata: MentionOptions,
) { ) {
// @ts-ignore // @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) { handleTelegrafPhone(instance: object, key: string, metadata: PhoneOptions) {
// @ts-ignore // @ts-ignore
this.telegraf.phone(metadata.phone, instance[key].bind(instance)); this.bot.phone(metadata.phone, instance[key].bind(instance));
} }
handleTelegrafHashtag( handleTelegrafHashtag(
@ -207,7 +232,7 @@ export class TelegrafExplorer implements OnModuleInit {
metadata: HashtagOptions, metadata: HashtagOptions,
) { ) {
// @ts-ignore // @ts-ignore
this.telegraf.hashtag(metadata.hashtag, instance[key].bind(instance)); this.bot.hashtag(metadata.hashtag, instance[key].bind(instance));
} }
handleTelegrafCashtag( handleTelegrafCashtag(
@ -216,11 +241,11 @@ export class TelegrafExplorer implements OnModuleInit {
metadata: CashtagOptions, metadata: CashtagOptions,
) { ) {
// @ts-ignore // @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) { 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( handleTelegrafInlineQuery(
@ -230,16 +255,13 @@ export class TelegrafExplorer implements OnModuleInit {
) { ) {
if (metadata.triggers) { if (metadata.triggers) {
// @ts-ignore // @ts-ignore
this.telegraf.inlineQuery( this.bot.inlineQuery(metadata.triggers, instance[key].bind(instance));
metadata.triggers,
instance[key].bind(instance),
);
} else { } else {
this.telegraf.on(metadata.updateType, instance[key].bind(instance)); this.bot.on(metadata.updateType, instance[key].bind(instance));
} }
} }
handleTelegrafGameQuery(instance: object, key: string) { handleTelegrafGameQuery(instance: object, key: string) {
this.telegraf.gameQuery(instance[key].bind(instance)); this.bot.gameQuery(instance[key].bind(instance));
} }
} }

View File

@ -1,56 +1,120 @@
import { DiscoveryModule } from '@nestjs/core'; import { DiscoveryModule, ModuleRef } from '@nestjs/core';
import { Module, DynamicModule, Provider, Type } from '@nestjs/common'; import {
Module,
DynamicModule,
Provider,
Type,
Global,
Inject,
OnApplicationShutdown,
Logger,
} from '@nestjs/common';
import { import {
TelegrafModuleOptions, TelegrafModuleOptions,
TelegrafModuleAsyncOptions, TelegrafModuleAsyncOptions,
TelegrafOptionsFactory, TelegrafOptionsFactory,
} from './interfaces'; } from './interfaces';
import { import {
TELEGRAF_BOT_NAME,
TELEGRAF_MODULE_OPTIONS, TELEGRAF_MODULE_OPTIONS,
TELEGRAF_PROVIDER,
} from './telegraf.constants'; } from './telegraf.constants';
import { TelegrafMetadataAccessor } from './telegraf-metadata.accessor'; import { MetadataAccessorService, UpdatesExplorerService } from './services';
import { TelegrafExplorer } from './telegraf.explorer'; import { getBotToken } from './utils';
import { TelegrafProvider } from './telegraf.provider'; import { Telegraf } from 'telegraf';
import { defer } from 'rxjs';
@Global()
@Module({ @Module({
imports: [DiscoveryModule], 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 { public static forRoot(options: TelegrafModuleOptions): DynamicModule {
const telegrafProvider = { const telegrafBotName = getBotToken(options.botName);
provide: TELEGRAF_PROVIDER,
useClass: TelegrafProvider, const telegrafBotProvider = {
inject: [TELEGRAF_MODULE_OPTIONS], provide: telegrafBotName,
useFactory: async (): Promise<any> =>
await defer(async () => {
const bot = new Telegraf(options.token);
this.applyBotMiddlewares(bot, options.middlewares);
await bot.launch(options.launchOptions);
return bot;
}).toPromise(),
}; };
return { return {
module: TelegrafCoreModule, module: TelegrafCoreModule,
providers: [ 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( public static forRootAsync(
options: TelegrafModuleAsyncOptions, options: TelegrafModuleAsyncOptions,
): DynamicModule { ): DynamicModule {
const telegrafProvider = { const telegrafBotName = getBotToken(options.botName);
provide: TELEGRAF_PROVIDER,
useClass: TelegrafProvider, const telegrafBotProvider = {
provide: telegrafBotName,
useFactory: async (
telegrafModuleOptions: TelegrafModuleOptions,
): Promise<any> => {
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], inject: [TELEGRAF_MODULE_OPTIONS],
}; };
const asyncProviders = this.createAsyncProviders(options); const asyncProviders = this.createAsyncProviders(options);
return { return {
module: TelegrafCoreModule, module: TelegrafCoreModule,
imports: options.imports, imports: options.imports,
providers: [...asyncProviders, telegrafProvider], providers: [
exports: [telegrafProvider], ...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( private static createAsyncProviders(
options: TelegrafModuleAsyncOptions, options: TelegrafModuleAsyncOptions,
): Provider[] { ): Provider[] {
@ -88,4 +152,9 @@ export class TelegrafCoreModule {
inject, inject,
}; };
} }
async onApplicationShutdown(): Promise<void> {
const bot = this.moduleRef.get<any>(this.botName);
bot && (await bot.stop());
}
} }

View File

@ -1,5 +1,6 @@
export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS'; 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_PREFIX = 'TELEGRAF';
export const DECORATORS = { export const DECORATORS = {

View File

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

View File

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

1
lib/utils/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './get-bot-token.util';