mirror of
https://github.com/Maks1mS/nestjs-telegraf.git
synced 2025-04-12 03:03:42 +03:00
Merge pull request #186 from bukhalo/feature/multiple-instances
feat(): multiple instances support
This commit is contained in:
commit
354fa40746
@ -1,5 +1,6 @@
|
||||
# source
|
||||
lib
|
||||
index.ts
|
||||
package-lock.json
|
||||
tsconfig.json
|
||||
.prettierrc
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Telegraf } from 'telegraf';
|
||||
import { getBotToken } from '../../utils';
|
||||
|
||||
export const InjectBot = (): ParameterDecorator => Inject(Telegraf);
|
||||
export const InjectBot = (name?: string): ParameterDecorator =>
|
||||
Inject(getBotToken(name));
|
||||
|
@ -1,73 +0,0 @@
|
||||
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 {
|
||||
private readonly stage = new Stage([]);
|
||||
|
||||
constructor(
|
||||
@Inject(Telegraf)
|
||||
private readonly telegraf: Telegraf<never>,
|
||||
private readonly discoveryService: DiscoveryService,
|
||||
private readonly metadataAccessor: TelegrafMetadataAccessor,
|
||||
private readonly metadataScanner: MetadataScanner,
|
||||
) {
|
||||
this.telegraf.use(this.stage.middleware());
|
||||
}
|
||||
|
||||
onModuleInit(): void {
|
||||
this.explore();
|
||||
}
|
||||
|
||||
private explore(): void {
|
||||
const sceneClasses = this.filterSceneClasses();
|
||||
|
||||
sceneClasses.forEach((wrapper) => {
|
||||
const { instance } = wrapper;
|
||||
|
||||
const sceneId = this.metadataAccessor.getSceneMetadata(
|
||||
instance.constructor,
|
||||
);
|
||||
const scene = new Scene(sceneId);
|
||||
this.stage.register(scene);
|
||||
|
||||
const prototype = Object.getPrototypeOf(instance);
|
||||
this.metadataScanner.scanFromPrototype(
|
||||
instance,
|
||||
prototype,
|
||||
(methodKey: string) =>
|
||||
this.registerIfListener(scene, instance, methodKey),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private filterSceneClasses(): InstanceWrapper[] {
|
||||
return this.discoveryService
|
||||
.getProviders()
|
||||
.filter((wrapper) => wrapper.instance)
|
||||
.filter((wrapper) =>
|
||||
this.metadataAccessor.isScene(wrapper.instance.constructor),
|
||||
);
|
||||
}
|
||||
|
||||
private registerIfListener(
|
||||
scene: Scene<never>,
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
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 { Telegraf } from 'telegraf';
|
||||
import { TelegrafMetadataAccessor } from '../telegraf.metadata-accessor';
|
||||
|
||||
@Injectable()
|
||||
export class TelegrafUpdateExplorer implements OnModuleInit {
|
||||
constructor(
|
||||
@Inject(Telegraf)
|
||||
private readonly telegraf: Telegraf<never>,
|
||||
private readonly discoveryService: DiscoveryService,
|
||||
private readonly metadataAccessor: TelegrafMetadataAccessor,
|
||||
private readonly metadataScanner: MetadataScanner,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
this.explore();
|
||||
}
|
||||
|
||||
private explore(): void {
|
||||
const updateClasses = this.filterUpdateClasses();
|
||||
|
||||
updateClasses.forEach((wrapper) => {
|
||||
const { instance } = wrapper;
|
||||
|
||||
const prototype = Object.getPrototypeOf(instance);
|
||||
this.metadataScanner.scanFromPrototype(
|
||||
instance,
|
||||
prototype,
|
||||
(methodKey: string) => this.registerIfListener(instance, methodKey),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private filterUpdateClasses(): InstanceWrapper[] {
|
||||
return this.discoveryService
|
||||
.getProviders()
|
||||
.filter((wrapper) => wrapper.instance)
|
||||
.filter((wrapper) =>
|
||||
this.metadataAccessor.isUpdate(wrapper.instance.constructor),
|
||||
);
|
||||
}
|
||||
|
||||
private registerIfListener(
|
||||
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;
|
||||
// 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);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { BaseScene as Scene } from 'telegraf';
|
||||
import { ComposerMethodArgs, SceneMethods } from '../telegraf.types';
|
||||
import { ComposerMethodArgs, SceneMethods } from '../types';
|
||||
import { UPDATE_LISTENER_METADATA } from '../telegraf.constants';
|
||||
import { ListenerMetadata } from '../interfaces';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Composer } from 'telegraf';
|
||||
import { ComposerMethodArgs, UpdateMethods } from '../telegraf.types';
|
||||
import { ComposerMethodArgs, UpdateMethods } from '../types';
|
||||
import { UPDATE_LISTENER_METADATA } from '../telegraf.constants';
|
||||
import { ListenerMetadata } from '../interfaces';
|
||||
|
||||
|
12
lib/index.ts
12
lib/index.ts
@ -1,5 +1,15 @@
|
||||
export * as Composer from 'telegraf/composer';
|
||||
export * as Markup from 'telegraf/markup';
|
||||
export * as BaseScene from 'telegraf/scenes/base';
|
||||
export * as session from 'telegraf/session';
|
||||
export * as Stage from 'telegraf/stage';
|
||||
export * as WizardScene from 'telegraf/scenes/wizard';
|
||||
export * as Extra from 'telegraf/extra';
|
||||
|
||||
export * from './decorators';
|
||||
export * from './interfaces';
|
||||
export * from './helpers';
|
||||
export * from './utils';
|
||||
export * from './telegraf.module';
|
||||
export * from './telegraf.types';
|
||||
export * from './types';
|
||||
export { Telegraf } from 'telegraf';
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './telegraf-options.interface';
|
||||
export * from './listener-metadata.interface';
|
||||
export * from './update-metadata.interface';
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
|
||||
import { Middleware, Context } from 'telegraf';
|
||||
import {
|
||||
TelegrafOptions,
|
||||
LaunchPollingOptions,
|
||||
LaunchWebhookOptions,
|
||||
TelegrafOptions,
|
||||
} from 'telegraf/typings/telegraf';
|
||||
|
||||
export interface TelegrafModuleOptions<C extends Context = Context> {
|
||||
@ -13,7 +13,9 @@ export interface TelegrafModuleOptions<C extends Context = Context> {
|
||||
polling?: LaunchPollingOptions;
|
||||
webhook?: LaunchWebhookOptions;
|
||||
};
|
||||
middlewares?: Middleware<C>[];
|
||||
botName?: string;
|
||||
include?: Function[];
|
||||
middlewares?: ReadonlyArray<Middleware<C>>;
|
||||
}
|
||||
|
||||
export interface TelegrafOptionsFactory {
|
||||
@ -22,6 +24,7 @@ export interface TelegrafOptionsFactory {
|
||||
|
||||
export interface TelegrafModuleAsyncOptions
|
||||
extends Pick<ModuleMetadata, 'imports'> {
|
||||
botName?: string;
|
||||
useExisting?: Type<TelegrafOptionsFactory>;
|
||||
useClass?: Type<TelegrafOptionsFactory>;
|
||||
useFactory?: (
|
||||
|
6
lib/interfaces/update-metadata.interface.ts
Normal file
6
lib/interfaces/update-metadata.interface.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface UpdateMetadata {
|
||||
name: string;
|
||||
type: string;
|
||||
methodName: string;
|
||||
callback?: Function | Record<string, any>;
|
||||
}
|
59
lib/services/base-explorer.service.ts
Normal file
59
lib/services/base-explorer.service.ts
Normal 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;
|
||||
}
|
||||
}
|
4
lib/services/index.ts
Normal file
4
lib/services/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './updates-explorer.service';
|
||||
export * from './metadata-accessor.service';
|
||||
export * from './scenes-explorer.service';
|
||||
export * from './updates-explorer.service';
|
@ -4,18 +4,31 @@ import {
|
||||
SCENE_METADATA,
|
||||
UPDATE_LISTENER_METADATA,
|
||||
UPDATE_METADATA,
|
||||
} from './telegraf.constants';
|
||||
import { ListenerMetadata } from './interfaces';
|
||||
} from '../telegraf.constants';
|
||||
import { ListenerMetadata } from '../interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class TelegrafMetadataAccessor {
|
||||
export class MetadataAccessorService {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
isUpdate(target: Function): boolean {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
return !!this.reflector.get(UPDATE_METADATA, target);
|
||||
}
|
||||
|
||||
isUpdateListener(target: Function) {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
return !!this.reflector.get(UPDATE_LISTENER_METADATA, target);
|
||||
}
|
||||
|
||||
isScene(target: Function): boolean {
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
return !!this.reflector.get(SCENE_METADATA, target);
|
||||
}
|
||||
|
98
lib/services/scenes-explorer.service.ts
Normal file
98
lib/services/scenes-explorer.service.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { DiscoveryService, ModuleRef, ModulesContainer } 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 { MetadataAccessorService } from './metadata-accessor.service';
|
||||
import { BaseExplorerService } from './base-explorer.service';
|
||||
import {
|
||||
TELEGRAF_BOT_NAME,
|
||||
TELEGRAF_MODULE_OPTIONS,
|
||||
} from '../telegraf.constants';
|
||||
import { TelegrafModuleOptions } from '../interfaces';
|
||||
import { Module } from '@nestjs/core/injector/module';
|
||||
|
||||
@Injectable()
|
||||
export class ScenesExplorerService
|
||||
extends BaseExplorerService
|
||||
implements OnModuleInit {
|
||||
private readonly stage = new Stage([]);
|
||||
|
||||
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: MetadataAccessorService,
|
||||
private readonly metadataScanner: MetadataScanner,
|
||||
private readonly modulesContainer: ModulesContainer,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private bot: Telegraf<any>;
|
||||
|
||||
onModuleInit(): void {
|
||||
this.bot = this.moduleRef.get<Telegraf<any>>(this.botName, {
|
||||
strict: false,
|
||||
});
|
||||
this.bot.use(this.stage.middleware());
|
||||
this.explore();
|
||||
}
|
||||
|
||||
private explore(): void {
|
||||
const modules = this.getModules(
|
||||
this.modulesContainer,
|
||||
this.telegrafModuleOptions.include || [],
|
||||
);
|
||||
const scenes = this.flatMap(modules, (instance, moduleRef) =>
|
||||
this.applyScenes(instance, moduleRef),
|
||||
);
|
||||
}
|
||||
|
||||
private applyScenes(wrapper: InstanceWrapper, moduleRef: Module) {
|
||||
const { instance } = wrapper;
|
||||
if (!instance) {
|
||||
return undefined;
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(instance);
|
||||
|
||||
const providers: InstanceWrapper[] = this.discoveryService.getProviders();
|
||||
const sceneProviders: InstanceWrapper[] = providers.filter(
|
||||
(wrapper: InstanceWrapper) =>
|
||||
this.metadataAccessor.isScene(wrapper.metatype),
|
||||
);
|
||||
|
||||
sceneProviders.forEach((wrapper) => {
|
||||
const { instance } = wrapper;
|
||||
if (!instance) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sceneId = this.metadataAccessor.getSceneMetadata(
|
||||
instance.constructor,
|
||||
);
|
||||
|
||||
const scene = new Scene(sceneId);
|
||||
this.stage.register(scene);
|
||||
|
||||
this.metadataScanner.scanFromPrototype(
|
||||
instance,
|
||||
prototype,
|
||||
(methodKey: string) => {
|
||||
const methodRef = instance[methodKey];
|
||||
if (this.metadataAccessor.isUpdateListener(methodRef)) {
|
||||
const metadata = this.metadataAccessor.getListenerMetadata(
|
||||
methodRef,
|
||||
);
|
||||
const middlewareFn = methodRef.bind(instance);
|
||||
const { method, args } = metadata;
|
||||
(scene[method] as any)(...args, middlewareFn);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
83
lib/services/updates-explorer.service.ts
Normal file
83
lib/services/updates-explorer.service.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { Inject, Injectable, 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 { MetadataAccessorService } from './metadata-accessor.service';
|
||||
import {
|
||||
TELEGRAF_BOT_NAME,
|
||||
TELEGRAF_MODULE_OPTIONS,
|
||||
} from '../telegraf.constants';
|
||||
import { Telegraf } from 'telegraf';
|
||||
import { TelegrafModuleOptions } from '../interfaces';
|
||||
import { BaseExplorerService } from './base-explorer.service';
|
||||
import { Module } from '@nestjs/core/injector/module';
|
||||
|
||||
@Injectable()
|
||||
export class UpdatesExplorerService
|
||||
extends BaseExplorerService
|
||||
implements OnModuleInit {
|
||||
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: MetadataAccessorService,
|
||||
private readonly metadataScanner: MetadataScanner,
|
||||
private readonly modulesContainer: ModulesContainer,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private bot: Telegraf<any>;
|
||||
|
||||
onModuleInit(): void {
|
||||
this.bot = this.moduleRef.get<Telegraf<any>>(this.botName, {
|
||||
strict: false,
|
||||
});
|
||||
this.explore();
|
||||
}
|
||||
|
||||
explore() {
|
||||
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;
|
||||
if (!instance) {
|
||||
return undefined;
|
||||
}
|
||||
this.metadataScanner.scanFromPrototype(instance, prototype, (name) => {
|
||||
const methodRef = instance[name];
|
||||
if (this.metadataAccessor.isUpdateListener(methodRef)) {
|
||||
const metadata = this.metadataAccessor.getListenerMetadata(methodRef);
|
||||
const middlewareFn = methodRef.bind(instance);
|
||||
const { method, args } = metadata;
|
||||
// NOTE: Use "any" to disable "Expected at least 1 arguments, but got 1 or more." error.
|
||||
// Use telegraf instance for non-scene listeners
|
||||
(this.bot[method] as any)(...args, middlewareFn);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
166
lib/telegraf-core.module.ts
Normal file
166
lib/telegraf-core.module.ts
Normal file
@ -0,0 +1,166 @@
|
||||
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,
|
||||
} from './telegraf.constants';
|
||||
import {
|
||||
MetadataAccessorService,
|
||||
ScenesExplorerService,
|
||||
UpdatesExplorerService,
|
||||
} from './services';
|
||||
import { getBotToken } from './utils';
|
||||
import { Telegraf } from 'telegraf';
|
||||
import { defer } from 'rxjs';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [DiscoveryModule],
|
||||
providers: [
|
||||
UpdatesExplorerService,
|
||||
ScenesExplorerService,
|
||||
MetadataAccessorService,
|
||||
],
|
||||
})
|
||||
export class TelegrafCoreModule implements OnApplicationShutdown {
|
||||
private static logger = new Logger(TelegrafCoreModule.name);
|
||||
|
||||
constructor(
|
||||
@Inject(TELEGRAF_BOT_NAME) private readonly botName: string,
|
||||
private readonly moduleRef: ModuleRef,
|
||||
) {}
|
||||
|
||||
public static forRoot(options: TelegrafModuleOptions): DynamicModule {
|
||||
const telegrafBotName = getBotToken(options.botName);
|
||||
|
||||
const telegrafBotProvider = {
|
||||
provide: telegrafBotName,
|
||||
useFactory: async (): Promise<any> =>
|
||||
await defer(async () => {
|
||||
const bot = new Telegraf<any>(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,
|
||||
},
|
||||
{
|
||||
provide: TELEGRAF_BOT_NAME,
|
||||
useValue: telegrafBotName,
|
||||
},
|
||||
telegrafBotProvider,
|
||||
],
|
||||
exports: [telegrafBotProvider],
|
||||
};
|
||||
}
|
||||
|
||||
public static forRootAsync(
|
||||
options: TelegrafModuleAsyncOptions,
|
||||
): DynamicModule {
|
||||
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<any>(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,
|
||||
{
|
||||
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[] {
|
||||
if (options.useExisting || options.useFactory) {
|
||||
return [this.createAsyncOptionsProvider(options)];
|
||||
}
|
||||
const useClass = options.useClass as Type<TelegrafOptionsFactory>;
|
||||
return [
|
||||
this.createAsyncOptionsProvider(options),
|
||||
{
|
||||
provide: useClass,
|
||||
useClass,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private static createAsyncOptionsProvider(
|
||||
options: TelegrafModuleAsyncOptions,
|
||||
): Provider {
|
||||
if (options.useFactory) {
|
||||
return {
|
||||
provide: TELEGRAF_MODULE_OPTIONS,
|
||||
useFactory: options.useFactory,
|
||||
inject: options.inject || [],
|
||||
};
|
||||
}
|
||||
// `as Type<TelegrafOptionsFactory>` is a workaround for microsoft/TypeScript#31603
|
||||
const inject = [
|
||||
(options.useClass || options.useExisting) as Type<TelegrafOptionsFactory>,
|
||||
];
|
||||
return {
|
||||
provide: TELEGRAF_MODULE_OPTIONS,
|
||||
useFactory: async (optionsFactory: TelegrafOptionsFactory) =>
|
||||
await optionsFactory.createTelegrafOptions(),
|
||||
inject,
|
||||
};
|
||||
}
|
||||
|
||||
async onApplicationShutdown(): Promise<void> {
|
||||
const bot = this.moduleRef.get<any>(this.botName);
|
||||
bot && (await bot.stop());
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS';
|
||||
export const TELEGRAF_BOT_NAME = 'TELEGRAF_BOT_NAME';
|
||||
export const DEFAULT_BOT_NAME = 'DEFAULT_BOT_NAME';
|
||||
|
||||
export const UPDATE_METADATA = 'UPDATE_METADATA';
|
||||
export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA';
|
||||
|
@ -1,106 +1,27 @@
|
||||
import { DiscoveryModule, ModuleRef } from '@nestjs/core';
|
||||
import { Module, DynamicModule } from '@nestjs/common';
|
||||
import { TelegrafCoreModule } from './telegraf-core.module';
|
||||
import {
|
||||
DynamicModule,
|
||||
Inject,
|
||||
Module,
|
||||
OnApplicationBootstrap,
|
||||
OnApplicationShutdown,
|
||||
Provider,
|
||||
} from '@nestjs/common';
|
||||
import { Telegraf } from 'telegraf';
|
||||
import {
|
||||
TelegrafModuleAsyncOptions,
|
||||
TelegrafModuleOptions,
|
||||
TelegrafOptionsFactory,
|
||||
TelegrafModuleAsyncOptions,
|
||||
} from './interfaces';
|
||||
import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants';
|
||||
import { TelegrafMetadataAccessor } from './telegraf.metadata-accessor';
|
||||
import { TelegrafUpdateExplorer } from './explorers/telegraf-update.explorer';
|
||||
import { TelegrafSceneExplorer } from './explorers/telegraf-scene.explorer';
|
||||
import { createProviders, TelegrafProvider } from './telegraf.providers';
|
||||
|
||||
@Module({
|
||||
imports: [DiscoveryModule],
|
||||
providers: [
|
||||
TelegrafMetadataAccessor,
|
||||
TelegrafSceneExplorer,
|
||||
TelegrafUpdateExplorer,
|
||||
],
|
||||
})
|
||||
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();
|
||||
}
|
||||
|
||||
@Module({})
|
||||
export class TelegrafModule {
|
||||
public static forRoot(options: TelegrafModuleOptions): DynamicModule {
|
||||
const providers = [...createProviders(options), TelegrafProvider];
|
||||
|
||||
return {
|
||||
module: TelegrafModule,
|
||||
providers,
|
||||
exports: providers,
|
||||
imports: [TelegrafCoreModule.forRoot(options)],
|
||||
exports: [TelegrafCoreModule],
|
||||
};
|
||||
}
|
||||
|
||||
public static forRootAsync(
|
||||
options: TelegrafModuleAsyncOptions,
|
||||
): DynamicModule {
|
||||
const providers = [...this.createAsyncProviders(options), TelegrafProvider];
|
||||
|
||||
return {
|
||||
module: TelegrafModule,
|
||||
imports: options.imports || [],
|
||||
providers,
|
||||
exports: providers,
|
||||
};
|
||||
}
|
||||
|
||||
private static createAsyncProviders(
|
||||
options: TelegrafModuleAsyncOptions,
|
||||
): Provider[] {
|
||||
if (options.useExisting || options.useFactory) {
|
||||
return [this.createAsyncOptionsProvider(options)];
|
||||
}
|
||||
|
||||
return [
|
||||
this.createAsyncOptionsProvider(options),
|
||||
{
|
||||
provide: options.useClass,
|
||||
useClass: options.useClass,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private static createAsyncOptionsProvider(
|
||||
options: TelegrafModuleAsyncOptions,
|
||||
): Provider {
|
||||
if (options.useFactory) {
|
||||
return {
|
||||
provide: TELEGRAF_MODULE_OPTIONS,
|
||||
useFactory: options.useFactory,
|
||||
inject: options.inject || [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
provide: TELEGRAF_MODULE_OPTIONS,
|
||||
useFactory: async (optionsFactory: TelegrafOptionsFactory) =>
|
||||
await optionsFactory.createTelegrafOptions(),
|
||||
inject: [options.useExisting || options.useClass],
|
||||
imports: [TelegrafCoreModule.forRootAsync(options)],
|
||||
exports: [TelegrafCoreModule],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { Telegraf } from 'telegraf';
|
||||
import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants';
|
||||
import { TelegrafModuleOptions } from './interfaces';
|
||||
|
||||
export const TelegrafProvider = {
|
||||
provide: Telegraf,
|
||||
useFactory: (options: TelegrafModuleOptions) => {
|
||||
const telegraf = new Telegraf(options.token, options.options);
|
||||
if (options.middlewares?.length > 0) {
|
||||
telegraf.use(...options.middlewares);
|
||||
}
|
||||
return telegraf;
|
||||
},
|
||||
inject: [TELEGRAF_MODULE_OPTIONS],
|
||||
};
|
||||
|
||||
export function createProviders(options: TelegrafModuleOptions): Provider[] {
|
||||
return [
|
||||
{
|
||||
provide: TELEGRAF_MODULE_OPTIONS,
|
||||
useValue: options,
|
||||
},
|
||||
];
|
||||
}
|
7
lib/utils/get-bot-token.util.ts
Normal file
7
lib/utils/get-bot-token.util.ts
Normal 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
1
lib/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './get-bot-token.util';
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "7.6.5",
|
||||
"@nestjs/core": "7.6.5",
|
||||
"@types/lodash": "^4.14.167",
|
||||
"@typescript-eslint/eslint-plugin": "4.11.1",
|
||||
"@typescript-eslint/parser": "4.11.1",
|
||||
"eslint": "7.17.0",
|
||||
@ -20,6 +21,7 @@
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"husky": "4.3.6",
|
||||
"lint-staged": "10.5.3",
|
||||
"lodash": "^4.17.20",
|
||||
"prettier": "2.2.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "6.6.3",
|
||||
@ -292,6 +294,12 @@
|
||||
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.167",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.167.tgz",
|
||||
"integrity": "sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
@ -3880,6 +3888,12 @@
|
||||
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.167",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.167.tgz",
|
||||
"integrity": "sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
|
||||
|
@ -37,6 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "7.6.5",
|
||||
"@nestjs/core": "7.6.5",
|
||||
"@types/lodash": "^4.14.167",
|
||||
"@typescript-eslint/eslint-plugin": "4.11.1",
|
||||
"@typescript-eslint/parser": "4.11.1",
|
||||
"eslint": "7.17.0",
|
||||
@ -44,6 +45,7 @@
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"husky": "4.3.6",
|
||||
"lint-staged": "10.5.3",
|
||||
"lodash": "^4.17.20",
|
||||
"prettier": "2.2.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "6.6.3",
|
||||
|
@ -8,10 +8,10 @@ slug: /bot-injection
|
||||
At times you may need to access the native `Telegraf` instance. For example, you may want to connect stage middleware. You can inject the Telegraf by using the `@InjectBot()` decorator as follows:
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectBot, TelegrafProvider } from 'nestjs-telegraf';
|
||||
import { InjectBot, TelegrafProvider, Context } from 'nestjs-telegraf';
|
||||
|
||||
@Injectable()
|
||||
export class BotSettingsService {
|
||||
constructor(@InjectBot() private bot: TelegrafProvider) {}
|
||||
constructor(@InjectBot() private bot: TelegrafProvider<Context>) {}
|
||||
}
|
||||
```
|
||||
|
31
website/docs/error-handling.md
Normal file
31
website/docs/error-handling.md
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
id: error-handling
|
||||
title: Error handling
|
||||
sidebar_label: Error handling
|
||||
slug: /error-handling
|
||||
---
|
||||
|
||||
By default, `nestjs-telegraf` catches all errors using the `Logger` built into NestJS.
|
||||
|
||||
Use can disable global errors catch with `disableGlobalCatch`:
|
||||
```typescript
|
||||
TelegrafModule.forRoot({
|
||||
disableGlobalCatch: true,
|
||||
}),
|
||||
```
|
||||
|
||||
After that you can override errors handling with bot instance `catch` function.
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectBot, TelegrafProvider, Context } from 'nestjs-telegraf';
|
||||
|
||||
@Injectable()
|
||||
export class BotSettingsService {
|
||||
constructor(@InjectBot() private bot: TelegrafProvider<Context>) {
|
||||
this.bot.catch((err, ctx) => {
|
||||
console.log(`Ooops, encountered an error for ${ctx.updateType}`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
@ -11,23 +11,26 @@ By default, the bot receives updates using long-polling and requires no addition
|
||||
|
||||
## Webhooks
|
||||
|
||||
If you want to configure a telegram bot webhook, you need to get a middleware from `TelegrafProvider` for connect it in your `main.ts` file.
|
||||
If you want to configure a telegram bot webhook, you need to get a middleware via `getBotToken` helper in your `main.ts` file.
|
||||
|
||||
To access it, you must use the `app.get()` method, followed by the provider reference:
|
||||
```typescript
|
||||
const telegrafProvider = app.get('TelegrafProvider');
|
||||
import { getBotToken } from 'nestjs-telegraf';
|
||||
|
||||
// ...
|
||||
const bot = app.get(getBotToken());
|
||||
```
|
||||
|
||||
Now you can connect middleware:
|
||||
```typescript
|
||||
app.use(telegrafProvider.webhookCallback('/secret-path'));
|
||||
app.use(bot.webhookCallback('/secret-path'));
|
||||
```
|
||||
|
||||
The last step is to specify launchOptions in `forRoot` method:
|
||||
```typescript
|
||||
TelegrafModule.forRootAsync({
|
||||
imports: [ConfigModule.forFeature(telegrafModuleConfig)],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
token: configService.get<string>('TELEGRAM_BOT_TOKEN'),
|
||||
launchOptions: {
|
||||
webhook: {
|
||||
|
13
website/docs/middlewares.md
Normal file
13
website/docs/middlewares.md
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
id: middlewares
|
||||
title: Middlewares
|
||||
sidebar_label: Middlewares
|
||||
slug: /middlewares
|
||||
---
|
||||
|
||||
`nestjs-telegraf` has support of the Telegraf middleware packages. To use an existing middleware package, simply import it and add it to the middlewares array:
|
||||
```typescript
|
||||
TelegrafModule.forRoot({
|
||||
middlewares: [session()],
|
||||
}),
|
||||
```
|
74
website/docs/multiple-bots.md
Normal file
74
website/docs/multiple-bots.md
Normal file
@ -0,0 +1,74 @@
|
||||
---
|
||||
id: multiple-bots
|
||||
title: Multiple bots
|
||||
sidebar_label: Multiple bots
|
||||
slug: /multiple-bots
|
||||
---
|
||||
|
||||
In some cases, you may need to run multiple bots at the same time. This can also be achieved with this module. To work with multiple bots, first create the bots. In this case, bot naming becomes mandatory.
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TelegrafModule } from 'nestjs-telegraf';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
TelegrafModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
botName: 'cat',
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
token: configService.get<string>('CAT_BOT_TOKEN'),
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
TelegrafModule.forRootAsync({
|
||||
imports: [ConfigModule.forFeature(telegrafModuleConfig)],
|
||||
botName: 'dog',
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
token: configService.get<string>('DOG_BOT_TOKEN'),
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
:::caution
|
||||
Please note that you shouldn't have multiple bots without a name, or with the same name, otherwise they will get overridden.
|
||||
:::
|
||||
|
||||
You can also inject the `Bot` for a given bot:
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectBot, Telegraf, Context } from 'nestjs-telegraf';
|
||||
|
||||
@Injectable()
|
||||
export class EchoService {
|
||||
constructor(@InjectBot('cat') private catBot: Telegraf<Context>) {}
|
||||
}
|
||||
```
|
||||
To inject a given `Bot` to a custom provider (for example, factory provider), use the `getBotToken()` function passing the name of the bot as an argument.
|
||||
```typescript
|
||||
{
|
||||
provide: CatsService,
|
||||
useFactory: (catBot: Telegraf<Context>) => {
|
||||
return new CatsService(catBot);
|
||||
},
|
||||
inject: [getBotToken('cat')],
|
||||
}
|
||||
```
|
||||
Another useful feature of the `nestjs-telegraf` module is the ability to choose which modules should handle updates for each launched bot. By default, module searches for handlers throughout the whole app. To limit this scan to only a subset of modules, use the include property.
|
||||
|
||||
```typescript
|
||||
TelegrafModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
botName: 'cat',
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
token: configService.get<string>('CAT_BOT_TOKEN'),
|
||||
include: [CatsModule],
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
```
|
@ -6,6 +6,9 @@ module.exports = {
|
||||
'telegraf-methods',
|
||||
'bot-injection',
|
||||
'async-configuration',
|
||||
'multiple-bots',
|
||||
'middlewares',
|
||||
'error-handling',
|
||||
],
|
||||
Extras: ['extras/standalone-applications'],
|
||||
'API Reference': ['api-reference/decorators'],
|
||||
|
Loading…
Reference in New Issue
Block a user