mirror of
https://github.com/Maks1mS/nestjs-telegraf.git
synced 2024-12-24 23:14:39 +03:00
feat!(): add custom execution context
This commit is contained in:
parent
85bb916709
commit
af632ea471
10
TODO.md
Normal file
10
TODO.md
Normal 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
|
38
lib/context/filters-context-creator.ts
Normal file
38
lib/context/filters-context-creator.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
278
lib/context/telegraf-context-creator.ts
Normal file
278
lib/context/telegraf-context-creator.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
38
lib/context/telegraf-proxy.ts
Normal file
38
lib/context/telegraf-proxy.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export * from './core';
|
export * from './core';
|
||||||
export * from './listeners';
|
export * from './listeners';
|
||||||
export * from './scene';
|
export * from './scene';
|
||||||
|
export * from './params';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling callback_data actions with regular expressions.
|
* Registers middleware for handling callback_data actions with regular expressions.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cashtag handling.
|
* Cashtag handling.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Command handling.
|
* Command handling.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling messages with email entity.
|
* Registers middleware for handling messages with email entity.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling callback_data actions with game query.
|
* Registers middleware for handling callback_data actions with game query.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hashtag handling.
|
* Hashtag handling.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling text messages.
|
* Registers middleware for handling text messages.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for /help command.
|
* Handler for /help command.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling inline_query actions with regular expressions.
|
* Registers middleware for handling inline_query actions with regular expressions.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mention handling.
|
* Mention handling.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for provided update type.
|
* Registers middleware for provided update type.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Phone number handling.
|
* Phone number handling.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for /settings command.
|
* Handler for /settings command.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for /start command.
|
* Handler for /start command.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling messages with text_link entity.
|
* Registers middleware for handling messages with text_link entity.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling messages with text_mention entity.
|
* Registers middleware for handling messages with text_mention entity.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers middleware for handling messages with url entity.
|
* Registers middleware for handling messages with url entity.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createUpdateListenerDecorator } from '../../helpers';
|
import { createUpdateListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a middleware.
|
* Registers a middleware.
|
||||||
|
8
lib/decorators/params/context.decorator.ts
Normal file
8
lib/decorators/params/context.decorator.ts
Normal 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;
|
3
lib/decorators/params/index.ts
Normal file
3
lib/decorators/params/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './context.decorator';
|
||||||
|
export * from './next.decorator';
|
||||||
|
export * from './message-text.decorator';
|
15
lib/decorators/params/message-text.decorator.ts
Normal file
15
lib/decorators/params/message-text.decorator.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
6
lib/decorators/params/next.decorator.ts
Normal file
6
lib/decorators/params/next.decorator.ts
Normal 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,
|
||||||
|
);
|
@ -1,3 +1,3 @@
|
|||||||
import { createSceneListenerDecorator } from '../../helpers';
|
import { createSceneListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
export const SceneEnter = createSceneListenerDecorator('enter');
|
export const SceneEnter = createSceneListenerDecorator('enter');
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import { createSceneListenerDecorator } from '../../helpers';
|
import { createSceneListenerDecorator } from '../../utils';
|
||||||
|
|
||||||
export const SceneLeave = createSceneListenerDecorator('leave');
|
export const SceneLeave = createSceneListenerDecorator('leave');
|
||||||
|
8
lib/enums/telegraf-paramtype.enum.ts
Normal file
8
lib/enums/telegraf-paramtype.enum.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export enum TelegrafParamtype {
|
||||||
|
CONTEXT,
|
||||||
|
NEXT,
|
||||||
|
SENDER,
|
||||||
|
MESSAGE,
|
||||||
|
MESSAGE_TEXT,
|
||||||
|
// TODO: Add more
|
||||||
|
}
|
1
lib/errors/index.ts
Normal file
1
lib/errors/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './telegraf.exception';
|
24
lib/errors/telegraf.exception.ts
Normal file
24
lib/errors/telegraf.exception.ts
Normal 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(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
lib/exceptions/base-telegraf-exception-filter.ts
Normal file
34
lib/exceptions/base-telegraf-exception-filter.ts
Normal 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
1
lib/exceptions/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './base-telegraf-exception-filter';
|
48
lib/exceptions/telegraf-exceptions-handler.ts
Normal file
48
lib/exceptions/telegraf-exceptions-handler.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
3
lib/execution-context/index.ts
Normal file
3
lib/execution-context/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './tg-arguments-host.interace';
|
||||||
|
export * from './telegraf-arguments-host';
|
||||||
|
export * from './telegraf-arguments-host';
|
22
lib/execution-context/telegraf-arguments-host.ts
Normal file
22
lib/execution-context/telegraf-arguments-host.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
32
lib/execution-context/telegraf-execution-context.ts
Normal file
32
lib/execution-context/telegraf-execution-context.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
6
lib/execution-context/tg-arguments-host.interace.ts
Normal file
6
lib/execution-context/tg-arguments-host.interace.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ArgumentsHost } from '@nestjs/common';
|
||||||
|
|
||||||
|
export interface TgArgumentsHost extends ArgumentsHost {
|
||||||
|
getContext<T = any>(): T;
|
||||||
|
getNext<T = any>(): T;
|
||||||
|
}
|
@ -1,54 +1,98 @@
|
|||||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
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 { MetadataScanner } from '@nestjs/core/metadata-scanner';
|
||||||
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
|
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 { 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()
|
@Injectable()
|
||||||
export class TelegrafUpdateExplorer implements OnModuleInit {
|
export class TelegrafUpdateExplorer implements OnModuleInit {
|
||||||
|
private readonly contextCreator: TelegrafContextCreator;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(Telegraf)
|
private readonly moduleRef: ModuleRef,
|
||||||
private readonly telegraf: Telegraf<never>,
|
private readonly modulesContainer: ModulesContainer,
|
||||||
private readonly discoveryService: DiscoveryService,
|
private readonly discoveryService: DiscoveryService,
|
||||||
private readonly metadataAccessor: TelegrafMetadataAccessor,
|
private readonly metadataAccessor: TelegrafMetadataAccessor,
|
||||||
private readonly metadataScanner: MetadataScanner,
|
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 {
|
onModuleInit(): void {
|
||||||
this.explore();
|
this.explore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private explore(): void {
|
private explore(): void {
|
||||||
const updateClasses = this.filterUpdateClasses();
|
this.modulesContainer.forEach(({ providers }, moduleName) => {
|
||||||
|
this.exploreProviders(providers, moduleName);
|
||||||
updateClasses.forEach((wrapper) => {
|
|
||||||
const { instance } = wrapper;
|
|
||||||
|
|
||||||
const prototype = Object.getPrototypeOf(instance);
|
|
||||||
this.metadataScanner.scanFromPrototype(
|
|
||||||
instance,
|
|
||||||
prototype,
|
|
||||||
(methodKey: string) => this.registerIfListener(instance, methodKey),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterUpdateClasses(): InstanceWrapper[] {
|
private exploreProviders(
|
||||||
return this.discoveryService
|
providers: Map<string, InstanceWrapper<IInjectable>>,
|
||||||
.getProviders()
|
moduleName: string,
|
||||||
.filter((wrapper) => wrapper.instance)
|
): void {
|
||||||
.filter((wrapper) =>
|
[...providers.values()]
|
||||||
this.metadataAccessor.isUpdate(wrapper.instance.constructor),
|
.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<string, Function>,
|
||||||
|
methodKey,
|
||||||
|
moduleName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerIfListener(
|
private registerIfListener(
|
||||||
instance: Record<string, Function>,
|
instance: Record<string, Function>,
|
||||||
methodKey: string,
|
methodKey: string,
|
||||||
|
moduleName: string,
|
||||||
): void {
|
): void {
|
||||||
const methodRef = instance[methodKey];
|
const methodRef = instance[methodKey] as (...args: unknown[]) => unknown;
|
||||||
const middlewareFn = methodRef.bind(instance);
|
const contextHandlerFn = this.contextCreator.create(
|
||||||
|
instance,
|
||||||
|
methodRef,
|
||||||
|
moduleName,
|
||||||
|
methodKey,
|
||||||
|
);
|
||||||
|
|
||||||
const listenerMetadata = this.metadataAccessor.getListenerMetadata(
|
const listenerMetadata = this.metadataAccessor.getListenerMetadata(
|
||||||
methodRef,
|
methodRef,
|
||||||
@ -56,8 +100,38 @@ export class TelegrafUpdateExplorer implements OnModuleInit {
|
|||||||
if (!listenerMetadata) return;
|
if (!listenerMetadata) return;
|
||||||
|
|
||||||
const { method, args } = listenerMetadata;
|
const { method, args } = listenerMetadata;
|
||||||
// NOTE: Use "any" to disable "Expected at least 1 arguments, but got 1 or more." error.
|
this.telegraf[method](
|
||||||
// Use telegraf instance for non-scene listeners
|
...args,
|
||||||
(this.telegraf[method] as any)(...args, middlewareFn);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
lib/factories/telegraf-params-factory.ts
Normal file
23
lib/factories/telegraf-params-factory.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
export * from './create-update-listener-decorator.helper';
|
|
||||||
export * from './create-scene-listener-decorator.helper';
|
|
5
lib/helpers/is-error-object.helper.ts
Normal file
5
lib/helpers/is-error-object.helper.ts
Normal 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;
|
||||||
|
}
|
5
lib/helpers/is-observable.helper.ts
Normal file
5
lib/helpers/is-observable.helper.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { isFunction } from '@nestjs/common/utils/shared.utils';
|
||||||
|
|
||||||
|
export function isObservable(result: any): boolean {
|
||||||
|
return result && isFunction(result.subscribe);
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
export * from './decorators';
|
export * from './decorators';
|
||||||
export * from './interfaces';
|
export * from './interfaces';
|
||||||
export * from './helpers';
|
export * from './utils';
|
||||||
|
export * from './errors';
|
||||||
|
export * from './execution-context';
|
||||||
export * from './telegraf.module';
|
export * from './telegraf.module';
|
||||||
export * from './telegraf.types';
|
export * from './telegraf.types';
|
||||||
|
5
lib/interfaces/telegraf-exception-filter.interface.ts
Normal file
5
lib/interfaces/telegraf-exception-filter.interface.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ArgumentsHost } from '@nestjs/common';
|
||||||
|
|
||||||
|
export interface TelegrafExceptionFilter<T = any> {
|
||||||
|
catch(exception: T, host: ArgumentsHost): any;
|
||||||
|
}
|
@ -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 TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS';
|
||||||
|
|
||||||
export const UPDATE_METADATA = 'UPDATE_METADATA';
|
export const UPDATE_METADATA = 'UPDATE_METADATA';
|
||||||
export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA';
|
export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA';
|
||||||
|
|
||||||
export const SCENE_METADATA = 'SCENE_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
2
lib/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './create-update-listener-decorator.util';
|
||||||
|
export * from './create-scene-listener-decorator.util';
|
39
lib/utils/param-decorator.util.ts
Normal file
39
lib/utils/param-decorator.util.ts
Normal 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,
|
||||||
|
);
|
||||||
|
};
|
@ -3,7 +3,7 @@ import { TelegrafModule } from '../lib';
|
|||||||
import { EchoService } from './echo.service';
|
import { EchoService } from './echo.service';
|
||||||
import { AppUpdate } from './app.update';
|
import { AppUpdate } from './app.update';
|
||||||
import { HelloScene } from './scenes/hello.scene';
|
import { HelloScene } from './scenes/hello.scene';
|
||||||
import { sessionMiddleware } from './middleware/session.middleware';
|
import { sessionMiddleware } from './common/middleware/session.middleware';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -12,6 +12,6 @@ import { sessionMiddleware } from './middleware/session.middleware';
|
|||||||
middlewares: [sessionMiddleware],
|
middlewares: [sessionMiddleware],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [EchoService, AppUpdate, HelloScene],
|
providers: [EchoService, AppUpdate],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -1,40 +1,63 @@
|
|||||||
import { SceneContext, Telegraf } from 'telegraf';
|
import { Telegraf } from 'telegraf';
|
||||||
import { Command, Help, InjectBot, On, Start, Update } from '../lib';
|
import { SceneContextMessageUpdate } from 'telegraf/typings/stage';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
Ctx,
|
||||||
|
Help,
|
||||||
|
InjectBot,
|
||||||
|
MessageText,
|
||||||
|
On,
|
||||||
|
Start,
|
||||||
|
Update,
|
||||||
|
} from '../lib';
|
||||||
import { EchoService } from './echo.service';
|
import { EchoService } from './echo.service';
|
||||||
import { HELLO_SCENE_ID } from './app.constants';
|
import { HELLO_SCENE_ID } from './app.constants';
|
||||||
import { Context } from './interfaces/context.interface';
|
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()
|
@Update()
|
||||||
export class AppUpdate {
|
export class AppUpdate {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectBot()
|
@InjectBot()
|
||||||
private readonly bot: Telegraf<SceneContext>,
|
private readonly bot: Telegraf<SceneContextMessageUpdate>,
|
||||||
private readonly echoService: EchoService,
|
private readonly echoService: EchoService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Start()
|
@Start()
|
||||||
async onStart(ctx: Context): Promise<void> {
|
async onStart(): Promise<string> {
|
||||||
const me = await this.bot.telegram.getMe();
|
const me = await this.bot.telegram.getMe();
|
||||||
await ctx.reply(`Hey, I'm ${me.first_name}`);
|
return `Hey, I'm ${me.first_name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Help()
|
@Help()
|
||||||
async onHelp(ctx: Context): Promise<void> {
|
@UseInterceptors(ResponseTimeInterceptor)
|
||||||
|
async onHelp(@Ctx() ctx: Context): Promise<void> {
|
||||||
await ctx.reply('Send me any text');
|
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')
|
@Command('scene')
|
||||||
async onSceneCommand(ctx: Context): Promise<void> {
|
async onSceneCommand(@Ctx() ctx: Context): Promise<void> {
|
||||||
await ctx.scene.enter(HELLO_SCENE_ID);
|
await ctx.scene.enter(HELLO_SCENE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@On('message')
|
@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');
|
console.log('New message received');
|
||||||
|
|
||||||
if ('text' in ctx.message) {
|
if ('text' in ctx.message) {
|
||||||
const messageText = ctx.message.text;
|
const echoText = this.echoService.echo(reversedMessage);
|
||||||
const echoText = this.echoService.echo(messageText);
|
|
||||||
await ctx.reply(echoText);
|
await ctx.reply(echoText);
|
||||||
} else {
|
} else {
|
||||||
await ctx.reply('Only text messages');
|
await ctx.reply('Only text messages');
|
||||||
|
7
sample/common/decorators/from.decorator.ts
Normal file
7
sample/common/decorators/from.decorator.ts
Normal 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,
|
||||||
|
);
|
11
sample/common/filters/telegraf-exception.filter.ts
Normal file
11
sample/common/filters/telegraf-exception.filter.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
21
sample/common/guards/admin.guard.ts
Normal file
21
sample/common/guards/admin.guard.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
20
sample/common/interceptors/response-time.interceptor.ts
Normal file
20
sample/common/interceptors/response-time.interceptor.ts
Normal 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`)));
|
||||||
|
}
|
||||||
|
}
|
8
sample/common/pipes/reverse-text.pipe.ts
Normal file
8
sample/common/pipes/reverse-text.pipe.ts
Normal 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('');
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@
|
|||||||
"noLib": false,
|
"noLib": false,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"target": "es6",
|
"target": "es2020",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
@ -12,6 +12,7 @@ The described functionality is under development, the functionality has not been
|
|||||||
### Update
|
### 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).
|
`@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}
|
```typescript {3}
|
||||||
import { Update, Context } from 'nestjs-telegraf';
|
import { Update, Context } from 'nestjs-telegraf';
|
||||||
|
@ -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:
|
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
|
```typescript
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectBot, TelegrafProvider } from 'nestjs-telegraf';
|
import { InjectBot } from 'nestjs-telegraf';
|
||||||
|
import { Telegraf } from 'telegraf';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BotSettingsService {
|
export class BotSettingsService {
|
||||||
constructor(@InjectBot() private bot: TelegrafProvider) {}
|
constructor(@InjectBot() private bot: Telegraf) {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -18,8 +18,8 @@ import {
|
|||||||
Help,
|
Help,
|
||||||
On,
|
On,
|
||||||
Hears,
|
Hears,
|
||||||
Context,
|
|
||||||
} from 'nestjs-telegraf';
|
} from 'nestjs-telegraf';
|
||||||
|
import { Context } from 'telegraf';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
|
@ -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:
|
To access it, you must use the `app.get()` method, followed by the provider reference:
|
||||||
```typescript
|
```typescript
|
||||||
const telegrafProvider = app.get('TelegrafProvider');
|
import { Telegraf } from 'telegraf';
|
||||||
|
const telegraf = app.get(Telegraf);
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can connect middleware:
|
Now you can connect middleware:
|
||||||
```typescript
|
```typescript
|
||||||
app.use(telegrafProvider.webhookCallback('/secret-path'));
|
app.use(telegraf.webhookCallback('/secret-path'));
|
||||||
```
|
```
|
||||||
|
|
||||||
The last step is to specify launchOptions in `forRoot` method:
|
The last step is to specify launchOptions in `forRoot` method:
|
||||||
|
Loading…
Reference in New Issue
Block a user