Merge branch 'feature/v2' of github.com:bukhalo/nestjs-telegraf into feature/multiple-instances

 Conflicts:
	lib/decorators/inject-bot.decorator.ts
	lib/index.ts
	lib/interfaces/context.interface.ts
	lib/interfaces/index.ts
	lib/interfaces/telegraf-options.interface.ts
	lib/services/metadata-accessor.service.ts
	lib/services/updates-explorer.service.ts
	lib/telegraf-core.module.ts
	lib/telegraf.constants.ts
	package-lock.json
This commit is contained in:
Alexander Bukhalo 2021-01-02 16:46:01 +03:00
commit b808fb646a
70 changed files with 673 additions and 561 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
# lock
package-lock.json
yarn.lock
# dependencies
/node_modules

View File

@ -1,6 +1,5 @@
# source
lib
index.ts
package-lock.json
tsconfig.json
.prettierrc

View File

@ -1,27 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
import { HearsTriggers } from 'telegraf/typings/composer';
import { Context } from '../interfaces';
export type TelegrafActionTriggers = HearsTriggers<Context>;
export interface ActionOptions {
triggers: TelegrafActionTriggers;
}
/**
* Registers middleware for handling callback_data actions with regular expressions.
*
* @see https://telegraf.js.org/#/?id=action
*/
export const Action = (triggers: TelegrafActionTriggers): MethodDecorator => {
return SetMetadata(DECORATORS.ACTION, { triggers });
};
/**
* Registers middleware for handling callback_data actions with regular expressions.
*
* @see https://telegraf.js.org/#/?id=action
* @deprecated since v2, use Action decorator instead.
*/
export const TelegrafAction = Action;

View File

@ -1,25 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
export type TelegrafCashtagCashtag = string | string[];
export interface CashtagOptions {
cashtag: TelegrafCashtagCashtag;
}
/**
* Cashtag handling.
*
* @see https://telegraf.js.org/#/?id=cashtag
*/
export const Cashtag = (cashtag: TelegrafCashtagCashtag): MethodDecorator => {
return SetMetadata(DECORATORS.CASHTAG, { cashtag });
};
/**
* Cashtag handling.
*
* @see https://telegraf.js.org/#/?id=cashtag
* @deprecated since v2, use Cashtag decorator instead.
*/
export const TelegrafCashtag = Cashtag;

View File

@ -1,25 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
export type TelegrafCommandCommands = string | string[];
export interface CommandOptions {
commands: TelegrafCommandCommands;
}
/**
* Command handling.
*
* @see https://telegraf.js.org/#/?id=command
*/
export const Command = (commands: TelegrafCommandCommands): MethodDecorator => {
return SetMetadata(DECORATORS.COMMAND, { commands });
};
/**
* Command handling.
*
* @see https://telegraf.js.org/#/?id=command
* @deprecated since v2, use Command decorator instead.
*/
export const TelegrafCommand = Command;

View File

@ -0,0 +1,3 @@
export * from './update.decorator';
export * from './scene.decorator';
export * from './inject-bot.decorator';

View File

@ -0,0 +1,4 @@
import { Inject } from '@nestjs/common';
import { Telegraf } from 'telegraf';
export const InjectBot = (): ParameterDecorator => Inject(Telegraf);

View File

@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';
import { SCENE_METADATA } from '../../telegraf.constants';
/**
* TODO
*/
export const Scene = (id: string): ClassDecorator =>
SetMetadata(SCENE_METADATA, id);

View File

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

View File

@ -1,30 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
export type TelegrafEntityEntity =
| string
| string[]
| RegExp
| RegExp[]
| Function;
export interface EntityOptions {
entity: TelegrafEntityEntity;
}
/**
* Entity handling.
*
* @see https://telegraf.js.org/#/?id=entity
*/
export const Entity = (entity: TelegrafEntityEntity): MethodDecorator => {
return SetMetadata(DECORATORS.ENTITY, { entity });
};
/**
* Entity handling.
*
* @see https://telegraf.js.org/#/?id=entity
* @deprecated since v2, use Entity decorator instead.
*/
export const TelegrafEntity = Entity;

View File

