From af632ea47115335513bbbe8a10523ed33e94ff70 Mon Sep 17 00:00:00 2001 From: Morb0 Date: Sun, 3 Jan 2021 01:30:57 +0300 Subject: [PATCH] feat!(): add custom execution context --- TODO.md | 10 + lib/context/filters-context-creator.ts | 38 +++ lib/context/telegraf-context-creator.ts | 278 ++++++++++++++++++ lib/context/telegraf-proxy.ts | 38 +++ lib/decorators/index.ts | 1 + lib/decorators/listeners/action.decorator.ts | 2 +- lib/decorators/listeners/cashtag.decorator.ts | 2 +- lib/decorators/listeners/command.decorator.ts | 2 +- lib/decorators/listeners/email.decorator.ts | 2 +- .../listeners/game-query.decorator.ts | 2 +- lib/decorators/listeners/hashtag.decorator.ts | 2 +- lib/decorators/listeners/hears.decorator.ts | 2 +- lib/decorators/listeners/help.decorator.ts | 2 +- .../listeners/inline-query.decorator.ts | 2 +- lib/decorators/listeners/mention.decorator.ts | 2 +- lib/decorators/listeners/on.decorator.ts | 2 +- lib/decorators/listeners/phone.decorator.ts | 2 +- .../listeners/settings.decorator.ts | 2 +- lib/decorators/listeners/start.decorator.ts | 2 +- .../listeners/text-link.decorator.ts | 2 +- .../listeners/text-mention.decorator.ts | 2 +- lib/decorators/listeners/url.decorator.ts | 2 +- lib/decorators/listeners/use.decorator.ts | 2 +- lib/decorators/params/context.decorator.ts | 8 + lib/decorators/params/index.ts | 3 + .../params/message-text.decorator.ts | 15 + lib/decorators/params/next.decorator.ts | 6 + lib/decorators/scene/scene-enter.decorator.ts | 2 +- lib/decorators/scene/scene-leave.decorator.ts | 2 +- lib/enums/telegraf-paramtype.enum.ts | 8 + lib/errors/index.ts | 1 + lib/errors/telegraf.exception.ts | 24 ++ .../base-telegraf-exception-filter.ts | 34 +++ lib/exceptions/index.ts | 1 + lib/exceptions/telegraf-exceptions-handler.ts | 48 +++ lib/execution-context/index.ts | 3 + .../telegraf-arguments-host.ts | 22 ++ .../telegraf-execution-context.ts | 32 ++ .../tg-arguments-host.interace.ts | 6 + lib/explorers/telegraf-update.explorer.ts | 130 ++++++-- lib/factories/telegraf-params-factory.ts | 23 ++ lib/helpers/index.ts | 2 - lib/helpers/is-error-object.helper.ts | 5 + lib/helpers/is-observable.helper.ts | 5 + lib/index.ts | 4 +- .../telegraf-exception-filter.interface.ts | 5 + lib/telegraf.constants.ts | 8 + .../create-scene-listener-decorator.util.ts} | 0 .../create-update-listener-decorator.util.ts} | 0 lib/utils/index.ts | 2 + lib/utils/param-decorator.util.ts | 39 +++ sample/app.module.ts | 4 +- sample/app.update.ts | 43 ++- sample/common/decorators/from.decorator.ts | 7 + .../filters/telegraf-exception.filter.ts | 11 + sample/common/guards/admin.guard.ts | 21 ++ .../interceptors/response-time.interceptor.ts | 20 ++ .../middleware/session.middleware.ts | 0 sample/common/pipes/reverse-text.pipe.ts | 8 + tsconfig.json | 2 +- website/docs/api-reference/decorators.md | 1 + website/docs/bot-injection.md | 5 +- website/docs/telegraf-methods.md | 2 +- website/docs/webhooks.md | 5 +- 64 files changed, 899 insertions(+), 69 deletions(-) create mode 100644 TODO.md create mode 100644 lib/context/filters-context-creator.ts create mode 100644 lib/context/telegraf-context-creator.ts create mode 100644 lib/context/telegraf-proxy.ts create mode 100644 lib/decorators/params/context.decorator.ts create mode 100644 lib/decorators/params/index.ts create mode 100644 lib/decorators/params/message-text.decorator.ts create mode 100644 lib/decorators/params/next.decorator.ts create mode 100644 lib/enums/telegraf-paramtype.enum.ts create mode 100644 lib/errors/index.ts create mode 100644 lib/errors/telegraf.exception.ts create mode 100644 lib/exceptions/base-telegraf-exception-filter.ts create mode 100644 lib/exceptions/index.ts create mode 100644 lib/exceptions/telegraf-exceptions-handler.ts create mode 100644 lib/execution-context/index.ts create mode 100644 lib/execution-context/telegraf-arguments-host.ts create mode 100644 lib/execution-context/telegraf-execution-context.ts create mode 100644 lib/execution-context/tg-arguments-host.interace.ts create mode 100644 lib/factories/telegraf-params-factory.ts delete mode 100644 lib/helpers/index.ts create mode 100644 lib/helpers/is-error-object.helper.ts create mode 100644 lib/helpers/is-observable.helper.ts create mode 100644 lib/interfaces/telegraf-exception-filter.interface.ts rename lib/{helpers/create-scene-listener-decorator.helper.ts => utils/create-scene-listener-decorator.util.ts} (100%) rename lib/{helpers/create-update-listener-decorator.helper.ts => utils/create-update-listener-decorator.util.ts} (100%) create mode 100644 lib/utils/index.ts create mode 100644 lib/utils/param-decorator.util.ts create mode 100644 sample/common/decorators/from.decorator.ts create mode 100644 sample/common/filters/telegraf-exception.filter.ts create mode 100644 sample/common/guards/admin.guard.ts create mode 100644 sample/common/interceptors/response-time.interceptor.ts rename sample/{ => common}/middleware/session.middleware.ts (100%) create mode 100644 sample/common/pipes/reverse-text.pipe.ts diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..4d12bdc --- /dev/null +++ b/TODO.md @@ -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 diff --git a/lib/context/filters-context-creator.ts b/lib/context/filters-context-creator.ts new file mode 100644 index 0000000..eb45b81 --- /dev/null +++ b/lib/context/filters-context-creator.ts @@ -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 { + return [] as T; + } +} diff --git a/lib/context/telegraf-context-creator.ts b/lib/context/telegraf-context-creator.ts new file mode 100644 index 0000000..2066dfb --- /dev/null +++ b/lib/context/telegraf-context-creator.ts @@ -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(); + + 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( + instance: Update, + methodRef: (...args: unknown[]) => void, + moduleName: string, + methodKey: string, + ): (...args: any[]) => Promise { + const contextType: TelegrafContextType = 'telegraf'; + const { argsLength, paramtypes, getParamsMetadata } = this.getMetadata( + 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 = ( + 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 (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( + 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( + 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 = (ctx: TContext, next: Function) => + paramsFactory.exchangeKeyForValue(numericType, ctx, next); + + return { index, extractValue, type: numericType, data, pipes }; + }); + } + + public createGuardsFn( + guards: any[], + instance: Controller, + callback: (...args: unknown[]) => any, + contextType?: TContext, + ): Function | null { + const canActivateFn = async (args: any[]) => { + const canActivate = await this.guardsConsumer.tryActivate( + 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 ( + 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( + value: T, + { metatype, type, data }: { metatype: any; type: any; data: any }, + pipes: PipeTransform[], + ): Promise { + return isEmpty(pipes) + ? value + : this.pipesConsumer.apply(value, { metatype, type, data }, pipes); + } +} diff --git a/lib/context/telegraf-proxy.ts b/lib/context/telegraf-proxy.ts new file mode 100644 index 0000000..448d0c4 --- /dev/null +++ b/lib/context/telegraf-proxy.ts @@ -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: (ctx: TContext, next: Function) => Promise, + exceptionsHandler: TelegrafExceptionsHandler, + ): (ctx: TContext, next: Function) => Promise { + return async (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( + exceptionsHandler: TelegrafExceptionsHandler, + args: unknown[], + error: T, + ): void { + const host = new ExecutionContextHost(args); + host.setType('telegraf'); + exceptionsHandler.handle(error, host); + } +} diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index 31fa986..ee4e5a9 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -1,3 +1,4 @@ export * from './core'; export * from './listeners'; export * from './scene'; +export * from './params'; diff --git a/lib/decorators/listeners/action.decorator.ts b/lib/decorators/listeners/action.decorator.ts index 4b14901..0a3ad3b 100644 --- a/lib/decorators/listeners/action.decorator.ts +++ b/lib/decorators/listeners/action.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling callback_data actions with regular expressions. diff --git a/lib/decorators/listeners/cashtag.decorator.ts b/lib/decorators/listeners/cashtag.decorator.ts index c3e37e9..f726b78 100644 --- a/lib/decorators/listeners/cashtag.decorator.ts +++ b/lib/decorators/listeners/cashtag.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Cashtag handling. diff --git a/lib/decorators/listeners/command.decorator.ts b/lib/decorators/listeners/command.decorator.ts index 0e7806e..5234a4c 100644 --- a/lib/decorators/listeners/command.decorator.ts +++ b/lib/decorators/listeners/command.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Command handling. diff --git a/lib/decorators/listeners/email.decorator.ts b/lib/decorators/listeners/email.decorator.ts index e9bb1a6..a22023a 100644 --- a/lib/decorators/listeners/email.decorator.ts +++ b/lib/decorators/listeners/email.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling messages with email entity. diff --git a/lib/decorators/listeners/game-query.decorator.ts b/lib/decorators/listeners/game-query.decorator.ts index 347f998..911a34f 100644 --- a/lib/decorators/listeners/game-query.decorator.ts +++ b/lib/decorators/listeners/game-query.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling callback_data actions with game query. diff --git a/lib/decorators/listeners/hashtag.decorator.ts b/lib/decorators/listeners/hashtag.decorator.ts index 86e9e13..e89d7a6 100644 --- a/lib/decorators/listeners/hashtag.decorator.ts +++ b/lib/decorators/listeners/hashtag.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Hashtag handling. diff --git a/lib/decorators/listeners/hears.decorator.ts b/lib/decorators/listeners/hears.decorator.ts index 79d71af..425f37c 100644 --- a/lib/decorators/listeners/hears.decorator.ts +++ b/lib/decorators/listeners/hears.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling text messages. diff --git a/lib/decorators/listeners/help.decorator.ts b/lib/decorators/listeners/help.decorator.ts index 36ac9e4..5750cb9 100644 --- a/lib/decorators/listeners/help.decorator.ts +++ b/lib/decorators/listeners/help.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Handler for /help command. diff --git a/lib/decorators/listeners/inline-query.decorator.ts b/lib/decorators/listeners/inline-query.decorator.ts index 722a348..7fef887 100644 --- a/lib/decorators/listeners/inline-query.decorator.ts +++ b/lib/decorators/listeners/inline-query.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling inline_query actions with regular expressions. diff --git a/lib/decorators/listeners/mention.decorator.ts b/lib/decorators/listeners/mention.decorator.ts index e4341a0..ac8c01d 100644 --- a/lib/decorators/listeners/mention.decorator.ts +++ b/lib/decorators/listeners/mention.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Mention handling. diff --git a/lib/decorators/listeners/on.decorator.ts b/lib/decorators/listeners/on.decorator.ts index 9f3c0fd..098c5e8 100644 --- a/lib/decorators/listeners/on.decorator.ts +++ b/lib/decorators/listeners/on.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for provided update type. diff --git a/lib/decorators/listeners/phone.decorator.ts b/lib/decorators/listeners/phone.decorator.ts index 0f68380..b58a100 100644 --- a/lib/decorators/listeners/phone.decorator.ts +++ b/lib/decorators/listeners/phone.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Phone number handling. diff --git a/lib/decorators/listeners/settings.decorator.ts b/lib/decorators/listeners/settings.decorator.ts index 6cca7c7..31b8aa6 100644 --- a/lib/decorators/listeners/settings.decorator.ts +++ b/lib/decorators/listeners/settings.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Handler for /settings command. diff --git a/lib/decorators/listeners/start.decorator.ts b/lib/decorators/listeners/start.decorator.ts index 48bdc5b..668dd52 100644 --- a/lib/decorators/listeners/start.decorator.ts +++ b/lib/decorators/listeners/start.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Handler for /start command. diff --git a/lib/decorators/listeners/text-link.decorator.ts b/lib/decorators/listeners/text-link.decorator.ts index c62439a..0c39246 100644 --- a/lib/decorators/listeners/text-link.decorator.ts +++ b/lib/decorators/listeners/text-link.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling messages with text_link entity. diff --git a/lib/decorators/listeners/text-mention.decorator.ts b/lib/decorators/listeners/text-mention.decorator.ts index 895a458..16046e0 100644 --- a/lib/decorators/listeners/text-mention.decorator.ts +++ b/lib/decorators/listeners/text-mention.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling messages with text_mention entity. diff --git a/lib/decorators/listeners/url.decorator.ts b/lib/decorators/listeners/url.decorator.ts index 843aa41..0ddbba1 100644 --- a/lib/decorators/listeners/url.decorator.ts +++ b/lib/decorators/listeners/url.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers middleware for handling messages with url entity. diff --git a/lib/decorators/listeners/use.decorator.ts b/lib/decorators/listeners/use.decorator.ts index ba83af5..95ad81e 100644 --- a/lib/decorators/listeners/use.decorator.ts +++ b/lib/decorators/listeners/use.decorator.ts @@ -1,4 +1,4 @@ -import { createUpdateListenerDecorator } from '../../helpers'; +import { createUpdateListenerDecorator } from '../../utils'; /** * Registers a middleware. diff --git a/lib/decorators/params/context.decorator.ts b/lib/decorators/params/context.decorator.ts new file mode 100644 index 0000000..e77fcd8 --- /dev/null +++ b/lib/decorators/params/context.decorator.ts @@ -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; diff --git a/lib/decorators/params/index.ts b/lib/decorators/params/index.ts new file mode 100644 index 0000000..6c5b237 --- /dev/null +++ b/lib/decorators/params/index.ts @@ -0,0 +1,3 @@ +export * from './context.decorator'; +export * from './next.decorator'; +export * from './message-text.decorator'; diff --git a/lib/decorators/params/message-text.decorator.ts b/lib/decorators/params/message-text.decorator.ts new file mode 100644 index 0000000..b72b66f --- /dev/null +++ b/lib/decorators/params/message-text.decorator.ts @@ -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)[] +): ParameterDecorator; +export function MessageText( + ...pipes: (Type | PipeTransform)[] +): ParameterDecorator { + return createPipesTelegrafParamDecorator(TelegrafParamtype.MESSAGE_TEXT)( + ...pipes, + ); +} diff --git a/lib/decorators/params/next.decorator.ts b/lib/decorators/params/next.decorator.ts new file mode 100644 index 0000000..063623e --- /dev/null +++ b/lib/decorators/params/next.decorator.ts @@ -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, +); diff --git a/lib/decorators/scene/scene-enter.decorator.ts b/lib/decorators/scene/scene-enter.decorator.ts index 914b2d7..ab07f0a 100644 --- a/lib/decorators/scene/scene-enter.decorator.ts +++ b/lib/decorators/scene/scene-enter.decorator.ts @@ -1,3 +1,3 @@ -import { createSceneListenerDecorator } from '../../helpers'; +import { createSceneListenerDecorator } from '../../utils'; export const SceneEnter = createSceneListenerDecorator('enter'); diff --git a/lib/decorators/scene/scene-leave.decorator.ts b/lib/decorators/scene/scene-leave.decorator.ts index 19b970e..d65a37f 100644 --- a/lib/decorators/scene/scene-leave.decorator.ts +++ b/lib/decorators/scene/scene-leave.decorator.ts @@ -1,3 +1,3 @@ -import { createSceneListenerDecorator } from '../../helpers'; +import { createSceneListenerDecorator } from '../../utils'; export const SceneLeave = createSceneListenerDecorator('leave'); diff --git a/lib/enums/telegraf-paramtype.enum.ts b/lib/enums/telegraf-paramtype.enum.ts new file mode 100644 index 0000000..2b013d9 --- /dev/null +++ b/lib/enums/telegraf-paramtype.enum.ts @@ -0,0 +1,8 @@ +export enum TelegrafParamtype { + CONTEXT, + NEXT, + SENDER, + MESSAGE, + MESSAGE_TEXT, + // TODO: Add more +} diff --git a/lib/errors/index.ts b/lib/errors/index.ts new file mode 100644 index 0000000..1290854 --- /dev/null +++ b/lib/errors/index.ts @@ -0,0 +1 @@ +export * from './telegraf.exception'; diff --git a/lib/errors/telegraf.exception.ts b/lib/errors/telegraf.exception.ts new file mode 100644 index 0000000..0e17137 --- /dev/null +++ b/lib/errors/telegraf.exception.ts @@ -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).message) + ) { + this.message = (this.error as Record).message; + } else if (this.constructor) { + this.message = this.constructor.name + .match(/[A-Z][a-z]+|[0-9]+/g) + .join(' '); + } + } +} diff --git a/lib/exceptions/base-telegraf-exception-filter.ts b/lib/exceptions/base-telegraf-exception-filter.ts new file mode 100644 index 0000000..81434d2 --- /dev/null +++ b/lib/exceptions/base-telegraf-exception-filter.ts @@ -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 + implements TelegrafExceptionFilter { + private static readonly logger = new Logger('TelegrafExceptionsHandler'); + + catch(exception: TError, host: ArgumentsHost): void { + const context = TelegrafArgumentsHost.create(host).getContext(); + 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); + } +} diff --git a/lib/exceptions/index.ts b/lib/exceptions/index.ts new file mode 100644 index 0000000..b7309ed --- /dev/null +++ b/lib/exceptions/index.ts @@ -0,0 +1 @@ +export * from './base-telegraf-exception-filter'; diff --git a/lib/exceptions/telegraf-exceptions-handler.ts b/lib/exceptions/telegraf-exceptions-handler.ts new file mode 100644 index 0000000..ba9985c --- /dev/null +++ b/lib/exceptions/telegraf-exceptions-handler.ts @@ -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( + 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; + } +} diff --git a/lib/execution-context/index.ts b/lib/execution-context/index.ts new file mode 100644 index 0000000..6d91739 --- /dev/null +++ b/lib/execution-context/index.ts @@ -0,0 +1,3 @@ +export * from './tg-arguments-host.interace'; +export * from './telegraf-arguments-host'; +export * from './telegraf-arguments-host'; diff --git a/lib/execution-context/telegraf-arguments-host.ts b/lib/execution-context/telegraf-arguments-host.ts new file mode 100644 index 0000000..79a2c10 --- /dev/null +++ b/lib/execution-context/telegraf-arguments-host.ts @@ -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 { + return this.getArgByIndex(0); + } + + getNext(): T { + return this.getArgByIndex(1); + } +} diff --git a/lib/execution-context/telegraf-execution-context.ts b/lib/execution-context/telegraf-execution-context.ts new file mode 100644 index 0000000..d0c9613 --- /dev/null +++ b/lib/execution-context/telegraf-execution-context.ts @@ -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 { + return super.getType(); + } + + getContext(): T { + return this.getArgByIndex(0); + } + + getNext(): T { + return this.getArgByIndex(0); + } +} diff --git a/lib/execution-context/tg-arguments-host.interace.ts b/lib/execution-context/tg-arguments-host.interace.ts new file mode 100644 index 0000000..fa5a8c1 --- /dev/null +++ b/lib/execution-context/tg-arguments-host.interace.ts @@ -0,0 +1,6 @@ +import { ArgumentsHost } from '@nestjs/common'; + +export interface TgArgumentsHost extends ArgumentsHost { + getContext(): T; + getNext(): T; +} diff --git a/lib/explorers/telegraf-update.explorer.ts b/lib/explorers/telegraf-update.explorer.ts index a848806..123febb 100644 --- a/lib/explorers/telegraf-update.explorer.ts +++ b/lib/explorers/telegraf-update.explorer.ts @@ -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, + 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, + ) { + 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(); - - updateClasses.forEach((wrapper) => { - const { instance } = wrapper; - - const prototype = Object.getPrototypeOf(instance); - this.metadataScanner.scanFromPrototype( - instance, - prototype, - (methodKey: string) => this.registerIfListener(instance, methodKey), - ); + this.modulesContainer.forEach(({ providers }, moduleName) => { + this.exploreProviders(providers, moduleName); }); } - private filterUpdateClasses(): InstanceWrapper[] { - return this.discoveryService - .getProviders() - .filter((wrapper) => wrapper.instance) - .filter((wrapper) => - this.metadataAccessor.isUpdate(wrapper.instance.constructor), - ); + private exploreProviders( + providers: Map>, + 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 as Record, + methodKey, + moduleName, + ), + ); + }); } private registerIfListener( instance: Record, 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) => { + 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, + ): Promise> { + const result = await defferedResult; + + if (result && isFunction(result.subscribe)) { + return result; + } + + if (result instanceof Promise) { + return fromPromise(result); + } + + return of(result); } } diff --git a/lib/factories/telegraf-params-factory.ts b/lib/factories/telegraf-params-factory.ts new file mode 100644 index 0000000..7f9adbf --- /dev/null +++ b/lib/factories/telegraf-params-factory.ts @@ -0,0 +1,23 @@ +import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum'; + +export class TelegrafParamsFactory { + exchangeKeyForValue< + TContext extends Record = 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; + } + } +} diff --git a/lib/helpers/index.ts b/lib/helpers/index.ts deleted file mode 100644 index 20b6077..0000000 --- a/lib/helpers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './create-update-listener-decorator.helper'; -export * from './create-scene-listener-decorator.helper'; diff --git a/lib/helpers/is-error-object.helper.ts b/lib/helpers/is-error-object.helper.ts new file mode 100644 index 0000000..9ff248c --- /dev/null +++ b/lib/helpers/is-error-object.helper.ts @@ -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; +} diff --git a/lib/helpers/is-observable.helper.ts b/lib/helpers/is-observable.helper.ts new file mode 100644 index 0000000..c928e23 --- /dev/null +++ b/lib/helpers/is-observable.helper.ts @@ -0,0 +1,5 @@ +import { isFunction } from '@nestjs/common/utils/shared.utils'; + +export function isObservable(result: any): boolean { + return result && isFunction(result.subscribe); +} diff --git a/lib/index.ts b/lib/index.ts index da17d36..61d71b0 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -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'; diff --git a/lib/interfaces/telegraf-exception-filter.interface.ts b/lib/interfaces/telegraf-exception-filter.interface.ts new file mode 100644 index 0000000..c1f4598 --- /dev/null +++ b/lib/interfaces/telegraf-exception-filter.interface.ts @@ -0,0 +1,5 @@ +import { ArgumentsHost } from '@nestjs/common'; + +export interface TelegrafExceptionFilter { + catch(exception: T, host: ArgumentsHost): any; +} diff --git a/lib/telegraf.constants.ts b/lib/telegraf.constants.ts index 1790594..c1cc166 100644 --- a/lib/telegraf.constants.ts +++ b/lib/telegraf.constants.ts @@ -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; diff --git a/lib/helpers/create-scene-listener-decorator.helper.ts b/lib/utils/create-scene-listener-decorator.util.ts similarity index 100% rename from lib/helpers/create-scene-listener-decorator.helper.ts rename to lib/utils/create-scene-listener-decorator.util.ts diff --git a/lib/helpers/create-update-listener-decorator.helper.ts b/lib/utils/create-update-listener-decorator.util.ts similarity index 100% rename from lib/helpers/create-update-listener-decorator.helper.ts rename to lib/utils/create-update-listener-decorator.util.ts diff --git a/lib/utils/index.ts b/lib/utils/index.ts new file mode 100644 index 0000000..4c2328a --- /dev/null +++ b/lib/utils/index.ts @@ -0,0 +1,2 @@ +export * from './create-update-listener-decorator.util'; +export * from './create-scene-listener-decorator.util'; diff --git a/lib/utils/param-decorator.util.ts b/lib/utils/param-decorator.util.ts new file mode 100644 index 0000000..f344951 --- /dev/null +++ b/lib/utils/param-decorator.util.ts @@ -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)[]) => ParameterDecorator { + return (...pipes: (Type | 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)[] +): 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, + ); +}; diff --git a/sample/app.module.ts b/sample/app.module.ts index 2d177de..9870db6 100644 --- a/sample/app.module.ts +++ b/sample/app.module.ts @@ -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 {} diff --git a/sample/app.update.ts b/sample/app.update.ts index 402e9fb..0680183 100644 --- a/sample/app.update.ts +++ b/sample/app.update.ts @@ -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, + private readonly bot: Telegraf, private readonly echoService: EchoService, ) {} @Start() - async onStart(ctx: Context): Promise { + async onStart(): Promise { 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 { + @UseInterceptors(ResponseTimeInterceptor) + async onHelp(@Ctx() ctx: Context): Promise { await ctx.reply('Send me any text'); } + @UseGuards(AdminGuard) + @Command('admin') + async onAdminCommand(@Ctx() ctx: Context): Promise { + await ctx.reply('Welcome judge'); + } + @Command('scene') - async onSceneCommand(ctx: Context): Promise { + async onSceneCommand(@Ctx() ctx: Context): Promise { await ctx.scene.enter(HELLO_SCENE_ID); } @On('message') - async onMessage(ctx: Context): Promise { + async onMessage( + @Ctx() ctx: Context, + @MessageText(new ReverseTextPipe()) reversedMessage: string, + ): Promise { 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'); diff --git a/sample/common/decorators/from.decorator.ts b/sample/common/decorators/from.decorator.ts new file mode 100644 index 0000000..5200f76 --- /dev/null +++ b/sample/common/decorators/from.decorator.ts @@ -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, +); diff --git a/sample/common/filters/telegraf-exception.filter.ts b/sample/common/filters/telegraf-exception.filter.ts new file mode 100644 index 0000000..421ff84 --- /dev/null +++ b/sample/common/filters/telegraf-exception.filter.ts @@ -0,0 +1,11 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; +import { TelegrafArgumentsHost, TelegrafException } from '../../../lib'; + +@Catch() +export class TelegrafExceptionFilter implements ExceptionFilter { + catch(exception: T, host: ArgumentsHost) { + const tgHost = TelegrafArgumentsHost.create(host); + console.log(tgHost); + return exception; + } +} diff --git a/sample/common/guards/admin.guard.ts b/sample/common/guards/admin.guard.ts new file mode 100644 index 0000000..9cb9ab9 --- /dev/null +++ b/sample/common/guards/admin.guard.ts @@ -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(); + + const isAdmin = this.ADMIN_IDS.includes(from.id); + if (!isAdmin) { + throw new TelegrafException('You are not admin >:('); + } + + return true; + } +} diff --git a/sample/common/interceptors/response-time.interceptor.ts b/sample/common/interceptors/response-time.interceptor.ts new file mode 100644 index 0000000..96b1b37 --- /dev/null +++ b/sample/common/interceptors/response-time.interceptor.ts @@ -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 { + console.log('Before...'); + + const start = Date.now(); + return next + .handle() + .pipe(tap(() => console.log(`Response time: ${Date.now() - start}ms`))); + } +} diff --git a/sample/middleware/session.middleware.ts b/sample/common/middleware/session.middleware.ts similarity index 100% rename from sample/middleware/session.middleware.ts rename to sample/common/middleware/session.middleware.ts diff --git a/sample/common/pipes/reverse-text.pipe.ts b/sample/common/pipes/reverse-text.pipe.ts new file mode 100644 index 0000000..82ee739 --- /dev/null +++ b/sample/common/pipes/reverse-text.pipe.ts @@ -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(''); + } +} diff --git a/tsconfig.json b/tsconfig.json index 921d963..db7cecd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "noLib": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "target": "es6", + "target": "es2020", "sourceMap": false, "baseUrl": "./", "outDir": "./dist", diff --git a/website/docs/api-reference/decorators.md b/website/docs/api-reference/decorators.md index d3a51d9..433c47c 100644 --- a/website/docs/api-reference/decorators.md +++ b/website/docs/api-reference/decorators.md @@ -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'; diff --git a/website/docs/bot-injection.md b/website/docs/bot-injection.md index 8aa67df..24276e9 100644 --- a/website/docs/bot-injection.md +++ b/website/docs/bot-injection.md @@ -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) {} } ``` diff --git a/website/docs/telegraf-methods.md b/website/docs/telegraf-methods.md index aa39a30..7cdfa6b 100644 --- a/website/docs/telegraf-methods.md +++ b/website/docs/telegraf-methods.md @@ -18,8 +18,8 @@ import { Help, On, Hears, - Context, } from 'nestjs-telegraf'; +import { Context } from 'telegraf'; @Injectable() export class AppService { diff --git a/website/docs/webhooks.md b/website/docs/webhooks.md index e61c97b..d9a2b87 100644 --- a/website/docs/webhooks.md +++ b/website/docs/webhooks.md @@ -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: