diff --git a/.prettierrc b/.prettierrc index eaaa46c..6e778b4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,4 @@ { - "trailingComma": "es5", - "singleQuote": true, - "semi": false + "trailingComma": "all", + "singleQuote": true } diff --git a/README.md b/README.md index a62f10c..57651fd 100644 --- a/README.md +++ b/README.md @@ -27,213 +27,126 @@ ## Installation ```bash -$ npm i nestjs-telegraf telegraf +$ npm i nestjs-telegraf ``` -## Usage - -### An example of package usage +Once the installation process is complete, we can import the TelegrafModule into the root AppModule. ```typescript -/* bot.module.ts */ +/* app.module.ts */ -import { Module, OnModuleInit, Logger } from '@nestjs/common' -import { ModuleRef } from '@nestjs/core' -import { ConfigModule } from '@nestjs/config' -import { TelegrafModule, TelegrafService } from 'nestjs-telegraf' -import botConfig from './bot.config' -import { TelegrafConfigService } from './telegraf-config.service' -import { BotService } from './bot.service' +import { Module } from '@nestjs/common'; +import { TelegrafModule } from 'nestjs-telegraf'; @Module({ imports: [ - TelegrafModule.fromFactory({ - imports: [ConfigModule.forFeature(botConfig)], - useClass: TelegrafConfigService, - }), + TelegrafModule.forRoot({ + token: 'TELEGRAM_BOT_TOKEN', + }) ], - exports: [TelegrafModule], - providers: [BotService], }) -export class BotModule implements OnModuleInit { - constructor( - private readonly moduleRef: ModuleRef, - private readonly telegrafService: TelegrafService - ) {} - - onModuleInit() { - this.telegrafService.init(this.moduleRef) - this.telegrafService.startPolling() - } -} +export class AppModule {} ``` -```typescript -/* telegraf-config.service.ts */ +The `forRoot()` method accepts the same configuration object as Telegraf class constructor from the Telegraf package, as described [here](https://telegraf.js.org/#/?id=constructor). -import { Injectable } from '@nestjs/common' -import { TelegrafOptionsFactory, TelegrafModuleOptions } from 'nestjs-telegraf' -import { ConfigService } from '@nestjs/config' +## Telegraf methods + +Each Telegraf instance method described [here](https://telegraf.js.org/#/?id=telegraf) has own decorator in `nestjs-telegraf` package. The name of the decorator corresponds to the name of the Telegraf method and starts with `Telegraf`. For example [`@TelegrafHears`](https://telegraf.js.org/#/?id=hears), [`@TelegrafOn`](https://telegraf.js.org/#/?id=on), [`@TelegrafAction`](https://telegraf.js.org/#/?id=action) and so on. + +Now let's try to repeat the example from the Telegraf [documentation page](https://telegraf.js.org/#/?id=example). + +```typescript +/* app.service.ts */ + +import { Injectable } from '@nestjs/common'; +import { + TelegrafStart, + TelegrafHelp, + TelegrafOn, + TelegrafHears, + ContextMessageUpdate, +} from 'nestjs-telegraf'; @Injectable() -export class TelegrafConfigService implements TelegrafOptionsFactory { - constructor(private readonly configService: ConfigService) {} +export class AppService { + @TelegrafStart() + start(ctx: ContextMessageUpdate) { + ctx.reply('Welcome'); + } - createTelegrafOptions(): TelegrafModuleOptions { - return { - token: this.configService.get('bot.token'), - } + @TelegrafHelp() + help(ctx: ContextMessageUpdate) { + ctx.reply('Send me a sticker'); + } + + @TelegrafOn('sticker') + on(ctx: ContextMessageUpdate) { + ctx.reply('👍'); + } + + @TelegrafHears('hi') + hears(ctx: ContextMessageUpdate) { + ctx.reply('Hey there'); } } ``` -```typescript -/* bot.config.ts */ +## Async configuration +When you need to pass module options asynchronously instead of statically, use the forRootAsync() method. As with most dynamic modules, Nest provides several techniques to deal with async configuration. -import { registerAs } from '@nestjs/config' - -interface Config { - token: string -} - -export default registerAs( - 'bot', - (): Config => ({ - token: process.env.TELEGRAM_BOT_TOKEN, - }) -) -``` - -### Telegraf - -#### Telegraf methods usage -You can decorate any `Telegraf` method with `@TelegramActionHandler` decorator. +One technique is to use a factory function: ```typescript -/* bot.service.ts */ - -import { Injectable } from '@nestjs/common' -import { TelegrafTelegramService } from 'nestjs-telegraf' -import { ContextMessageUpdate } from 'telegraf' - -@Injectable() -export class BotService { - /* This decorator handle /start command */ - @TelegramActionHandler({ onStart: true }) - async onStart(ctx: ContextMessageUpdate) { - await ctx.reply('/start command reply') - } -} -``` - -##### Today available actions for decorator: - -- [`onStart`](https://telegraf.js.org/#/?id=start) Handler for /start command. - -- [`command`](https://telegraf.js.org/#/?id=command) Command handling. - -- [`message`](https://telegraf.js.org/#/?id=hears) Registers middleware for handling text messages. - -- [`action`](https://telegraf.js.org/#/?id=action) Registers middleware for handling `callback_data` actions with regular expressions. - -#### Telegraf middlewares usage - -See https://github.com/bukhalo/nestjs-telegraf/issues/7#issuecomment-577582322 - -#### Telegraf proxy usage - -```typescript - -/* bot.config.ts */ - -import { registerAs } from '@nestjs/config' - -interface Config { - token: string - socksHost: string - socksPort: string | number - socksUser: string - socksPassword: string -} - -export default registerAs( - 'bot', - (): Config => ({ - token: process.env.TELEGRAM_BOT_TOKEN, - socksHost: process.env.TELEGRAM_BOT_SOCKS_HOST, - socksPort: process.env.TELEGRAM_BOT_SOCKS_PORT, - socksUser: process.env.TELEGRAM_BOT_SOCKS_USER, - socksPassword: process.env.TELEGRAM_BOT_SOCKS_PASS, +TelegrafModule.forRootAsync({ + useFactory: () => ({ + token: 'TELEGRAM_BOT_TOKEN', }), -); - +}); ``` +Like other [factory providers](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory), our factory function can be async and can inject dependencies through inject. + ```typescript +TelegrafModule.forRootAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + token: configService.getString('TELEGRAM_BOT_TOKEN'), + }), + inject: [ConfigService], +}); +``` -/* telegraf-config.service.ts */ +Alternatively, you can configure the TelegrafModule using a class instead of a factory, as shown below: -import { Injectable } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' -import { TelegrafModuleOptions, TelegrafOptionsFactory } from 'nestjs-telegraf' -import { SocksProxyAgent } from 'socks-proxy-agent' +```typescript +TelegrafModule.forRootAsync({ + useClass: TelegrafConfigService, +}); +``` +The construction above instantiates `TelegrafConfigService` inside `TelegrafModule`, using it to create the required options object. Note that in this example, the `TelegrafConfigService` has to implement the `TelegrafOptionsFactory` interface, as shown below. The `TelegrafModule` will call the `createTelegrafOptions()` method on the instantiated object of the supplied class. + +```typescript @Injectable() -export class TelegrafConfigService implements TelegrafOptionsFactory { - private agent - - constructor(private readonly configService: ConfigService) {} - - createTelegrafOptions(): TelegrafModuleOptions { - const proxyConfig = { - host: this.configService.get('bot.socksHost'), - port: this.configService.get('bot.socksPort'), - userId: this.configService.get('bot.socksUser'), - password: this.configService.get('bot.socksPassword'), - } - this.agent = new SocksProxyAgent(proxyConfig) - +class TelegrafConfigService implements TelegrafOptionsFactory { + createMongooseOptions(): TelegrafModuleOptions { return { - token: this.configService.get('bot.token'), - telegrafOptions: { telegram: { agent: this.agent } }, + token: 'TELEGRAM_BOT_TOKEN', }; } } - ``` -### Telegram - -#### Telegram methods usage - -Inject `TelegrafTelegramService` from `nestjs-telegraf` package for use [Telegram instance](https://telegraf.js.org/#/?id=telegram) from `telegraf` package. +If you want to reuse an existing options provider instead of creating a private copy inside the `TelegrafModule`, use the `useExisting` syntax. ```typescript -/* bot.service.ts */ - -import { Injectable } from '@nestjs/common' -import { TelegrafTelegramService, TelegramActionHandler } from 'nestjs-telegraf' -import { ContextMessageUpdate } from 'telegraf' - -@Injectable() -export class BotService { - constructor( - private readonly telegrafTelegramService: TelegrafTelegramService - ) {} - - @TelegramActionHandler({ onStart: true }) - async start(ctx: ContextMessageUpdate) { - const me = await this.telegrafTelegramService.getMe() - console.log(me) - } -} +TelegrafModule.forRootAsync({ + imports: [ConfigModule], + useExisting: ConfigService, +}); ``` -## Examples - -You can see the basic use of the package in this repository: -https://github.com/bukhalo/nestjs-telegraf-sample - ## Support Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index 246b998..340866c 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -1,3 +1,15 @@ -export * from './pipe-context.decorator' -export * from './telegram-action-handler.decorator' -export * from './telegram-catch.decorator' +export * from './telegraf-use.decorator'; +export * from './telegraf-on.decorator'; +export * from './telegraf-hears.decorator'; +export * from './telegraf-command.decorator'; +export * from './telegraf-start.decorator'; +export * from './telegraf-help.decorator'; +export * from './telegraf-settings.decorator'; +export * from './telegraf-entity.decorator'; +export * from './telegraf-mention.decorator'; +export * from './telegraf-phone.decorator'; +export * from './telegraf-hashtag.decorator'; +export * from './telegraf-cashtag.decorator'; +export * from './telegraf-action.decorator'; +export * from './telegraf-inline-query.decorator'; +export * from './telegraf-game-query.decorator'; diff --git a/lib/decorators/pipe-context.decorator.ts b/lib/decorators/pipe-context.decorator.ts deleted file mode 100644 index 01aeec0..0000000 --- a/lib/decorators/pipe-context.decorator.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Type } from '@nestjs/common' -import { ContextTransformer } from '../interfaces' -import { addHandlerToStore } from './' - -export const PipeContext = (transform: Type>) => ( - target: Object, - propertyKey: string, - parameterIndex: number, -) => { - addHandlerToStore(target, propertyKey, { - transformations: [ - { - index: parameterIndex, - transform, - }, - ], - }) -} diff --git a/lib/decorators/telegraf-action.decorator.ts b/lib/decorators/telegraf-action.decorator.ts new file mode 100644 index 0000000..2fd6781 --- /dev/null +++ b/lib/decorators/telegraf-action.decorator.ts @@ -0,0 +1,21 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; +import { HearsTriggers } from 'telegraf'; + +export type TelegrafActionTriggers = HearsTriggers; + +export interface TelegrafActionMetadata { + triggers: TelegrafActionTriggers; +} + +/** + * Registers middleware for handling callback_data actions with regular expressions. + * @param triggers Triggers + * + * https://telegraf.js.org/#/?id=action + */ +export function TelegrafAction( + triggers: TelegrafActionTriggers, +): MethodDecorator { + return SetMetadata(DECORATORS.ACTION, { triggers }); +} diff --git a/lib/decorators/telegraf-cashtag.decorator.ts b/lib/decorators/telegraf-cashtag.decorator.ts new file mode 100644 index 0000000..136c8c8 --- /dev/null +++ b/lib/decorators/telegraf-cashtag.decorator.ts @@ -0,0 +1,20 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +export type TelegrafCashtagCashtag = string | string[]; + +export interface TelegrafCashtagMetadata { + cashtag: TelegrafCashtagCashtag; +} + +/** + * Cashtag handling. + * @param cashtag Cashtag + * + * https://telegraf.js.org/#/?id=cashtag + */ +export function TelegrafCashtag( + cashtag: TelegrafCashtagCashtag, +): MethodDecorator { + return SetMetadata(DECORATORS.CASHTAG, { cashtag }); +} diff --git a/lib/decorators/telegraf-command.decorator.ts b/lib/decorators/telegraf-command.decorator.ts new file mode 100644 index 0000000..7f1570c --- /dev/null +++ b/lib/decorators/telegraf-command.decorator.ts @@ -0,0 +1,20 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +export type TelegrafCommandCommands = string | string[]; + +export interface TelegrafCommandMetadata { + commands: TelegrafCommandCommands; +} + +/** + * Command handling. + * @param commands Commands + * + * https://telegraf.js.org/#/?id=command + */ +export function TelegrafCommand( + commands: TelegrafCommandCommands, +): MethodDecorator { + return SetMetadata(DECORATORS.COMMAND, { commands }); +} diff --git a/lib/decorators/telegraf-entity.decorator.ts b/lib/decorators/telegraf-entity.decorator.ts new file mode 100644 index 0000000..b32c173 --- /dev/null +++ b/lib/decorators/telegraf-entity.decorator.ts @@ -0,0 +1,23 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +export type TelegrafEntityEntity = + | string + | string[] + | RegExp + | RegExp[] + | Function; + +export interface TelegrafEntityMetadata { + entity: TelegrafEntityEntity; +} + +/** + * Entity handling. + * @param entity Entity name + * + * https://telegraf.js.org/#/?id=entity + */ +export function TelegrafEntity(entity: TelegrafEntityEntity): MethodDecorator { + return SetMetadata(DECORATORS.ENTITY, { entity }); +} diff --git a/lib/decorators/telegraf-game-query.decorator.ts b/lib/decorators/telegraf-game-query.decorator.ts new file mode 100644 index 0000000..8439345 --- /dev/null +++ b/lib/decorators/telegraf-game-query.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +/** + * Registers middleware for handling callback_data actions with game query. + * + * https://telegraf.js.org/#/?id=inlinequery + */ +export function TelegrafGameQuery(): MethodDecorator { + return SetMetadata(DECORATORS.GAME_QUERY, {}); +} diff --git a/lib/decorators/telegraf-hashtag.decorator.ts b/lib/decorators/telegraf-hashtag.decorator.ts new file mode 100644 index 0000000..3265213 --- /dev/null +++ b/lib/decorators/telegraf-hashtag.decorator.ts @@ -0,0 +1,20 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +export type TelegrafHashtagHashtag = string | string[]; + +export interface TelegrafHashtagMetadata { + hashtag: TelegrafHashtagHashtag; +} + +/** + * Hashtag handling. + * @param hashtag Hashtag + * + * https://telegraf.js.org/#/?id=hashtag + */ +export function TelegrafHashtag( + hashtag: TelegrafHashtagHashtag, +): MethodDecorator { + return SetMetadata(DECORATORS.HASHTAG, { hashtag }); +} diff --git a/lib/decorators/telegraf-hears.decorator.ts b/lib/decorators/telegraf-hears.decorator.ts new file mode 100644 index 0000000..17b8d1b --- /dev/null +++ b/lib/decorators/telegraf-hears.decorator.ts @@ -0,0 +1,21 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; +import { HearsTriggers } from 'telegraf'; + +export type TelegrafHearsTriggers = HearsTriggers; + +export interface TelegrafHearsMetadata { + triggers: TelegrafHearsTriggers; +} + +/** + * Registers middleware for handling text messages. + * @param triggers Triggers + * + * https://telegraf.js.org/#/?id=hears + */ +export function TelegrafHears( + triggers: TelegrafHearsTriggers, +): MethodDecorator { + return SetMetadata(DECORATORS.HEARS, { triggers: triggers }); +} diff --git a/lib/decorators/telegraf-help.decorator.ts b/lib/decorators/telegraf-help.decorator.ts new file mode 100644 index 0000000..7bade62 --- /dev/null +++ b/lib/decorators/telegraf-help.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +/** + * Handler for /help command. + * + * https://telegraf.js.org/#/?id=help + */ +export function TelegrafHelp(): MethodDecorator { + return SetMetadata(DECORATORS.HELP, {}); +} diff --git a/lib/decorators/telegraf-inline-query.decorator.ts b/lib/decorators/telegraf-inline-query.decorator.ts new file mode 100644 index 0000000..1a4cc4f --- /dev/null +++ b/lib/decorators/telegraf-inline-query.decorator.ts @@ -0,0 +1,20 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +export type TelegrafInlineQueryTriggers = string | string[] | RegExp | RegExp[]; + +export interface TelegrafInlineQueryMetadata { + triggers: TelegrafInlineQueryTriggers; +} + +/** + * Registers middleware for handling inline_query actions with regular expressions. + * @param triggers Triggers + * + * https://telegraf.js.org/#/?id=inlinequery + */ +export function TelegrafInlineQuery( + triggers: TelegrafInlineQueryTriggers, +): MethodDecorator { + return SetMetadata(DECORATORS.INLINE_QUERY, { triggers }); +} diff --git a/lib/decorators/telegraf-mention.decorator.ts b/lib/decorators/telegraf-mention.decorator.ts new file mode 100644 index 0000000..90dbc6b --- /dev/null +++ b/lib/decorators/telegraf-mention.decorator.ts @@ -0,0 +1,20 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +export type TelegrafMentionUsername = string | string[]; + +export interface TelegrafMentionMetadata { + username: TelegrafMentionUsername; +} + +/** + * Mention handling. + * @param username Username + * + * https://telegraf.js.org/#/?id=mention + */ +export function TelegrafMention( + username: TelegrafMentionUsername, +): MethodDecorator { + return SetMetadata(DECORATORS.MENTION, { username }); +} diff --git a/lib/decorators/telegraf-on.decorator.ts b/lib/decorators/telegraf-on.decorator.ts new file mode 100644 index 0000000..4fe3ac0 --- /dev/null +++ b/lib/decorators/telegraf-on.decorator.ts @@ -0,0 +1,25 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; +import { UpdateType, MessageSubTypes } from 'telegraf/typings/telegram-types'; + +export type TelegrafOnUpdateTypes = + | UpdateType + | UpdateType[] + | MessageSubTypes + | MessageSubTypes[]; + +export interface TelegrafOnMetadata { + updateTypes: TelegrafOnUpdateTypes; +} + +/** + * Registers middleware for provided update type. + * @param updateTypes Update type + * + * https://telegraf.js.org/#/?id=on + */ +export function TelegrafOn( + updateTypes: TelegrafOnUpdateTypes, +): MethodDecorator { + return SetMetadata(DECORATORS.ON, { updateTypes: updateTypes }); +} diff --git a/lib/decorators/telegraf-phone.decorator.ts b/lib/decorators/telegraf-phone.decorator.ts new file mode 100644 index 0000000..8182150 --- /dev/null +++ b/lib/decorators/telegraf-phone.decorator.ts @@ -0,0 +1,18 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +export type TelegrafPhonePhone = string | string[]; + +export interface TelegrafPhoneMetadata { + phone: TelegrafPhonePhone; +} + +/** + * Phone number handling. + * @param phone Phone number + * + * https://telegraf.js.org/#/?id=phone + */ +export function TelegrafPhone(phone: TelegrafPhonePhone): MethodDecorator { + return SetMetadata(DECORATORS.PHONE, { phone }); +} diff --git a/lib/decorators/telegraf-settings.decorator.ts b/lib/decorators/telegraf-settings.decorator.ts new file mode 100644 index 0000000..aed3741 --- /dev/null +++ b/lib/decorators/telegraf-settings.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +/** + * Handler for /settings command. + * + * https://telegraf.js.org/#/?id=settings + */ +export function TelegrafSettings(): MethodDecorator { + return SetMetadata(DECORATORS.SETTINGS, {}); +} diff --git a/lib/decorators/telegraf-start.decorator.ts b/lib/decorators/telegraf-start.decorator.ts new file mode 100644 index 0000000..3e0c820 --- /dev/null +++ b/lib/decorators/telegraf-start.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +/** + * Handler for /start command. + * + * https://telegraf.js.org/#/?id=start + */ +export function TelegrafStart(): MethodDecorator { + return SetMetadata(DECORATORS.START, {}); +} diff --git a/lib/decorators/telegraf-use.decorator.ts b/lib/decorators/telegraf-use.decorator.ts new file mode 100644 index 0000000..2ff0dd7 --- /dev/null +++ b/lib/decorators/telegraf-use.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from '@nestjs/common'; +import { DECORATORS } from '../telegraf.constants'; + +/** + * Registers a middleware. + * + * https://telegraf.js.org/#/?id=use + */ +export function TelegrafUse(): MethodDecorator { + return SetMetadata(DECORATORS.USE, {}); +} diff --git a/lib/decorators/telegram-action-handler.decorator.ts b/lib/decorators/telegram-action-handler.decorator.ts deleted file mode 100644 index b19a619..0000000 --- a/lib/decorators/telegram-action-handler.decorator.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { HandleParameters } from '../interfaces' - -type Decorator = ( - params: HandleParameters, -) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void - -type HandlerDecorator = Decorator & { - handlers?: Map> -} - -export const TelegramActionHandler: HandlerDecorator = ( - parameters: HandleParameters, -) => (target: any, propertyKey: string) => { - // eslint-disable-next-line no-use-before-define - addHandlerToStore(target, propertyKey, parameters) -} - -export const addHandlerToStore = ( - instance: Object, - name: string, - config: HandleParameters, -) => { - const handlerClass = instance.constructor - - if (!TelegramActionHandler.handlers) { - TelegramActionHandler.handlers = new Map() - } - - if (!TelegramActionHandler.handlers.get(handlerClass)) { - TelegramActionHandler.handlers.set(handlerClass, new Map()) - } - - const oldParameters = - TelegramActionHandler.handlers.get(handlerClass).get(name) || {} - - TelegramActionHandler.handlers.get(handlerClass).set(name, { - ...oldParameters, - ...config, - transformations: [ - ...(oldParameters.transformations || []), - ...(config.transformations || []), - ], - }) -} diff --git a/lib/decorators/telegram-catch.decorator.ts b/lib/decorators/telegram-catch.decorator.ts deleted file mode 100644 index f924ea9..0000000 --- a/lib/decorators/telegram-catch.decorator.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Type } from '@nestjs/common' -import { TelegramErrorHandler } from '../interfaces' - -type Decorator = (error: any) => ClassDecorator - -type HandlerDecorator = Decorator & { - handlers?: Map> -} - -export const TelegramCatch: HandlerDecorator = error => target => { - if (!TelegramCatch.handlers) { - TelegramCatch.handlers = new Map() - } - - TelegramCatch.handlers.set(error, target as any) - - return target -} diff --git a/lib/exeptions/index.ts b/lib/exeptions/index.ts deleted file mode 100644 index 40f3902..0000000 --- a/lib/exeptions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './invalid-configuration.exeption' diff --git a/lib/exeptions/invalid-configuration.exeption.ts b/lib/exeptions/invalid-configuration.exeption.ts deleted file mode 100644 index deddd73..0000000 --- a/lib/exeptions/invalid-configuration.exeption.ts +++ /dev/null @@ -1,10 +0,0 @@ -export class InvalidConfigurationException extends Error { - public constructor( - public readonly invalidField, - public readonly invalidCause, - ) { - super( - `Options validation failed, "${invalidField}" invalid — ${invalidCause}`, - ) - } -} diff --git a/lib/index.ts b/lib/index.ts index d7b43b6..6ca2523 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,5 +1,3 @@ -export * from './telegraf.module' -export * from './interfaces' -export * from './decorators' -export * from './telegraf.service' -export * from './telegraf-telegram.service' +export * from './telegraf.module'; +export * from './interfaces'; +export * from './decorators'; diff --git a/lib/interfaces/context-transformer.interface.ts b/lib/interfaces/context-transformer.interface.ts deleted file mode 100644 index 61422ac..0000000 --- a/lib/interfaces/context-transformer.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ContextMessageUpdate } from 'telegraf' - -export interface ContextTransformer { - transform: (ctx: ContextMessageUpdate) => Promise -} diff --git a/lib/interfaces/handle-parameters.interface.ts b/lib/interfaces/handle-parameters.interface.ts deleted file mode 100644 index 561953c..0000000 --- a/lib/interfaces/handle-parameters.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ContextTransformer } from './' -import { HearsTriggers } from 'telegraf' -import { UpdateType, MessageSubTypes } from 'telegraf/typings/telegram-types' -import { Type } from '@nestjs/common' - -interface ArgumentTransformation { - index: number - transform: Type -} - -export interface HandleParameters { - onStart?: boolean - on?: UpdateType | UpdateType[] | MessageSubTypes | MessageSubTypes[] - command?: string - message?: string | RegExp - action?: HearsTriggers - transformations?: ArgumentTransformation[] -} diff --git a/lib/interfaces/handler.interface.ts b/lib/interfaces/handler.interface.ts deleted file mode 100644 index ac61f9b..0000000 --- a/lib/interfaces/handler.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HandleParameters } from './' - -export interface Handler { - handle: (...args: any[]) => Promise - config: HandleParameters -} diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index 4f31766..08c29f5 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -1,5 +1,2 @@ -export * from './telegraf-options.interface' -export * from './handler.interface' -export * from './handle-parameters.interface' -export * from './telegram-error-handler.interface' -export * from './context-transformer.interface' +export { ContextMessageUpdate } from 'telegraf'; +export * from './telegraf-options.interface'; diff --git a/lib/interfaces/telegraf-options.interface.ts b/lib/interfaces/telegraf-options.interface.ts index 64b70a6..e3b1969 100644 --- a/lib/interfaces/telegraf-options.interface.ts +++ b/lib/interfaces/telegraf-options.interface.ts @@ -1,22 +1,21 @@ -import { ModuleMetadata, Type } from '@nestjs/common/interfaces' -import { TelegrafOptions } from 'telegraf' +import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; +import { TelegrafOptions } from 'telegraf'; export interface TelegrafModuleOptions { - token: string - sitePublicUrl?: string - telegrafOptions?: TelegrafOptions + token: string; + options?: TelegrafOptions; } export interface TelegrafOptionsFactory { - createTelegrafOptions(): TelegrafModuleOptions + createTelegrafOptions(): TelegrafModuleOptions; } export interface TelegrafModuleAsyncOptions extends Pick { - useExisting?: Type - useClass?: Type + useExisting?: Type; + useClass?: Type; useFactory?: ( ...args: any[] - ) => Promise | TelegrafModuleOptions - inject?: any[] + ) => Promise | TelegrafModuleOptions; + inject?: any[]; } diff --git a/lib/interfaces/telegram-error-handler.interface.ts b/lib/interfaces/telegram-error-handler.interface.ts deleted file mode 100644 index 893f309..0000000 --- a/lib/interfaces/telegram-error-handler.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ContextMessageUpdate } from 'telegraf' - -export interface TelegramErrorHandler { - catch(ctx: ContextMessageUpdate, error: E): Promise -} diff --git a/lib/telegraf-core.module.ts b/lib/telegraf-core.module.ts new file mode 100644 index 0000000..49b2ee4 --- /dev/null +++ b/lib/telegraf-core.module.ts @@ -0,0 +1,91 @@ +import { Module, DynamicModule, Provider, Type } from '@nestjs/common'; +import { + TelegrafModuleOptions, + TelegrafModuleAsyncOptions, + TelegrafOptionsFactory, +} from './interfaces'; +import { + TELEGRAF_MODULE_OPTIONS, + TELEGRAF_PROVIDER, +} from './telegraf.constants'; +import { TelegrafMetadataAccessor } from './telegraf-metadata.accessor'; +import { TelegrafExplorer } from './telegraf.explorer'; +import { DiscoveryModule } from '@nestjs/core'; +import { TelegrafProvider } from './telegraf.provider'; + +@Module({ + imports: [DiscoveryModule], + providers: [TelegrafMetadataAccessor, TelegrafExplorer], +}) +export class TelegrafCoreModule { + public static forRoot(options: TelegrafModuleOptions): DynamicModule { + const telegrafProvider = { + provide: TELEGRAF_PROVIDER, + useClass: TelegrafProvider, + inject: [TELEGRAF_MODULE_OPTIONS], + }; + return { + module: TelegrafCoreModule, + providers: [ + { provide: TELEGRAF_MODULE_OPTIONS, useValue: options }, + telegrafProvider, + ], + exports: [telegrafProvider], + }; + } + + public static forRootAsync( + options: TelegrafModuleAsyncOptions, + ): DynamicModule { + const telegrafProvider = { + provide: TELEGRAF_PROVIDER, + useClass: TelegrafProvider, + inject: [TELEGRAF_MODULE_OPTIONS], + }; + const asyncProviders = this.createAsyncProviders(options); + return { + module: TelegrafCoreModule, + imports: options.imports, + providers: [...asyncProviders, telegrafProvider], + exports: [telegrafProvider], + }; + } + + private static createAsyncProviders( + options: TelegrafModuleAsyncOptions, + ): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createAsyncOptionsProvider(options)]; + } + const useClass = options.useClass as Type; + return [ + this.createAsyncOptionsProvider(options), + { + provide: useClass, + useClass, + }, + ]; + } + + private static createAsyncOptionsProvider( + options: TelegrafModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + return { + provide: TELEGRAF_MODULE_OPTIONS, + useFactory: options.useFactory, + inject: options.inject || [], + }; + } + // `as Type` is a workaround for microsoft/TypeScript#31603 + const inject = [ + (options.useClass || options.useExisting) as Type, + ]; + return { + provide: TELEGRAF_MODULE_OPTIONS, + useFactory: async (optionsFactory: TelegrafOptionsFactory) => + await optionsFactory.createTelegrafOptions(), + inject, + }; + } +} diff --git a/lib/telegraf-metadata.accessor.ts b/lib/telegraf-metadata.accessor.ts new file mode 100644 index 0000000..b35b8cf --- /dev/null +++ b/lib/telegraf-metadata.accessor.ts @@ -0,0 +1,186 @@ +import { Injectable, Type } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { + TelegrafActionMetadata, + TelegrafCashtagMetadata, + TelegrafCommandMetadata, + TelegrafEntityMetadata, + TelegrafHashtagMetadata, + TelegrafHearsMetadata, + TelegrafInlineQueryMetadata, + TelegrafMentionMetadata, + TelegrafOnMetadata, + TelegrafPhoneMetadata, + TelegrafStart, +} from './decorators'; +import { DECORATORS } from './telegraf.constants'; + +@Injectable() +export class TelegrafMetadataAccessor { + constructor(private readonly reflector: Reflector) {} + + isTelegrafUse(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.USE, target); + } + + isTelegrafOn(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.ON, target); + } + + getTelegrafOnMetadata( + target: Type | Function, + ): TelegrafOnMetadata | undefined { + return this.reflector.get(DECORATORS.ON, target); + } + + isTelegrafHears(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.HEARS, target); + } + + getTelegrafHearsMetadata( + target: Type | Function, + ): TelegrafHearsMetadata | undefined { + return this.reflector.get(DECORATORS.HEARS, target); + } + + isTelegrafCommand(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.COMMAND, target); + } + + getTelegrafCommandMetadata( + target: Type | Function, + ): TelegrafCommandMetadata | undefined { + return this.reflector.get(DECORATORS.COMMAND, target); + } + + isTelegrafStart(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.START, target); + } + + isTelegrafHelp(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.HELP, target); + } + + isTelegrafSettings(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.SETTINGS, target); + } + + isTelegrafEntity(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.ENTITY, target); + } + + getTelegrafEntityMetadata( + target: Type | Function, + ): TelegrafEntityMetadata | undefined { + return this.reflector.get(DECORATORS.ENTITY, target); + } + + isTelegrafMention(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.MENTION, target); + } + + getTelegrafMentionMetadata( + target: Type | Function, + ): TelegrafMentionMetadata | undefined { + return this.reflector.get(DECORATORS.MENTION, target); + } + + isTelegrafPhone(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.PHONE, target); + } + + getTelegrafPhoneMetadata( + target: Type | Function, + ): TelegrafPhoneMetadata | undefined { + return this.reflector.get(DECORATORS.PHONE, target); + } + + isTelegrafHashtag(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.HASHTAG, target); + } + + getTelegrafHashtagMetadata( + target: Type | Function, + ): TelegrafHashtagMetadata | undefined { + return this.reflector.get(DECORATORS.HASHTAG, target); + } + + isTelegrafCashtag(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.CASHTAG, target); + } + + getTelegrafCashtagMetadata( + target: Type | Function, + ): TelegrafCashtagMetadata | undefined { + return this.reflector.get(DECORATORS.CASHTAG, target); + } + + isTelegrafAction(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.ACTION, target); + } + + getTelegrafActionMetadata( + target: Type | Function, + ): TelegrafActionMetadata | undefined { + return this.reflector.get(DECORATORS.ACTION, target); + } + + isTelegrafInlineQuery(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.INLINE_QUERY, target); + } + + getTelegrafInlineQueryMetadata( + target: Type | Function, + ): TelegrafInlineQueryMetadata | undefined { + return this.reflector.get(DECORATORS.INLINE_QUERY, target); + } + + isTelegrafGameQuery(target: Type | Function): boolean { + if (!target) { + return false; + } + return !!this.reflector.get(DECORATORS.GAME_QUERY, target); + } +} diff --git a/lib/telegraf-telegram.service.ts b/lib/telegraf-telegram.service.ts deleted file mode 100644 index 8b262b7..0000000 --- a/lib/telegraf-telegram.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -const Telegram = require('telegraf/telegram'); -import { Telegram as TelegramClient } from 'telegraf'; - -import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants'; -import { TelegrafModuleOptions } from './interfaces'; - -@Injectable() -export class TelegrafTelegramService extends TelegramClient { - constructor(@Inject(TELEGRAF_MODULE_OPTIONS) options: TelegrafModuleOptions) { - super(options.token, {}); - } -} diff --git a/lib/telegraf.constants.ts b/lib/telegraf.constants.ts index b62570c..c70b4b9 100644 --- a/lib/telegraf.constants.ts +++ b/lib/telegraf.constants.ts @@ -1,2 +1,21 @@ -export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS' -export const TokenInjectionToken = Symbol('TokenInjectionToken') +export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS'; +export const TELEGRAF_PROVIDER = 'TELEGRAF_PROVIDER'; + +export const DECORATORS_PREFIX = 'TELEGRAF'; +export const DECORATORS = { + USE: `${DECORATORS_PREFIX}/USE`, + ON: `${DECORATORS_PREFIX}/ON`, + HEARS: `${DECORATORS_PREFIX}/HEARS`, + COMMAND: `${DECORATORS_PREFIX}/COMMAND`, + START: `${DECORATORS_PREFIX}/START`, + HELP: `${DECORATORS_PREFIX}/HELP`, + SETTINGS: `${DECORATORS_PREFIX}/SETTINGS`, + ENTITY: `${DECORATORS_PREFIX}/ENTITY`, + MENTION: `${DECORATORS_PREFIX}/MENTION`, + PHONE: `${DECORATORS_PREFIX}/PHONE`, + HASHTAG: `${DECORATORS_PREFIX}/HASHTAG`, + CASHTAG: `${DECORATORS_PREFIX}/CASHTAG`, + ACTION: `${DECORATORS_PREFIX}/ACTION`, + INLINE_QUERY: `${DECORATORS_PREFIX}/INLINE_QUERY`, + GAME_QUERY: `${DECORATORS_PREFIX}/GAME_QUERY`, +}; diff --git a/lib/telegraf.explorer.ts b/lib/telegraf.explorer.ts new file mode 100644 index 0000000..503d68b --- /dev/null +++ b/lib/telegraf.explorer.ts @@ -0,0 +1,258 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { DiscoveryService, ModuleRef } from '@nestjs/core'; +import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; +import { MetadataScanner } from '@nestjs/core/metadata-scanner'; +import Telegraf from 'telegraf'; +import { TelegrafMetadataAccessor } from './telegraf-metadata.accessor'; +import { TelegrafProvider } from './telegraf.provider'; +import { TELEGRAF_PROVIDER } from './telegraf.constants'; +import { ContextMessageUpdate } from 'telegraf'; +import { + TelegrafActionMetadata, + TelegrafCashtagMetadata, + TelegrafCommandMetadata, + TelegrafEntityMetadata, + TelegrafHashtagMetadata, + TelegrafHearsMetadata, + TelegrafInlineQueryMetadata, + TelegrafMentionMetadata, + TelegrafOnMetadata, + TelegrafPhoneMetadata, +} from './decorators'; + +@Injectable() +export class TelegrafExplorer implements OnModuleInit { + constructor( + private readonly moduleRef: ModuleRef, + private readonly discoveryService: DiscoveryService, + private readonly metadataAccessor: TelegrafMetadataAccessor, + private readonly metadataScanner: MetadataScanner, + ) {} + + onModuleInit() { + this.explore(); + } + + explore() { + const providers: InstanceWrapper[] = this.discoveryService.getProviders(); + providers.forEach((wrapper: InstanceWrapper) => { + const { instance } = wrapper; + + if (!instance) { + return; + } + + const telegraf = this.moduleRef.get>( + TELEGRAF_PROVIDER, + { strict: false }, + ); + + this.metadataScanner.scanFromPrototype( + instance, + Object.getPrototypeOf(instance), + (key: string) => { + if (this.metadataAccessor.isTelegrafUse(instance[key])) { + this.handleTelegrafUse(instance, key, telegraf); + } else if (this.metadataAccessor.isTelegrafOn(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafOnMetadata( + instance[key], + ); + this.handleTelegrafOn(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafHears(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafHearsMetadata( + instance[key], + ); + this.handleTelegrafHears(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafCommand(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafCommandMetadata( + instance[key], + ); + this.handleTelegrafCommand(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafStart(instance[key])) { + this.handleTelegrafStart(instance, key, telegraf); + } else if (this.metadataAccessor.isTelegrafHelp(instance[key])) { + this.handleTelegrafHelp(instance, key, telegraf); + } else if (this.metadataAccessor.isTelegrafSettings(instance[key])) { + this.handleTelegrafSettings(instance, key, telegraf); + } else if (this.metadataAccessor.isTelegrafEntity(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafEntityMetadata( + instance[key], + ); + this.handleTelegrafEntity(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafMention(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafMentionMetadata( + instance[key], + ); + this.handleTelegrafMention(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafPhone(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafPhoneMetadata( + instance[key], + ); + this.handleTelegrafPhone(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafHashtag(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafHashtagMetadata( + instance[key], + ); + this.handleTelegrafHashtag(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafCashtag(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafCashtagMetadata( + instance[key], + ); + this.handleTelegrafCashtag(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafAction(instance[key])) { + const metadata = this.metadataAccessor.getTelegrafActionMetadata( + instance[key], + ); + this.handleTelegrafAction(instance, key, telegraf, metadata); + } else if ( + this.metadataAccessor.isTelegrafInlineQuery(instance[key]) + ) { + const metadata = this.metadataAccessor.getTelegrafInlineQueryMetadata( + instance[key], + ); + this.handleTelegrafInlineQuery(instance, key, telegraf, metadata); + } else if (this.metadataAccessor.isTelegrafGameQuery(instance[key])) { + this.handleTelegrafGameQuery(instance, key, telegraf); + } + }, + ); + }); + } + + handleTelegrafUse( + instance: object, + key: string, + telegraf: Telegraf, + ) { + telegraf.use(instance[key].bind(instance)); + } + + handleTelegrafOn( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafOnMetadata, + ) { + telegraf.on(metadata.updateTypes, instance[key].bind(instance)); + } + + handleTelegrafHears( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafHearsMetadata, + ) { + telegraf.hears(metadata.triggers, instance[key].bind(instance)); + } + + handleTelegrafCommand( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafCommandMetadata, + ) { + telegraf.command(metadata.commands, instance[key].bind(instance)); + } + + handleTelegrafStart( + instance: object, + key: string, + telegraf: Telegraf, + ) { + telegraf.start(instance[key].bind(instance)); + } + + handleTelegrafHelp( + instance: object, + key: string, + telegraf: Telegraf, + ) { + telegraf.help(instance[key].bind(instance)); + } + + handleTelegrafSettings( + instance: object, + key: string, + telegraf: Telegraf, + ) { + // @ts-ignore + telegraf.settings(instance[key].bind(instance)); + } + + handleTelegrafEntity( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafEntityMetadata, + ) { + // @ts-ignore + telegraf.entity(metadata.entity, instance[key].bind(instance)); + } + + handleTelegrafMention( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafMentionMetadata, + ) { + // @ts-ignore + telegraf.mention(metadata.username, instance[key].bind(instance)); + } + + handleTelegrafPhone( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafPhoneMetadata, + ) { + // @ts-ignore + telegraf.phone(metadata.phone, instance[key].bind(instance)); + } + + handleTelegrafHashtag( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafHashtagMetadata, + ) { + // @ts-ignore + telegraf.hashtag(metadata.hashtag, instance[key].bind(instance)); + } + + handleTelegrafCashtag( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafCashtagMetadata, + ) { + // @ts-ignore + telegraf.cashtag(metadata.cashtag, instance[key].bind(instance)); + } + + handleTelegrafAction( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafActionMetadata, + ) { + telegraf.action(metadata.triggers, instance[key].bind(instance)); + } + + handleTelegrafInlineQuery( + instance: object, + key: string, + telegraf: Telegraf, + metadata: TelegrafInlineQueryMetadata, + ) { + // @ts-ignore + telegraf.inlineQuery(metadata.triggers, instance[key].bind(instance)); + } + + handleTelegrafGameQuery( + instance: object, + key: string, + telegraf: Telegraf, + ) { + telegraf.gameQuery(instance[key].bind(instance)); + } +} diff --git a/lib/telegraf.module.ts b/lib/telegraf.module.ts index 9bf8840..a0c34de 100644 --- a/lib/telegraf.module.ts +++ b/lib/telegraf.module.ts @@ -1,63 +1,25 @@ -import { Module, DynamicModule, Provider } from '@nestjs/common'; +import { Module, DynamicModule } from '@nestjs/common'; +import { TelegrafCoreModule } from './telegraf-core.module'; import { + TelegrafModuleOptions, TelegrafModuleAsyncOptions, - TelegrafOptionsFactory } from './interfaces'; -import { - TELEGRAF_MODULE_OPTIONS, - TokenInjectionToken -} from './telegraf.constants'; -import { TelegrafService, TelegrafTelegramService } from './'; @Module({}) export class TelegrafModule { - static fromFactory(options: TelegrafModuleAsyncOptions): DynamicModule { + public static forRoot(options?: TelegrafModuleOptions): DynamicModule { return { module: TelegrafModule, - imports: options.imports || [], - providers: [ - ...this.createAsyncProviders(options), - TelegrafService, - TelegrafTelegramService, - { - provide: TokenInjectionToken, - useClass: options.useClass - } - ], - exports: [TelegrafService, TelegrafTelegramService] + imports: [TelegrafCoreModule.forRoot(options)], }; } - private static createAsyncProviders( - options: TelegrafModuleAsyncOptions - ): Provider[] { - if (options.useExisting || options.useFactory) { - return [this.createAsyncOptionsProvider(options)]; - } - return [ - this.createAsyncOptionsProvider(options), - { - provide: options.useClass, - useClass: options.useClass - } - ]; - } - - private static createAsyncOptionsProvider( - options: TelegrafModuleAsyncOptions - ): Provider { - if (options.useFactory) { - return { - provide: TELEGRAF_MODULE_OPTIONS, - useFactory: options.useFactory, - inject: options.inject || [] - }; - } + public static forRootAsync( + options: TelegrafModuleAsyncOptions, + ): DynamicModule { return { - provide: TELEGRAF_MODULE_OPTIONS, - useFactory: async (optionsFactory: TelegrafOptionsFactory) => - await optionsFactory.createTelegrafOptions(), - inject: [options.useExisting || options.useClass] + module: TelegrafModule, + imports: [TelegrafCoreModule.forRootAsync(options)], }; } } diff --git a/lib/telegraf.provider.ts b/lib/telegraf.provider.ts new file mode 100644 index 0000000..b13626f --- /dev/null +++ b/lib/telegraf.provider.ts @@ -0,0 +1,33 @@ +import { + Injectable, + Inject, + OnApplicationBootstrap, + Logger, + OnApplicationShutdown, +} from '@nestjs/common'; +import Telegraf, { ContextMessageUpdate } from 'telegraf'; +import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants'; +import { TelegrafModuleOptions } from './interfaces'; + +@Injectable() +// @ts-ignore +export class TelegrafProvider + extends Telegraf + implements OnApplicationBootstrap, OnApplicationShutdown { + private logger = new Logger('Telegraf'); + + constructor(@Inject(TELEGRAF_MODULE_OPTIONS) options: TelegrafModuleOptions) { + super(options.token, options.options); + } + + onApplicationBootstrap() { + this.catch(e => { + this.logger.error(e); + }); + this.startPolling(); + } + + async onApplicationShutdown(signal?: string) { + await this.stop(); + } +} diff --git a/lib/telegraf.service.ts b/lib/telegraf.service.ts deleted file mode 100644 index 5ba6e8a..0000000 --- a/lib/telegraf.service.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { Inject, Injectable, Logger } from '@nestjs/common' -import { ModuleRef } from '@nestjs/core' -import { flatten, head } from 'lodash' -import Telegraf, { ContextMessageUpdate } from 'telegraf' -import { TelegramActionHandler, TelegramCatch } from './decorators' -import { InvalidConfigurationException } from './exeptions' -import { - ContextTransformer, - Handler, - TelegrafOptionsFactory, - TelegramErrorHandler, -} from './interfaces' -import { TokenInjectionToken } from './telegraf.constants' - -@Injectable() -export class TelegrafService { - private readonly logger = new Logger(TelegrafService.name, true) - private readonly sitePublicUrl?: string - public readonly bot: Telegraf - private ref: ModuleRef - - public constructor( - @Inject(TokenInjectionToken) options: TelegrafOptionsFactory - ) { - const { - token, - sitePublicUrl, - telegrafOptions, - } = options.createTelegrafOptions() - this.sitePublicUrl = sitePublicUrl - this.bot = new Telegraf(token, telegrafOptions) - } - - public init(ref: ModuleRef, devMode: boolean = false) { - this.ref = ref - - const handlers = this.createHandlers() - - this.setupOnStart(handlers) - this.setupOn(handlers) - this.setupOnMessage(handlers) - this.setupOnCommand(handlers) - this.setupActions(handlers) - - if (devMode) { - this.startPolling() - } - } - - public getMiddleware(path: string) { - if (!this.sitePublicUrl) { - throw new InvalidConfigurationException( - 'sitePublicUrl', - 'does not exist, but webook used' - ) - } - - const url = `${this.sitePublicUrl}/${path}` - - this.bot.telegram - .setWebhook(url) - .then(() => this.logger.log(`Webhook set success @ ${url}`)) - - return this.bot.webhookCallback(`/${path}`) - } - - public startPolling() { - this.bot.telegram.deleteWebhook().then( - () => this.bot.startPolling(), - () => { - // okay, never mind - } - ) - } - - private createHandlers(): Handler[] { - return flatten( - Array.from((TelegramActionHandler.handlers || new Map()).entries()).map( - ([handlerClass, classConfig]) => { - const handlerInstance = this.ref.get(handlerClass, { strict: false }) - - return Array.from(classConfig.entries()).map( - ([methodName, methodCondig]) => ({ - handle: handlerInstance[methodName].bind(handlerInstance), - config: methodCondig, - }) - ) - } - ) - ) - } - - private setupOnStart(handlers: Handler[]): void { - const onStart = handlers.filter(({ config }) => config.onStart) - - if (onStart.length !== 1) { - throw new Error() - } - - this.bot.start(this.adoptHandle(head(onStart))) - } - - private setupOn(handlers: Handler[]): void { - const onHandlers = handlers.filter(({ config }) => config.on) - - onHandlers.forEach(handler => { - this.bot.on(handler.config.on, this.adoptHandle(handler)) - }) - } - - private setupOnMessage(handlers: Handler[]): void { - const onMessageHandlers = handlers.filter( - ({ config }) => config.message !== undefined - ) - - onMessageHandlers.forEach(handler => { - if (handler.config.message) { - this.bot.hears(handler.config.message, this.adoptHandle(handler)) - } else { - this.bot.on('message', this.adoptHandle(handler)) - } - }) - } - - private setupOnCommand(handlers: Handler[]): void { - const commandHandlers = handlers.filter(({ config }) => config.command) - - commandHandlers.forEach(handler => { - this.bot.command(handler.config.command, this.adoptHandle(handler)) - }) - } - - private setupActions(handlers: Handler[]): void { - const commandHandlers = handlers.filter(({ config }) => config.action) - - commandHandlers.forEach(handler => { - this.bot.action(handler.config.action, this.adoptHandle(handler)) - }) - } - - private adoptHandle({ handle, config }: Handler) { - const errorHandler = this.createCatch() - - return async (ctx: ContextMessageUpdate) => { - const args = await Promise.all( - (config.transformations || []) - .sort((a, b) => a.index - b.index) - .map(({ transform }) => - this.ref - .get(transform, { strict: false }) - .transform(ctx) - ) - ) - - return handle(ctx, ...args).catch(errorHandler(ctx)) - } - } - - private createCatch() { - const handlers = Array.from( - (TelegramCatch.handlers || new Map()).entries() - ).map(([errorType, handlerType]) => { - const handler = this.ref.get(handlerType, { - strict: false, - }) - - return { - errorType, - handler, - } - }) - - return (ctx: ContextMessageUpdate) => (e: any) => { - for (const { errorType, handler } of handlers) { - if (e instanceof (errorType as any)) { - return handler.catch(ctx, e) - } - } - - throw e - } - } -} diff --git a/package-lock.json b/package-lock.json index 5f55a11..0ab762f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "nestjs-telegraf", - "version": "0.7.3", + "version": "1.0.0-alpha.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -102,17 +102,10 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, - "@types/lodash": { - "version": "4.14.149", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", - "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", - "dev": true - }, "@types/node": { - "version": "13.1.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.6.tgz", - "integrity": "sha512-Jg1F+bmxcpENHP23sVKkNuU3uaxPnsBMW0cLjleiikFKomJQbsn0Cqk2yDvQArqzZN6ABfBkZ0To7pQ8sLdWDg==", - "dev": true + "version": "13.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.2.tgz", + "integrity": "sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg==" }, "@types/parse-json": { "version": "4.0.0", @@ -902,11 +895,6 @@ "p-locate": "^4.1.0" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -975,16 +963,14 @@ "dev": true }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "module-alias": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", - "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==", - "dev": true + "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" }, "ms": { "version": "2.0.0", @@ -1001,8 +987,7 @@ "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "normalize-path": { "version": "3.0.0", @@ -1239,8 +1224,7 @@ "sandwich-stream": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", - "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==", - "dev": true + "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==" }, "semver-compare": { "version": "1.0.0", @@ -1349,7 +1333,6 @@ "version": "3.36.0", "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-3.36.0.tgz", "integrity": "sha512-9o6AJKRiTm5vMWYI6WpTfBHzu4FMpWBNKxvnMxRds/cbMY9RnsVVjdi8i4bFFlfd+xbi73EbrnI3dybayryICA==", - "dev": true, "requires": { "@types/node": "^13.1.0", "debug": "^4.0.1", @@ -1364,7 +1347,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -1372,16 +1354,14 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, "telegram-typings": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/telegram-typings/-/telegram-typings-3.6.1.tgz", - "integrity": "sha512-njVv1EAhIZnmQVLocZEADYUyqA1WIXuVcDYlsp+mXua/XB0pxx+PKtMSPeZ/EE4wPWTw9h/hA9ASTT6yQelkiw==", - "dev": true + "integrity": "sha512-njVv1EAhIZnmQVLocZEADYUyqA1WIXuVcDYlsp+mXua/XB0pxx+PKtMSPeZ/EE4wPWTw9h/hA9ASTT6yQelkiw==" }, "timers-ext": { "version": "0.1.7", diff --git a/package.json b/package.json index af35665..92e9b66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nestjs-telegraf", - "version": "0.7.3", + "version": "1.0.0-alpha.1", "description": "Telegraf module for Nest framework", "keywords": [ "nest", @@ -34,24 +34,21 @@ "publish:npm": "npm publish --access public" }, "dependencies": { - "lodash": "4.17.15" + "telegraf": "^3.36.0" }, "devDependencies": { "@nestjs/common": "6.11.11", "@nestjs/core": "6.11.11", - "@types/lodash": "4.14.149", "husky": "4.2.3", "lint-staged": "10.0.8", "prettier": "1.19.1", "reflect-metadata": "0.1.13", - "telegraf": "3.36.0", "typescript": "3.8.3" }, "peerDependencies": { "@nestjs/common": "^6.7.0", "@nestjs/core": "^6.7.0", - "reflect-metadata": "^0.1.13", - "telegraf": "^3.35.0" + "reflect-metadata": "^0.1.13" }, "husky": { "hooks": {