@ -1,19 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
/**
* Registers middleware for handling callback_data actions with game query.
*
* @see https://telegraf.js.org/#/?id=inlinequery
*/
export const GameQuery = (): MethodDecorator => {
return SetMetadata(DECORATORS.GAME_QUERY, {});
};
/**
* Registers middleware for handling callback_data actions with game query.
*
* @see https://telegraf.js.org/#/?id=inlinequery
* @deprecated since v2, use Action decorator instead.
*/
export const TelegrafGameQuery = GameQuery;

View File

@ -1,25 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
export type TelegrafHashtagHashtag = string | string[];
export interface HashtagOptions {
hashtag: TelegrafHashtagHashtag;
}
/**
* Hashtag handling.
*
* @see https://telegraf.js.org/#/?id=hashtag
*/
export const Hashtag = (hashtag: TelegrafHashtagHashtag): MethodDecorator => {
return SetMetadata(DECORATORS.HASHTAG, { hashtag });
};
/**
* Hashtag handling.
*
* @see https://telegraf.js.org/#/?id=hashtag
* @deprecated since v2, use Hashtag decorator instead.
*/
export const TelegrafHashtag = Hashtag;

View File

@ -1,27 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
import { HearsTriggers } from 'telegraf/typings/composer';
import { Context } from '../interfaces';
export type TelegrafHearsTriggers = HearsTriggers<Context>;
export interface HearsOptions {
triggers: TelegrafHearsTriggers;
}
/**
* Registers middleware for handling text messages.
*
* @see https://telegraf.js.org/#/?id=hears
*/
export const Hears = (triggers: TelegrafHearsTriggers): MethodDecorator => {
return SetMetadata(DECORATORS.HEARS, { triggers: triggers });
};
/**
* Registers middleware for handling text messages.
*
* @see https://telegraf.js.org/#/?id=hears
* @deprecated since v2, use Hears decorator instead.
*/
export const TelegrafHears = Hears;

View File

@ -1,19 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
/**
* Handler for /help command.
*
* @see https://telegraf.js.org/#/?id=help
*/
export const Help = (): MethodDecorator => {
return SetMetadata(DECORATORS.HELP, {});
};
/**
* Handler for /help command.
*
* @see https://telegraf.js.org/#/?id=help
* @deprecated since v2, use Help decorator instead.
*/
export const TelegrafHelp = Help;

View File

@ -1,18 +1,3 @@
export * from './action.decorator';
export * from './cashtag.decorator';
export * from './command.decorator';
export * from './entity.decorator';
export * from './game-query.decorator';
export * from './hashtag.decorator';
export * from './hears.decorator';
export * from './help.decorator';
export * from './inject-bot.decorator';
export * from './inline-query.decorator';
export * from './mention.decorator';
export * from './on.decorator';
export * from './phone.decorator';
export * from './settings.decorator';
export * from './start.decorator';
export * from './update.decorator';
export * from './update-hooks.decorators';
export * from './use.decorator';
export * from './core';
export * from './listeners';
export * from './scene';

View File

@ -1,36 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
import * as tt from 'telegraf/typings/telegram-types';
export type TelegrafInlineQueryTriggers = string | string[] | RegExp | RegExp[];
export interface InlineQueryOptions {
triggers?: TelegrafInlineQueryTriggers;
updateType:
| tt.UpdateType
| tt.UpdateType[]
| tt.MessageSubTypes
| tt.MessageSubTypes[];
}
/**
* Registers middleware for handling inline_query actions with regular expressions.
*
* @see https://telegraf.js.org/#/?id=inlinequery
*/
export const InlineQuery = (
triggers?: TelegrafInlineQueryTriggers,
): MethodDecorator => {
return SetMetadata(DECORATORS.INLINE_QUERY, {
triggers,
updateType: 'inline_query',
});
};
/**
* Registers middleware for handling inline_query actions with regular expressions.
*
* @see https://telegraf.js.org/#/?id=inlinequery
* @deprecated since v2, use InlineQuery decorator instead.
*/
export const TelegrafInlineQuery = InlineQuery;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
export type TelegrafMentionUsername = string | string[];
export interface MentionOptions {
username: TelegrafMentionUsername;
}
/**
* Mention handling.
*
* @see https://telegraf.js.org/#/?id=mention
*/
export const Mention = (username: TelegrafMentionUsername): MethodDecorator => {
return SetMetadata(DECORATORS.MENTION, { username });
};
/**
* Mention handling.
*
* @see https://telegraf.js.org/#/?id=mention
* @deprecated since v2, use Mention decorator instead.
*/
export const TelegrafMention = Mention;

