merge refactor/v2

This commit is contained in:
Morb0 2021-01-05 00:32:31 +03:00
commit 320ee3077e
81 changed files with 34665 additions and 4110 deletions

View File

@ -1,64 +0,0 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "Sedjj",
"name": "Eldar Salimzebarov",
"avatar_url": "https://avatars3.githubusercontent.com/u/5383030?v=4",
"profile": "https://github.com/Sedjj",
"contributions": [
"bug"
]
},
{
"login": "lamuertepeluda",
"name": "Vito Macchia",
"avatar_url": "https://avatars3.githubusercontent.com/u/2249342?v=4",
"profile": "http://www.ismb.it/vito.macchia",
"contributions": [
"code",
"bug"
]
},
{
"login": "edgesite",
"name": "KITAHARA SETSUNA",
"avatar_url": "https://avatars3.githubusercontent.com/u/10336620?v=4",
"profile": "https://github.com/edgesite",
"contributions": [
"code",
"bug"
]
},
{
"login": "bukhalo",
"name": "Aleksandr Bukhalo",
"avatar_url": "https://avatars2.githubusercontent.com/u/14031838?v=4",
"profile": "https://bukhalo.com/",
"contributions": [
"code",
"doc",
"review"
]
},
{
"login": "VyacheslavSaloidWork",
"name": "Vyacheslav Saloid",
"avatar_url": "https://avatars3.githubusercontent.com/u/43011265?v=4",
"profile": "https://github.com/VyacheslavSaloidWork",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
"projectName": "nestjs-telegraf",
"projectOwner": "bukhalo",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
}

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
custom: ["https://www.tinkoff.ru/sl/95M2htqoxux"] custom: ['https://www.tinkoff.ru/sl/95M2htqoxux']

View File

@ -1,11 +0,0 @@
version: 2
updates:
# Configuration for npm
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
reviewers:
- "bukhalo"
assignees:
- "bukhalo"

View File

