telegraf-721
v1.0.45
Published
Improvement to the telegraf library with functional programming, error handling, ...
Downloads
6
Readme
telegraf-721
Based on telegraf and telegraf-i18n
It has the following additions to improve the telegraf library usage:
Functional Programming
Either
Use left(l: L) or right(r: R) static functions in Either class to create Either for erroneous and valid values correspondingly.
export class HttpsError{ constructor(private message: string) { } get getError(){ return this.message } }
export class HttpsResponse{ constructor(public response: any) { } }
import axios from "axios"; import {Either} from "telegraf-721"; export class AxiosDatasource{ ... async get(): Promise<Either<RestResponseFailure, RestResponse>> { try{ const response = awiat this.axios.get(this.url) return Either.right(new HttpsResponse(response)) }catch(error){ return Either.left(new HttpsError(error.message)) } } }
The result of the above function(Either type) can be consumed using functions of the Either instance; fold, foldLeft, foldRight, getOrElse, getLeft or getRight.
import {AxiosDatasource} from "./axios_datasource" const someFunction = () => { const response = await new AxiosDatasource().get() response.fold(async l => { // Display or log error using l.getError }, async r => { // Consume valid value using r.response })
Option
You can use option similar to the Either
You can use Option.none() and Option.some() to create Option object.
Similar functions to consume the value of an Option is also present here.
Dependency Injection
You can register all your instances globally using the DependencyProvider class. You can register both singletons and lazy singletons. The provider class is a singleton on its own. It has a getInstance static function for instantiation, but you won't have to use it because it is instantiated internally. You just need to import that.
import {provider} from "telegraf-721" provider.registerSingleton( "unique indentifier", new AxiosDatasource() ) provider.registerLazySingleton( "unique indentifier", () => new AxiosDatasource() )
The first usage is better suited for instances that are more likely to be used or are necessary during instantiation. The second usage is better for instances that may not be needed during the lifecycle of the application or are not need during instantiation. You can fetch the instance using the get function.
import {provider} from "telegraf-721" const axiosDatasource = provider.get<AxiosDatasource>("unique indentifier")
You should specify the type of the instance you are fetching if you read values of the instance since it can't induce the type of the instance automatically.
Bot Helpers
So far, we have only added basic thing to write clean code. In this section we move to telegraf specific things.
MyCommand
This class is used to define a command in telegram. Simple usage with the dependency providerimport {provider, MyCommand} from "telegraf-721"; import {CommonCommandHandlers} from "../somewhere"; provider.registerLazySingleton( "startCommand", () => new MyCommand( "start", CommonCommandHandlers.start ) )
MyInlineKeyboard
This is a parent class to different classes that define an inline keyboard in telegram. This library has the following extensions:- MyUrlInlineKeyboard: An inline keyboard with url property set.
import {MyUrlInlineKeyboard} from "telegraf-721"; new MyUrlInlineKeyboard( "bot.urlbutton.label", "www.mywebsite.com" )
- MyWebAppInlineKeyboard: An inline keyboard with web_app.url property set.
import {MyWebAppInlineKeyboard} from "telegraf-721"; new MyWebAppInlineKeyboard( "bot.webappbutton.label", "www.myminiapp.com" )
- MyCallbackInlineKeyboard: Parent to the classes below
- MyCoreCallbackInlineKeyboard: An inline keyboard with callback_data property set.
The callback_data will be the coreCallback or a regExp with the coreCallback and the dataPattern you provide.
The regExp looks like ^${this.coreCallback}|${this.dataPattern}$
Usage with only a coreCallback
Usage with both coreCallback and dataPatternimport {MyCoreCallbackInlineKeyboard} from "telegraf-721"; import {InlineKeyboardHandlers} from "../somewhere"; new MyCoreCallbackInlineKeyboard( "bot.continueButton.label", "continue", InlineKeyboardHandlers.continue )
The above InlineKeyboard can be used to show a list of inline keyboards with different number values. See example under MyMarkup below.import {MyCoreCallbackInlineKeyboard} from "telegraf-721"; import {InlineKeyboardHandlers} from "../somewhere"; import {itemNumberPattern} from "../somewhere"; new MyCoreCallbackInlineKeyboard( "bot.continueButton.label", "itemNumber", InlineKeyboardHandlers.onItemSelected, itemNumberPattern )
- MyDataPatternInlineKeyboard: An inline keyboard with callback_data property set. This one is similar to MyCoreCallbackInlineKeyboard except for the coreCallback
import {MyDataPatternInlineKeyboard} from "telegraf-721"; import {InlineKeyboardHandlers} from "../somewhere"; new MyDataPatternInlineKeyboard( "NOT DEFINED", '.+', InlineKeyboardHandlers.matchAny )
MyKeyboard
Just like MyInlineKeyboard, this is a parent class for the different forms of keyboards- HandledKeyboard: Parent class for keyboards with handlers.
- MyLabelPatternKeyboard: A keyboard for matching a range of values with regExp like the MyDataPatternInlineKeyboard.
import {MyLabelPatternKeyboard} from "telegraf-721"; import {KeyboardHandlers} from "../somewhere"; new MyLabelPatternKeyboard( ".+", KeyboardHandlers.matchAny )
- MyLabelKeyboard: A keyboard only text set.
import {MyLabelPatternKeyboard} from "telegraf-721"; import {KeyboardHandlers} from "../somewhere"; new MyLabelKeyboard( "common.buttonLabels.back", KeyboardHandlers.back )
- MyLabeledOnlyKeyboard: Similar to the above keyboard, but no handler function is attached. The result of tapping such button invokes a scene or bot handler function instead
- MyExtraFunctionKeyboard: A keyboard with request_contact or request_location set.
import {MyExtraFunctionKeyboard} from "telegraf-721"; new MyExtraFunctionKeyboard( "user.buttonLabels.shareContact", { requestContact: true } )
MyMarkup
This class is used to create a reply_markup attribute for sendMessage and other functions that accept this attribute. It has four basic functions:- getInlineKeyboardMarkup: Used to create InlineKeyboardMarkup
Previous Definition:
reply_markup creationimport {MyCoreCallbackInlineKeyboard} from "telegraf-721"; import {InlineKeyboardHandlers} from "../somewhere"; import {itemNumberPattern} from "../somewhere"; provider.registerLazySingleton( "numberedButton", new MyCoreCallbackInlineKeyboard( "bot.continueButton.label", "itemNumber", InlineKeyboardHandlers.onItemSelected, itemNumberPattern ) )
This function takes a third parameter layout with type ButtonLayoutimport {MyMarkup, MyCoreCallbackInlineKeyboard} from "telegraf-721"; const array = Array.from({ length: 8 }, (_, i) => i + 1); array.forEach(item => { keyboards.push( provider.get<MyCoreCallbackInlineKeyboard>(numberedButton).mutateAndGet({ localizationKey: item.toString(), data: item, createNewInstance: true, translated: true }) ) }) MyMarkup.getInlineKeyboardMarkup(ctx, keyboards)
By default, countInRow is set to 2 actual is an array of numbers, where each number indicates the number of column in the nth(array index) row. It is given priority over countInRow if both are passedexport interface ButtonLayout { countInRow?: number, actual?: number[] }
- getKeyboardMarkup: Used to create ReplyKeyboardMarkup
This also has similar third parameter.import {MyMarkup} from "telegraf-721"; MyMarkup.getKeyboardMarkup(ctx, [ provider.get("keyboard1"), provider.get("keyboard2"), ] )
- getRemoveKeyboardMarkup: Used to create ReplyKeyboardRemove
- getForceReplyMarkup: Used to create ForceReply
- getInlineKeyboardMarkup: Used to create InlineKeyboardMarkup
Previous Definition:
MyScene
A child class of the WizardScene of telegraf.
The code below shows the registration of a MyScene instance on the provider.
You can see that it takes three parameters:import {provider, MyScene} from "telegraf-721"; import {UserRegistrationSceneHandlers} from "../somewhere"; provider.registerLazySingleton( "userRegistrationScene", () => new MyScene( "userRegistration", { enter: UserRegistrationSceneHandlers.enter, steps: [ UserRegistrationSceneHandlers.phoneNumber, UserRegistrationSceneHandlers.firstName, UserRegistrationSceneHandlers.lastName, UserRegistrationSceneHandlers.password, ], leave: UserRegistrationSceneHandlers.leave }, { keyboards: [ provider.get("keyboard1"), provider.get("keyboard2") ], inlineKeyboards: [ provider.get("registrationBackKeyboard"), provider.get("inlineKeyboard2") ], commands: [ provider.get("startCommand") ] } ) )
- id: unique identifier of the scene
- handlers: different functions that are invoked during the entry, stay and leaving of the scene.
You can move from one step of the scene to the next or back like so:
you can also use ctx.wizard.selectStep(5) to jump to a specific step.export class UserRegistrationSceneHandlers{ static async phoneNumber(ctx: TelegrafContext) { // logic to check input if(logicPass) { return ctx.wizard.next() } } } export class UserRegistrationKeyboardHandlers{ static async back(ctx: TelegrafContext) { switch (ctx.wizard.cursor) { case 2: return ctx.wizard.back() } } } }
- interactors: different interactive elements you want to be invoked while you are in that scene.
You can collect your scenes in a stage and attach them to your bot.import {provider} from "telegraf-721" provider.registerSingleton("mainStage", new Scenes.Stage( [ provider.get("userRegistrationScene"), provider.get("anotherScene1"), provider.get("anotherScene2"), ] ))
MyBot
Contains the telegraf instance and other interactive elements that the telegraf instance should respond to
Typical usage:
The bots core function that makes a request to the api has been updated.import {provider, MyBot} from "telegraf-721" const botConfig = provider.get<Config>("botConfig") const bot = new MyBot( "botToken", { middlewares: [ provider.get<Scenes.Stage<any>>("mainStage").middleware(), ], interactors: { keyboards: [ provider.get("botKeyboard1") ], inlineKeyboards: [ provider.get("botInlineKeyboard1") ], commands: [ provider.get("startCommand") ] }, translatorMiddleware: new I18n({ directory: path.join(process.cwd(), '/assets/locales'), defaultLanguage: 'en', sessionName: 'session', useSession: true, allowMissing: true, defaultLanguageOnMissing: true, }).middleware(), session: botConfig.redisUrl ? new RedisSession({ store: { url: botConfig.redisUrl!, host: '127.0.0.1', port: 6379, ...(botConfig.redisUrl?.startsWith('rediss') ? {tls: {}} : {}) }, getSessionKey: (ctx) => { if (!ctx.from || !ctx.chat) { return } return `MyBot|${ctx.from.id}:${ctx.chat.id}` } }) : session(), testEnv: botConfig.testEnv } );
This was done to enforce a better error handling. The function looks like the following:
As you can see from the function, any call to the telegram API responds with an Either.async function mutateTelegrafAPICallFunction(tg: Telegram) { const oldCallApi: typeof tg.callApi = tg.callApi.bind(tg); tg.callApi = async function newCallApi(method, payload, signal) { return oldCallApi(method, payload, signal) .then((value) => { return Either.right(new Success(value)) }) .catch((e) => { console.log("Error caught in callApi\n", e) return Either.left(new SimpleFailure(e.message)) }) as any; }; }
This forces the consumer to handle both error and valid value cases, if the consumer plans to work with the API call response.
If not at least the error will be caught and logged to the console, preventing the whole application from crashing.
This is why the return type of all functions in the TelegrafContext has been altered from the original type.
Example:... reply(text: string, extra?: tt.ExtraReplyMessage): Promise<Either<Failure, Success<tt.Message>>> ...