Merge pull request #192 from Morb0/refactor/v2

Refactor & Fix v2
This commit is contained in:
Alexander Bukhalo 2021-01-05 15:46:21 +03:00 committed by GitHub
commit 6533b4e71c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 12207 additions and 4401 deletions

4
.gitignore vendored
View File

@ -1,7 +1,3 @@
# lock
package-lock.json
yarn.lock
# dependencies
/node_modules

View File

@ -1,6 +1,5 @@
# source
lib
index.ts
package-lock.json
tsconfig.json
.prettierrc

1
index.d.ts vendored
View File

@ -1 +0,0 @@
export * from './dist';

View File

@ -1,6 +0,0 @@
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
exports.__esModule = true;
__export(require("./dist"));

View File

@ -1 +0,0 @@
export * from './dist';

View File

@ -1,5 +1,5 @@
import { Inject } from '@nestjs/common';
import { getBotToken } from '../../utils';
export const InjectBot = (name?: string): ParameterDecorator =>
Inject(getBotToken(name));
export const InjectBot = (botName?: string): ParameterDecorator =>
Inject(getBotToken(botName));

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling callback_data actions with regular expressions.
*
* @see https://telegraf.js.org/#/?id=action
*/
export const Action = createUpdateListenerDecorator('action');
export const Action = createListenerDecorator('action');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Cashtag handling.
*
* @see https://telegraf.js.org/#/?id=cashtag
*/
export const Cashtag = createUpdateListenerDecorator('cashtag');
export const Cashtag = createMissedListenerDecorator<[string | string[]]>('cashtag');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Command handling.
*
* @see https://telegraf.js.org/#/?id=command
*/
export const Command = createUpdateListenerDecorator('command');
export const Command = createListenerDecorator('command');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with email entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-email
*/
export const Email = createUpdateListenerDecorator('email');
export const Email = createMissedListenerDecorator<[string | string[]]>('email');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling callback_data actions with game query.
*
* @see https://telegraf.js.org/#/?id=inlinequery
*/
export const GameQuery = createUpdateListenerDecorator('gameQuery');
export const GameQuery = createListenerDecorator('gameQuery');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Hashtag handling.
*
* @see https://telegraf.js.org/#/?id=hashtag
*/
export const Hashtag = createUpdateListenerDecorator('hashtag');
export const Hashtag = createMissedListenerDecorator<[string | string[]]>('hashtag');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling text messages.
*
* @see https://telegraf.js.org/#/?id=hears
*/
export const Hears = createUpdateListenerDecorator('hears');
export const Hears = createListenerDecorator('hears');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Handler for /help command.
*
* @see https://telegraf.js.org/#/?id=help
*/
export const Help = createUpdateListenerDecorator('help');
export const Help = createListenerDecorator('help');

View File

@ -1,8 +1,9 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
import { HearsTriggers } from 'telegraf/typings/composer';
/**
* Registers middleware for handling inline_query actions with regular expressions.
*
* @see https://telegraf.js.org/#/?id=inlinequery
*/
export const InlineQuery = createUpdateListenerDecorator('inlineQuery');
export const InlineQuery = createMissedListenerDecorator<[HearsTriggers<unknown>]>('inlineQuery');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Mention handling.
*
* @see https://telegraf.js.org/#/?id=mention
*/
export const Mention = createUpdateListenerDecorator('mention');
export const Mention = createMissedListenerDecorator<[string | string[]]>('mention');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for provided update type.
*
* @see https://telegraf.js.org/#/?id=on
*/
export const On = createUpdateListenerDecorator('on');
export const On = createListenerDecorator('on');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Phone number handling.
*
* @see https://telegraf.js.org/#/?id=phone
*/
export const Phone = createUpdateListenerDecorator('phone');
export const Phone = createMissedListenerDecorator<[string | string[]]>('phone');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Handler for /settings command.
*
* @see https://telegraf.js.org/#/?id=settings
*/
export const Settings = createUpdateListenerDecorator('settings');
export const Settings = createMissedListenerDecorator<[]>('settings');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Handler for /start command.
*
* @see https://telegraf.js.org/#/?id=start
*/
export const Start = createUpdateListenerDecorator('start');
export const Start = createListenerDecorator('start');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with text_link entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-textlink
*/
export const TextLink = createUpdateListenerDecorator('textLink');
export const TextLink = createMissedListenerDecorator<[string | string[]]>('textLink');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with text_mention entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-textlink
*/
export const TextMention = createUpdateListenerDecorator('textMention');
export const TextMention = createMissedListenerDecorator<[string | string[]]>('textMention');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createMissedListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with url entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-url
*/
export const Url = createUpdateListenerDecorator('url');
export const Url = createMissedListenerDecorator<[string | string[]]>('url');

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
/**
* Registers a middleware.
*
* @see https://telegraf.js.org/#/?id=use
*/
export const Use = createUpdateListenerDecorator('use');
export const Use = createListenerDecorator('use');

