initial commit

This commit is contained in:
Alexander Bukhalo
2022-11-20 14:13:48 +03:00
parent 1bb274e8e2
commit 2d23eba148
94 changed files with 65 additions and 17013 deletions

View File

@@ -1,5 +0,0 @@
export * from './update.decorator';
export * from './scene.decorator';
export * from './wizard.decorator';
export * from './inject-bot.decorator';
export * from './inject-all-bots.decorator';

View File

@@ -1,9 +0,0 @@
import { Inject } from '@nestjs/common';
import { Telegraf } from 'telegraf';
import { getAllBotsToken } from '../../utils/get-all-bots-token.util';
export type AllBotsMap = Map<string, Telegraf<any>>;
export const InjectAllBots = (): ParameterDecorator =>
Inject(getAllBotsToken());

View File

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

View File

@@ -1,14 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { SceneOptions } from 'telegraf/typings/scenes/base';
import { SceneMetadata } from '../../interfaces';
import { SCENE_METADATA } from '../../telegraf.constants';
export const Scene = (
sceneId: string,
options?: SceneOptions<any>,
): ClassDecorator =>
SetMetadata<string, SceneMetadata>(SCENE_METADATA, {
sceneId,
type: 'base',
options,
});

View File

@@ -1,8 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { UPDATE_METADATA } from '../../telegraf.constants';
/**
* `@Update` decorator, it's like NestJS `@Controller` decorator,
* but for Telegram Bot API updates.
*/
export const Update = (): ClassDecorator => SetMetadata(UPDATE_METADATA, true);

View File

@@ -1,14 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { SceneOptions } from 'telegraf/typings/scenes/base';
import { SceneMetadata } from '../../interfaces';
import { SCENE_METADATA } from '../../telegraf.constants';
export const Wizard = (
sceneId: string,
options?: SceneOptions<any>,
): ClassDecorator =>
SetMetadata<string, SceneMetadata>(SCENE_METADATA, {
sceneId,
type: 'wizard',
options,
});

View File

@@ -1,4 +0,0 @@
export * from './core';
export * from './listeners';
export * from './scene';
export * from './params';

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with email entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-email
*/
export const Email = createListenerDecorator('email');

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
export * from './on.decorator';
export * from './use.decorator';
export * from './action.decorator';
export * from './cashtag.decorator';
export * from './command.decorator';
export * from './game-query.decorator';
export * from './hashtag.decorator';
export * from './hears.decorator';
export * from './help.decorator';
export * from './inline-query.decorator';
export * from './mention.decorator';
export * from './phone.decorator';
export * from './settings.decorator';
export * from './start.decorator';
export * from './email.decorator';
export * from './url.decorator';
export * from './text-link.decorator';
export * from './text-mention.decorator';

View File

@@ -1,8 +0,0 @@
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling inline_query actions with regular expressions.
*
* @see https://telegraf.js.org/#/?id=inlinequery
*/
export const InlineQuery = createListenerDecorator('inlineQuery');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with text_link entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-textlink
*/
export const TextLink = createListenerDecorator('textLink');

View File

@@ -1,8 +0,0 @@
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with text_mention entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-textlink
*/
export const TextMention = createListenerDecorator('textMention');

View File