View File

@ -1,30 +0,0 @@
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 OnOptions {
updateTypes: TelegrafOnUpdateTypes;
}
/**
* Registers middleware for provided update type.
*
* @see https://telegraf.js.org/#/?id=on
*/
export const On = (updateTypes: TelegrafOnUpdateTypes): MethodDecorator => {
return SetMetadata(DECORATORS.ON, { updateTypes: updateTypes });
};
/**
* Registers middleware for provided update type.
*
* @see https://telegraf.js.org/#/?id=on
* @deprecated since v2, use On decorator instead.
*/
export const TelegrafOn = On;

View File

@ -1,25 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
export type TelegrafPhonePhone = string | string[];
export interface PhoneOptions {
phone: TelegrafPhonePhone;
}
/**
* Phone number handling.
*
* @see https://telegraf.js.org/#/?id=phone
*/
export const Phone = (phone: TelegrafPhonePhone): MethodDecorator => {
return SetMetadata(DECORATORS.PHONE, { phone });
};
/**
* Phone number handling.
*
* @see https://telegraf.js.org/#/?id=phone
* @deprecated since v2, use Phone decorator instead.
*/
export const TelegrafPhone = Phone;

View File

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

View File

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

View File

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

View File

@ -1,19 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
/**
* Handler for /settings command.
*
* @see https://telegraf.js.org/#/?id=settings
*/
export const Settings = (): MethodDecorator => {
return SetMetadata(DECORATORS.SETTINGS, {});
};
/**
* Handler for /settings command.
*
* @see https://telegraf.js.org/#/?id=settings
* @deprecated since v2, use Settings decorator instead.
*/
export const TelegrafSettings = Settings;

View File

@ -1,19 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
/**
* Handler for /start command.
*
* @see https://telegraf.js.org/#/?id=start
*/
export const Start = (): MethodDecorator => {
return SetMetadata(DECORATORS.START, {});
};
/**
* Handler for /start command.
*
* @see https://telegraf.js.org/#/?id=start
* @deprecated since v2, use Start decorator instead.
*/
export const TelegrafStart = Start;

View File

@ -1,104 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
import * as tt from 'telegraf/typings/telegram-types';
export interface UpdateHookOptions {
updateType:
| tt.UpdateType
| tt.UpdateType[]
| tt.MessageSubTypes
| tt.MessageSubTypes[];
}
/**
* New incoming message of any kind text, photo, sticker, etc.
* @constructor
*/
export const Message = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'message',
});
};
/**
* New version of a message that is known to the bot and was edited
* @constructor
*/
export const EditedMessage = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'edited_message',
});
};
/**
* New incoming channel post of any kind text, photo, sticker, etc.
* @constructor
*/
export const ChannelPost = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'channel_post',
});
};
/**
* New version of a channel post that is known to the bot and was edited
* @constructor
*/
export const EditedChannelPost = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'edited_channel_post',
});
};
/**
* New incoming inline query
* See this decorator in inline-query.decorator.ts
* @constructor
*/
// export const InlineQuery = (): MethodDecorator => {
// return SetMetadata(DECORATORS.UPDATE_HOOK, {
// updateType: 'inline_query',
// });
// };
/**
* The result of an inline query that was chosen by a user and sent to their chat partner.
* @constructor
*/
export const ChosenInlineResult = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'chosen_inline_result',
});
};
/**
* New incoming callback query
* @constructor
*/
export const CallbackQuery = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'callback_query',
});
};
/**
* New incoming shipping query. Only for invoices with flexible price
* @constructor
*/
export const ShippingQuery = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'shipping_query',
});
};
/**
* New incoming pre-checkout query. Contains full information about checkout
* @constructor
*/
export const PreCheckoutQuery = (): MethodDecorator => {
return SetMetadata(DECORATORS.UPDATE_HOOK, {
updateType: 'pre_checkout_query',
});
};
// Two more decorators are missing here. For 'poll' and 'poll_answer' update types.

View File

@ -1,19 +0,0 @@
import { SetMetadata } from '@nestjs/common';
import { DECORATORS } from '../telegraf.constants';
/**
* Registers a middleware.
*
* @see https://telegraf.js.org/#/?id=use
*/
export const Use = (): MethodDecorator => {
return SetMetadata(DECORATORS.USE, {});
};
/**
* Registers a middleware.
*
* @see https://telegraf.js.org/#/?id=use
* @deprecated since v2, use Use decorator instead.
*/
export const TelegrafUse = Use;

View File

@ -0,0 +1,73 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService } from '@nestjs/core';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { BaseScene as Scene, Stage, Telegraf } from 'telegraf';
import { TelegrafMetadataAccessor } from '../telegraf.metadata-accessor';
@Injectable()
export class TelegrafSceneExplorer implements OnModuleInit {
private readonly stage = new Stage([]);
constructor(
@Inject(Telegraf)
private readonly telegraf: Telegraf<never>,
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: TelegrafMetadataAccessor,
private readonly metadataScanner: MetadataScanner,
) {
this.telegraf.use(this.stage.middleware());
}
onModuleInit(): void {
this.explore();
}
private explore(): void {
const sceneClasses = this.filterSceneClasses();
sceneClasses.forEach((wrapper) => {
const { instance } = wrapper;
const sceneId = this.metadataAccessor.getSceneMetadata(
instance.constructor,
);
const scene = new Scene(sceneId);
this.stage.register(scene);
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodKey: string) =>
this.registerIfListener(scene, instance, methodKey),
);
});
}
private filterSceneClasses(): InstanceWrapper[] {
return this.discoveryService
.getProviders()
.filter((wrapper) => wrapper.instance)
.filter((wrapper) =>
this.metadataAccessor.isScene(wrapper.instance.constructor),
);
}
private registerIfListener(
scene: Scene<never>,
instance: Record<string, Function>,
methodKey: string,
): void {
const methodRef = instance[methodKey];
const middlewareFn = methodRef.bind(instance);
const listenerMetadata = this.metadataAccessor.getListenerMetadata(
methodRef,
);
if (!listenerMetadata) return;
const { method, args } = listenerMetadata;
(scene[method] as any)(...args, middlewareFn);
}
}

View File

@ -0,0 +1,63 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService } from '@nestjs/core';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { Telegraf } from 'telegraf';
import { TelegrafMetadataAccessor } from '../telegraf.metadata-accessor';
@Injectable()
export class TelegrafUpdateExplorer implements OnModuleInit {
constructor(
@Inject(Telegraf)
private readonly telegraf: Telegraf<never>,
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: TelegrafMetadataAccessor,
private readonly metadataScanner: MetadataScanner,
) {}
onModuleInit(): void {
this.explore();
}
private explore(): void {
const updateClasses = this.filterUpdateClasses();
updateClasses.forEach((wrapper) => {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodKey: string) => this.registerIfListener(instance, methodKey),
);
});
}
private filterUpdateClasses(): InstanceWrapper[] {
return this.discoveryService
.getProviders()
.filter((wrapper) => wrapper.instance)
.filter((wrapper) =>
this.metadataAccessor.isUpdate(wrapper.instance.constructor),
);
}
private registerIfListener(
instance: Record<string, Function>,
methodKey: string,
): void {
const methodRef = instance[methodKey];
const middlewareFn = methodRef.bind(instance);
const listenerMetadata = this.metadataAccessor.getListenerMetadata(
methodRef,
);
if (!listenerMetadata) return;
const { method, args } = listenerMetadata;
// NOTE: Use "any" to disable "Expected at least 1 arguments, but got 1 or more." error.
// Use telegraf instance for non-scene listeners
(this.telegraf[method] as any)(...args, middlewareFn);
}
}

View File

@ -0,0 +1,18 @@
import { SetMetadata } from '@nestjs/common';
import { BaseScene as Scene } from 'telegraf';
import { ComposerMethodArgs, SceneMethods } from '../telegraf.types';
import { UPDATE_LISTENER_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces';
export function createSceneListenerDecorator<Method extends SceneMethods>(
method: Method,
) {
return (
...args: ComposerMethodArgs<Scene<never>, Method>
): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,
args,
} as ListenerMetadata);
};
}

