feat!(): add custom execution context

This commit is contained in:
Morb0 2021-01-03 01:30:57 +03:00
parent 85bb916709
commit af632ea471
64 changed files with 899 additions and 69 deletions

10
TODO.md Normal file
View File

@ -0,0 +1,10 @@
PR TODO List:
* [ ] Include scene support to one explorer
* [ ] Split explorer to more sense components
* [ ] Refactor new execution context code
* [ ] Add support for new return types (files, images, music, git, etc)
* [ ] Add more param decorators (only often used)
* [ ] Review exception filter
* [ ] Add custom error messages (now used "Internal Server Error")
* [ ] Allow disabling default error send to chat
* [ ] Test all components

View File

@ -0,0 +1,38 @@
import { NestContainer } from '@nestjs/core';
import { BaseExceptionFilterContext } from '@nestjs/core/exceptions/base-exception-filter-context';
import { EXCEPTION_FILTERS_METADATA } from '@nestjs/common/constants';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { TelegrafExceptionsHandler } from '../exceptions/telegraf-exceptions-handler';
export class FiltersContextCreator extends BaseExceptionFilterContext {
constructor(container: NestContainer) {
super(container);
}
public create(
instance: object,
callback: (...args: any[]) => void,
moduleKey: string,
): TelegrafExceptionsHandler {
this.moduleContext = moduleKey;
const exceptionHandler = new TelegrafExceptionsHandler();
const filters = this.createContext(
instance,
callback,
EXCEPTION_FILTERS_METADATA,
);
if (isEmpty(filters)) {
return exceptionHandler;
}
exceptionHandler.setCustomFilters(filters.reverse());
return exceptionHandler;
}
public getGlobalMetadata<T extends any[]>(): T {
return [] as T;
}
}

View File

@ -0,0 +1,278 @@
import { Controller, PipeTransform } from '@nestjs/common/interfaces';
import { PipesContextCreator } from '@nestjs/core/pipes/pipes-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
import { GuardsContextCreator } from '@nestjs/core/guards/guards-context-creator';
import { GuardsConsumer } from '@nestjs/core/guards/guards-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import {
ContextUtils,
ParamProperties,
} from '@nestjs/core/helpers/context-utils';
import { HandlerMetadataStorage } from '@nestjs/core/helpers/handler-metadata-storage';
import { FORBIDDEN_MESSAGE } from '@nestjs/core/guards/constants';
import { ParamsMetadata } from '@nestjs/core/helpers/interfaces';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { FiltersContextCreator } from './filters-context-creator';
import { TelegrafContextType } from '../execution-context/telegraf-execution-context';
import { TelegrafProxy } from './telegraf-proxy';
import { TelegrafException } from '../errors';
import {
CUSTOM_LISTENER_AGRS_METADATA,
LISTENER_ARGS_METADATA,
} from '../telegraf.constants';
import { TelegrafParamsFactory } from '../factories/telegraf-params-factory';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
export type Update = Controller;
type TelegrafParamProperties = ParamProperties & { metatype?: any };
export interface TelegrafHandlerMetadata {
argsLength: number;
paramtypes: any[];
getParamsMetadata: (moduleKey: string) => TelegrafParamProperties[];
}
export class TelegrafContextCreator {
private readonly contextUtils = new ContextUtils();
private readonly telegrafParamsFactory = new TelegrafParamsFactory();
private readonly handlerMetadataStorage = new HandlerMetadataStorage<TelegrafHandlerMetadata>();
constructor(
private readonly telegrafProxy: TelegrafProxy,
private readonly exceptionFiltersContext: FiltersContextCreator,
private readonly pipesContextCreator: PipesContextCreator,
private readonly pipesConsumer: PipesConsumer,
private readonly guardsContextCreator: GuardsContextCreator,
private readonly guardsConsumer: GuardsConsumer,
private readonly interceptorsContextCreator: InterceptorsContextCreator,
private readonly interceptorsConsumer: InterceptorsConsumer,
) {}
public create<T extends ParamsMetadata = ParamsMetadata>(
instance: Update,
methodRef: (...args: unknown[]) => void,
moduleName: string,
methodKey: string,
): (...args: any[]) => Promise<void> {
const contextType: TelegrafContextType = 'telegraf';
const { argsLength, paramtypes, getParamsMetadata } = this.getMetadata<T>(
instance,
methodKey,
contextType,
);
const exceptionHandler = this.exceptionFiltersContext.create(
instance,
methodRef,
moduleName,
);
const pipes = this.pipesContextCreator.create(
instance,
methodRef,
moduleName,
);
const guards = this.guardsContextCreator.create(
instance,
methodRef,
moduleName,
);
const interceptors = this.interceptorsContextCreator.create(
instance,
methodRef,
moduleName,
);
const paramsMetadata = getParamsMetadata(moduleName);
const paramsOptions = paramsMetadata
? this.contextUtils.mergeParamsMetatypes(paramsMetadata, paramtypes)
: [];
const fnApplyPipes = this.createPipesFn(pipes, paramsOptions);
const fnCanActivate = this.createGuardsFn(
guards,
instance,
methodRef,
contextType,
);
const handler = <TContext>(
initialArgs: unknown[],
ctx: TContext,
next: Function,
) => async () => {
if (fnApplyPipes) {
await fnApplyPipes(initialArgs, ctx, next);
return methodRef.apply(instance, initialArgs);
}
return methodRef.apply(instance, [ctx, next]);
};
const targetCallback = async <TContext>(ctx: TContext, next: Function) => {
const initialArgs = this.contextUtils.createNullArray(argsLength);
fnCanActivate && (await fnCanActivate([ctx, next]));
return this.interceptorsConsumer.intercept(
interceptors,
[ctx, next],
instance,
methodRef,
handler(initialArgs, ctx, next),
contextType,
);
};
return this.telegrafProxy.create(targetCallback, exceptionHandler);
}
public getMetadata<
TMetadata,
TContext extends TelegrafContextType = TelegrafContextType
>(
instance: Controller,
methodName: string,
contextType: TContext,
): TelegrafHandlerMetadata {
const cachedMetadata = this.handlerMetadataStorage.get(
instance,
methodName,
);
if (cachedMetadata) return cachedMetadata;
const metadata =
this.contextUtils.reflectCallbackMetadata<TMetadata>(
instance,
methodName,
LISTENER_ARGS_METADATA,
) || {};
const keys = Object.keys(metadata);
const argsLength = this.contextUtils.getArgumentsLength(keys, metadata);
const contextFactory = this.contextUtils.getContextFactory(
contextType,
instance,
instance[methodName],
);
const getParamsMetadata = (moduleKey: string) =>
this.exchangeKeysForValues(
keys,
metadata,
moduleKey,
this.telegrafParamsFactory,
contextFactory,
);
const paramtypes = this.contextUtils.reflectCallbackParamtypes(
instance,
methodName,
);
const handlerMetadata: TelegrafHandlerMetadata = {
argsLength,
paramtypes,
getParamsMetadata,
};
this.handlerMetadataStorage.set(instance, methodName, handlerMetadata);
return handlerMetadata;
}
public exchangeKeysForValues<TMetadata = any>(
keys: string[],
metadata: TMetadata,
moduleContext: string,
paramsFactory: TelegrafParamsFactory,
contextFactory: (args: unknown[]) => ExecutionContextHost,
): ParamProperties[] {
this.pipesContextCreator.setModuleContext(moduleContext);
return keys.map((key) => {
const { index, data, pipes: pipesCollection } = metadata[key];
const pipes = this.pipesContextCreator.createConcreteContext(
pipesCollection,
);
const type = this.contextUtils.mapParamType(key);
if (key.includes(CUSTOM_LISTENER_AGRS_METADATA)) {
const { factory } = metadata[key];
const customExtractValue = this.contextUtils.getCustomFactory(
factory,
data,
contextFactory,
);
return { index, extractValue: customExtractValue, type, data, pipes };
}
const numericType = Number(type);
const extractValue = <TContext>(ctx: TContext, next: Function) =>
paramsFactory.exchangeKeyForValue(numericType, ctx, next);
return { index, extractValue, type: numericType, data, pipes };
});
}
public createGuardsFn<TContext extends string = TelegrafContextType>(
guards: any[],
instance: Controller,
callback: (...args: unknown[]) => any,
contextType?: TContext,
): Function | null {
const canActivateFn = async (args: any[]) => {
const canActivate = await this.guardsConsumer.tryActivate<TContext>(
guards,
args,
instance,
callback,
contextType,
);
if (!canActivate) {
throw new TelegrafException(FORBIDDEN_MESSAGE);
}
};
return guards.length ? canActivateFn : null;
}
public createPipesFn(
pipes: PipeTransform[],
paramsOptions: (ParamProperties & { metatype?: unknown })[],
) {
const pipesFn = async <TContext>(
args: unknown[],
ctx: TContext,
next: Function,
) => {
const resolveParamValue = async (
param: ParamProperties & { metatype?: unknown },
) => {
const {
index,
extractValue,
type,
data,
metatype,
pipes: paramPipes,
} = param;
const value = extractValue(ctx, next);
args[index] = await this.getParamValue(
value,
{ metatype, type, data },
pipes.concat(paramPipes),
);
};
await Promise.all(paramsOptions.map(resolveParamValue));
};
return paramsOptions.length ? pipesFn : null;
}
public async getParamValue<T>(
value: T,
{ metatype, type, data }: { metatype: any; type: any; data: any },
pipes: PipeTransform[],
): Promise<any> {
return isEmpty(pipes)
? value
: this.pipesConsumer.apply(value, { metatype, type, data }, pipes);
}
}