View File

@ -1,3 +1,3 @@
import { createSceneListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
export const SceneEnter = createSceneListenerDecorator('enter');
export const SceneEnter = createListenerDecorator('enter');

View File

@ -1,3 +1,3 @@
import { createSceneListenerDecorator } from '../../helpers';
import { createListenerDecorator } from '../../utils';
export const SceneLeave = createSceneListenerDecorator('leave');
export const SceneLeave = createListenerDecorator('leave');

View File

@ -1,18 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { Composer } from 'telegraf';
import { ComposerMethodArgs, UpdateMethods } from '../types';
import { UPDATE_LISTENER_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces';
export function createUpdateListenerDecorator<Method extends UpdateMethods>(
method: unknown,
) {
return (
...args: ComposerMethodArgs<Composer<never>, Method>
): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,
args,
} as ListenerMetadata);
};
}

View File

@ -1,2 +0,0 @@
export * from './create-update-listener-decorator.helper';
export * from './create-scene-listener-decorator.helper';

View File

@ -1,15 +1,6 @@
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 './types';
export { Telegraf } from 'telegraf';
export * from './telegraf.constants';
export * from './telegraf.module';

View File

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

View File

@ -8,12 +8,12 @@ import {
export interface TelegrafModuleOptions<C extends Context = Context> {
token: string;
botName?: string;
options?: TelegrafOptions;
launchOptions?: {
polling?: LaunchPollingOptions;
webhook?: LaunchWebhookOptions;
};
botName?: string;
include?: Function[];
middlewares?: ReadonlyArray<Middleware<C>>;
}

View File

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

View File

@ -1,7 +1,6 @@
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';
import { flattenDeep, identity, isEmpty } from 'lodash';
export class BaseExplorerService {
getModules(
@ -21,11 +20,11 @@ export class BaseExplorerService {
): Module[] {
const modules = [...modulesContainer.values()];
return modules.filter(({ metatype }) =>
include.some((item) => item === metatype),
include.includes(metatype),
);
}
flatMap<T = UpdateMetadata>(
flatMap<T>(
modules: Module[],
callback: (instance: InstanceWrapper, moduleRef: Module) => T | T[],
): T[] {
@ -37,23 +36,4 @@ export class BaseExplorerService {
};
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;
}
}

View File

@ -1,4 +1,3 @@
export * from './updates-explorer.service';
export * from './listeners-explorer.service';
export * from './metadata-accessor.service';
export * from './scenes-explorer.service';
export * from './updates-explorer.service';
export * from './listeners-explorer.service';

View File

@ -0,0 +1,116 @@
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 { Module } from '@nestjs/core/injector/module';
import { BaseScene, Composer, Stage, Telegraf } from 'telegraf';
import { MetadataAccessorService } from './metadata-accessor.service';
import {
TELEGRAF_BOT_NAME,
TELEGRAF_MODULE_OPTIONS,
} from '../telegraf.constants';
import { TelegrafModuleOptions } from '../interfaces';
import { BaseExplorerService } from './base-explorer.service';
@Injectable()
export class ListenersExplorerService
extends BaseExplorerService
implements OnModuleInit {
private readonly stage = new Stage([]);
private bot: Telegraf<any>;
constructor(
@Inject(TELEGRAF_BOT_NAME)
private readonly botName: string,
@Inject(TELEGRAF_MODULE_OPTIONS)
private readonly telegrafOptions: TelegrafModuleOptions,
private readonly moduleRef: ModuleRef,
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: MetadataAccessorService,
private readonly metadataScanner: MetadataScanner,
private readonly modulesContainer: ModulesContainer,
) {
super();
}
onModuleInit(): void {
this.bot = this.moduleRef.get<Telegraf<never>>(this.botName, {
strict: false,
});
this.bot.use(this.stage.middleware());
this.explore();
}
explore(): void {
const modules = this.getModules(
this.modulesContainer,
this.telegrafOptions.include || [],
);
this.registerUpdates(modules);
this.registerScenes(modules);
}
private registerUpdates(modules: Module[]): void {
const updates = this.flatMap<InstanceWrapper>(modules, (instance) =>
this.filterUpdates(instance),
);
updates.forEach(({ instance }) =>
this.registerInstanceMethodListeners(this.bot, instance),
);
}
private registerScenes(modules: Module[]): void {
const scenes = this.flatMap<InstanceWrapper>(modules, (instance) =>
this.filterScenes(instance),
);
scenes.forEach(({ instance }) => {
const sceneId = this.metadataAccessor.getSceneMetadata(
instance.constructor,
);
const scene = new BaseScene(sceneId);
this.stage.register(scene);
this.registerInstanceMethodListeners(scene, instance);
});
}
private filterUpdates(wrapper: InstanceWrapper): InstanceWrapper<unknown> {
const { instance } = wrapper;
if (!instance) return undefined;
const isUpdate = this.metadataAccessor.isUpdate(wrapper.metatype);
if (!isUpdate) return undefined;
return wrapper;
}
private filterScenes(wrapper: InstanceWrapper): InstanceWrapper<unknown> {
const { instance } = wrapper;
if (!instance) return undefined;
const isScene = this.metadataAccessor.isScene(wrapper.metatype);
if (!isScene) return undefined;
return wrapper;
}
private registerInstanceMethodListeners(
composer: Composer<never>,
instance: Record<string, Function>,
): void {
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(instance, prototype, (name) => {
const methodRef = instance[name];
const metadata = this.metadataAccessor.getListenerMetadata(methodRef);
if (!metadata) return;
const middlewareFn = methodRef.bind(instance);
const { method, args } = metadata;
composer[method](...args, middlewareFn);
});
}
}

View File

@ -18,13 +18,6 @@ export class MetadataAccessorService {
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;

View File

@ -1,98 +0,0 @@
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);
}
},
);
});
}
}