@@ -1,8 +0,0 @@
import { createListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with url entity.
*
* @see https://telegraf.js.org/#/?id=telegraf-url
*/
export const Url = createListenerDecorator('url');

View File

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

View File

@@ -1,8 +0,0 @@
import { createTelegrafParamDecorator } from '../../utils/param-decorator.util';
import { TelegrafParamtype } from '../../enums/telegraf-paramtype.enum';
export const Context: () => ParameterDecorator = createTelegrafParamDecorator(
TelegrafParamtype.CONTEXT,
);
export const Ctx = Context;

View File

@@ -1,4 +0,0 @@
export * from './context.decorator';
export * from './next.decorator';
export * from './message.decorator';
export * from './sender.decorator';

View File

@@ -1,21 +0,0 @@
import { PipeTransform, Type } from '@nestjs/common';
import { createTelegrafPipesParamDecorator } from '../../utils/param-decorator.util';
import { TelegrafParamtype } from '../../enums/telegraf-paramtype.enum';
export function Message(): ParameterDecorator;
export function Message(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
export function Message(
property: string,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
export function Message(
property?: string | (Type<PipeTransform> | PipeTransform),
...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
return createTelegrafPipesParamDecorator(TelegrafParamtype.MESSAGE)(
property,
...pipes,
);
}

View File

@@ -1,6 +0,0 @@
import { createTelegrafParamDecorator } from '../../utils/param-decorator.util';
import { TelegrafParamtype } from '../../enums/telegraf-paramtype.enum';
export const Next: () => ParameterDecorator = createTelegrafParamDecorator(
TelegrafParamtype.NEXT,
);

View File

@@ -1,21 +0,0 @@
import { PipeTransform, Type } from '@nestjs/common';
import { createTelegrafPipesParamDecorator } from '../../utils/param-decorator.util';
import { TelegrafParamtype } from '../../enums/telegraf-paramtype.enum';
export function Sender(): ParameterDecorator;
export function Sender(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
export function Sender(
property: string,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
export function Sender(
property?: string | (Type<PipeTransform> | PipeTransform),
...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
return createTelegrafPipesParamDecorator(TelegrafParamtype.SENDER)(
property,
...pipes,
);
}

View File

@@ -1,3 +0,0 @@
export * from './scene-enter.decorator';
export * from './scene-leave.decorator';
export * from './wizard-step.decorator';

View File

@@ -1,5 +0,0 @@
import { createListenerDecorator } from '../../utils';
import { Scenes } from 'telegraf';
export const SceneEnter =
createListenerDecorator<Scenes.BaseScene<never>>('enter');

View File

@@ -1,5 +0,0 @@
import { createListenerDecorator } from '../../utils';
import { Scenes } from 'telegraf';
export const SceneLeave =
createListenerDecorator<Scenes.BaseScene<never>>('leave');

View File

@@ -1,6 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { WizardStepMetadata } from '../../interfaces';
import { WIZARD_STEP_METADATA } from '../../telegraf.constants';
export const WizardStep = (step: number) =>
SetMetadata<string, WizardStepMetadata>(WIZARD_STEP_METADATA, { step });

View File

@@ -1,7 +0,0 @@
export enum TelegrafParamtype {
CONTEXT,
NEXT,
SENDER,
MESSAGE,
// TODO-Possible-Feature: Add more paramtypes
}

View File

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

View File

@@ -1 +0,0 @@
export class TelegrafException extends Error {}

View File

@@ -1,3 +0,0 @@
export * from './tg-arguments-host.interface';
export * from './telegraf-arguments-host';
export * from './telegraf-execution-context';

View File

@@ -1,23 +0,0 @@
import { ArgumentsHost } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { TgArgumentsHost } from './tg-arguments-host.interface';
export class TelegrafArgumentsHost
extends ExecutionContextHost
implements TgArgumentsHost
{
static create(context: ArgumentsHost): TelegrafArgumentsHost {
const type = context.getType();
const tgContext = new TelegrafArgumentsHost(context.getArgs());
tgContext.setType(type);
return tgContext;
}
getContext<T = any>(): T {
return this.getArgByIndex(0);
}
getNext<T = any>(): T {
return this.getArgByIndex(1);
}
}

View File

@@ -1,33 +0,0 @@
import { ContextType, ExecutionContext } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { TgArgumentsHost } from './tg-arguments-host.interface';
export type TelegrafContextType = 'telegraf' | ContextType;
export class TelegrafExecutionContext
extends ExecutionContextHost
implements TgArgumentsHost
{
static create(context: ExecutionContext): TelegrafExecutionContext {
const type = context.getType();
const tgContext = new TelegrafExecutionContext(
context.getArgs(),
context.getClass(),
context.getHandler(),
);
tgContext.setType(type);
return tgContext;
}
getType<TContext extends string = TelegrafContextType>(): TContext {
return super.getType();
}
getContext<T = any>(): T {
return this.getArgByIndex(0);
}
getNext<T = any>(): T {
return this.getArgByIndex(1);
}
}

View File

@@ -1,6 +0,0 @@
import { ArgumentsHost } from '@nestjs/common';
export interface TgArgumentsHost extends ArgumentsHost {
getContext<T = any>(): T;
getNext<T = any>(): T;
}

View File

@@ -1,28 +0,0 @@
import { ParamData } from '@nestjs/common';
import { ParamsFactory } from '@nestjs/core/helpers/external-context-creator';
import { Context } from 'telegraf';
import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum';
export class TelegrafParamsFactory implements ParamsFactory {
exchangeKeyForValue(
type: TelegrafParamtype,
data: ParamData,
args: unknown[],
): unknown {
const ctx = args[0] as Context;
const next = args[1] as Function;
switch (type) {
case TelegrafParamtype.CONTEXT:
return ctx;
case TelegrafParamtype.NEXT:
return next;
case TelegrafParamtype.SENDER:
return data && ctx.from ? ctx.from[data as string] : ctx.from;
case TelegrafParamtype.MESSAGE:
return data && ctx.message ? ctx.message[data as string] : ctx.message;
default:
return null;
}
}
}

View File

@@ -1,9 +0,0 @@
export * from './decorators';
export * from './interfaces';
export * from './utils';
export * from './types';
export * from './services';
export * from './errors';
export * from './execution-context';
export * from './telegraf.constants';
export * from './telegraf.module';

View File

@@ -1,4 +0,0 @@
export * from './telegraf-options.interface';
export * from './listener-metadata.interface';
export * from './scene-metadata.interface';
export * from './telegraf-exception-filter.interface';

View File

@@ -1,4 +0,0 @@
export interface ListenerMetadata {
method: string;
args: unknown[];
}

View File

@@ -1,11 +0,0 @@
import { SceneOptions } from 'telegraf/typings/scenes/base';
export interface SceneMetadata {
sceneId: string;
type: 'base' | 'wizard';
options?: SceneOptions<any>;
}
export interface WizardStepMetadata {
step: number;
}

View File

@@ -1,5 +0,0 @@
import { ArgumentsHost } from '@nestjs/common';
export interface TelegrafExceptionFilter<T = any> {
catch(exception: T, host: ArgumentsHost): any;
}

View File

@@ -1,28 +0,0 @@
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
import { Middleware, Telegraf } from 'telegraf';
export interface TelegrafModuleOptions {
token: string;
botName?: string;
options?: Partial<Telegraf.Options<any>>;
launchOptions?: Telegraf.LaunchOptions | false;
include?: Function[];
middlewares?: ReadonlyArray<Middleware<any>>;
}
export interface TelegrafOptionsFactory {
createTelegrafOptions():
| Promise<TelegrafModuleOptions>
| TelegrafModuleOptions;
}
export interface TelegrafModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> {
botName?: string;
useExisting?: Type<TelegrafOptionsFactory>;
useClass?: Type<TelegrafOptionsFactory>;
useFactory?: (
...args: any[]
) => Promise<TelegrafModuleOptions> | TelegrafModuleOptions;
inject?: any[];
}

View File

@@ -1,52 +0,0 @@
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { Module } from '@nestjs/core/injector/module';
import { flattenDeep, identity, isEmpty } from 'lodash';
export class BaseExplorerService {
getModules(
modulesContainer: Map<string, Module>,
include: Function[],
): Module[] {
if (!include || isEmpty(include)) {
return [...modulesContainer.values()];
}
return this.includeWhitelisted(modulesContainer, include);
}
includeWhitelisted(
modulesContainer: Map<string, Module>,
include: Function[],
): Module[] {
const modules = [...modulesContainer.values()];
return modules.filter(({ metatype }) => include.includes(metatype));
}
flatMap<T>(
modules: Module[],
callback: (instance: InstanceWrapper, moduleRef: Module) => T | T[],
): T[] {
const visitedModules = new Set<Module>();
const unwrap = (moduleRef: Module) => {
// protection from circular recursion
if (visitedModules.has(moduleRef)) {
return [];
} else {
visitedModules.add(moduleRef);
}
const providers = [...moduleRef.providers.values()];
const defined = providers.map((wrapper) => callback(wrapper, moduleRef));
const imported: (T | T[])[] = moduleRef.imports?.size
? [...moduleRef.imports.values()].reduce((prev, cur) => {
return [...prev, ...unwrap(cur)];
}, [])
: [];
return [...defined, ...imported];
};
return flattenDeep(modules.map(unwrap)).filter(identity);
}
}

View File

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

View File

@@ -1,239 +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 { Module } from '@nestjs/core/injector/module';
import { ParamMetadata } from '@nestjs/core/helpers/interfaces';
import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator';
import { Composer, Context, Scenes, Telegraf } from 'telegraf';
import { MetadataAccessorService } from './metadata-accessor.service';
import {
PARAM_ARGS_METADATA,
TELEGRAF_BOT_NAME,
TELEGRAF_MODULE_OPTIONS,
TELEGRAF_STAGE,
} from '../telegraf.constants';
import { BaseExplorerService } from './base-explorer.service';
import { TelegrafParamsFactory } from '../factories/telegraf-params-factory';
import { TelegrafContextType } from '../execution-context';
import { ListenerMetadata, TelegrafModuleOptions } from '../interfaces';
@Injectable()
export class ListenersExplorerService
extends BaseExplorerService
implements OnModuleInit
{
private readonly telegrafParamsFactory = new TelegrafParamsFactory();
private bot: Telegraf<any>;
constructor(
@Inject(TELEGRAF_STAGE)
private readonly stage: Scenes.Stage<any>,
@Inject(TELEGRAF_MODULE_OPTIONS)
private readonly telegrafOptions: TelegrafModuleOptions,
@Inject(TELEGRAF_BOT_NAME)
private readonly botName: string,
private readonly moduleRef: ModuleRef,
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: MetadataAccessorService,
private readonly metadataScanner: MetadataScanner,
private readonly modulesContainer: ModulesContainer,
private readonly externalContextCreator: ExternalContextCreator,
) {
super();
}
onModuleInit(): void {
this.bot = this.moduleRef.get<Telegraf<any>>(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((wrapper) => this.registerListeners(this.bot, wrapper));
}
private registerScenes(modules: Module[]): void {
const scenes = this.flatMap<InstanceWrapper>(modules, (wrapper) =>
this.filterScenes(wrapper),
);
scenes.forEach((wrapper) => {
const { sceneId, type, options } = this.metadataAccessor.getSceneMetadata(
wrapper.instance.constructor,
);
const scene =
type === 'base'
? new Scenes.BaseScene<any>(sceneId, options || ({} as any))
: new Scenes.WizardScene<any>(sceneId, options || ({} as any));
this.stage.register(scene);
if (type === 'base') {
this.registerListeners(scene, wrapper);
} else {
this.registerWizardListeners(scene as Scenes.WizardScene<any>, wrapper);
}
});
}
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 registerListeners(
composer: Composer<any>,
wrapper: InstanceWrapper<unknown>,
): void {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(instance, prototype, (name) =>
this.registerIfListener(composer, instance, prototype, name),
);
}
private registerWizardListeners(
wizard: Scenes.WizardScene<any>,
wrapper: InstanceWrapper<unknown>,
): void {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);
type WizardMetadata = { step: number; methodName: string };
const wizardSteps: WizardMetadata[] = [];
const basicListeners = [];
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodName) => {
const methodRef = prototype[methodName];
const metadata = this.metadataAccessor.getWizardStepMetadata(methodRef);
if (!metadata) {
basicListeners.push(methodName);
return undefined;
}
wizardSteps.push({ step: metadata.step, methodName });
},
);
for (const methodName of basicListeners) {
this.registerIfListener(wizard, instance, prototype, methodName);
}
const group = wizardSteps
.sort((a, b) => a.step - b.step)
.reduce<{ [key: number]: WizardMetadata[] }>(
(prev, cur) => ({
...prev,
[cur.step]: [...(prev[cur.step] || []), cur],
}),
{},
);
wizard.steps = Object.values(group).map((stepsMetadata) => {
const composer = new Composer();
stepsMetadata.forEach((stepMethod) => {
this.registerIfListener(
composer,
instance,
prototype,
stepMethod.methodName,
[{ method: 'use', args: [] }],
);
});
return composer.middleware();
});
}
private registerIfListener(
composer: Composer<any>,
instance: any,
prototype: any,
methodName: string,
defaultMetadata?: ListenerMetadata[],
): void {
const methodRef = prototype[methodName];
const metadata =
this.metadataAccessor.getListenerMetadata(methodRef) || defaultMetadata;
if (!metadata || metadata.length < 1) {
return undefined;
}
const listenerCallbackFn = this.createContextCallback(
instance,
prototype,
methodName,
);
for (const { method, args } of metadata) {
/* Basic callback */
// composer[method](...args, listenerCallbackFn);
/* Complex callback return value handing */
composer[method](
...args,
async (ctx: Context, next: Function): Promise<void> => {
const result = await listenerCallbackFn(ctx, next);
if (result) {
await ctx.reply(String(result));
}
// TODO-Possible-Feature: Add more supported return types
},
);
}
}
createContextCallback<T extends Record<string, unknown>>(
instance: T,
prototype: unknown,
methodName: string,
) {
const paramsFactory = this.telegrafParamsFactory;
return this.externalContextCreator.create<
Record<number, ParamMetadata>,
TelegrafContextType
>(
instance,
prototype[methodName],
methodName,
PARAM_ARGS_METADATA,
paramsFactory,
undefined,
undefined,
undefined,
'telegraf',
);
}
}

View File

@@ -1,40 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
SCENE_METADATA,
LISTENERS_METADATA,
UPDATE_METADATA,
WIZARD_STEP_METADATA,
} from '../telegraf.constants';
import {
ListenerMetadata,
SceneMetadata,
WizardStepMetadata,
} from '../interfaces';
@Injectable()
export class MetadataAccessorService {
constructor(private readonly reflector: Reflector) {}
isUpdate(target: Function): boolean {
if (!target) return false;
return !!this.reflector.get(UPDATE_METADATA, target);
}
isScene(target: Function): boolean {
if (!target) return false;
return !!this.reflector.get(SCENE_METADATA, target);
}
getListenerMetadata(target: Function): ListenerMetadata[] | undefined {
return this.reflector.get(LISTENERS_METADATA, target);
}
getSceneMetadata(target: Function): SceneMetadata | undefined {
return this.reflector.get(SCENE_METADATA, target);
}
getWizardStepMetadata(target: Function): WizardStepMetadata | undefined {
return this.reflector.get(WIZARD_STEP_METADATA, target);
}
}

View File

@@ -1,8 +0,0 @@
import { Provider } from '@nestjs/common';
import { Scenes } from 'telegraf';
import { TELEGRAF_STAGE } from './telegraf.constants';
export const telegrafStageProvider: Provider = {
provide: TELEGRAF_STAGE,
useClass: Scenes.Stage,
};

View File

@@ -1,10 +0,0 @@
import { Provider } from '@nestjs/common';
import { Telegraf } from 'telegraf';
import { TELEGRAF_ALL_BOTS } from './telegraf.constants';
export const allBotsMap = new Map<string, Telegraf<any>>();
export const telegrafAllBotsProvider: Provider = {
provide: TELEGRAF_ALL_BOTS,
useValue: allBotsMap,
};

View File

@@ -1,160 +0,0 @@
import { DiscoveryModule, ModuleRef } from '@nestjs/core';
import {
DynamicModule,
Global,
Inject,
Module,
OnApplicationShutdown,
Provider,
Type,
} from '@nestjs/common';
import {
TelegrafModuleAsyncOptions,
TelegrafModuleOptions,
TelegrafOptionsFactory,
} from './interfaces';
import {
TELEGRAF_BOT_NAME,
TELEGRAF_MODULE_OPTIONS,
} from './telegraf.constants';
import { ListenersExplorerService, MetadataAccessorService } from './services';
import { telegrafStageProvider } from './stage.provider';
import {
allBotsMap,
telegrafAllBotsProvider,
} from './telegraf-all-bots.provider';
import { createBotFactory, getBotToken } from './utils';
@Global()
@Module({
imports: [DiscoveryModule],
providers: [ListenersExplorerService, MetadataAccessorService],
})
export class TelegrafCoreModule implements OnApplicationShutdown {
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 telegrafBotNameProvider = {
provide: TELEGRAF_BOT_NAME,
useValue: telegrafBotName,
};
const telegrafBotProvider: Provider = {
provide: telegrafBotName,
useFactory: async () => {
const bot = await createBotFactory(options);
allBotsMap.set(telegrafBotName, bot);
return bot;
},
};
return {
module: TelegrafCoreModule,
providers: [
{
provide: TELEGRAF_MODULE_OPTIONS,
useValue: options,
},
telegrafStageProvider,
telegrafBotNameProvider,
telegrafBotProvider,
telegrafAllBotsProvider,
],
exports: [
telegrafStageProvider,
telegrafBotNameProvider,
telegrafBotProvider,
telegrafAllBotsProvider,
],
};
}
public static forRootAsync(
options: TelegrafModuleAsyncOptions,
): DynamicModule {
const telegrafBotName = getBotToken(options.botName);
const telegrafBotNameProvider = {
provide: TELEGRAF_BOT_NAME,
useValue: telegrafBotName,
};
const telegrafBotProvider: Provider = {
provide: telegrafBotName,
useFactory: async (options: TelegrafModuleOptions) => {
const bot = await createBotFactory(options);
allBotsMap.set(telegrafBotName, bot);
return bot;
},
inject: [TELEGRAF_MODULE_OPTIONS],
};
const asyncProviders = this.createAsyncProviders(options);
return {
module: TelegrafCoreModule,
imports: options.imports,
providers: [
...asyncProviders,
telegrafStageProvider,
telegrafBotNameProvider,
telegrafBotProvider,
telegrafAllBotsProvider,
],
exports: [
telegrafStageProvider,
telegrafBotNameProvider,
telegrafBotProvider,
telegrafAllBotsProvider,
],
};
}
async onApplicationShutdown(): Promise<void> {
const bot = this.moduleRef.get<any>(this.botName);
bot && (await bot.stop());
}
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,
};
}
}

View File

@@ -1,16 +0,0 @@
import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
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 SCENE_METADATA = 'SCENE_METADATA';
export const LISTENERS_METADATA = 'LISTENERS_METADATA';
export const WIZARD_STEP_METADATA = 'WIZARD_STEP_METADATA';
export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA;
export const TELEGRAF_STAGE = 'TelegrafStage';
export const TELEGRAF_ALL_BOTS = 'TELEGRAF_ALL_BOTS';

View File

@@ -1,27 +0,0 @@
import { Module, DynamicModule } from '@nestjs/common';
import { TelegrafCoreModule } from './telegraf-core.module';
import {
TelegrafModuleOptions,
TelegrafModuleAsyncOptions,
} from './interfaces';
@Module({})
export class TelegrafModule {
public static forRoot(options: TelegrafModuleOptions): DynamicModule {
return {
module: TelegrafModule,
imports: [TelegrafCoreModule.forRoot(options)],
exports: [TelegrafCoreModule],
};
}
public static forRootAsync(
options: TelegrafModuleAsyncOptions,
): DynamicModule {
return {
module: TelegrafModule,
imports: [TelegrafCoreModule.forRootAsync(options)],
exports: [TelegrafCoreModule],
};
}
}

View File

@@ -1,20 +0,0 @@
import { Composer, Middleware } from 'telegraf';
export type Filter<T extends any[], F> = T extends []
? []
: T extends [infer Head, ...infer Tail]
? Head extends F
? Filter<Tail, F>
: [Head, ...Filter<Tail, F>]
: [];
export type OnlyFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
type ParametersOrNever<T> = T extends (...args: any[]) => any ? Parameters<T> : never;
export type ComposerMethodArgs<
T extends Composer<never>,
U extends OnlyFunctionPropertyNames<T> = OnlyFunctionPropertyNames<T>,
> = Filter<ParametersOrNever<T[U]>, Middleware<never>>;