View File

@ -0,0 +1,38 @@
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { catchError } from 'rxjs/operators';
import { EMPTY } from 'rxjs';
import { isObservable } from '../helpers/is-observable.helper';
import { TelegrafExceptionsHandler } from '../exceptions/telegraf-exceptions-handler';
export class TelegrafProxy {
public create(
targetCallback: <TContext>(ctx: TContext, next: Function) => Promise<any>,
exceptionsHandler: TelegrafExceptionsHandler,
): <TContext>(ctx: TContext, next: Function) => Promise<any> {
return async <TContext>(ctx: TContext, next: Function) => {
try {
const result = await targetCallback(ctx, next);
return !isObservable(result)
? result
: result.pipe(
catchError((error) => {
this.handleError(exceptionsHandler, [ctx, next], error);
return EMPTY;
}),
);
} catch (error) {
this.handleError(exceptionsHandler, [ctx, next], error);
}
};
}
handleError<T>(
exceptionsHandler: TelegrafExceptionsHandler,
args: unknown[],
error: T,
): void {
const host = new ExecutionContextHost(args);
host.setType('telegraf');
exceptionsHandler.handle(error, host);
}
}

View File

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

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling callback_data actions with regular expressions.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Cashtag handling.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Command handling.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with email entity.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling callback_data actions with game query.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Hashtag handling.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling text messages.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Handler for /help command.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling inline_query actions with regular expressions.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Mention handling.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for provided update type.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Phone number handling.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Handler for /settings command.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Handler for /start command.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with text_link entity.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with text_mention entity.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers middleware for handling messages with url entity.

View File

