{"id":21853636,"url":"https://github.com/incetarik/telegram-bot-framework","last_synced_at":"2025-04-14T16:41:16.514Z","repository":{"id":138327143,"uuid":"257311703","full_name":"incetarik/telegram-bot-framework","owner":"incetarik","description":"A bot framework for Telegram utilizing Telegraf library.","archived":false,"fork":false,"pushed_at":"2024-12-10T11:32:10.000Z","size":326,"stargazers_count":10,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-28T05:23:38.630Z","etag":null,"topics":["bot","telegraf","telegram","telegram-bot"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/incetarik.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-20T14:46:04.000Z","updated_at":"2024-12-10T11:32:06.000Z","dependencies_parsed_at":null,"dependency_job_id":"8a8c9594-5231-4598-aaef-4f60fa34e060","html_url":"https://github.com/incetarik/telegram-bot-framework","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/incetarik%2Ftelegram-bot-framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/incetarik%2Ftelegram-bot-framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/incetarik%2Ftelegram-bot-framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/incetarik%2Ftelegram-bot-framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/incetarik","download_url":"https://codeload.github.com/incetarik/telegram-bot-framework/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248918133,"owners_count":21183124,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot","telegraf","telegram","telegram-bot"],"created_at":"2024-11-28T01:26:00.631Z","updated_at":"2025-04-14T16:41:16.497Z","avatar_url":"https://github.com/incetarik.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# telegram-bot-framework\nTelegram bot framework wrapper for Telegram bot development,\nutilizes `Telegraf` library.\n\nThis library provides set of decorators that could be used for a class and its\nproperties and functions for the behavior of the bot.\n\n**NOTE**: To track last changes, please see the\n[CHANGELOG](https://github.com/incetarik/telegram-bot-framework/blob/master/CHANGELOG.md)\nfile.\n\n- `@bot(settings?: IBotSettings)` class decorator: This decorator is used for\na class to mark it is the logical implementations of the bot behaviors.\nThe functions inside of the class will be used as `action` and `command`\nfunctions. Additionally, it enables this class to have its user/client\ndependent properties.\n\n- `@action(settings?: IActionDecoratorOpts)` method decorator: This decorator\nis used to mark a function as an action of the bot. So that when a command is\ncalled, it could have `callback_data` equals to the function name or the name\ndefined in the settings to execute the function.\n\n- `@command(settings?: ICommandDecoratorOpts)` method decorator: This decorator\nis used to mark a function as a command of the bot. So that when a command is\nsent by the client matching the name of the function, or with the name given in\nthe settings.\n\n- `@hears(settings: IHearsDecoratorOpts | string)` method decorator: This\ndecorator is used to mark a function as a `hears` handler of the bot.\nThis decorator takes either a string for **exact match** or an object\ndescribing the information and the regular expression to match when user sends\na message. Additionally, the match groups will be passed as parameters to the\nfunction.\n\n- `@help()` method decorator: This decorator is used to mark a function as the\ncorresponding function for `/help` command. This decorator is provided to\nprovide a way of having a function with any name to make it `/help` function.\nInstead, you may use a function with **help** name as usual and it will be\nassumed as the function for `/help`. The **help** function may be both async\nand generator.\n\n- `@state(settings?: IBotStateSettings)` property decorator: This decorator is\nused to mark a property as user/client dependent property.\nHence, the property is user based, each user will have its own value of\nthe property. In this way, it is possible to have properties for the bot\nitself and the current user invoking the function.\n\n## General Information\nThe recommended style of coding is, declaring functions inside of the class\nwith their roles in the bot. For example, if the function is corresponding\nto a command the bot has, then decorating it with `@command()` decorator.\nLikewise, if the function is corresponding to an action the bot has, the\n`@action()` decorator.\n\nEach of the decorators have their default values, so it could be left empty\n(no parameters given).\n\nThe system provided with the `@bot()` decorator allows all of the functions\nto be async, hence, it is recommended to make all of your functions async if\nit is better.\nAdditionally, the system also provides a way of handling with generator\nfunctions, so it is even better if you want to have more control over your\nfunction. In this way, you can simply `yield` a message, or an input, and\nthe generator will be awaiting for the user input that you can get from the\n`yield` value.\n\nThe function may be both async and generator functions. Hence, you can both\n`await` for async actions, and `yield` for inputs or messages.\n\nThe system supports multilanguage responses, the `input` and `message` property\nof the `yield` values or the messages passed to `input$()` and `message$()`\nfunctions would be `keys` defined before to get the translated message. You can\nstill use `_` (underscore) function to get the translation and format it\nhowever you wish.\n\nThe `@bot()` class decorator adds some functions and properties to the class\nto provide easy access to the context and the `Telegraf` reference including\nwith helper functions like `input$()` and `message$()`.\n\nGenerally, bots will be reacting to the multiple users all-together. So\nit is recommended to have such properties defined in the class with `@state()`\ndecorator.\n\nWe may count the `@command()` functions as the enterance points to the bot,\nsince user will be able to call this functions directly with any `/command`.\nAnd the `@action()` are defined in the `@command()` to provide alternative\nexecution flow/steps. There are several issues with this:\n- User may re-execute a command function, which then the state should be\ninvalidated.\n- User may execute a command function and does not reply with any message.\nWhich may cause memory problems since the user infomation is kept in memory.\n\nTo prevent such cases, the `@command()` decorator provides `resetStates`\noption which is an array of strings or strings with new values. Use this\nproperty if some of your user properties should be reset before executing\nanother flow. Additionally, `@command()` provides timeout property.\n\nActions should have the same name with `callback_data`, so `@action()` function\nname should match. It is also possible to rename/alias these function names\nby setting `name` property for both of `@command()` and `@action()`.\n\nFor async functions, since they are not generators, it is not possible to yield\nan object to ask for input or sending a message. For this, you can use\n`input$()` and `message$()` async functions to provide the same functionality.\n\nTo start the bot, just create an instance of the class. And then just\n`.run()` the bot.\n\n## Approach\n- Ensure you have BOT_TOKEN environment (or you can change this)\n- Decorate your class with `@bot()`\n- Define the properties you will use for the bot itself and user data.\n- Decorate user data with `@state()`\n- Define command functions and decorate them with `@command()`. Better\nif async generators.\n- Define action functions and decorate them with `@action()`.\n- Define custom functions when user sends a message and that matches with\n`@hears()`.\n- Have a cancel/cleanup function and reset all user variable. Also\n`cancelInput()` to prevent to process previous input from user if there\nis any function awaiting any.\n\nSeveral cases to be **careful**:\nFirst, if you have function that awaits for an user input and that function\ncan be called multiple times (like, by an action function), then the previous\nawait statement would be resulted in `cancelled` state so you should be careful\nabout checking whether the value returned from the user input (`input$()`)\nis cancelled or not.\n\nFor example, if you have a search engine and the actual logic implementation of\nthe search is extracted as another function called `doSearch(text: string)`,\nand your command function is `search()` and it uses `doSearch(text: string)`,\nand you send input message with some `extra` input actions for navigating next\nand previous which will chage the user `state` property of `pageIndex`, in that\ncase you should have a check whether the returned value from the search is\na cancel symbol or not. Because whenever the user navigates to other page and\nexecutes `doSearch(text: string)` function again after increasing the\n`pageIndex` so the next page will be returned, the previous page would be still\nwaiting for an user input. And if user replies to the message, the message will\nbe processed by the latest call and the first call will result with cancel sym.\n\nYou should also be careful about user states. They are not belonging to\nthe class' own instance only, they are owned differently for each user. So\nchanging for one user may not affect all users. For common properties, do not\ndecorate them with `@state()` decorator since that is not a user state in this\ncase but the state of the bot.\n\nThe result of the `input$()` may result as `false`, which means that the user\ninput did not match. In that case, you may simply cancel the operation by\nreturning from an if block you check for this.\n\nIf you want to have dynamic action visibilities, then set `hide` property to\none of your `@state()` properties. For example, in the previous example, if\nyou know how much page you have, for next button you could have the condition\nsuch as `this.searchIndex === pageCount - 1`, so at last page, the next button\nis now hidden.\n\nTo edit the same message again and again to provide some functionality similar\nto pagination, use `didMessageSend(message: Message)` function to get the last\nsent message by the bot and set it to one `state` property, such as\n`@state() private _messageToUpdate?: Message`, then pass this variable to the\ninput call like:\n\n```js\nconst selection = await this.input$({\n  input: `Page: ${pageIndex}\\n${list}`,\n  edit: this._messageToUpdate,\n  match: /^(next|prev|cancel|\\d\\d?)$/i,\n  matchError: 'Please enter \"next\", \"prev\", \"cancel\" or a number',\n  cancelPrevious: true,\n  didMessageSend: message =\u003e this._messageToUpdate = message,\n})\n\nif (this.isCancelled(selection)) {\n  // Cancelled by second call (the await above)\n  return\n}\n\nif (!selection) {\n  // User message did not match\n  return\n}\n```\n\nIn this way, you will be able to update the message to update next time and\npass that variable to `edit` property.\n\nAnother **important** thing in the given example above is `cancelPrevious`\nproperty. It indicates that this input call will cancel the previous one so\nthat the previous function call will be resolved with cancel symbol. Which is\nhandled by the following condition in the example.\n\nLastly, for **hears** decorated functions, when the string is passed as a\nfilter, then the string is expected to match exactly. If you don't want this\nyou may pass an object containing `match` property as string or `RegExp`.\n\nThe hears functions **WILL BE** ignored by default if the message is matched\nwith the filter but it is sent during a `@command` function execution.\nThis behavior may be changed in the decorator setting.\n\n## Notes\n- You can set usage limits or timeouts (for updating or reading) for the\nstate properties and also provide the value to assign when it is expired or\ntimed out.\n- You can set a timeout for a command function.\n- You can reset several state properties and/or provide the new value for\nresetting.\n- You can reach the matches with `${NUM}` properties such as `this.$0` for\nfull match and `this.$9` for the ninth match of the `@hears()` function.\n- You can keep the last match of any `@hears()` function by setting\n`keepMatchResults` property of the decorator to true.\n- You will have matched groups in your parameters for `@hears()` functions.\n- You can add middlewares or config the `Telegraf` instance by\noverriding/defining the `run()` function inside the class manually and\nusing the `this.ref` property to manage all of changes before you start.\nDon't forget to call `this.init()` to make all of these things work and\n`this.ref.launch()` and `this.ref.startPolling()`\n\n- You can listen for events, the package uses RxJS Observables.\n- You can disable emitting an event for property changes or action/command\nfunctions.\n- You can set `run` function manually, in that case do not forget to call\n`init()` function to provide all of these functionalities. If you do not\ndefine `run` function manually, then it will be defined automatically that\nstarts the bot.\n- You can reach the underlying `Telegraf` instance by `this.ref`.\n- You can reach the current `ContextMessageUpdate` by `this.context`.\n- You can always make a pull request to improve the library!\n\n## Setup\nTo use this package, adding this package name into `dependencies` section of\nyour `package.json` file would be enough. Ensure that your node executing this\nlibrary supports decorators. You may use a transpiler such as Babel to compile\nthis to older versions of JavaScript. Hence, you will not have decorator\nproblem.\n\nIf you are using TypeScript, you need to add `typescript` into your\n`devDependencies` section of `package.json`. Then you need to have a\n`tsconfig.json` file which is used for setting TypeScript compiler properties.\nEnsure that you have `experimentalDecorators` property set to `true` in\n`compilerOptions`.\n\nYou can create this file by `tsc --init` command if you have TypeScript.\n\nAfter you done the language and the package sides, you just need to run the\nfile your code is written (or compiled file by TypeScript) and your bot should\nbe serving.\n\n## Examples\nA hello world bot would be like this\n```js\n@bot()\nclass SayHiBot {\n  @command()\n  async * sayHi() {\n    yield { message: 'Hello 👋' }\n  }\n}\n\nconst sayHiBot = new SayHiBot()\nsayHiBot.run()\n```\n\n---\nA bot multiplying a number and reverses a string, and listening for something\nwould be like\n\n```js\n@bot()\nclass OpBot {\n  @command()\n  async * multiply() {\n    const firstNumStr = yield {\n      input: 'Please enter the first number',\n      match: /\\d+/,\n      matchError: 'Invalid number'\n    }\n\n    const firstNum = parseInt(firstNumStr, 10)\n\n    const secondNumStr = yield {\n      input: 'Please enter a digit to multiply',\n      match: /[1-9]/,\n      matchError: 'Please enter a digit between 1-9'\n    }\n\n    const secondNum = parseInt(secondNumStr, 10)\n    const result = firstNum * secondNum\n    yield {\n      message: `Result is ${result}`\n    }\n  }\n\n  @command({ name: 'reverse' })\n  async * reverseaString() {\n    let message = yield {\n      input: 'Enter a message to reverse',\n    }\n\n    message = message.split('').reverse().join('')\n    yield { message }\n  }\n\n  @hears('now')\n  * sendNow() {\n    yield (new Date()).toLocaleString()\n  }\n\n  @hears({ match: /(\\d+)\\s+\\+\\s+(\\d+)/ })\n  * add(left: string, right: string, _allMatch: string) {\n    yield parseInt(left) + parseInt(right)\n\n    // When user sends 3 +  5\n    // left      -\u003e \"3\"\n    // right     -\u003e \"5\"\n    // _allMatch -\u003e \"3 +  5\"\n    //\n    // Likewise, you can reach these matches with ${NUM} properties.\n    // this.$0   -\u003e \"3 +  5\"\n    // this.$1   -\u003e \"3\"\n    // this.$2   -\u003e \"5\"\n  }\n}\n```\n\n---\nAn advanced bot supports pagination. The following example is the one of the\nextreme cases you may face with. Here, we have a video downloader bot which\nlooks through the pages from its source and returns us an object containing\nthe names of the videos as string array and the total number of pages and\nthe current page number.\n\nIn this example, the bot is editing the last message it sent so the bot will\nnot be sending many messages since they are similar to each other but instead\nwill update the last sent message so you will have a pagination-like message\nwith three buttons under of it and you also provide functions for them in the\nclass with `@action()`. Likewise, you are hiding the previous and the next\nbuttons according to the current page index. When cancelled, the cleanup is\ndone.\n\nNote that the next condition after the value is returned from the observable,\nit is checking whether the returned value is a cancel symbol or not. Because\nthe `input$()` takes an object describing that the previous message will be\ncancelled (`cancelPrevious`) and client may send messages until it `match`-es\nwith the condition and if the client fails for 3 times, the operation is\ncancelled (returned false, so the second condition will end the execution).\n\nAlso note that the `/search` command has a timeout of 5 seconds and whenever\nit is sent again by the user, it invalidates and resets the previous states\nof given list.\n\n```ts\n@bot()\nclass VideoDownloaderBot {\n  @state() _messageToEdit?: Message\n  @state() pageIndex: number = 0\n  @state() downloader = new VideoDownloader()\n  @state() isSearching = false\n  @state() searchQuery?: string\n\n  private doSearch(text?: string) {\n    if (this.isSearching) { return }\n    text = text || this.searchQuery\n\n    this.isSearching = true\n    this.downloader.search$(text, { pageIndex: this.pageIndex }).subscribe(async value =\u003e {\n      this.isSearching = false\n      const { lines } = value\n      const selection = await this.input$({\n        input: `Page: ${value.pageNumber}\\n${lines.join('\\n')}`,\n        match: /^(next|prev|cancel|\\d\\d?)$/i,\n        matchError: 'Invalid Input',\n        extra: Extra.markup(Markup.inlineKeyboard([\n          { text: '⬅️', callback_data: 'didPrevClick', hide: this.pageIndex === 1 },\n          { text: '❌', callback_data: 'didCancelClick', hide: false },\n          { text: '➡️', callback_data: 'didNextClick', hide: this.pageIndex === value.pageCount - 1 },\n        ])),\n        edit: this._messageToEdit,\n        cancelPrevious: true,\n        retry: 3,\n        didMessageSend: async (message) =\u003e { this._messageToEdit = message }\n      })\n\n      if (this.isCancelled(selection)) {\n        return\n      }\n\n      if (!selection) {\n        await this.message$('Operation is cancelled')\n        return\n      }\n    })\n  }\n\n  @command({ timeout: 5000, resetStates: [ 'searchQuery', 'pageIndex', 'isSearching', '_messageToEdit' ] })\n  async * search() {\n    if (this.isSearching) { return }\n    this.isSearching = false\n    this.pageIndex = 1\n\n    const text = yield {\n      input: 'What are you looking for?',\n      match: /\\w.{2,}/,\n      matchError: 'Invalid inut'\n    }\n\n    this.searchQuery = text\n\n    yield { message: 'Searching…' }\n    await this.doSearch(text)\n  }\n\n  @action()\n  async didPrevClick() {\n    --this.searchIndex\n    await this.doSearch()\n  }\n\n  @action()\n  async didNextClick() {\n    ++this.searchIndex\n    await this.doSearch()\n  }\n\n  @action({ emitsEvent: false })\n  async didCancelClick() {\n    await this.message$('Operation is cancelled')\n    if (this._messageToUpdate) {\n      await this.context.deleteMessage(this._messageToUpdate!.message_id)\n    }\n\n    this.cancelInput()\n\n    this.pageIndex = 1\n    this.searchQuery = undefined\n    this._messageToUpdate = undefined\n    this.isSearching = false\n  }\n}\n```\n\n---\nAnother example includes an assembly (just add and sub) operating machine\nexample.\n\n```ts\ninterface ASMBot extends IBot {}\n\n@bot()\nclass ASMBot {\n  @state() stack: number[] = []\n\n  @command()\n  async *asm() {\n    yield {\n      message: 'Please use `\"push $num\"` and one of `\"add\"`, `\"sub\"`, `\"pop\"`, `\"stop\"`',\n      // Mark the message as markdown\n      extra: Extra.markdown(true)\n    }\n\n    while (true) {\n      const next = yield {\n        input: 'Next:',\n        match: /add|sub|pop|stop|(push\\s+\\d{1,5})/,\n        // This will be asked (sending \"Next:\") if user input was not valid\n        keepAsking: true,\n        // Expect user to enter this correctly, forever\n        retry: Infinity\n      }\n\n      switch (next.slice(0, 4).trim()) {\n        case 'add': {\n          const right = Number(this.stack.pop() ?? '0')\n          const left = Number(this.stack.pop() ?? '0')\n          this.stack.push(left + right)\n          break\n        }\n        case 'sub': {\n          const right = Number(this.stack.pop() ?? '0')\n          const left = Number(this.stack.pop() ?? '0')\n          this.stack.push(left - right)\n          break\n        }\n        case 'pop': {\n          const value = this.stack.pop()\n          if (typeof value === 'undefined') {\n            yield { message: 'No value found' }\n          }\n          else {\n            yield { message: `${value}` }\n          }\n          break\n        }\n        case 'push': {\n          const [ , value ] = next.split(' ')\n          const num = Number(value)\n          this.stack.push(num)\n          break\n        }\n        case 'stop': {\n          yield {\n            message: `Execution is stopped\\nStack:\\`\\`\\`\\n\\n${JSON.stringify(this.stack, undefined, 1)} \\`\\`\\``,\n            extra: Extra.markdown(true)\n          }\n          return\n        }\n      }\n    }\n  }\n}\n\nconst b = new ASMBot()\nb.run()\n```\n\nIf you want to support to the project:\n\n```md\n- Bitcoin     : 153jv3MQVNSvyi2i9UFr9L4ogFyJh2SNt6\n- Bitcoin Cash: qqkx22yyjqy4jz9nvzd3wfcvet6yazeaxq2k756hhf\n- Ether       : 0xf542BED91d0218D9c195286e660da2275EF8eC84\n- Stellar     : GATF6DAKFCYY3MLNAIWVISARP52EWPOPFFZT4JMFENPNPERCMTSDFNY5\n```\n\nThank You.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fincetarik%2Ftelegram-bot-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fincetarik%2Ftelegram-bot-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fincetarik%2Ftelegram-bot-framework/lists"}