View File

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

View File

@@ -1,30 +0,0 @@
import { Composer } from 'telegraf';
import { ComposerMethodArgs, OnlyFunctionPropertyNames } from '../types';
import { LISTENERS_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces';
export function createListenerDecorator<
TComposer extends Composer<never>,
TMethod extends OnlyFunctionPropertyNames<TComposer> = OnlyFunctionPropertyNames<TComposer>,
>(method: TMethod) {
return (...args: ComposerMethodArgs<TComposer, TMethod>): MethodDecorator => {
return (
_target: any,
_key?: string | symbol,
descriptor?: TypedPropertyDescriptor<any>,
) => {
const metadata = [
{
method,
args,
} as ListenerMetadata,
];
const previousValue =
Reflect.getMetadata(LISTENERS_METADATA, descriptor.value) || [];
const value = [...previousValue, ...metadata];
Reflect.defineMetadata(LISTENERS_METADATA, value, descriptor.value);
return descriptor;
};
};
}

View File

@@ -1,3 +0,0 @@
import { TELEGRAF_ALL_BOTS } from '../telegraf.constants';
export const getAllBotsToken = (): string => TELEGRAF_ALL_BOTS;

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
import { assignMetadata, PipeTransform, Type } from '@nestjs/common';
import { isNil, isString } from '@nestjs/common/utils/shared.utils';
import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum';
import { PARAM_ARGS_METADATA } from '../telegraf.constants';
export type ParamData = object | string | number;
export const createTelegrafParamDecorator =
(paramtype: TelegrafParamtype) =>
(data?: ParamData): ParameterDecorator =>
(target, key, index) => {
const args =
Reflect.getMetadata(PARAM_ARGS_METADATA, target.constructor, key) || {};
Reflect.defineMetadata(
PARAM_ARGS_METADATA,
assignMetadata(args, paramtype, index, data),
target.constructor,
key,
);
};
export const createTelegrafPipesParamDecorator =
(paramtype: TelegrafParamtype) =>
(
data?: any,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator =>
(target, key, index) => {
addPipesMetadata(paramtype, data, pipes, target, key, index);
};
export const addPipesMetadata = (
paramtype: TelegrafParamtype,
data: any,
pipes: (Type<PipeTransform> | PipeTransform)[],
target: Record<string, any>,
key: string | symbol,
index: number,
) => {
const args =
Reflect.getMetadata(PARAM_ARGS_METADATA, target.constructor, key) || {};
const hasParamData = isNil(data) || isString(data);
const paramData = hasParamData ? data : undefined;
const paramPipes = hasParamData ? pipes : [data, ...pipes];
Reflect.defineMetadata(
PARAM_ARGS_METADATA,
assignMetadata(args, paramtype, index, paramData, ...paramPipes),
target.constructor,
key,
);
};