@ -1,4 +1,4 @@
import { createUpdateListenerDecorator } from '../../helpers';
import { createUpdateListenerDecorator } from '../../utils';
/**
* Registers a middleware.

View File

@ -0,0 +1,8 @@
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

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

View File

@ -0,0 +1,15 @@
import { PipeTransform, Type } from '@nestjs/common';
import { createPipesTelegrafParamDecorator } from '../../utils/param-decorator.util';
import { TelegrafParamtype } from '../../enums/telegraf-paramtype.enum';
export function MessageText(): ParameterDecorator;
export function MessageText(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
export function MessageText(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
return createPipesTelegrafParamDecorator(TelegrafParamtype.MESSAGE_TEXT)(
...pipes,
);
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
export enum TelegrafParamtype {
CONTEXT,
NEXT,
SENDER,
MESSAGE,
MESSAGE_TEXT,
// TODO: Add more
}

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

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

View File

@ -0,0 +1,24 @@
import { isObject, isString } from '@nestjs/common/utils/shared.utils';
export class TelegrafException extends Error {
constructor(private readonly error: string | object) {
super();
this.initMessage();
}
// TODO: Check real error format
public initMessage() {
if (isString(this.error)) {
this.message = this.error;
} else if (
isObject(this.error) &&
isString((this.error as Record<string, any>).message)
) {
this.message = (this.error as Record<string, any>).message;
} else if (this.constructor) {
this.message = this.constructor.name
.match(/[A-Z][a-z]+|[0-9]+/g)
.join(' ');
}
}
}

View File

@ -0,0 +1,34 @@
import { ArgumentsHost, Logger } from '@nestjs/common';
import { MESSAGES } from '@nestjs/core/constants';
import { Context } from 'telegraf';
import { TelegrafExceptionFilter } from '../interfaces/telegraf-exception-filter.interface';
import { TelegrafException } from '../errors';
import { isErrorObject } from '../helpers/is-error-object.helper';
import { TelegrafArgumentsHost } from '../execution-context';
export class BaseTelegrafExceptionFilter<TError = any>
implements TelegrafExceptionFilter {
private static readonly logger = new Logger('TelegrafExceptionsHandler');
catch(exception: TError, host: ArgumentsHost): void {
const context = TelegrafArgumentsHost.create(host).getContext<Context>();
this.handleError(exception, context);
}
public handleError(exception: TError, context: Context): void {
if (!(exception instanceof TelegrafException)) {
return this.handleUnknownError(exception, context);
}
context.reply(exception.message);
}
public handleUnknownError(exception: TError, context: Context): void {
context.reply(MESSAGES.UNKNOWN_EXCEPTION_MESSAGE);
const errorMessage = isErrorObject(exception)
? exception.message
: exception;
BaseTelegrafExceptionFilter.logger.error(errorMessage);
}
}

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

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

View File

@ -0,0 +1,48 @@
import { ArgumentsHost } from '@nestjs/common';
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions';
import { BaseTelegrafExceptionFilter } from './base-telegraf-exception-filter';
import { TelegrafException } from '../errors';
import { InvalidExceptionFilterException } from '@nestjs/core/errors/exceptions/invalid-exception-filter.exception';
import { isEmpty } from '@nestjs/common/utils/shared.utils';
export class TelegrafExceptionsHandler extends BaseTelegrafExceptionFilter {
private filters: ExceptionFilterMetadata[] = [];
public handle(
exception: Error | TelegrafException | any,
host: ArgumentsHost,
): void {
const isFilterInvoked = this.invokeCustomFilters(exception, host);
if (!isFilterInvoked) {
super.catch(exception, host);
}
}
public invokeCustomFilters<T = any>(
exception: T,
args: ArgumentsHost,
): boolean {
if (isEmpty(this.filters)) return false;
const filter = this.filters.find(({ exceptionMetatypes }) => {
const hasMetatype =
!exceptionMetatypes.length ||
exceptionMetatypes.some(
(ExceptionMetatype) => exception instanceof ExceptionMetatype,
);
return hasMetatype;
});
filter && filter.func(exception, args);
return !!filter;
}
public setCustomFilters(filters: ExceptionFilterMetadata[]): void {
if (!Array.isArray(filters)) {
throw new InvalidExceptionFilterException();
}
this.filters = filters;
}
}

View File

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

View File

@ -0,0 +1,22 @@
import { ArgumentsHost } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { TgArgumentsHost } from './tg-arguments-host.interace';
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

@ -0,0 +1,32 @@
import { ContextType, ExecutionContext } from '@nestjs/common';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { TgArgumentsHost } from './tg-arguments-host.interace';
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(0);
}
}

View File

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

View File

@ -1,54 +1,98 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService } from '@nestjs/core';
import { Injectable as IInjectable } from '@nestjs/common/interfaces/injectable.interface';
import { DiscoveryService, ModuleRef, ModulesContainer } from '@nestjs/core';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { Telegraf } from 'telegraf';
import { PipesContextCreator } from '@nestjs/core/pipes/pipes-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
import { GuardsContextCreator } from '@nestjs/core/guards/guards-context-creator';
import { GuardsConsumer } from '@nestjs/core/guards/guards-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils';
import { fromPromise } from 'rxjs/internal-compatibility';
import { filter, mergeAll } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { Context, Telegraf } from 'telegraf';
import { TelegrafMetadataAccessor } from '../telegraf.metadata-accessor';
import { TelegrafContextCreator } from '../context/telegraf-context-creator';
import { TelegrafProxy } from '../context/telegraf-proxy';
import { FiltersContextCreator } from '../context/filters-context-creator';
@Injectable()
export class TelegrafUpdateExplorer implements OnModuleInit {
private readonly contextCreator: TelegrafContextCreator;
constructor(
@Inject(Telegraf)
private readonly telegraf: Telegraf<never>,
private readonly moduleRef: ModuleRef,
private readonly modulesContainer: ModulesContainer,
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: TelegrafMetadataAccessor,
private readonly metadataScanner: MetadataScanner,
) {}
@Inject(Telegraf) private readonly telegraf: Telegraf<never>,
) {
this.contextCreator = this.getContextCreator();
}
private getContextCreator(): TelegrafContextCreator {
const { container } = this.moduleRef as any;
return new TelegrafContextCreator(
new TelegrafProxy(),
new FiltersContextCreator(container),
new PipesContextCreator(container),
new PipesConsumer(),
new GuardsContextCreator(container),
new GuardsConsumer(),
new InterceptorsContextCreator(container),
new InterceptorsConsumer(),
);
}
onModuleInit(): void {
this.explore();
}
private explore(): void {
const updateClasses = this.filterUpdateClasses();
this.modulesContainer.forEach(({ providers }, moduleName) => {
this.exploreProviders(providers, moduleName);
});
}
updateClasses.forEach((wrapper) => {
private exploreProviders(
providers: Map<string, InstanceWrapper<IInjectable>>,
moduleName: string,
): void {
[...providers.values()]
.filter((wrapper) => wrapper && !wrapper.isNotMetatype)
.forEach((wrapper) => {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodKey: string) => this.registerIfListener(instance, methodKey),
(methodKey: string) =>
this.registerIfListener(
instance as Record<string, Function>,
methodKey,
moduleName,
),
);
});
}
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,
moduleName: string,
): void {
const methodRef = instance[methodKey];
const middlewareFn = methodRef.bind(instance);
const methodRef = instance[methodKey] as (...args: unknown[]) => unknown;
const contextHandlerFn = this.contextCreator.create(
instance,
methodRef,
moduleName,
methodKey,
);
const listenerMetadata = this.metadataAccessor.getListenerMetadata(
methodRef,
@ -56,8 +100,38 @@ export class TelegrafUpdateExplorer implements OnModuleInit {
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);
this.telegraf[method](
...args,
async (ctx: Context, next: () => Promise<void>) => {
const defferedResult = contextHandlerFn.call(instance, ctx, next);
const result = this.pickResult(defferedResult);
fromPromise(result)
.pipe(
mergeAll(),
filter((response: any) => !isNil(response)),
)
.subscribe((text) => {
// TODO: More processing method return logic (files, images, etc)
// Example: https://github.com/nestjs/nest/blob/01dc358aade27d3d7ca510506696aa62bfb1cc43/packages/platform-socket.io/adapters/io-adapter.ts#L56
return ctx.reply(text);
});
},
);
}
private async pickResult(
defferedResult: Promise<any>,
): Promise<Observable<any>> {
const result = await defferedResult;
if (result && isFunction(result.subscribe)) {
return result;
}
if (result instanceof Promise) {
return fromPromise(result);
}
return of(result);
}
}

View File

@ -0,0 +1,23 @@
import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum';
export class TelegrafParamsFactory {
exchangeKeyForValue<
TContext extends Record<string, any> = any,
TResult = any
>(type: number, ctx: TContext, next: Function): TResult {
switch (type as TelegrafParamtype) {
case TelegrafParamtype.CONTEXT:
return ctx as any;
case TelegrafParamtype.NEXT:
return next as any;
case TelegrafParamtype.SENDER:
return ctx.from;
case TelegrafParamtype.MESSAGE:
return ctx.message;
case TelegrafParamtype.MESSAGE_TEXT:
return ctx.message.text;
default:
return null;
}
}
}

View File

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

View File

@ -0,0 +1,5 @@
import { isObject } from '@nestjs/common/utils/shared.utils';
export function isErrorObject(err: any): err is Error {
return isObject(err) && !!(err as Error).message;
}

View File

@ -0,0 +1,5 @@
import { isFunction } from '@nestjs/common/utils/shared.utils';
export function isObservable(result: any): boolean {
return result && isFunction(result.subscribe);
}

View File

@ -1,5 +1,7 @@
export * from './decorators';
export * from './interfaces';
export * from './helpers';
export * from './utils';
export * from './errors';
export * from './execution-context';
export * from './telegraf.module';
export * from './telegraf.types';

View File

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

View File

@ -1,6 +1,14 @@
import {
CUSTOM_ROUTE_AGRS_METADATA,
ROUTE_ARGS_METADATA,
} from '@nestjs/common/constants';
export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS';
export const UPDATE_METADATA = 'UPDATE_METADATA';
export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA';
export const SCENE_METADATA = 'SCENE_METADATA';
export const LISTENER_ARGS_METADATA = ROUTE_ARGS_METADATA;
export const CUSTOM_LISTENER_AGRS_METADATA = CUSTOM_ROUTE_AGRS_METADATA;

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

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

View File

@ -0,0 +1,39 @@
import { assignMetadata, PipeTransform, Type } from '@nestjs/common';
import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum';
import { LISTENER_ARGS_METADATA } from '../telegraf.constants';
export function createTelegrafParamDecorator(
paramtype: TelegrafParamtype,
): (...pipes: (Type<PipeTransform> | PipeTransform)[]) => ParameterDecorator {
return (...pipes: (Type<PipeTransform> | PipeTransform)[]) => (
target,
key,
index,
) => {
const args =
Reflect.getMetadata(LISTENER_ARGS_METADATA, target.constructor, key) ||
{};
Reflect.defineMetadata(
LISTENER_ARGS_METADATA,
assignMetadata(args, paramtype, index, undefined, ...pipes),
target.constructor,
key,
);
};
}
export const createPipesTelegrafParamDecorator = (
paramtype: TelegrafParamtype,
) => (
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator => (target, key, index) => {
const args =
Reflect.getMetadata(LISTENER_ARGS_METADATA, target.constructor, key) || {};
Reflect.defineMetadata(
LISTENER_ARGS_METADATA,
assignMetadata(args, paramtype, index, undefined, ...pipes),
target.constructor,
key,
);
};

View File

@ -3,7 +3,7 @@ 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';
import { sessionMiddleware } from './common/middleware/session.middleware';
@Module({
imports: [
@ -12,6 +12,6 @@ import { sessionMiddleware } from './middleware/session.middleware';
middlewares: [sessionMiddleware],
}),
],
providers: [EchoService, AppUpdate, HelloScene],
providers: [EchoService, AppUpdate],
})
export class AppModule {}

View File

@ -1,40 +1,63 @@
import { SceneContext, Telegraf } from 'telegraf';
import { Command, Help, InjectBot, On, Start, Update } from '../lib';
import { Telegraf } from 'telegraf';
import { SceneContextMessageUpdate } from 'telegraf/typings/stage';
import {
Command,
Ctx,
Help,
InjectBot,
MessageText,
On,
Start,
Update,
} from '../lib';
import { EchoService } from './echo.service';
import { HELLO_SCENE_ID } from './app.constants';
import { Context } from './interfaces/context.interface';
import { UseGuards, UseInterceptors } from '@nestjs/common';
import { AdminGuard } from './common/guards/admin.guard';
import { ResponseTimeInterceptor } from './common/interceptors/response-time.interceptor';
import { ReverseTextPipe } from './common/pipes/reverse-text.pipe';
@Update()
export class AppUpdate {
constructor(
@InjectBot()
private readonly bot: Telegraf<SceneContext>,
private readonly bot: Telegraf<SceneContextMessageUpdate>,
private readonly echoService: EchoService,
) {}
@Start()
async onStart(ctx: Context): Promise<void> {
async onStart(): Promise<string> {
const me = await this.bot.telegram.getMe();
await ctx.reply(`Hey, I'm ${me.first_name}`);
return `Hey, I'm ${me.first_name}`;
}
@Help()
async onHelp(ctx: Context): Promise<void> {
@UseInterceptors(ResponseTimeInterceptor)
async onHelp(@Ctx() ctx: Context): Promise<void> {
await ctx.reply('Send me any text');
}
@UseGuards(AdminGuard)
@Command('admin')
async onAdminCommand(@Ctx() ctx: Context): Promise<void> {
await ctx.reply('Welcome judge');
}
@Command('scene')
async onSceneCommand(ctx: Context): Promise<void> {
async onSceneCommand(@Ctx() ctx: Context): Promise<void> {
await ctx.scene.enter(HELLO_SCENE_ID);
}
@On('message')
async onMessage(ctx: Context): Promise<void> {
async onMessage(
@Ctx() ctx: Context,
@MessageText(new ReverseTextPipe()) reversedMessage: string,
): Promise<void> {
console.log('New message received');
if ('text' in ctx.message) {
const messageText = ctx.message.text;
const echoText = this.echoService.echo(messageText);
const echoText = this.echoService.echo(reversedMessage);
await ctx.reply(echoText);
} else {
await ctx.reply('Only text messages');

View File

@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { TelegrafExecutionContext } from '../../../lib/execution-context';
export const From = createParamDecorator(
(_, ctx: ExecutionContext) =>
TelegrafExecutionContext.create(ctx).getContext().from,
);

View File

@ -0,0 +1,11 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { TelegrafArgumentsHost, TelegrafException } from '../../../lib';
@Catch()
export class TelegrafExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const tgHost = TelegrafArgumentsHost.create(host);
console.log(tgHost);
return exception;
}
}

View File

@ -0,0 +1,21 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { TelegrafExecutionContext } from '../../../lib/execution-context/telegraf-execution-context';
import { Context } from '../../interfaces/context.interface';
import { TelegrafException } from '../../../lib/errors';
@Injectable()
export class AdminGuard implements CanActivate {
private readonly ADMIN_IDS = [];
canActivate(context: ExecutionContext): boolean {
const ctx = TelegrafExecutionContext.create(context);
const { from } = ctx.getContext<Context>();
const isAdmin = this.ADMIN_IDS.includes(from.id);
if (!isAdmin) {
throw new TelegrafException('You are not admin >:(');
}
return true;
}
}

View File

@ -0,0 +1,20 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class ResponseTimeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const start = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`Response time: ${Date.now() - start}ms`)));
}
}

View File

@ -0,0 +1,8 @@
import { Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ReverseTextPipe implements PipeTransform {
transform(value: string): string {
return value.split('').reverse().join('');
}
}

View File

@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "es2020",
"sourceMap": false,
"baseUrl": "./",
"outDir": "./dist",

View File

@ -12,6 +12,7 @@ The described functionality is under development, the functionality has not been
### Update
`@Update` class decorator, it's like NestJS [`@Controller`](https://docs.nestjs.com/controllers) decorator, but for [Telegram Bot API updates](https://core.telegram.org/bots/api#getting-updates).
It is required for the class that will receive updates from Telegram.
```typescript {3}
import { Update, Context } from 'nestjs-telegraf';

View File

@ -8,10 +8,11 @@ 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 } from 'nestjs-telegraf';
import { Telegraf } from 'telegraf';
@Injectable()
export class BotSettingsService {
constructor(@InjectBot() private bot: TelegrafProvider) {}
constructor(@InjectBot() private bot: Telegraf) {}
}
```

View File

@ -18,8 +18,8 @@ import {
Help,
On,
Hears,
Context,
} from 'nestjs-telegraf';
import { Context } from 'telegraf';
@Injectable()
export class AppService {

View File

@ -9,12 +9,13 @@ If you want to configure a telegram bot webhook, you need to get a middleware fr
To access it, you must use the `app.get()` method, followed by the provider reference:
```typescript
const telegrafProvider = app.get('TelegrafProvider');
import { Telegraf } from 'telegraf';
const telegraf = app.get(Telegraf);
```
Now you can connect middleware:
```typescript
app.use(telegrafProvider.webhookCallback('/secret-path'));
app.use(telegraf.webhookCallback('/secret-path'));
```
The last step is to specify launchOptions in `forRoot` method: