feat: add initial code

This commit is contained in:
Igor Kamyshev 2019-02-28 10:29:26 +02:00
commit 4324f0f1cf
20 changed files with 5033 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

6
.travis.yml Normal file
View File

@ -0,0 +1,6 @@
language: node_js
node_js:
- '8'
- '10'
cache: yarn
script: yarn ci

4
lib/Bot.ts Normal file
View File

@ -0,0 +1,4 @@
import Telegraf from 'telegraf'
import { Context } from './Context'
export type Bot = Telegraf<Context>

3
lib/Context.ts Normal file
View File

@ -0,0 +1,3 @@
import { ContextMessageUpdate } from 'telegraf'
export type Context = ContextMessageUpdate

View File

@ -0,0 +1,5 @@
import { ContextMessageUpdate } from 'telegraf'
export interface ContextTransformer<T = any> {
transform: (ctx: ContextMessageUpdate) => Promise<T>
}

13
lib/HandleParameters.ts Normal file
View File

@ -0,0 +1,13 @@
import { ContextTransformer } from './ContextTransformer'
import { Type } from '@nestjs/common'
interface ArgumentTransformation {
index: number
transform: Type<ContextTransformer>
}
export interface HandleParameters {
onStart?: boolean
command?: string
transformations?: ArgumentTransformation[]
}

6
lib/Handler.ts Normal file
View File

@ -0,0 +1,6 @@
import { HandleParameters } from './HandleParameters'
export interface Handler {
handle: (...args: any[]) => Promise<void>
config: HandleParameters
}

121
lib/TelegramBot.ts Normal file
View File

@ -0,0 +1,121 @@
import { Injectable, Inject } from '@nestjs/common'
import { ModuleRef } from '@nestjs/core'
import Telegraf, { ContextMessageUpdate } from 'telegraf'
import { flatten, head } from 'lodash'
import { ContextTransformer } from './ContextTransformer'
import { TelegramCatch } from './decorators/TelegramCatch'
import { TelegramErrorHandler } from './interfaces/TelegramErrorHandler'
import { Handler } from './Handler'
import { Bot } from './Bot'
import { TelegramActionHandler } from './decorators/TelegramActionHandler'
import { TokenInjectionToken } from './TokenInjectionToken'
import { TelegramModuleOptionsFactory } from 'TelegramModuleOptionsFactory'
@Injectable()
export class TelegramBot {
private readonly token: string
private bot: Bot
private ref: ModuleRef
public constructor(
@Inject(TokenInjectionToken) factory: TelegramModuleOptionsFactory,
) {
this.token = factory.createOptions().token
}
public init(ref: ModuleRef) {
this.ref = ref
const bot = new Telegraf(this.token)
const handlers = this.createHandlers()
this.setupOnStart(bot, handlers)
this.setupOnCommand(bot, handlers)
this.bot = bot
}
public start() {
this.bot.startPolling()
}
private createHandlers(): Handler[] {
return flatten(
Array.from((TelegramActionHandler.handlers || new Map()).entries()).map(
([handlerClass, classConfig]) => {
const handlerInstance = this.ref.get(handlerClass, { strict: false })
return Array.from(classConfig.entries()).map(
([methodName, methodCondig]) => ({
handle: handlerInstance[methodName].bind(handlerInstance),
config: methodCondig,
}),
)
},
),
)
}
private setupOnStart(bot: Bot, handlers: Handler[]): void {
const onStart = handlers.filter(({ config }) => config.onStart)
if (onStart.length !== 1) {
throw new Error()
}
bot.start(this.adoptHandle(head(onStart)))
}
private setupOnCommand(bot: Bot, handlers: Handler[]): void {
const commandHandlers = handlers.filter(({ config }) => config.command)
commandHandlers.forEach(handler => {
bot.command(handler.config.command, this.adoptHandle(handler))
})
}
private adoptHandle({ handle, config }: Handler) {
const errorHandler = this.createCatch()
return async (ctx: ContextMessageUpdate) => {
const args = await Promise.all(
(config.transformations || [])
.sort((a, b) => a.index - b.index)
.map(({ transform }) =>
this.ref
.get<ContextTransformer>(transform, { strict: false })
.transform(ctx),
),
)
return handle(ctx, ...args).catch(errorHandler(ctx))
}
}
private createCatch() {
const handlers = Array.from(
(TelegramCatch.handlers || new Map()).entries(),
).map(([errorType, handlerType]) => {
const handler = this.ref.get<TelegramErrorHandler>(handlerType, {
strict: false,
})
return {
errorType,
handler,
}
})
return (ctx: ContextMessageUpdate) => (e: any) => {
for (const { errorType, handler } of handlers) {
if (e instanceof (errorType as any)) {
return handler.catch(ctx, e)
}
}
throw e
}
}
}

View File

@ -0,0 +1,3 @@
export interface TelegramModuleOptions {
token: string
}

View File

@ -0,0 +1,5 @@
import { TelegramModuleOptions } from './TelegramModuleOptions'
export interface TelegramModuleOptionsFactory {
createOptions(): TelegramModuleOptions
}

View File

@ -0,0 +1 @@
export const TokenInjectionToken = Symbol('TokenInjectionToken')

View File

@ -0,0 +1,18 @@
import { Type } from '@nestjs/common'
import { ContextTransformer } from '../ContextTransformer'
import { addHandlerToStore } from './TelegramActionHandler'
export const PipeContext = <T>(transform: Type<ContextTransformer<T>>) => (
target: Object,
propertyKey: string,
parameterIndex: number,
) => {
addHandlerToStore(target, propertyKey, {
transformations: [
{
index: parameterIndex,
transform,
},
],
})
}

View File

@ -0,0 +1,44 @@
import { HandleParameters } from '../HandleParameters'
type Decorator = (
params: HandleParameters,
) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => void
type HandlerDecorator = Decorator & {
handlers?: Map<any, Map<string, HandleParameters>>
}
export const TelegramActionHandler: HandlerDecorator = (
parameters: HandleParameters,
) => (target: any, propertyKey: string) => {
// eslint-disable-next-line no-use-before-define
addHandlerToStore(target, propertyKey, parameters)
}
export const addHandlerToStore = (
instance: Object,
name: string,
config: HandleParameters,
) => {
const handlerClass = instance.constructor
if (!TelegramActionHandler.handlers) {
TelegramActionHandler.handlers = new Map()
}
if (!TelegramActionHandler.handlers.get(handlerClass)) {
TelegramActionHandler.handlers.set(handlerClass, new Map())
}
const oldParameters =
TelegramActionHandler.handlers.get(handlerClass).get(name) || {}
TelegramActionHandler.handlers.get(handlerClass).set(name, {
...oldParameters,
...config,
transformations: [
...(oldParameters.transformations || []),
...(config.transformations || []),
],
})
}

View File

@ -0,0 +1,18 @@
import { Type } from '@nestjs/common'
import { TelegramErrorHandler } from '../interfaces/TelegramErrorHandler'
type Decorator = (error: any) => ClassDecorator
type HandlerDecorator = Decorator & {
handlers?: Map<Error, Type<TelegramErrorHandler>>
}
export const TelegramCatch: HandlerDecorator = error => target => {
if (!TelegramCatch.handlers) {
TelegramCatch.handlers = new Map()
}
TelegramCatch.handlers.set(error, target as any)
return target
}

14
lib/index.ts Normal file
View File

@ -0,0 +1,14 @@
export { TelegramModule } from './telegram.module'
export { TelegramBot } from './TelegramBot'
export { TelegramModuleOptionsFactory } from './TelegramModuleOptionsFactory'
export { TelegramModuleOptions } from './TelegramModuleOptions'
export { PipeContext } from './decorators/PipeContext'
export { TelegramActionHandler } from './decorators/TelegramActionHandler'
export { TelegramCatch } from './decorators/TelegramCatch'
export { TelegramErrorHandler } from './interfaces/TelegramErrorHandler'
export { ContextTransformer } from './ContextTransformer'
export { Context } from './Context'

View File

@ -0,0 +1,5 @@
import { ContextMessageUpdate } from 'telegraf'
export interface TelegramErrorHandler<E = any> {
catch(ctx: ContextMessageUpdate, error: E): Promise<void>
}

36
lib/telegram.module.ts Normal file
View File

@ -0,0 +1,36 @@
import {
MiddlewareConsumer,
Module,
NestModule,
DynamicModule,
} from '@nestjs/common'
import { ModuleMetadata, Type } from '@nestjs/common/interfaces'
import { TelegramBot } from './TelegramBot'
import { TelegramModuleOptionsFactory } from './TelegramModuleOptionsFactory'
import { TokenInjectionToken } from './TokenInjectionToken'
interface TelegramFactory extends Pick<ModuleMetadata, 'imports'> {
useClass?: Type<TelegramModuleOptionsFactory>
inject?: any[]
}
@Module({})
export class TelegramModule implements NestModule {
public configure(consumer: MiddlewareConsumer) {
// pass
}
static fromFactory(factory: TelegramFactory): DynamicModule {
return {
module: TelegramModule,
providers: [
TelegramBot,
{
provide: TokenInjectionToken,
useClass: factory.useClass,
},
],
exports: [TelegramBot],
}
}
}

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "nest-telegram",
"version": "0.0.0",
"main": "dist/index.js",
"repository": "git@github.com:igorkamyshev/nest-telegram.git",
"author": "Igor Kamyshev <igor@kamyshev.me>",
"license": "MIT",
"devDependencies": {
"@solid-soda/scripts": "^1.2.4",
"@team-griffin/install-self-peers": "^1.1.1",
"@types/lodash": "^4.14.121"
},
"husky": {
"hooks": {
"pre-commit": "yarn soda lint-staged",
"commit-msg": "yarn soda commitlint"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "rimraf dist && tsc",
"prepare": "install-self-peers -- --ignore-scripts && yarn build",
"ci": "yarn soda lint",
"s": "yarn soda"
},
"peerDependencies": {
"@nestjs/common": "^5.7.3",
"@nestjs/core": "^5.7.3",
"reflect-metadata": "^0.1.13"
},
"dependencies": {
"lodash": "^4.17.11",
"telegraf": "^3.27.1"
}
}

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"lib": ["es2017"],
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./lib"
},
"include": ["lib/**/*"],
"exclude": ["node_modules"]
}

4673
yarn.lock Normal file

File diff suppressed because it is too large Load Diff