@ -12,12 +12,12 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [12.x, 13.x] node-version: [14.x, 15.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm ci - run: npm ci

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
# lock # lock
package-lock.json
yarn.lock yarn.lock
# dependencies # dependencies

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 - present Bukhalo Aleksandr Aleksandrovich Copyright (c) 2019 - present Alexander Bukhalo (a@bukhalo.com)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -71,40 +71,3 @@ export class AppService {
} }
} }
``` ```
## 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).
## People
- Authors - [Aleksandr Bukhalo](https://bukhalo.com/) & [Igor Kamyshev](https://kamyshev.me/)
- Maintainers - [Aleksandr Bukhalo](https://bukhalo.com/)
- Website - [https://nestjs.com](https://nestjs.com/)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/Sedjj"><img src="https://avatars3.githubusercontent.com/u/5383030?v=4" width="100px;" alt=""/><br /><sub><b>Eldar Salimzebarov</b></sub></a><br /><a href="https://github.com/bukhalo/nestjs-telegraf/issues?q=author%3ASedjj" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://www.ismb.it/vito.macchia"><img src="https://avatars3.githubusercontent.com/u/2249342?v=4" width="100px;" alt=""/><br /><sub><b>Vito Macchia</b></sub></a><br /><a href="https://github.com/bukhalo/nestjs-telegraf/commits?author=lamuertepeluda" title="Code">💻</a> <a href="https://github.com/bukhalo/nestjs-telegraf/issues?q=author%3Alamuertepeluda" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/edgesite"><img src="https://avatars3.githubusercontent.com/u/10336620?v=4" width="100px;" alt=""/><br /><sub><b>KITAHARA SETSUNA</b></sub></a><br /><a href="https://github.com/bukhalo/nestjs-telegraf/commits?author=edgesite" title="Code">💻</a> <a href="https://github.com/bukhalo/nestjs-telegraf/issues?q=author%3Aedgesite" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://bukhalo.com/"><img src="https://avatars2.githubusercontent.com/u/14031838?v=4" width="100px;" alt=""/><br /><sub><b>Aleksandr Bukhalo</b></sub></a><br /><a href="https://github.com/bukhalo/nestjs-telegraf/commits?author=bukhalo" title="Code">💻</a> <a href="https://github.com/bukhalo/nestjs-telegraf/commits?author=bukhalo" title="Documentation">📖</a> <a href="https://github.com/bukhalo/nestjs-telegraf/pulls?q=is%3Apr+reviewed-by%3Abukhalo" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://github.com/VyacheslavSaloidWork"><img src="https://avatars3.githubusercontent.com/u/43011265?v=4" width="100px;" alt=""/><br /><sub><b>Vyacheslav Saloid</b></sub></a><br /><a href="https://github.com/bukhalo/nestjs-telegraf/issues?q=author%3AVyacheslavSaloidWork" title="Bug reports">🐛</a></td>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@ -1,4 +1,5 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { Telegraf } from 'telegraf'; import { getBotToken } from '../../utils';
export const InjectBot = (): ParameterDecorator => Inject(Telegraf); export const InjectBot = (name?: string): ParameterDecorator =>
Inject(getBotToken(name));

View File

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

View File

@ -1,8 +1,10 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Cashtag handling. * Cashtag handling.
* *
* @see https://telegraf.js.org/#/?id=cashtag * @see https://telegraf.js.org/#/?id=cashtag
*/ */
export const Cashtag = createUpdateListenerDecorator('cashtag'); export const Cashtag = createMissedListenerDecorator<[string | string[]]>(
'cashtag',
);

View File

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

View File

@ -1,8 +1,10 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Registers middleware for handling messages with email entity. * Registers middleware for handling messages with email entity.
* *
* @see https://telegraf.js.org/#/?id=telegraf-email * @see https://telegraf.js.org/#/?id=telegraf-email
*/ */
export const Email = createUpdateListenerDecorator('email'); export const Email = createMissedListenerDecorator<[string | string[]]>(
'email',
);

View File

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

View File

@ -1,8 +1,10 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Hashtag handling. * Hashtag handling.
* *
* @see https://telegraf.js.org/#/?id=hashtag * @see https://telegraf.js.org/#/?id=hashtag
*/ */
export const Hashtag = createUpdateListenerDecorator('hashtag'); export const Hashtag = createMissedListenerDecorator<[string | string[]]>(
'hashtag',
);

View File

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

View File

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

View File

@ -1,8 +1,11 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
import { HearsTriggers } from 'telegraf/typings/composer';
/** /**
* Registers middleware for handling inline_query actions with regular expressions. * Registers middleware for handling inline_query actions with regular expressions.
* *
* @see https://telegraf.js.org/#/?id=inlinequery * @see https://telegraf.js.org/#/?id=inlinequery
*/ */
export const InlineQuery = createUpdateListenerDecorator('inlineQuery'); export const InlineQuery = createMissedListenerDecorator<
[HearsTriggers<unknown>]
>('inlineQuery');

View File

@ -1,8 +1,10 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Mention handling. * Mention handling.
* *
* @see https://telegraf.js.org/#/?id=mention * @see https://telegraf.js.org/#/?id=mention
*/ */
export const Mention = createUpdateListenerDecorator('mention'); export const Mention = createMissedListenerDecorator<[string | string[]]>(
'mention',
);

View File

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

View File

@ -1,8 +1,10 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Phone number handling. * Phone number handling.
* *
* @see https://telegraf.js.org/#/?id=phone * @see https://telegraf.js.org/#/?id=phone
*/ */
export const Phone = createUpdateListenerDecorator('phone'); export const Phone = createMissedListenerDecorator<[string | string[]]>(
'phone',
);

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Registers middleware for handling messages with text_link entity. * Registers middleware for handling messages with text_link entity.
* *
* @see https://telegraf.js.org/#/?id=telegraf-textlink * @see https://telegraf.js.org/#/?id=telegraf-textlink
*/ */
export const TextLink = createUpdateListenerDecorator('textLink'); export const TextLink = createMissedListenerDecorator<[string | string[]]>(
'textLink',
);

View File

@ -1,8 +1,10 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Registers middleware for handling messages with text_mention entity. * Registers middleware for handling messages with text_mention entity.
* *
* @see https://telegraf.js.org/#/?id=telegraf-textlink * @see https://telegraf.js.org/#/?id=telegraf-textlink
*/ */
export const TextMention = createUpdateListenerDecorator('textMention'); export const TextMention = createMissedListenerDecorator<[string | string[]]>(
'textMention',
);

View File

@ -1,8 +1,8 @@
import { createUpdateListenerDecorator } from '../../utils'; import { createMissedListenerDecorator } from '../../utils';
/** /**
* Registers middleware for handling messages with url entity. * Registers middleware for handling messages with url entity.
* *
* @see https://telegraf.js.org/#/?id=telegraf-url * @see https://telegraf.js.org/#/?id=telegraf-url
*/ */
export const Url = createUpdateListenerDecorator('url'); export const Url = createMissedListenerDecorator<[string | string[]]>('url');

View File

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

View File

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

View File

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

View File

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

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

View File

@ -1,19 +1,21 @@
import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
import { Middleware, Context } from 'telegraf'; import { Middleware, Context } from 'telegraf';
import { import {
TelegrafOptions,
LaunchPollingOptions, LaunchPollingOptions,
LaunchWebhookOptions, LaunchWebhookOptions,
TelegrafOptions,
} from 'telegraf/typings/telegraf'; } from 'telegraf/typings/telegraf';
export interface TelegrafModuleOptions<C extends Context = Context> { export interface TelegrafModuleOptions<C extends Context = Context> {
token: string; token: string;
name?: string;
options?: TelegrafOptions; options?: TelegrafOptions;
launchOptions?: { launchOptions?: {
polling?: LaunchPollingOptions; polling?: LaunchPollingOptions;
webhook?: LaunchWebhookOptions; webhook?: LaunchWebhookOptions;
}; };
middlewares?: Middleware<C>[]; include?: Function[];
middlewares?: ReadonlyArray<Middleware<C>>;
} }
export interface TelegrafOptionsFactory { export interface TelegrafOptionsFactory {
@ -22,6 +24,7 @@ export interface TelegrafOptionsFactory {
export interface TelegrafModuleAsyncOptions export interface TelegrafModuleAsyncOptions
extends Pick<ModuleMetadata, 'imports'> { extends Pick<ModuleMetadata, 'imports'> {
botName?: string;
useExisting?: Type<TelegrafOptionsFactory>; useExisting?: Type<TelegrafOptionsFactory>;
useClass?: Type<TelegrafOptionsFactory>; useClass?: Type<TelegrafOptionsFactory>;
useFactory?: ( useFactory?: (

View File

@ -0,0 +1,37 @@
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { Module } from '@nestjs/core/injector/module';
import { flattenDeep, identity, isEmpty } from 'lodash';
export class BaseExplorerService {
getModules(
modulesContainer: Map<string, Module>,
include: Function[],
): Module[] {
if (!include || isEmpty(include)) {
return [...modulesContainer.values()];
}
const whitelisted = this.includeWhitelisted(modulesContainer, include);
return whitelisted;
}
includeWhitelisted(
modulesContainer: Map<string, Module>,
include: Function[],
): Module[] {
const modules = [...modulesContainer.values()];
return modules.filter(({ metatype }) => include.includes(metatype));
}
flatMap<T>(
modules: Module[],
callback: (instance: InstanceWrapper, moduleRef: Module) => T | T[],
): T[] {
const invokeMap = () => {
return modules.map((moduleRef) => {
const providers = [...moduleRef.providers.values()];
return providers.map((wrapper) => callback(wrapper, moduleRef));
});
};
return flattenDeep(invokeMap()).filter(identity);
}
}

3
lib/services/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './listeners-explorer.service';
export * from './metadata-accessor.service';
export * from './listeners-explorer.service';

View File

@ -0,0 +1,111 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, ModuleRef, ModulesContainer } from '@nestjs/core';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { Module } from '@nestjs/core/injector/module';
import { BaseScene, Composer, Stage, Telegraf } from 'telegraf';
import { MetadataAccessorService } from './metadata-accessor.service';
import { TELEGRAF_MODULE_OPTIONS } from '../telegraf.constants';
import { TelegrafModuleOptions } from '../interfaces';
import { BaseExplorerService } from './base-explorer.service';
import { getBotToken } from '../utils';
@Injectable()
export class ListenersExplorerService
extends BaseExplorerService
implements OnModuleInit {
private readonly stage = new Stage([]);
private bot: Telegraf<any>;
constructor(
@Inject(TELEGRAF_MODULE_OPTIONS)
private readonly telegrafOptions: TelegrafModuleOptions,
private readonly moduleRef: ModuleRef,
private readonly discoveryService: DiscoveryService,
private readonly metadataAccessor: MetadataAccessorService,
private readonly metadataScanner: MetadataScanner,
private readonly modulesContainer: ModulesContainer,
) {
super();
}
onModuleInit(): void {
const botToken = getBotToken(this.telegrafOptions.name);
this.bot = this.moduleRef.get<Telegraf<never>>(botToken);
this.bot.use(this.stage.middleware());
this.explore();
}
explore(): void {
const modules = this.getModules(
this.modulesContainer,
this.telegrafOptions.include || [],
);
this.registerUpdates(modules);
this.registerScenes(modules);
}
private registerUpdates(modules: Module[]): void {
const updates = this.flatMap<InstanceWrapper>(modules, (instance) =>
this.filterUpdates(instance),
);
updates.forEach(({ instance }) =>
this.registerInstanceMethodListeners(this.bot, instance),
);
}
private registerScenes(modules: Module[]): void {
const scenes = this.flatMap<InstanceWrapper>(modules, (instance) =>
this.filterScenes(instance),
);
scenes.forEach(({ instance }) => {
const sceneId = this.metadataAccessor.getSceneMetadata(
instance.constructor,
);
const scene = new BaseScene(sceneId);
this.stage.register(scene);
this.registerInstanceMethodListeners(scene, instance);
});
}
private filterUpdates(wrapper: InstanceWrapper): InstanceWrapper<unknown> {
const { instance } = wrapper;
if (!instance) return undefined;
const isUpdate = this.metadataAccessor.isUpdate(wrapper.metatype);
if (!isUpdate) return undefined;
return wrapper;
}
private filterScenes(wrapper: InstanceWrapper): InstanceWrapper<unknown> {
const { instance } = wrapper;
if (!instance) return undefined;
const isScene = this.metadataAccessor.isScene(wrapper.metatype);
if (!isScene) return undefined;
return wrapper;
}
private registerInstanceMethodListeners(
composer: Composer<never>,
instance: Record<string, Function>,
): void {
const prototype = Object.getPrototypeOf(instance);
this.metadataScanner.scanFromPrototype(instance, prototype, (name) => {
const methodRef = instance[name];
const metadata = this.metadataAccessor.getListenerMetadata(methodRef);
if (!metadata) return;
const middlewareFn = methodRef.bind(instance);
const { method, args } = metadata;
composer[method](...args, middlewareFn);
});
}
}

View File

@ -4,18 +4,25 @@ import {
SCENE_METADATA, SCENE_METADATA,
UPDATE_LISTENER_METADATA, UPDATE_LISTENER_METADATA,
UPDATE_METADATA, UPDATE_METADATA,
} from './telegraf.constants'; } from '../telegraf.constants';
import { ListenerMetadata } from './interfaces'; import { ListenerMetadata } from '../interfaces';
@Injectable() @Injectable()
export class TelegrafMetadataAccessor { export class MetadataAccessorService {
constructor(private readonly reflector: Reflector) {} constructor(private readonly reflector: Reflector) {}
isUpdate(target: Function): boolean { isUpdate(target: Function): boolean {
// TODO: We really need this check?
if (!target) {
return false;
}
return !!this.reflector.get(UPDATE_METADATA, target); return !!this.reflector.get(UPDATE_METADATA, target);
} }
isScene(target: Function): boolean { isScene(target: Function): boolean {
if (!target) {
return false;
}
return !!this.reflector.get(SCENE_METADATA, target); return !!this.reflector.get(SCENE_METADATA, target);
} }

115
lib/telegraf-core.module.ts Normal file
View File

@ -0,0 +1,115 @@
import { DiscoveryModule, ModuleRef } from '@nestjs/core';
import {
Module,
DynamicModule,
Provider,
Type,
Global,
Inject,
OnApplicationShutdown,
} from '@nestjs/common';
import {
TelegrafModuleOptions,
TelegrafModuleAsyncOptions,
TelegrafOptionsFactory,
} from './interfaces';
import { TELEGRAF_MODULE_OPTIONS } from './telegraf.constants';
import { MetadataAccessorService, ListenersExplorerService } from './services';
import { getBotToken, createBotFactory } from './utils';
@Global()
@Module({
imports: [DiscoveryModule],
providers: [ListenersExplorerService, MetadataAccessorService],
})
export class TelegrafCoreModule implements OnApplicationShutdown {
constructor(
@Inject(TELEGRAF_MODULE_OPTIONS)
private readonly options: TelegrafModuleOptions,
private readonly moduleRef: ModuleRef,
) {}
public static forRoot(options: TelegrafModuleOptions): DynamicModule {
const telegrafBotProvider: Provider = {
provide: getBotToken(options.name),
useFactory: async () => await createBotFactory(options),
};
return {
module: TelegrafCoreModule,
providers: [
{
provide: TELEGRAF_MODULE_OPTIONS,
useValue: options,
},
telegrafBotProvider,
],
exports: [telegrafBotProvider],
};
}
public static forRootAsync(
options: TelegrafModuleAsyncOptions,
): DynamicModule {
const telegrafBotName = getBotToken(options.botName);
const telegrafBotProvider: Provider = {
provide: telegrafBotName,
useFactory: async (options: TelegrafModuleOptions) =>
await createBotFactory(options),
inject: [TELEGRAF_MODULE_OPTIONS],
};
const asyncProviders = this.createAsyncProviders(options);
return {
module: TelegrafCoreModule,
imports: options.imports,
providers: [...asyncProviders, telegrafBotProvider],
exports: [telegrafBotProvider],
};
}
async onApplicationShutdown(): Promise<void> {
const botName = getBotToken(this.options.name);
const bot = this.moduleRef.get<any>(botName);
bot && (await bot.stop());
}
private static createAsyncProviders(
options: TelegrafModuleAsyncOptions,
): Provider[] {
if (options.useExisting || options.useFactory) {
return [this.createAsyncOptionsProvider(options)];
}
const useClass = options.useClass as Type<TelegrafOptionsFactory>;
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<TelegrafOptionsFactory>` is a workaround for microsoft/TypeScript#31603
const inject = [
(options.useClass || options.useExisting) as Type<TelegrafOptionsFactory>,
];
return {
provide: TELEGRAF_MODULE_OPTIONS,
useFactory: async (optionsFactory: TelegrafOptionsFactory) =>
await optionsFactory.createTelegrafOptions(),
inject,
};
}
}

View File

@ -1,6 +1,7 @@
import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS'; export const TELEGRAF_MODULE_OPTIONS = 'TELEGRAF_MODULE_OPTIONS';
export const DEFAULT_BOT_NAME = 'DEFAULT_BOT_NAME';
export const UPDATE_METADATA = 'UPDATE_METADATA'; export const UPDATE_METADATA = 'UPDATE_METADATA';
export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA'; export const UPDATE_LISTENER_METADATA = 'UPDATE_LISTENER_METADATA';

View File

@ -1,106 +1,27 @@
import { DiscoveryModule, ModuleRef } from '@nestjs/core'; import { Module, DynamicModule } from '@nestjs/common';
import { TelegrafCoreModule } from './telegraf-core.module';
import { import {
DynamicModule,
Inject,
Module,
OnApplicationBootstrap,
OnApplicationShutdown,
Provider,
} from '@nestjs/common';
import { Telegraf } from 'telegraf';
import {
TelegrafModuleAsyncOptions,
TelegrafModuleOptions, TelegrafModuleOptions,
TelegrafOptionsFactory, TelegrafModuleAsyncOptions,
} from './interfaces'; } 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 { public static forRoot(options: TelegrafModuleOptions): DynamicModule {
const providers = [...createProviders(options), TelegrafProvider];
return { return {
module: TelegrafModule, module: TelegrafModule,
providers, imports: [TelegrafCoreModule.forRoot(options)],
exports: providers, exports: [TelegrafCoreModule],
}; };
} }
public static forRootAsync( public static forRootAsync(
options: TelegrafModuleAsyncOptions, options: TelegrafModuleAsyncOptions,
): DynamicModule { ): DynamicModule {
const providers = [...this.createAsyncProviders(options), TelegrafProvider];
return { return {
module: TelegrafModule, module: TelegrafModule,
imports: options.imports || [], imports: [TelegrafCoreModule.forRootAsync(options)],
providers, exports: [TelegrafCoreModule],
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],
}; };
} }
} }

View File

@ -1,23 +0,0 @@
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);
telegraf.use(...options.middlewares);
return telegraf;
},
inject: [TELEGRAF_MODULE_OPTIONS],
};
export function createProviders(options: TelegrafModuleOptions): Provider[] {
return [
{
provide: TELEGRAF_MODULE_OPTIONS,
useValue: options,
},
];
}

View File

@ -17,5 +17,5 @@ export type ComposerMethodArgs<
U extends OnlyFunctionPropertyNames<T> = OnlyFunctionPropertyNames<T> U extends OnlyFunctionPropertyNames<T> = OnlyFunctionPropertyNames<T>
> = Filter<Parameters<T[U]>, Middleware<never>>; > = Filter<Parameters<T[U]>, Middleware<never>>;
export type UpdateMethods = OnlyFunctionPropertyNames<Composer<never>>; export type ComposerMethods = OnlyFunctionPropertyNames<Composer<never>>;
export type SceneMethods = OnlyFunctionPropertyNames<BaseScene<never>>; export type SceneMethods = OnlyFunctionPropertyNames<BaseScene<never>>;

View File

@ -0,0 +1,13 @@
import { Telegraf } from 'telegraf';
import { TelegrafModuleOptions } from '../interfaces';
export async function createBotFactory(
options: TelegrafModuleOptions,
): Promise<Telegraf<never>> {
const bot = new Telegraf<never>(options.token, options.options);
bot.use(...(options.middlewares ?? []));
await bot.launch(options.launchOptions);
return bot;
}

View File

@ -0,0 +1,29 @@
import { SetMetadata } from '@nestjs/common';
import { BaseScene as Scene } from 'telegraf';
import { ComposerMethodArgs, SceneMethods } from '../types';
import { UPDATE_LISTENER_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces';
export function createListenerDecorator<TMethod extends SceneMethods>(
method: TMethod,
) {
return (
...args: ComposerMethodArgs<Scene<never>, TMethod>
): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,
args,
} as ListenerMetadata);
};
}
export function createMissedListenerDecorator<TArgs extends any[]>(
method: string,
) {
return (...args: TArgs): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,
args,
} as ListenerMetadata);
};
}

View File

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

@ -1,18 +0,0 @@
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: Method,
) {
return (
...args: ComposerMethodArgs<Composer<never>, Method>
): MethodDecorator => {
return SetMetadata(UPDATE_LISTENER_METADATA, {
method,
args,
} as ListenerMetadata);
};
}

View File

@ -0,0 +1,5 @@
import { DEFAULT_BOT_NAME } from '../telegraf.constants';
export function getBotToken(name?: string): string {
return name && name !== DEFAULT_BOT_NAME ? `${name}Bot` : DEFAULT_BOT_NAME;
}

View File

@ -1,2 +1,3 @@
export * from './create-update-listener-decorator.util'; export * from './get-bot-token.util';
export * from './create-scene-listener-decorator.util'; export * from './create-bot-factory.util';
export * from './create-listener-decorator.util';

1673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
{ {
"name": "nestjs-telegraf", "name": "nestjs-telegraf",
"version": "2.0.0", "version": "2.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"description": "Telegraf module for NestJS", "description": "Telegraf module for NestJS",
"keywords": [ "keywords": [
"nest", "nest",
@ -19,10 +21,9 @@
"url": "https://github.com/bukhalo/nestjs-telegraf/issues" "url": "https://github.com/bukhalo/nestjs-telegraf/issues"
}, },
"license": "MIT", "license": "MIT",
"author": "Aleksandr Bukhalo <a@bukhalo.com>", "author": "Alexander Bukhalo <a@bukhalo.com>",
"contributors": [ "contributors": [
"Aleksandr Bukhalo <a@bukhalo.com> (https://bukhalo.com/)", "Alexander Bukhalo <a@bukhalo.com> (https://bukhalo.com/)"
"Igor Kamyshev <igor@kamyshev.me> (https://kamyshev.me/)"
], ],
"repository": "git@github.com:bukhalo/nestjs-telegraf.git", "repository": "git@github.com:bukhalo/nestjs-telegraf.git",
"scripts": { "scripts": {
@ -31,27 +32,28 @@
"prepublish:npm": "npm run build", "prepublish:npm": "npm run build",
"publish:npm": "npm publish --access public", "publish:npm": "npm publish --access public",
"test": "", "test": "",
"sample-app": "ts-node --transpile-only -r tsconfig-paths/register sample/main.ts" "typedoc:build": "typedoc --tsconfig ./tsconfig.typedoc.json"
}, },
"dependencies": { "dependencies": {
"telegraf": "^3.38.0" "telegraf": "3.38.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/common": "7.6.1", "@nestjs/common": "7.6.5",
"@nestjs/core": "7.6.1", "@nestjs/core": "7.6.5",
"@typescript-eslint/eslint-plugin": "4.9.1", "@types/lodash": "4.14.167",
"@typescript-eslint/parser": "4.9.1", "@typescript-eslint/eslint-plugin": "4.11.1",
"eslint": "7.15.0", "@typescript-eslint/parser": "4.11.1",
"eslint-config-prettier": "7.0.0", "eslint": "7.17.0",
"eslint-config-prettier": "7.1.0",
"eslint-plugin-import": "2.22.1", "eslint-plugin-import": "2.22.1",
"husky": "4.3.5", "husky": "4.3.6",
"lint-staged": "10.5.3", "lint-staged": "10.5.3",
"lodash": "4.17.20",
"prettier": "2.2.1", "prettier": "2.2.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rxjs": "6.6.3", "rxjs": "6.6.3",
"ts-node": "^9.1.1", "typedoc": "0.20.9",
"tsconfig-paths": "^3.9.0", "typescript": "4.1.3"
"typescript": "4.1.2"
}, },
"peerDependencies": { "peerDependencies": {
"@nestjs/common": "^6.7.0 || ^7.0.0", "@nestjs/common": "^6.7.0 || ^7.0.0",
@ -65,8 +67,7 @@
}, },
"lint-staged": { "lint-staged": {
"*.ts": [ "*.ts": [
"prettier --write", "prettier --write"
"git add"
] ]
} }
} }

10
renovate.json Normal file
View File

@ -0,0 +1,10 @@
{
"semanticCommits": true,
"packageRules": [
{
"depTypeList": ["devDependencies"],
"automerge": true
}
],
"extends": ["config:base"]
}

View File

@ -0,0 +1,2 @@
ECHO_BOT_TOKEN=
GREETER_BOT_TOKEN=

View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

398
sample/01-complete-app/.gitignore vendored Normal file
View File

@ -0,0 +1,398 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
coverage/
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
=======
# Local
.env
dist

View File

@ -0,0 +1,14 @@
{
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "/src/.*\\.(test|spec).(ts|tsx|js)$",
"collectCoverageFrom" : ["src/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**"],
"coverageReporters": ["json", "lcov"]
}

11357
sample/01-complete-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
{
"name": "nest-typescript-starter",
"private": true,
"version": "1.0.0",
"description": "Nest TypeScript starter repository",
"license": "MIT",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "7.5.1",
"@nestjs/core": "7.5.1",
"@nestjs/platform-express": "7.5.1",
"dotenv": "^8.2.0",
"nestjs-telegraf": "*",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "6.6.3",
"telegraf": "^3.38.0"
},
"devDependencies": {
"@nestjs/cli": "7.5.1",
"@nestjs/schematics": "7.1.3",
"@nestjs/testing": "7.5.1",
"@types/express": "4.17.8",
"@types/jest": "26.0.15",
"@types/node": "14.14.6",
"@types/supertest": "2.0.10",
"@typescript-eslint/eslint-plugin": "4.6.1",
"@typescript-eslint/parser": "4.6.1",
"eslint": "7.12.1",
"eslint-config-prettier": "6.15.0",
"eslint-plugin-prettier": "3.1.4",
"jest": "26.6.3",
"prettier": "2.1.2",
"supertest": "6.0.0",
"ts-jest": "26.4.3",
"ts-loader": "8.0.8",
"ts-node": "9.0.0",
"tsconfig-paths": "3.9.0",
"typescript": "4.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -1 +1,3 @@
export const HELLO_SCENE_ID = 'HELLO_SCENE_ID'; export const HELLO_SCENE_ID = 'HELLO_SCENE_ID';
export const GreeterBotName = 'greeter';

View File

@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TelegrafModule } from 'nestjs-telegraf';
import { EchoModule } from './echo/echo.module';
import { GreeterModule } from './greeter/greeter.module';
import { sessionMiddleware } from './middleware/session.middleware';
import { GreeterBotName } from './app.constants';
@Module({
imports: [
TelegrafModule.forRoot({
token: process.env.ECHO_BOT_TOKEN,
middlewares: [sessionMiddleware],
include: [EchoModule],
}),
TelegrafModule.forRoot({
name: GreeterBotName,
token: process.env.GREETER_BOT_TOKEN,
middlewares: [sessionMiddleware],
include: [GreeterModule],
}),
EchoModule,
GreeterModule,
],
})
export class AppModule {}

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { EchoUpdate } from './echo.update';
import { EchoService } from './echo.service';
import { HelloScene } from './scenes/hello.scene';
@Module({
providers: [EchoUpdate, EchoService, HelloScene],
})
export class EchoModule {}

View File

@ -0,0 +1,43 @@
import { Telegraf } from 'telegraf';
import { Command, Help, InjectBot, On, Start, Update } from 'nestjs-telegraf';
import { EchoService } from './echo.service';
import { HELLO_SCENE_ID } from '../app.constants';
import { Context } from '../interfaces/context.interface';
@Update()
export class EchoUpdate {
constructor(
@InjectBot()
private readonly bot: Telegraf<Context>,
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');
}
}
}

View File

@ -1,6 +1,6 @@
import { HELLO_SCENE_ID } from '../app.constants'; import { Scene, SceneEnter, SceneLeave, Command } from 'nestjs-telegraf';
import { Context } from '../interfaces/context.interface'; import { HELLO_SCENE_ID } from '../../app.constants';
import { Scene, SceneEnter, SceneLeave, Command } from '../../lib'; import { Context } from '../../interfaces/context.interface';
@Scene(HELLO_SCENE_ID) @Scene(HELLO_SCENE_ID)
export class HelloScene { export class HelloScene {

View File

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { GreeterUpdate } from './greeter.update';
@Module({
providers: [GreeterUpdate],
})
export class GreeterModule {}

View File

@ -0,0 +1,16 @@
import { Hears, Start, Update } from '../../../../lib';
import { Context } from '../interfaces/context.interface';
@Update()
export class GreeterUpdate {
@Start()
async onStart(ctx: Context): Promise<void> {
await ctx.reply('Say hello to me');
}
@Hears(['hi', 'hello', 'hey', 'qq'])
async onGreetings(ctx: Context): Promise<void> {
const { first_name } = ctx.from;
await ctx.reply(`Hey ${first_name}`);
}
}

View File

@ -1,3 +1,4 @@
import 'dotenv/config';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./src",
"incremental": true
},
"include": ["src/**/*"]
}

View File

@ -1,17 +0,0 @@
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 './common/middleware/session.middleware';
@Module({
imports: [
TelegrafModule.forRoot({
token: '1467731595:AAHCvH65H9VQYKF9jE-E8c2rXsQBVAYseg8', // Don't steal >:(
middlewares: [sessionMiddleware],
}),
],
providers: [EchoService, AppUpdate],
})
export class AppModule {}

View File

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

View File

@ -7,12 +7,12 @@
"noLib": false, "noLib": false,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"target": "es2020", "target": "es2017",
"sourceMap": false, "sourceMap": false,
"baseUrl": "./", "rootDir": "./lib",
"outDir": "./dist", "outDir": "./dist",
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["lib/**/*", "sample/**/*"], "include": ["lib/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"] "exclude": ["node_modules", "**/*.spec.ts"]
} }

10
tsconfig.typedoc.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./"
},
"typedocOptions": {
"entryPoints": ["./index.ts", "./lib"],
"out": "docs"
}
}

View File

@ -8,11 +8,10 @@ slug: /bot-injection
At times you may need to access the native `Telegraf` instance. For example, you may want to connect stage middleware. You can inject the Telegraf by using the `@InjectBot()` decorator as follows: At times you may need to access the native `Telegraf` instance. For example, you may want to connect stage middleware. You can inject the Telegraf by using the `@InjectBot()` decorator as follows:
```typescript ```typescript
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectBot } from 'nestjs-telegraf'; import { InjectBot, TelegrafProvider, Context } from 'nestjs-telegraf';
import { Telegraf } from 'telegraf';
@Injectable() @Injectable()
export class BotSettingsService { export class BotSettingsService {
constructor(@InjectBot() private bot: Telegraf) {} constructor(@InjectBot() private bot: TelegrafProvider<Context>) {}
} }
``` ```

View File

@ -0,0 +1,31 @@
---
id: error-handling
title: Error handling
sidebar_label: Error handling
slug: /error-handling
---
By default, `nestjs-telegraf` catches all errors using the `Logger` built into NestJS.
Use can disable global errors catch with `disableGlobalCatch`:
```typescript
TelegrafModule.forRoot({
disableGlobalCatch: true,
}),
```
After that you can override errors handling with bot instance `catch` function.
```typescript
import { Injectable } from '@nestjs/common';
import { InjectBot, TelegrafProvider, Context } from 'nestjs-telegraf';
@Injectable()
export class BotSettingsService {
constructor(@InjectBot() private bot: TelegrafProvider<Context>) {
this.bot.catch((err, ctx) => {
console.log(`Ooops, encountered an error for ${ctx.updateType}`, err);
});
}
}
```

View File

@ -0,0 +1,27 @@
---
id: standalone-applications
title: Standalone applications
sidebar_label: Standalone applications
slug: standalone-applications
---
If you initialized your application with the [Nest CLI](https://docs.nestjs.com/cli/overview), [Express](https://expressjs.com/) framework will be installed by default along with Nest. Nest and NestJS Telegraf does not require Express for work. So if you don't plan to getting bot updates through webhooks, and you don't need a web server, you can remove Express.
To do this, change the `bootstrap` function in the `main.ts` file of your project on something like that:
```typescript
async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule);
}
bootstrap();
```
This initializes Nest as a **standalone application** (without any network listeners).
All that remains is to remove unused dependencies:
```bash
npm un @nestjs/platform-express @types/express
```
:::info
More information about standalone applications located at [Nest documentation](https://docs.nestjs.com/standalone-applications)
:::

View File

@ -1,28 +1,36 @@
--- ---
id: webhooks id: getting-updates
title: Webhooks title: Getting updates
sidebar_label: Webhooks sidebar_label: Getting updates
slug: webhooks slug: getting-updates
--- ---
If you want to configure a telegram bot webhook, you need to get a middleware from `TelegrafProvider` for connect it in your `main.ts` file. ## Long polling
By default, the bot receives updates using long-polling and requires no additional action.
## Webhooks
If you want to configure a telegram bot webhook, you need to get a middleware via `getBotToken` helper in your `main.ts` file.
To access it, you must use the `app.get()` method, followed by the provider reference: To access it, you must use the `app.get()` method, followed by the provider reference:
```typescript ```typescript
import { Telegraf } from 'telegraf'; import { getBotToken } from 'nestjs-telegraf';
const telegraf = app.get(Telegraf);
// ...
const bot = app.get(getBotToken());
``` ```
Now you can connect middleware: Now you can connect middleware:
```typescript ```typescript
app.use(telegraf.webhookCallback('/secret-path')); app.use(bot.webhookCallback('/secret-path'));
``` ```
The last step is to specify launchOptions in `forRoot` method: The last step is to specify launchOptions in `forRoot` method:
```typescript ```typescript
TelegrafModule.forRootAsync({ TelegrafModule.forRootAsync({
imports: [ConfigModule.forFeature(telegrafModuleConfig)], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: (configService: ConfigService) => ({
token: configService.get<string>('TELEGRAM_BOT_TOKEN'), token: configService.get<string>('TELEGRAM_BOT_TOKEN'),
launchOptions: { launchOptions: {
webhook: { webhook: {

View File

@ -0,0 +1,13 @@
---
id: middlewares
title: Middlewares
sidebar_label: Middlewares
slug: /middlewares
---
`nestjs-telegraf` has support of the Telegraf middleware packages. To use an existing middleware package, simply import it and add it to the middlewares array:
```typescript
TelegrafModule.forRoot({
middlewares: [session()],
}),
```

View File

@ -0,0 +1,74 @@
---
id: multiple-bots
title: Multiple bots
sidebar_label: Multiple bots
slug: /multiple-bots
---
In some cases, you may need to run multiple bots at the same time. This can also be achieved with this module. To work with multiple bots, first create the bots. In this case, bot naming becomes mandatory.
```typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TelegrafModule } from 'nestjs-telegraf';
@Module({
imports: [
ConfigModule.forRoot(),
TelegrafModule.forRootAsync({
imports: [ConfigModule],
botName: 'cat',
useFactory: (configService: ConfigService) => ({
token: configService.get<string>('CAT_BOT_TOKEN'),
}),
inject: [ConfigService],
}),
TelegrafModule.forRootAsync({
imports: [ConfigModule.forFeature(telegrafModuleConfig)],
botName: 'dog',
useFactory: async (configService: ConfigService) => ({
token: configService.get<string>('DOG_BOT_TOKEN'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
```
:::caution
Please note that you shouldn't have multiple bots without a name, or with the same name, otherwise they will get overridden.
:::
You can also inject the `Bot` for a given bot:
```typescript
import { Injectable } from '@nestjs/common';
import { InjectBot, Telegraf, Context } from 'nestjs-telegraf';
@Injectable()
export class EchoService {
constructor(@InjectBot('cat') private catBot: Telegraf<Context>) {}
}
```
To inject a given `Bot` to a custom provider (for example, factory provider), use the `getBotToken()` function passing the name of the bot as an argument.
```typescript
{
provide: CatsService,
useFactory: (catBot: Telegraf<Context>) => {
return new CatsService(catBot);
},
inject: [getBotToken('cat')],
}
```
Another useful feature of the `nestjs-telegraf` module is the ability to choose which modules should handle updates for each launched bot. By default, module searches for handlers throughout the whole app. To limit this scan to only a subset of modules, use the include property.
```typescript
TelegrafModule.forRootAsync({
imports: [ConfigModule],
botName: 'cat',
useFactory: (configService: ConfigService) => ({
token: configService.get<string>('CAT_BOT_TOKEN'),
include: [CatsModule],
}),
inject: [ConfigService],
}),
```

23879
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,12 +11,12 @@
"serve": "docusaurus serve" "serve": "docusaurus serve"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "2.0.0-alpha.63", "@docusaurus/core": "2.0.0-alpha.70",
"@docusaurus/preset-classic": "2.0.0-alpha.63", "@docusaurus/preset-classic": "2.0.0-alpha.70",
"@mdx-js/react": "^1.5.8", "@mdx-js/react": "1.6.22",
"clsx": "^1.1.1", "clsx": "1.1.1",
"react": "^16.8.4", "react": "17.0.1",
"react-dom": "^16.8.4" "react-dom": "17.0.1"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -31,11 +31,11 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^2.0.0-alpha.51", "@docusaurus/module-type-aliases": "2.0.0-alpha.f37987f32",
"@tsconfig/docusaurus": "^1.0.2", "@tsconfig/docusaurus": "1.0.2",
"@types/react": "^16.9.49", "@types/react": "17.0.0",
"@types/react-helmet": "^6.1.0", "@types/react-helmet": "6.1.0",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "5.1.7",
"typescript": "^4.0.2" "typescript": "4.1.3"
} }
} }

View File

@ -2,11 +2,15 @@ module.exports = {
docs: { docs: {
'Getting Started': [ 'Getting Started': [
'installation', 'installation',
'getting-updates',
'telegraf-methods', 'telegraf-methods',
'bot-injection', 'bot-injection',
'async-configuration', 'async-configuration',
'webhooks', 'multiple-bots',
'middlewares',
'error-handling',
], ],
Extras: ['extras/standalone-applications'],
'API Reference': ['api-reference/decorators'], 'API Reference': ['api-reference/decorators'],
}, },
}; };