View File

@ -1,83 +0,0 @@
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);
}
});
});
}
}

View File

@ -1,61 +1,48 @@
import { DiscoveryModule, ModuleRef } from '@nestjs/core';
import {
Module,
DynamicModule,
Provider,
Type,
Global,
Inject,
Module,
OnApplicationShutdown,
Logger,
Provider,
Type,
} from '@nestjs/common';
import {
TelegrafModuleOptions,
TelegrafModuleAsyncOptions,
TelegrafModuleOptions,
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';
import { ListenersExplorerService, MetadataAccessorService } from './services';
import { createBotFactory, getBotToken } from './utils';
@Global()
@Module({
imports: [DiscoveryModule],
providers: [
UpdatesExplorerService,
ScenesExplorerService,
MetadataAccessorService,
],
providers: [ListenersExplorerService, MetadataAccessorService],
})
export class TelegrafCoreModule implements OnApplicationShutdown {
private static logger = new Logger(TelegrafCoreModule.name);
constructor(
@Inject(TELEGRAF_BOT_NAME) private readonly botName: string,
@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 = {
const telegrafBotNameProvider = {
provide: TELEGRAF_BOT_NAME,
useValue: telegrafBotName,
};
const telegrafBotProvider: Provider = {
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(),
useFactory: async () => await createBotFactory(options),
};
return {
@ -65,10 +52,7 @@ export class TelegrafCoreModule implements OnApplicationShutdown {
provide: TELEGRAF_MODULE_OPTIONS,
useValue: options,
},
{
provide: TELEGRAF_BOT_NAME,
useValue: telegrafBotName,
},
telegrafBotNameProvider,
telegrafBotProvider,
],
exports: [telegrafBotProvider],
@ -80,20 +64,15 @@ export class TelegrafCoreModule implements OnApplicationShutdown {
): DynamicModule {
const telegrafBotName = getBotToken(options.botName);
const telegrafBotProvider = {
provide: telegrafBotName,
useFactory: async (
telegrafModuleOptions: TelegrafModuleOptions,
): Promise<any> => {
const { botName, ...telegrafOptions } = telegrafModuleOptions;
const telegrafBotNameProvider = {
provide: TELEGRAF_BOT_NAME,
useValue: telegrafBotName,
};
return await defer(async () => {
const bot = new Telegraf<any>(telegrafOptions.token);
this.applyBotMiddlewares(bot, telegrafOptions.middlewares);
await bot.launch(telegrafOptions.launchOptions);
return bot;
}).toPromise();
},
const telegrafBotProvider: Provider = {
provide: telegrafBotName,
useFactory: async (options: TelegrafModuleOptions) =>
await createBotFactory(options),
inject: [TELEGRAF_MODULE_OPTIONS],
};
@ -103,22 +82,16 @@ export class TelegrafCoreModule implements OnApplicationShutdown {
imports: options.imports,
providers: [
...asyncProviders,
{
provide: TELEGRAF_BOT_NAME,
useValue: telegrafBotName,
},
telegrafBotNameProvider,
telegrafBotProvider,
],
exports: [telegrafBotProvider],
exports: [telegrafBotNameProvider, telegrafBotProvider],
};
}
private static applyBotMiddlewares(bot, middlewares) {
if (middlewares) {
middlewares.forEach((middleware) => {
bot.use(middleware);
});
}
async onApplicationShutdown(): Promise<void> {
const bot = this.moduleRef.get<any>(this.botName);
bot && (await bot.stop());
}
private static createAsyncProviders(
@ -158,9 +131,4 @@ export class TelegrafCoreModule implements OnApplicationShutdown {
inject,
};
}
async onApplicationShutdown(): Promise<void> {
const bot = this.moduleRef.get<any>(this.botName);
bot && (await bot.stop());
}
}

View File

@ -17,5 +17,5 @@ export type ComposerMethodArgs<
U extends OnlyFunctionPropertyNames<T> = OnlyFunctionPropertyNames<T>
> = Filter<Parameters<T[U]>, Middleware<never>>;
export type UpdateMethods = OnlyFunctionPropertyNames<Composer<never>>;
export type ComposerMethods = OnlyFunctionPropertyNames<Composer<never>>;
export type SceneMethods = OnlyFunctionPropertyNames<BaseScene<never>>;

View File

@ -0,0 +1,13 @@
import { Telegraf } from 'telegraf';
import { TelegrafModuleOptions } from '../interfaces';
export async function createBotFactory(
options: TelegrafModuleOptions,
): Promise<Telegraf<never>> {
const bot = new Telegraf<never>(options.token, options.options);
bot.use(...(options.middlewares ?? []));
await bot.launch(options.launchOptions);
return bot;
}

View File

@ -4,11 +4,24 @@ import { ComposerMethodArgs, SceneMethods } from '../types';
import { UPDATE_LISTENER_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces';
export function createSceneListenerDecorator<Method extends SceneMethods>(
method: Method,
export function createListenerDecorator<TMethod extends SceneMethods>(
method: TMethod,
) {
return (
...args: ComposerMethodArgs<Scene<never>, Method>
...args: ComposerMethodArgs<Scene<never>, TMethod>
): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,
args,
} as ListenerMetadata);
};
}
export function createMissedListenerDecorator<TArgs extends any[]>(
method: string,
) {
return (
...args: TArgs
): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,

View File

@ -1,7 +1,5 @@
import { DEFAULT_BOT_NAME } from '../telegraf.constants';
export function getBotToken(name?: string) {
return name && name !== DEFAULT_BOT_NAME
? `${name}_BOT_NAME`
: DEFAULT_BOT_NAME;
export function getBotToken(name?: string): string {
return name && name !== DEFAULT_BOT_NAME ? `${name}Bot` : DEFAULT_BOT_NAME;
}

View File

@ -1 +1,3 @@
export * from './get-bot-token.util';
export * from './create-bot-factory.util';
export * from './create-listener-decorator.util';

3995
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
{
"name": "nestjs-telegraf",
"version": "2.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"description": "Telegraf module for NestJS",
"keywords": [
"nest",
@ -65,8 +67,7 @@
},
"lint-staged": {
"*.ts": [
"prettier --write",
"git add"
"prettier --write"
]
}
}

View File

@ -0,0 +1,2 @@
ECHO_BOT_TOKEN=
GREETER_BOT_TOKEN=

View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

398
sample/01-complete-app/.gitignore vendored Normal file
View File

@ -0,0 +1,398 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
coverage/
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
=======
# Local
.env
dist

View File

@ -0,0 +1,14 @@
{
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "/src/.*\\.(test|spec).(ts|tsx|js)$",
"collectCoverageFrom" : ["src/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**"],
"coverageReporters": ["json", "lcov"]
}

11357
sample/01-complete-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
{
"name": "nest-typescript-starter",
"private": true,
"version": "1.0.0",
"description": "Nest TypeScript starter repository",
"license": "MIT",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "7.5.1",
"@nestjs/core": "7.5.1",
"@nestjs/platform-express": "7.5.1",
"dotenv": "8.2.0",
"nestjs-telegraf": "*",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "6.6.3",
"telegraf": "3.38.0"
},
"devDependencies": {
"@nestjs/cli": "7.5.1",
"@nestjs/schematics": "7.1.3",
"@nestjs/testing": "7.5.1",
"@types/express": "4.17.8",
"@types/jest": "26.0.15",
"@types/node": "14.14.6",
"@types/supertest": "2.0.10",
"@typescript-eslint/eslint-plugin": "4.6.1",
"@typescript-eslint/parser": "4.6.1",
"eslint": "7.12.1",
"eslint-config-prettier": "6.15.0",
"eslint-plugin-prettier": "3.1.4",
"jest": "26.6.3",
"prettier": "2.1.2",
"supertest": "6.0.0",
"ts-jest": "26.4.3",
"ts-loader": "8.0.8",
"ts-node": "9.0.0",
"tsconfig-paths": "3.9.0",
"typescript": "4.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -1 +1,3 @@
export const HELLO_SCENE_ID = 'HELLO_SCENE_ID';
export const GreeterBotName = 'greeter';

View File

@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { TelegrafModule } from 'nestjs-telegraf';
import { EchoModule } from './echo/echo.module';
import { GreeterModule } from './greeter/greeter.module';
import { sessionMiddleware } from './middleware/session.middleware';
import { GreeterBotName } from './app.constants';
@Module({
imports: [
TelegrafModule.forRoot({
token: process.env.ECHO_BOT_TOKEN,
middlewares: [sessionMiddleware],
include: [EchoModule],
}),
TelegrafModule.forRootAsync({
botName: GreeterBotName,
useFactory: () => ({
token: process.env.GREETER_BOT_TOKEN,
middlewares: [sessionMiddleware],
include: [GreeterModule],
}),
}),
EchoModule,
GreeterModule,
],
})
export class AppModule {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { EchoUpdate } from './echo.update';
import { EchoService } from './echo.service';
import { HelloScene } from './scenes/hello.scene';
@Module({
providers: [EchoUpdate, EchoService, HelloScene],
})
export class EchoModule {}

View File

@ -1,14 +1,22 @@
import { Telegraf } from 'telegraf';
import { Command, Help, InjectBot, On, Start, Update } from '../lib';
import {
Command,
getBotToken,
Help,
InjectBot,
On,
Start,
Update,
} from 'nestjs-telegraf';
import { EchoService } from './echo.service';
import { HELLO_SCENE_ID } from './app.constants';
import { Context } from './interfaces/context.interface';
import { GreeterBotName, HELLO_SCENE_ID } from '../app.constants';
import { Context } from '../interfaces/context.interface';
@Update()
export class AppUpdate {
export class EchoUpdate {
constructor(
@InjectBot()
private readonly bot: Telegraf<any>, // TODO: fix any
@InjectBot(GreeterBotName)
private readonly bot: Telegraf<Context>,
private readonly echoService: EchoService,
) {}

View File

@ -1,6 +1,6 @@
import { HELLO_SCENE_ID } from '../app.constants';
import { Context } from '../interfaces/context.interface';
import { Scene, SceneEnter, SceneLeave, Command } from '../../lib';
import { Scene, SceneEnter, SceneLeave, Command } from 'nestjs-telegraf';
import { HELLO_SCENE_ID } from '../../app.constants';
import { Context } from '../../interfaces/context.interface';
@Scene(HELLO_SCENE_ID)
export class HelloScene {

View File

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { GreeterUpdate } from './greeter.update';
@Module({
providers: [GreeterUpdate],
})
export class GreeterModule {}

View File

@ -0,0 +1,16 @@
import { Hears, Start, Update } from '../../../../lib';
import { Context } from '../interfaces/context.interface';
@Update()
export class GreeterUpdate {
@Start()
async onStart(ctx: Context): Promise<void> {
await ctx.reply('Say hello to me');
}
@Hears(['hi', 'hello', 'hey', 'qq'])
async onGreetings(ctx: Context): Promise<void> {
const { first_name } = ctx.from;
await ctx.reply(`Hey ${first_name}`);
}
}

View File

@ -1,3 +1,4 @@
import 'dotenv/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./src",
"incremental": true
},
"include": ["src/**/*"]
}

View File

@ -1,17 +0,0 @@
import { Module } from '@nestjs/common';
import { TelegrafModule } from '../lib';
import { EchoService } from './echo.service';
import { AppUpdate } from './app.update';
import { HelloScene } from './scenes/hello.scene';
import { sessionMiddleware } from './middleware/session.middleware';
@Module({
imports: [
TelegrafModule.forRoot({
token: '1467731595:AAHCvH65H9VQYKF9jE-E8c2rXsQBVAYseg8', // Don't steal >:(
middlewares: [sessionMiddleware],
}),
],
providers: [EchoService, AppUpdate, HelloScene],
})
export class AppModule {}

View File

@ -7,12 +7,12 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "es2017",
"sourceMap": false,
"outDir": "./dist",
"rootDir": "./lib",
"outDir": "./dist",
"skipLibCheck": true
},
"include": ["lib/**/*", "../index.ts"],
"include": ["lib/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}