View File

@ -0,0 +1,18 @@
import { SetMetadata } from '@nestjs/common';
import { Composer } from 'telegraf';
import { ComposerMethodArgs, UpdateMethods } from '../telegraf.types';
import { UPDATE_LISTENER_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces';
export function createUpdateListenerDecorator<Method extends UpdateMethods>(
method: unknown,
) {
return (
...args: ComposerMethodArgs<Composer<never>, Method>
): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,
args,
} as ListenerMetadata);
};
}

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

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

View File

@ -8,13 +8,8 @@ export * as Extra from 'telegraf/extra';
export * from './decorators';
export * from './interfaces';
export * from './helpers';
export * from './utils';
export * from './telegraf.module';
export * from './telegraf.types';
export { Telegraf } from 'telegraf';
/**
* Backward compatibility with versions < 1.4.0,
* after removing TelegrafProvider service
* TODO: remove that on next major release
*/
export { Telegraf as TelegrafProvider } from 'telegraf';

View File

@ -1,3 +1,4 @@
export * from './context.interface';
export * from './telegraf-options.interface';
export * from './listener-metadata.interface';
export * from './update-metadata.interface';

View File

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

View File

@ -1,13 +1,15 @@
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
import { Middleware, Context } from 'telegraf';
import {
TelegrafOptions,
LaunchPollingOptions,
LaunchWebhookOptions,
TelegrafOptions,
} from 'telegraf/typings/telegraf';
import { Middleware } from 'telegraf/typings/composer';
import { Context } from './context.interface';
export interface TelegrafModuleOptions {
export interface TelegrafModuleOptions<C extends Context = Context> {
token: string;
options?: TelegrafOptions;
launchOptions?: {
@ -18,6 +20,7 @@ export interface TelegrafModuleOptions {
include?: Function[];
middlewares?: ReadonlyArray<Middleware<Context>>;
disableGlobalCatch?: boolean;
middlewares?: Middleware<C>[];
}
export interface TelegrafOptionsFactory {

View File

@ -2,23 +2,7 @@ export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS';
export const TELEGRAF_BOT_NAME = 'TELEGRAF_BOT_NAME';
export const DEFAULT_BOT_NAME = 'DEFAULT_BOT_NAME';
export const 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`,
UPDATE: `${DECORATORS_PREFIX}/UPDATE`,
UPDATE_HOOK: `${DECORATORS_PREFIX}/UPDATE_HOOK`,
};
export const UPDATE_METADATA = 'UPDATE_METADATA';
export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA';
export const SCENE_METADATA = 'SCENE_METADATA';

View File

@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
SCENE_METADATA,
UPDATE_LISTENER_METADATA,
UPDATE_METADATA,
} from './telegraf.constants';
import { ListenerMetadata } from './interfaces';
@Injectable()
export class TelegrafMetadataAccessor {
constructor(private readonly reflector: Reflector) {}
isUpdate(target: Function): boolean {
return !!this.reflector.get(UPDATE_METADATA, target);
}
isScene(target: Function): boolean {
return !!this.reflector.get(SCENE_METADATA, target);
}
getListenerMetadata(target: Function): ListenerMetadata | undefined {
return this.reflector.get(UPDATE_LISTENER_METADATA, target);
}
getSceneMetadata(target: Function): string | undefined {
return this.reflector.get(SCENE_METADATA, target);
}
}

View File

@ -1,27 +1,106 @@
import { Module, DynamicModule } from '@nestjs/common';
import { TelegrafCoreModule } from './telegraf-core.module';
import { DiscoveryModule, ModuleRef } from '@nestjs/core';
import {
DynamicModule,
Inject,
Module,
OnApplicationBootstrap,
OnApplicationShutdown,
Provider,
} from '@nestjs/common';
import { Telegraf } from 'telegraf';
import {
TelegrafModuleOptions,
TelegrafModuleAsyncOptions,
TelegrafModuleOptions,
TelegrafOptionsFactory,
} from './interfaces';
import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants';
import { TelegrafMetadataAccessor } from './telegraf.metadata-accessor';
import { TelegrafUpdateExplorer } from './explorers/telegraf-update.explorer';
import { TelegrafSceneExplorer } from './explorers/telegraf-scene.explorer';
import { createProviders, TelegrafProvider } from './telegraf.providers';
@Module({
imports: [DiscoveryModule],
providers: [
TelegrafMetadataAccessor,
TelegrafSceneExplorer,
TelegrafUpdateExplorer,
],
})
export class TelegrafModule
implements OnApplicationBootstrap, OnApplicationShutdown {
constructor(
@Inject(TELEGRAF_MODULE_OPTIONS)
private readonly options: TelegrafModuleOptions,
private readonly moduleRef: ModuleRef,
) {}
async onApplicationBootstrap(): Promise<void> {
const { launchOptions } = this.options;
const telegraf = this.moduleRef.get(Telegraf);
await telegraf.launch(launchOptions);
}
async onApplicationShutdown(): Promise<void> {
const telegraf = this.moduleRef.get(Telegraf);
await telegraf.stop();
}
@Module({})
export class TelegrafModule {
public static forRoot(options: TelegrafModuleOptions): DynamicModule {
const providers = [...createProviders(options), TelegrafProvider];
return {
module: TelegrafModule,
imports: [TelegrafCoreModule.forRoot(options)],
exports: [TelegrafCoreModule],
providers,
exports: providers,
};
}
public static forRootAsync(
options: TelegrafModuleAsyncOptions,
): DynamicModule {
const providers = [...this.createAsyncProviders(options), TelegrafProvider];
return {
module: TelegrafModule,
imports: [TelegrafCoreModule.forRootAsync(options)],
exports: [TelegrafCoreModule],
imports: options.imports || [],
providers,
exports: providers,
};
}
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 || [],
};
}
return {
provide: TELEGRAF_MODULE_OPTIONS,
useFactory: async (optionsFactory: TelegrafOptionsFactory) =>
await optionsFactory.createTelegrafOptions(),
inject: [options.useExisting || options.useClass],
};
}
}

25
lib/telegraf.providers.ts Normal file
View File

@ -0,0 +1,25 @@
import { Provider } from '@nestjs/common';
import { Telegraf } from 'telegraf';
import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants';
import { TelegrafModuleOptions } from './interfaces';
export const TelegrafProvider = {
provide: Telegraf,
useFactory: (options: TelegrafModuleOptions) => {
const telegraf = new Telegraf(options.token, options.options);
if (options.middlewares?.length > 0) {
telegraf.use(...options.middlewares);
}
return telegraf;
},
inject: [TELEGRAF_MODULE_OPTIONS],
};
export function createProviders(options: TelegrafModuleOptions): Provider[] {
return [
{
provide: TELEGRAF_MODULE_OPTIONS,
useValue: options,
},
];
}

21
lib/telegraf.types.ts Normal file
View File

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

34
package-lock.json generated
View File

@ -1,27 +1,25 @@
{
"name": "nestjs-telegraf",
"version": "1.3.1",
"version": "2.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "1.3.1",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"telegraf": "3.38.0"
"telegraf": "^3.38.0"
},
"devDependencies": {
"@nestjs/common": "7.6.5",
"@nestjs/core": "7.6.5",
"@types/lodash": "^4.14.167",
"@typescript-eslint/eslint-plugin": "4.11.1",
"@typescript-eslint/parser": "4.11.1",
"eslint": "7.16.0",
"eslint": "7.17.0",
"eslint-config-prettier": "7.1.0",
"eslint-plugin-import": "2.22.1",
"husky": "4.3.6",
"lint-staged": "10.5.3",
"lodash": "^4.17.20",
"prettier": "2.2.1",
"reflect-metadata": "0.1.13",
"rxjs": "6.6.3",
@ -294,12 +292,6 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.167",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.167.tgz",
"integrity": "sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw==",
"dev": true
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -944,9 +936,9 @@
}
},
"node_modules/eslint": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.16.0.tgz",
"integrity": "sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz",
"integrity": "sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
@ -3888,12 +3880,6 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/lodash": {
"version": "4.14.167",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.167.tgz",
"integrity": "sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw==",
"dev": true
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -4377,9 +4363,9 @@
"dev": true
},
"eslint": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.16.0.tgz",
"integrity": "sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz",
"integrity": "sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "nestjs-telegraf",
"version": "1.3.1",
"version": "2.0.0",
"description": "Telegraf module for NestJS",
"keywords": [
"nest",
@ -32,7 +32,7 @@
"test": ""
},
"dependencies": {
"telegraf": "3.38.0"
"telegraf": "^3.38.0"
},
"devDependencies": {
"@nestjs/common": "7.6.5",
@ -40,7 +40,7 @@
"@types/lodash": "^4.14.167",
"@typescript-eslint/eslint-plugin": "4.11.1",
"@typescript-eslint/parser": "4.11.1",
"eslint": "7.16.0",
"eslint": "7.17.0",
"eslint-config-prettier": "7.1.0",
"eslint-plugin-import": "2.22.1",
"husky": "4.3.6",

1
sample/app.constants.ts Normal file
View File

@ -0,0 +1 @@
export const HELLO_SCENE_ID = 'HELLO_SCENE_ID';

17
sample/app.module.ts Normal file
View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { TelegrafModule } from '../lib';
import { EchoService } from './echo.service';
import { AppUpdate } from './app.update';
import { HelloScene } from './scenes/hello.scene';
import { sessionMiddleware } from './middleware/session.middleware';
@Module({
imports: [
TelegrafModule.forRoot({
token: '1467731595:AAHCvH65H9VQYKF9jE-E8c2rXsQBVAYseg8', // Don't steal >:(
middlewares: [sessionMiddleware],
}),
],
providers: [EchoService, AppUpdate, HelloScene],
})
export class AppModule {}

43
sample/app.update.ts Normal file
View File

@ -0,0 +1,43 @@
import { Telegraf } from 'telegraf';
import { Command, Help, InjectBot, On, Start, Update } from '../lib';
import { EchoService } from './echo.service';
import { HELLO_SCENE_ID } from './app.constants';
import { Context } from './interfaces/context.interface';
@Update()
export class AppUpdate {
constructor(
@InjectBot()
private readonly bot: Telegraf<any>, // TODO: fix any
private readonly echoService: EchoService,
) {}
@Start()
async onStart(ctx: Context): Promise<void> {
const me = await this.bot.telegram.getMe();
await ctx.reply(`Hey, I'm ${me.first_name}`);
}
@Help()
async onHelp(ctx: Context): Promise<void> {
await ctx.reply('Send me any text');
}
@Command('scene')
async onSceneCommand(ctx: Context): Promise<void> {
await ctx.scene.enter(HELLO_SCENE_ID);
}
@On('message')
async onMessage(ctx: Context): Promise<void> {
console.log('New message received');
if ('text' in ctx.message) {
const messageText = ctx.message.text;
const echoText = this.echoService.echo(messageText);
await ctx.reply(echoText);
} else {
await ctx.reply('Only text messages');
}
}
}

8
sample/echo.service.ts Normal file
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class EchoService {
echo(text: string): string {
return `Echo: ${text}`;
}
}

View File

@ -0,0 +1,4 @@
import { SceneContextMessageUpdate } from 'telegraf/typings/stage';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Context extends SceneContextMessageUpdate {}

7
sample/main.ts Normal file
View File

@ -0,0 +1,7 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
await NestFactory.createApplicationContext(AppModule);
}
bootstrap();

View File

@ -0,0 +1,3 @@
import { session } from 'telegraf';
export const sessionMiddleware = session();

View File

@ -0,0 +1,29 @@
import { HELLO_SCENE_ID } from '../app.constants';
import { Context } from '../interfaces/context.interface';
import { Scene, SceneEnter, SceneLeave, Command } from '../../lib';
@Scene(HELLO_SCENE_ID)
export class HelloScene {
@SceneEnter()
async onSceneEnter(ctx: Context): Promise<void> {
console.log('Enter to scene');
await ctx.reply('Welcome on scene ✋');
}
@SceneLeave()
async onSceneLeave(ctx: Context): Promise<void> {
console.log('Leave from scene');
await ctx.reply('Bye Bye 👋');
}
@Command('hello')
async onHelloCommand(ctx: Context): Promise<void> {
console.log('Use say hello');
await ctx.reply('Hi');
}
@Command('leave')
async onLeaveCommand(ctx: Context): Promise<void> {
await ctx.scene.leave();
}
}