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 { 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 './interfaces';
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 './telegraf-options.interface';
export * from './update-metadata.interface';

View File

@ -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<Middleware<any>>;
}
export interface TelegrafOptionsFactory {
@ -20,6 +24,7 @@ export interface TelegrafOptionsFactory {
export interface TelegrafModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> {
botName?: string;
useExisting?: Type<TelegrafOptionsFactory>;
useClass?: Type<TelegrafOptionsFactory>;
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,
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<any> | Function): boolean {

View File

@ -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<any>;
onModuleInit() {
this.telegraf = this.moduleRef.get<TelegrafProvider>(TELEGRAF_PROVIDER, {
onModuleInit(): void {
this.logger.debug(this.botName);
this.bot = this.moduleRef.get<Telegraf<any>>(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));
}
}

View File

@ -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<any> =>
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<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],
};
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<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_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 = {

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