Revert "initial commit"

This reverts commit 2d23eba148.
This commit is contained in:
Alexander Bukhalo 2022-11-20 14:16:17 +03:00
parent 2d23eba148
commit 43816099a6
No known key found for this signature in database
94 changed files with 17013 additions and 65 deletions

26
.eslintrc.js Normal file
View File

@ -0,0 +1,26 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'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/no-explicit-any': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-types': 'off',
},
};

View File

@ -1,3 +1,5 @@
export * from './update.decorator'; export * from './update.decorator';
export * from './scene.decorator';
export * from './wizard.decorator';
export * from './inject-bot.decorator'; export * from './inject-bot.decorator';
export * from './inject-all-bots.decorator'; export * from './inject-all-bots.decorator';

View File

@ -1,9 +1,9 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { Bot } from 'grammy'; import { Telegraf } from 'telegraf';
import { getAllBotsToken } from '../../utils/get-all-bots-token.util'; import { getAllBotsToken } from '../../utils/get-all-bots-token.util';
export type AllBotsMap = Map<string, Bot<any>>; export type AllBotsMap = Map<string, Telegraf<any>>;
export const InjectAllBots = (): ParameterDecorator => export const InjectAllBots = (): ParameterDecorator =>
Inject(getAllBotsToken()); Inject(getAllBotsToken());

View File

@ -0,0 +1,14 @@
import { SetMetadata } from '@nestjs/common';
import { SceneOptions } from 'telegraf/typings/scenes/base';
import { SceneMetadata } from '../../interfaces';
import { SCENE_METADATA } from '../../telegraf.constants';
export const Scene = (
sceneId: string,
options?: SceneOptions<any>,
): ClassDecorator =>
SetMetadata<string, SceneMetadata>(SCENE_METADATA, {
sceneId,
type: 'base',
options,
});

View File

@ -0,0 +1,14 @@
import { SetMetadata } from '@nestjs/common';
import { SceneOptions } from 'telegraf/typings/scenes/base';
import { SceneMetadata } from '../../interfaces';
import { SCENE_METADATA } from '../../telegraf.constants';
export const Wizard = (
sceneId: string,
options?: SceneOptions<any>,
): ClassDecorator =>
SetMetadata<string, SceneMetadata>(SCENE_METADATA, {
sceneId,
type: 'wizard',
options,
});

View File

@ -1,3 +1,4 @@
export * from './core'; export * from './core';
export * from './listeners'; export * from './listeners';
export * from './scene';
export * from './params'; export * from './params';

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
import { createListenerDecorator } from '../../utils';
/**
* Handler for /help command.
*
* @see https://telegraf.js.org/#/?id=help
*/
export const Help = createListenerDecorator('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 { createListenerDecorator } from '../../utils';
/**
* Mention handling.
*
* @see https://telegraf.js.org/#/?id=mention
*/
export const Mention = createListenerDecorator('mention');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import { createListenerDecorator } from '../../utils';
import { Scenes } from 'telegraf';
export const SceneEnter =
createListenerDecorator<Scenes.BaseScene<never>>('enter');

View File

@ -0,0 +1,5 @@
import { createListenerDecorator } from '../../utils';
import { Scenes } from 'telegraf';
export const SceneLeave =
createListenerDecorator<Scenes.BaseScene<never>>('leave');

View File

@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
import { WizardStepMetadata } from '../../interfaces';
import { WIZARD_STEP_METADATA } from '../../telegraf.constants';
export const WizardStep = (step: number) =>
SetMetadata<string, WizardStepMetadata>(WIZARD_STEP_METADATA, { step });

View File

@ -1,6 +1,6 @@
import { ParamData } from '@nestjs/common'; import { ParamData } from '@nestjs/common';
import { ParamsFactory } from '@nestjs/core/helpers/external-context-creator'; import { ParamsFactory } from '@nestjs/core/helpers/external-context-creator';
import { Context } from 'grammy'; import { Context } from 'telegraf';
import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum'; import { TelegrafParamtype } from '../enums/telegraf-paramtype.enum';
export class TelegrafParamsFactory implements ParamsFactory { export class TelegrafParamsFactory implements ParamsFactory {

View File

@ -1,3 +1,4 @@
export * from './telegraf-options.interface'; export * from './telegraf-options.interface';
export * from './listener-metadata.interface'; export * from './listener-metadata.interface';
export * from './scene-metadata.interface';
export * from './telegraf-exception-filter.interface'; export * from './telegraf-exception-filter.interface';

View File

@ -0,0 +1,11 @@
import { SceneOptions } from 'telegraf/typings/scenes/base';
export interface SceneMetadata {
sceneId: string;
type: 'base' | 'wizard';
options?: SceneOptions<any>;
}
export interface WizardStepMetadata {
step: number;
}

View File

@ -1,11 +1,11 @@
import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
import { Middleware, PollingOptions, BotConfig } from 'grammy'; import { Middleware, Telegraf } from 'telegraf';
export interface TelegrafModuleOptions { export interface TelegrafModuleOptions {
token: string; token: string;
config?: BotConfig<any>;
pollingOptions?: PollingOptions | false;
botName?: string; botName?: string;
options?: Partial<Telegraf.Options<any>>;
launchOptions?: Telegraf.LaunchOptions | false;
include?: Function[]; include?: Function[];
middlewares?: ReadonlyArray<Middleware<any>>; middlewares?: ReadonlyArray<Middleware<any>>;
} }

View File

@ -5,7 +5,7 @@ import { MetadataScanner } from '@nestjs/core/metadata-scanner';
import { Module } from '@nestjs/core/injector/module'; import { Module } from '@nestjs/core/injector/module';
import { ParamMetadata } from '@nestjs/core/helpers/interfaces'; import { ParamMetadata } from '@nestjs/core/helpers/interfaces';
import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator'; import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator';
import { Composer, Context, Bot } from 'grammy'; import { Composer, Context, Scenes, Telegraf } from 'telegraf';
import { MetadataAccessorService } from './metadata-accessor.service'; import { MetadataAccessorService } from './metadata-accessor.service';
import { import {
@ -25,9 +25,11 @@ export class ListenersExplorerService
implements OnModuleInit implements OnModuleInit
{ {
private readonly telegrafParamsFactory = new TelegrafParamsFactory(); private readonly telegrafParamsFactory = new TelegrafParamsFactory();
private bot: Bot; private bot: Telegraf<any>;
constructor( constructor(
@Inject(TELEGRAF_STAGE)
private readonly stage: Scenes.Stage<any>,
@Inject(TELEGRAF_MODULE_OPTIONS) @Inject(TELEGRAF_MODULE_OPTIONS)
private readonly telegrafOptions: TelegrafModuleOptions, private readonly telegrafOptions: TelegrafModuleOptions,
@Inject(TELEGRAF_BOT_NAME) @Inject(TELEGRAF_BOT_NAME)
@ -43,9 +45,10 @@ export class ListenersExplorerService
} }
onModuleInit(): void { onModuleInit(): void {
this.bot = this.moduleRef.get<Bot>(this.botName, { this.bot = this.moduleRef.get<Telegraf<any>>(this.botName, {
strict: false, strict: false,
}); });
this.bot.use(this.stage.middleware());
this.explore(); this.explore();
} }
@ -57,6 +60,7 @@ export class ListenersExplorerService
); );
this.registerUpdates(modules); this.registerUpdates(modules);
this.registerScenes(modules);
} }
private registerUpdates(modules: Module[]): void { private registerUpdates(modules: Module[]): void {
@ -66,6 +70,28 @@ export class ListenersExplorerService
updates.forEach((wrapper) => this.registerListeners(this.bot, wrapper)); updates.forEach((wrapper) => this.registerListeners(this.bot, wrapper));
} }
private registerScenes(modules: Module[]): void {
const scenes = this.flatMap<InstanceWrapper>(modules, (wrapper) =>
this.filterScenes(wrapper),
);
scenes.forEach((wrapper) => {
const { sceneId, type, options } = this.metadataAccessor.getSceneMetadata(
wrapper.instance.constructor,
);
const scene =
type === 'base'
? new Scenes.BaseScene<any>(sceneId, options || ({} as any))
: new Scenes.WizardScene<any>(sceneId, options || ({} as any));
this.stage.register(scene);
if (type === 'base') {
this.registerListeners(scene, wrapper);
} else {
this.registerWizardListeners(scene as Scenes.WizardScene<any>, wrapper);
}
});
}
private filterUpdates(wrapper: InstanceWrapper): InstanceWrapper<unknown> { private filterUpdates(wrapper: InstanceWrapper): InstanceWrapper<unknown> {
const { instance } = wrapper; const { instance } = wrapper;
if (!instance) return undefined; if (!instance) return undefined;
@ -76,6 +102,16 @@ export class ListenersExplorerService
return wrapper; 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 registerListeners( private registerListeners(
composer: Composer<any>, composer: Composer<any>,
wrapper: InstanceWrapper<unknown>, wrapper: InstanceWrapper<unknown>,
@ -87,6 +123,60 @@ export class ListenersExplorerService
); );
} }
private registerWizardListeners(
wizard: Scenes.WizardScene<any>,
wrapper: InstanceWrapper<unknown>,
): void {
const { instance } = wrapper;
const prototype = Object.getPrototypeOf(instance);
type WizardMetadata = { step: number; methodName: string };
const wizardSteps: WizardMetadata[] = [];
const basicListeners = [];
this.metadataScanner.scanFromPrototype(
instance,
prototype,
(methodName) => {
const methodRef = prototype[methodName];
const metadata = this.metadataAccessor.getWizardStepMetadata(methodRef);
if (!metadata) {
basicListeners.push(methodName);
return undefined;
}
wizardSteps.push({ step: metadata.step, methodName });
},
);
for (const methodName of basicListeners) {
this.registerIfListener(wizard, instance, prototype, methodName);
}
const group = wizardSteps
.sort((a, b) => a.step - b.step)
.reduce<{ [key: number]: WizardMetadata[] }>(
(prev, cur) => ({
...prev,
[cur.step]: [...(prev[cur.step] || []), cur],
}),
{},
);
wizard.steps = Object.values(group).map((stepsMetadata) => {
const composer = new Composer();
stepsMetadata.forEach((stepMethod) => {
this.registerIfListener(
composer,
instance,
prototype,
stepMethod.methodName,
[{ method: 'use', args: [] }],
);
});
return composer.middleware();
});
}
private registerIfListener( private registerIfListener(
composer: Composer<any>, composer: Composer<any>,
instance: any, instance: any,

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
SCENE_METADATA,
LISTENERS_METADATA,
UPDATE_METADATA,
WIZARD_STEP_METADATA,
} from '../telegraf.constants';
import {
ListenerMetadata,
SceneMetadata,
WizardStepMetadata,
} from '../interfaces';
@Injectable()
export class MetadataAccessorService {
constructor(private readonly reflector: Reflector) {}
isUpdate(target: Function): boolean {
if (!target) return false;
return !!this.reflector.get(UPDATE_METADATA, target);
}
isScene(target: Function): boolean {
if (!target) return false;
return !!this.reflector.get(SCENE_METADATA, target);
}
getListenerMetadata(target: Function): ListenerMetadata[] | undefined {
return this.reflector.get(LISTENERS_METADATA, target);
}
getSceneMetadata(target: Function): SceneMetadata | undefined {
return this.reflector.get(SCENE_METADATA, target);
}
getWizardStepMetadata(target: Function): WizardStepMetadata | undefined {
return this.reflector.get(WIZARD_STEP_METADATA, target);
}
}

8
lib/stage.provider.ts Normal file
View File

@ -0,0 +1,8 @@
import { Provider } from '@nestjs/common';
import { Scenes } from 'telegraf';
import { TELEGRAF_STAGE } from './telegraf.constants';
export const telegrafStageProvider: Provider = {
provide: TELEGRAF_STAGE,
useClass: Scenes.Stage,
};

View File

@ -1,8 +1,8 @@
import { Provider } from '@nestjs/common'; import { Provider } from '@nestjs/common';
import { Bot } from 'grammy'; import { Telegraf } from 'telegraf';
import { TELEGRAF_ALL_BOTS } from './telegraf.constants'; import { TELEGRAF_ALL_BOTS } from './telegraf.constants';
export const allBotsMap = new Map<string, Bot<any>>(); export const allBotsMap = new Map<string, Telegraf<any>>();
export const telegrafAllBotsProvider: Provider = { export const telegrafAllBotsProvider: Provider = {
provide: TELEGRAF_ALL_BOTS, provide: TELEGRAF_ALL_BOTS,

View File

@ -18,6 +18,7 @@ import {
TELEGRAF_MODULE_OPTIONS, TELEGRAF_MODULE_OPTIONS,
} from './telegraf.constants'; } from './telegraf.constants';
import { ListenersExplorerService, MetadataAccessorService } from './services'; import { ListenersExplorerService, MetadataAccessorService } from './services';
import { telegrafStageProvider } from './stage.provider';
import { import {
allBotsMap, allBotsMap,
telegrafAllBotsProvider, telegrafAllBotsProvider,
@ -60,11 +61,13 @@ export class TelegrafCoreModule implements OnApplicationShutdown {
provide: TELEGRAF_MODULE_OPTIONS, provide: TELEGRAF_MODULE_OPTIONS,
useValue: options, useValue: options,
}, },
telegrafStageProvider,
telegrafBotNameProvider, telegrafBotNameProvider,
telegrafBotProvider, telegrafBotProvider,
telegrafAllBotsProvider, telegrafAllBotsProvider,
], ],
exports: [ exports: [
telegrafStageProvider,
telegrafBotNameProvider, telegrafBotNameProvider,
telegrafBotProvider, telegrafBotProvider,
telegrafAllBotsProvider, telegrafAllBotsProvider,
@ -98,11 +101,13 @@ export class TelegrafCoreModule implements OnApplicationShutdown {
imports: options.imports, imports: options.imports,
providers: [ providers: [
...asyncProviders, ...asyncProviders,
telegrafStageProvider,
telegrafBotNameProvider, telegrafBotNameProvider,
telegrafBotProvider, telegrafBotProvider,
telegrafAllBotsProvider, telegrafAllBotsProvider,
], ],
exports: [ exports: [
telegrafStageProvider,
telegrafBotNameProvider, telegrafBotNameProvider,
telegrafBotProvider, telegrafBotProvider,
telegrafAllBotsProvider, telegrafAllBotsProvider,

View File

@ -6,10 +6,10 @@ import {
} from './interfaces'; } from './interfaces';
@Module({}) @Module({})
export class GrammyModule { export class TelegrafModule {
public static forRoot(options: TelegrafModuleOptions): DynamicModule { public static forRoot(options: TelegrafModuleOptions): DynamicModule {
return { return {
module: GrammyModule, module: TelegrafModule,
imports: [TelegrafCoreModule.forRoot(options)], imports: [TelegrafCoreModule.forRoot(options)],
exports: [TelegrafCoreModule], exports: [TelegrafCoreModule],
}; };
@ -19,7 +19,7 @@ export class GrammyModule {
options: TelegrafModuleAsyncOptions, options: TelegrafModuleAsyncOptions,
): DynamicModule { ): DynamicModule {
return { return {
module: GrammyModule, module: TelegrafModule,
imports: [TelegrafCoreModule.forRootAsync(options)], imports: [TelegrafCoreModule.forRootAsync(options)],
exports: [TelegrafCoreModule], exports: [TelegrafCoreModule],
}; };

View File

@ -1,4 +1,4 @@
import { Composer, Middleware } from 'grammy'; import { Composer, Middleware } from 'telegraf';
export type Filter<T extends any[], F> = T extends [] export type Filter<T extends any[], F> = T extends []
? [] ? []

View File

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

View File

@ -1,4 +1,4 @@
import { Composer } from 'grammy'; import { Composer } from 'telegraf';
import { ComposerMethodArgs, OnlyFunctionPropertyNames } from '../types'; import { ComposerMethodArgs, OnlyFunctionPropertyNames } from '../types';
import { LISTENERS_METADATA } from '../telegraf.constants'; import { LISTENERS_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces'; import { ListenerMetadata } from '../interfaces';

View File

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

View File

@ -0,0 +1,24 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'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"]
}

15862
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,71 @@
{
"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": "8.0.9",
"@nestjs/core": "8.0.9",
"@nestjs/platform-express": "8.0.9",
"dotenv": "10.0.0",
"nestjs-telegraf": "2.4.0",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"telegraf": "4.4.2"
},
"devDependencies": {
"@nestjs/cli": "8.2.8",
"@nestjs/schematics": "8.0.5",
"@nestjs/testing": "8.0.9",
"@types/express": "4.17.13",
"@types/jest": "27.4.0",
"@types/node": "16.11.22",
"@types/supertest": "2.0.11",
"@typescript-eslint/eslint-plugin": "5.11.0",
"@typescript-eslint/parser": "5.11.0",
"eslint": "8.8.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "4.0.0",
"jest": "27.5.1",
"prettier": "2.5.1",
"supertest": "6.2.2",
"ts-jest": "27.1.3",
"ts-loader": "9.2.6",
"ts-node": "10.5.0",
"tsconfig-paths": "3.12.0",
"typescript": "4.4.4"
},
"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

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

View File

@ -0,0 +1,26 @@
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,
include: [EchoModule],
}),
TelegrafModule.forRootAsync({
botName: GreeterBotName,
useFactory: () => ({
token: process.env.GREETER_BOT_TOKEN,
middlewares: [sessionMiddleware],
include: [GreeterModule],
}),
}),
EchoModule,
GreeterModule,
],
})
export class AppModule {}

View File

@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { TelegrafExecutionContext } from 'nestjs-telegraf';
export const UpdateType = createParamDecorator(
(_, ctx: ExecutionContext) =>
TelegrafExecutionContext.create(ctx).getContext().updateType,
);

View File

@ -0,0 +1,13 @@
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { TelegrafArgumentsHost } from 'nestjs-telegraf';
import { Context } from '../../interfaces/context.interface';
@Catch()
export class TelegrafExceptionFilter implements ExceptionFilter {
async catch(exception: Error, host: ArgumentsHost): Promise<void> {
const telegrafHost = TelegrafArgumentsHost.create(host);
const ctx = telegrafHost.getContext<Context>();
await ctx.replyWithHTML(`<b>Error</b>: ${exception.message}`);
}
}

View File

@ -0,0 +1,20 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { TelegrafExecutionContext, TelegrafException } from 'nestjs-telegraf';
import { Context } from '../../interfaces/context.interface';
@Injectable()
export class AdminGuard implements CanActivate {
private readonly ADMIN_IDS = [];
canActivate(context: ExecutionContext): boolean {
const ctx = TelegrafExecutionContext.create(context);
const { from } = ctx.getContext<Context>();
const isAdmin = this.ADMIN_IDS.includes(from.id);
if (!isAdmin) {
throw new TelegrafException('You are not admin 😡');
}
return true;
}
}

View File

@ -0,0 +1,20 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class ResponseTimeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const start = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`Response time: ${Date.now() - start}ms`)));
}
}

View File

@ -0,0 +1,8 @@
import { Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ReverseTextPipe implements PipeTransform {
transform(value: string): string {
return value.split('').reverse().join('');
}
}

View File

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

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,53 @@
import { UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
import {
Help,
InjectBot,
On,
Message,
Start,
Update,
Command,
} from 'nestjs-telegraf';
import { Telegraf } from 'telegraf';
import { EchoService } from './echo.service';
import { GreeterBotName } from '../app.constants';
import { Context } from '../interfaces/context.interface';
import { ReverseTextPipe } from '../common/pipes/reverse-text.pipe';
import { ResponseTimeInterceptor } from '../common/interceptors/response-time.interceptor';
import { AdminGuard } from '../common/guards/admin.guard';
import { TelegrafExceptionFilter } from '../common/filters/telegraf-exception.filter';
@Update()
@UseInterceptors(ResponseTimeInterceptor)
@UseFilters(TelegrafExceptionFilter)
export class EchoUpdate {
constructor(
@InjectBot(GreeterBotName)
private readonly bot: Telegraf<Context>,
private readonly echoService: EchoService,
) {}
@Start()
async onStart(): Promise<string> {
const me = await this.bot.telegram.getMe();
return `Hey, I'm ${me.first_name}`;
}
@Help()
async onHelp(): Promise<string> {
return 'Send me any text';
}
@Command('admin')
@UseGuards(AdminGuard)
onAdminCommand(): string {
return 'Welcome judge';
}
@On('text')
onMessage(
@Message('text', new ReverseTextPipe()) reversedText: string,
): string {
return this.echoService.echo(reversedText);
}
}

View File

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

View File

@ -0,0 +1,26 @@
import { Command, Ctx, Hears, Start, Update, Sender } from 'nestjs-telegraf';
import { UpdateType as TelegrafUpdateType } from 'telegraf/typings/telegram-types';
import { Context } from '../interfaces/context.interface';
import { HELLO_SCENE_ID } from '../app.constants';
import { UpdateType } from '../common/decorators/update-type.decorator';
@Update()
export class GreeterUpdate {
@Start()
onStart(): string {
return 'Say hello to me';
}
@Hears(['hi', 'hello', 'hey', 'qq'])
onGreetings(
@UpdateType() updateType: TelegrafUpdateType,
@Sender('first_name') firstName: string,
): string {
return `Hey ${firstName}`;
}
@Command('scene')
async onSceneCommand(@Ctx() ctx: Context): Promise<void> {
await ctx.scene.enter(HELLO_SCENE_ID);
}
}

View File

@ -0,0 +1,29 @@
import { Scene, SceneEnter, SceneLeave, Command } from 'nestjs-telegraf';
import { HELLO_SCENE_ID } from '../../app.constants';
import { Context } from '../../interfaces/context.interface';
@Scene(HELLO_SCENE_ID)
export class RandomNumberScene {
@SceneEnter()
onSceneEnter(): string {
console.log('Enter to scene');
return 'Welcome on scene ✋';
}
@SceneLeave()
onSceneLeave(): string {
console.log('Leave from scene');
return 'Bye Bye 👋';
}
@Command(['rng', 'random'])
onRandomCommand(): number {
console.log('Use "random" command');
return Math.floor(Math.random() * 11);
}
@Command('leave')
async onLeaveCommand(ctx: Context): Promise<void> {
await ctx.scene.leave();
}
}

View File

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

View File

@ -0,0 +1,8 @@
import 'dotenv/config';
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,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,6 +0,0 @@
export * from './on.decorator';
export * from './use.decorator';
export * from './command.decorator';
export * from './game-query.decorator';
export * from './hears.decorator';
export * from './inline-query.decorator';

View File

@ -1,18 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { LISTENERS_METADATA, UPDATE_METADATA } from '../telegraf.constants';
import { ListenerMetadata } from '../interfaces';
@Injectable()
export class MetadataAccessorService {
constructor(private readonly reflector: Reflector) {}
isUpdate(target: Function): boolean {
if (!target) return false;
return !!this.reflector.get(UPDATE_METADATA, target);
}
getListenerMetadata(target: Function): ListenerMetadata[] | undefined {
return this.reflector.get(LISTENERS_METADATA, target);
}
}

View File

@ -1,16 +0,0 @@
import { Bot } from 'grammy';
import { TelegrafModuleOptions } from '../interfaces';
export async function createBotFactory(
options: TelegrafModuleOptions,
): Promise<Bot> {
const bot = new Bot(options.token, options.config);
bot.use(...(options.middlewares ?? []));
if (options.pollingOptions !== false) {
bot.start(options.pollingOptions);
}
return bot;
}

View File

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/grammy"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}