├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── ScreenShot.png ├── api-scheme ├── GenerateTGModels.js └── models │ ├── ModelsGenerator.js │ ├── SchemeClass.js │ └── SchemeClassField.js ├── jsdoc.json ├── lib ├── BaseTelegramDataSource.js ├── Telegram.js ├── TelegramDataSource.js ├── api │ ├── InputFile.js │ ├── TelegramApi.js │ ├── TelegramApiError.js │ └── TelegramApiRequest.js ├── ipc │ └── TelegramIPC.js ├── localization │ ├── Ivan.js │ └── Localization.js ├── logger │ ├── BaseLogger.js │ ├── ConsoleLogger.js │ └── WebAdminLogger.js ├── models │ ├── CallbackGame.js │ ├── InlineQueryResult.js │ └── InputMessageContent.js ├── mvc │ ├── BaseScopeExtension.js │ ├── InlineScope.js │ ├── Scope.js │ ├── TelegramBaseCallbackQueryController.js │ ├── TelegramBaseController.js │ └── TelegramBaseInlineQueryController.js ├── routing │ ├── TelegramRoute.js │ ├── TelegramRouter.js │ └── commands │ │ ├── AnyCommand.js │ │ ├── BaseCommand.js │ │ ├── CustomFilterCommand.js │ │ ├── RegexpCommand.js │ │ └── TextCommand.js ├── statistics │ └── Statistics.js ├── storage │ ├── BaseStorage.js │ ├── session │ │ ├── InMemoryStorage.js │ │ ├── TelegramSession.js │ │ └── TelegramSessionStorage.js │ └── sharedStorage │ │ ├── SharedStorage.js │ │ └── models │ │ ├── GetMessage.js │ │ ├── Message.js │ │ ├── RemoveMessage.js │ │ ├── ResponseMessage.js │ │ └── SetMessage.js ├── updateFetchers │ ├── BaseUpdateFetcher.js │ ├── LongPoolingUpdateFetcher.js │ └── WebhookUpdateFetcher.js ├── updateProcessors │ ├── BaseUpdateProcessor.js │ ├── InlineQueryUpdateProcessor.js │ ├── MessageUpdateProcessor.js │ ├── UpdateProcessorDelegate.js │ └── UpdateProcessorsManager.js ├── utils │ └── CallbackQueue.js └── webAdmin │ ├── client │ ├── client.js │ ├── index.html │ ├── logo.svg │ └── style.css │ └── server │ └── WebAdmin.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | docs 4 | lib/models/* 5 | !lib/models/InlineQueryResult.js 6 | !lib/models/InputMessageContent.js 7 | !lib/models/CallbackGame.js 8 | ScreenShot.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | docs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Narek Abovyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telegram-node-bot 2 | Very powerful module for creating Telegram bots. 3 | 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=KDM7K3BBVV2E8) 5 | 6 | [Full API reference](http://nabovyan.xyz/telegram-node-bot/) 7 | 8 | [help chat](http://nabovyan.xyz/tg-dev-chat) 9 | 10 | ## Installation 11 | 12 | To install the stable version: 13 | 14 | ```bash 15 | npm install --save telegram-node-bot 16 | ``` 17 | 18 | This assumes you are using [npm](https://www.npmjs.com/) as your package manager. 19 | If you don’t, you can access these files on [unpkg](https://unpkg.com/telegram-node-bot/), download them, or point your package manager to them. 20 | 21 | ## Whats new in 4.0? 22 | 23 | * Bug fixes 24 | * Clustering 25 | * New router 26 | * Web admin 27 | 28 | ## Get started 29 | 30 | First of all you need to create your bot and get Token, you can do it right in telegram, just write to @BotFather. 31 | 32 | Now let's write simple bot! 33 | 34 | ```js 35 | 'use strict' 36 | 37 | const Telegram = require('telegram-node-bot') 38 | const TelegramBaseController = Telegram.TelegramBaseController 39 | const TextCommand = Telegram.TextCommand 40 | const tg = new Telegram.Telegram('YOUR_TOKEN') 41 | 42 | class PingController extends TelegramBaseController { 43 | /** 44 | * @param {Scope} $ 45 | */ 46 | pingHandler($) { 47 | $.sendMessage('pong') 48 | } 49 | 50 | get routes() { 51 | return { 52 | 'pingCommand': 'pingHandler' 53 | } 54 | } 55 | } 56 | 57 | tg.router 58 | .when( 59 | new TextCommand('ping', 'pingCommand'), 60 | new PingController() 61 | ) 62 | ``` 63 | That's it! 64 | 65 | ![Bot](ScreenShot.png) 66 | 67 | ## Introduction 68 | 69 | I'm using something like MVC, so we have router and controllers. 70 | First you need to declare your commands and which controller will handle it. 71 | Then you need to write controllers and handle specific commands in it. 72 | 73 | ## Router 74 | Lets say our bot has three commands: /start, /stop and /restart 75 | And we want that commands to be handled by different controllers. 76 | 77 | Router declaration code will be like this: 78 | 79 | ```js 80 | tg.router 81 | .when(new TextCommand('/start', 'startCommand'), new StartController()) 82 | .when(new TextCommand('/stop', 'stopCommand'), new StopController()) 83 | .when(new TextCommand('/restart', 'restartCommand'), new RestartController()) 84 | ``` 85 | 86 | Probably we will have a case when user send us command we didn't know, for that case router have `otherwise` function: 87 | 88 | ```js 89 | tg.router 90 | .when(new TextCommand('/start', 'startCommand'), new StartController()) 91 | .when(new TextCommand('/stop', 'stopCommand'), new StopController()) 92 | .when(new TextCommand('/restart', 'restartCommand'), new RestartController()) 93 | .otherwise(new OtherwiseController()) 94 | ``` 95 | 96 | Now all unknown commands will be handled by OtherwiseController: 97 | 98 | ```js 99 | class OtherwiseController extends TelegramBaseController { 100 | handle() { 101 | console.log('otherwise') 102 | } 103 | } 104 | ``` 105 | 106 | In this cases for all controllers will be called `handle` method to handle request. But you can pass your custom handler name as second parameter to any command: 107 | 108 | ```js 109 | tg.router 110 | .when( 111 | new TextCommand('/start', 'startHandler'), 112 | new StartController() 113 | ) 114 | ``` 115 | Then you must add `routes` property to your controller like this: 116 | ```js 117 | class StartConstoller extends TelegramBaseController { 118 | /** 119 | * @param {Scope} $ 120 | */ 121 | start($) { 122 | $.sendMessage('Hello!') 123 | } 124 | 125 | get routes() { 126 | return { 127 | 'startHandler': 'start' 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | You can define controller for inline queries using `inlineQuery` method: 134 | 135 | ```js 136 | tg.router 137 | .inlineQuery(new InlineModeController()) 138 | ``` 139 | 140 | And controllers for callback queries using `callbackQuery`: 141 | 142 | ```js 143 | tg.router 144 | .callbackQuery(new CallbackQueryController()) 145 | ``` 146 | 147 | ## List of all commands 148 | 149 | * TextCommand - just text command like `/start` 150 | ```js 151 | tg.router 152 | .when( 153 | new TextCommand('/start', 'startHandler'), 154 | new StartController() 155 | ) 156 | ``` 157 | * RegextCommand - any regexp command 158 | ```js 159 | tg.router 160 | .when( 161 | new RegexpCommand(/test/g, 'testHandler'), 162 | new TestController() 163 | ) 164 | ``` 165 | * CustomFilterCommand - custom command 166 | ```js 167 | tg.router 168 | .when( 169 | new CustomFilterCommand($ => { 170 | return $.message.text == 'some text' 171 | }, 'customFilterHandler'), 172 | new CustomFilterHandlerController() 173 | ) 174 | ``` 175 | 176 | You can also create your own command, just extend `BaseCommand` 177 | 178 | 179 | ## Controllers 180 | 181 | There are three types of controllers: 182 | 183 | * Controller for messages - `TelegramBaseController` 184 | * Controller for CallbackQueries - `TelegramBaseCallbackQueryController` 185 | * Controller for InlineQueries - `TelegramBaseInlineQueryController` 186 | 187 | 188 | ## TelegramBaseController 189 | 190 | To create controller for message updates you must extend `TelegramBaseController`. 191 | 192 | If you want specific methods of your controller be called for specific commands, you should return a plain object in `routes` property where key is a route and value is name of your method. 193 | In that case `handle` method will not be called and scope will be passed to your method. 194 | Example: 195 | 196 | ```js 197 | class TestController extends TelegramBaseController { 198 | get routes() { 199 | return { 200 | 'test': 'testHandler' 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | If there are no routes defined then `handle` method of your controller will be called. 207 | 208 | There is also `before` method, this method will be called after all updates and you should return the scope, you can modify scope if you want: 209 | 210 | ```js 211 | class TestController extends TelegramBaseController { 212 | before(scope) { 213 | scope.someData = true 214 | 215 | return scope 216 | } 217 | } 218 | ``` 219 | Remember: if you want to handle command in controller you need to declare it in router. 220 | 221 | All instances of TelegramBaseController also have private `_api` property which is a reference to `TelegramApi` and private `_localization` property which is a reference to `Ivan` 222 | 223 | ## TelegramBaseCallbackQueryController 224 | 225 | To create such controller you must extend TelegramBaseCallbackQueryController. 226 | 227 | This controllers are very simple, they have only one method - `handle`, this method will be called for all queries and instance of `CallbackQuery` will be passed. 228 | 229 | ## TelegramBaseInlineQueryController 230 | 231 | To create such controller you must extend TelegramBaseInlineQueryController. 232 | 233 | These controllers also have `handle` method which will be called for all queries and an instance of `InlineScope` will be passed. 234 | Also they its have `chosenResult` method which will be called when user select some result, an instance of `ChosenInlineResult` 235 | 236 | Also as the `TelegramBaseController` it has `_api` and `_localization` properties. 237 | 238 | ## Getting updates 239 | You can use long-pooling or webhooks to get updates. 240 | Long-pooling used by default. To use webhooks you need to init library like this: 241 | ```js 242 | const tg = new Telegram.Telegram('YOUR_TOKEN', { 243 | webhook: { 244 | url: 'https://61f66256.ngrok.io', 245 | port: 3000, 246 | host: 'localhost' 247 | } 248 | }) 249 | ``` 250 | You can also create any other custom update fetcher: just extend Telegram.BaseUpdateFetcher and pass it to library: 251 | ```js 252 | const tg = new Telegram.Telegram('YOUR_TOKEN', { 253 | updateFetcher: new MyUpdateFetcher() 254 | }) 255 | ``` 256 | 257 | 258 | ## Clustering 259 | By default library will create one worker per cpu. You can change it like this: 260 | 261 | ```js 262 | const tg = new Telegram.Telegram('YOUR_TOKEN', { 263 | workers: 1 264 | }) 265 | ``` 266 | 267 | If you want run some code on main process use `tg.onMaster` method: 268 | 269 | ```js 270 | const tg = new Telegram.Telegram('YOUR_TOKEN', { 271 | workers: 1 272 | }) 273 | 274 | tg.sendMessage(123, 'test message') //will be sent 2 times (one time on master and one time on worker) 275 | 276 | tg.onMaster(() => { 277 | tg.sendMessage(123, 'test message') //will be sent one time 278 | }) 279 | ``` 280 | 281 | ## Web admin 282 | By default library will start web admin at localhost:7777, to change that use `webAdmin` properpty: 283 | ```js 284 | const tg = new Telegram.Telegram('YOUR_TOKEN', { 285 | webAdmin: { 286 | port: 1234, 287 | host: 'localhost' 288 | } 289 | }) 290 | ``` 291 | 292 | ## API 293 | You can call api methods two ways: 294 | 295 | Directly from tg: 296 | 297 | ```js 298 | tg.api.sendMessage(chatId, 'Hi') 299 | ``` 300 | 301 | Or if you using controllers controller will pass you context `$` that already knows current chat id, so it's more easy to use: 302 | 303 | ```js 304 | $.sendMessage('Hi') 305 | ``` 306 | 307 | All methods have required parameters and optional parameters, you can find them in [api documentation](https://core.telegram.org/bots/api#available-methods) 308 | If you want to pass optional parameters you should pass them as an object: 309 | ```js 310 | $.sendMessage('Hi', { disable_notification: true }) 311 | ``` 312 | 313 | ## Scope 314 | 315 | There is two types of scope: 316 | 317 | * scope for message controllers - `Scope` 318 | * scope for inline mode controller - `InlineScope` 319 | 320 | Message controllers scope: 321 | 322 | scope will be passed to `handle` method or to your methods defined in `routes` 323 | 324 | Main feature of scope is that scope already knows current chat id, so there is no need to pass that parameter. 325 | Scope have all api methods that have chatId as their first parameter already filled. 326 | 327 | Scope also contains some information about update. 328 | 329 | 330 | Inline controllers scope also has all api methods filled with userId. 331 | 332 | ## Forms 333 | 334 | In message controllers scope has `runForm` method. 335 | 336 | With `$.runForm` method you can create forms: 337 | 338 | ```js 339 | const form = { 340 | name: { 341 | q: 'Send me your name', 342 | error: 'sorry, wrong input', 343 | validator: (message, callback) => { 344 | if(message.text) { 345 | callback(true, message.text) //you must pass the result also 346 | return 347 | } 348 | 349 | callback(false) 350 | } 351 | }, 352 | age: { 353 | q: 'Send me your age', 354 | error: 'sorry, wrong input', 355 | validator: (message, callback) => { 356 | if(message.text && IsNumeric(message.text)) { 357 | callback(true, toInt(message.text)) 358 | return 359 | } 360 | 361 | callback(false) 362 | } 363 | } 364 | } 365 | 366 | $.runForm(form, (result) => { 367 | console.log(result) 368 | }) 369 | ``` 370 | 371 | Bot will send the 'q' message to user, wait for message, validate it with your validator function and save the answer, if validation fails bot will ask again that question. 372 | You can also do some filtering in your validator, so you can pass the result as second parameter to callback. 373 | You can also pass keyboard to `keyboard` field. 374 | 375 | ## Menu 376 | 377 | You can create menu with $.runMenu function: 378 | 379 | ```js 380 | $.runMenu({ 381 | message: 'Select:', 382 | options: { 383 | parse_mode: 'Markdown' // in options field you can pass some additional data, like parse_mode 384 | }, 385 | 'Exit': { 386 | message: 'Do you realy want to exit?', 387 | resizeKeyboard: true, 388 | 'yes': () => { 389 | 390 | }, 391 | 'no': () => { 392 | 393 | } 394 | }, 395 | 'anyMatch': () => { //will be executed at any other message 396 | 397 | } 398 | }) 399 | ``` 400 | 401 | Bot will create keyboard and send it with your message, when user tap button bot will call its callback, if it's submenu bot will send submenu. 402 | 403 | Layouting menu: 404 | 405 | You can pass the maximum number of buttons in line like this: 406 | 407 | ```js 408 | $.runMenu({ 409 | message: 'Select:', 410 | layout: 2, 411 | 'test1': () => {}, //will be on first line 412 | 'test2': () => {}, //will be on first line 413 | 'test3': () => {}, //will be on second line 414 | 'test4': () => {}, //will be on second line 415 | 'test5': () => {}, //will be on third line 416 | }) 417 | ``` 418 | Or you can pass an array of number of buttons for each line: 419 | 420 | ```js 421 | $.runMenu({ 422 | message: 'Select:', 423 | layout: [1, 2, 1, 1], 424 | 'test1': () => {}, //will be on first line 425 | 'test2': () => {}, //will be on second line 426 | 'test3': () => {}, //will be on second line 427 | 'test4': () => {}, //will be on third line 428 | 'test5': () => {}, //will be on fourth line 429 | }) 430 | ``` 431 | 432 | ## Inline Menu 433 | 434 | You can create inline menu using $.runInlineMenu: 435 | 436 | ```js 437 | $.runInlineMenu({ 438 | layout: 2, //some layouting here 439 | method: 'sendMessage', //here you must pass the method name 440 | params: ['text'], //here you must pass the parameters for that method 441 | menu: [ 442 | { 443 | text: '1', //text of the button 444 | callback: (callbackQuery, message) => { //to your callback will be passed callbackQuery and response from method 445 | console.log(1) 446 | } 447 | }, 448 | { 449 | text: 'Exit', 450 | message: 'Are you sure?', 451 | layout: 2, 452 | menu: [ //Sub menu (current message will be edited) 453 | { 454 | text: 'Yes!', 455 | callback: () => { 456 | 457 | } 458 | }, 459 | { 460 | text: 'No!', 461 | callback: () => { 462 | 463 | } 464 | } 465 | ] 466 | } 467 | ] 468 | }) 469 | ``` 470 | 471 | ## waitForRequest 472 | 473 | Messages controller scope has `waitForRequest` method after calling that the next update from current user will be passed to promise. 474 | 475 | ## waitForCallbackQuery 476 | 477 | If you send some inline keyboard after that you can call this method, pass to it string or array of string with callback data or your InlineKeyboardMarkup and then when user press button CallbackQuery will be passed to Promise 478 | 479 | ```js 480 | $.sendMessage('Send me your name') 481 | $.waitForRequest 482 | .then($ => { 483 | $.sendMessage(`Hi ${$.message.text}!`) 484 | }) 485 | ``` 486 | ## Sessions 487 | 488 | For user: 489 | 490 | ```js 491 | $.setUserSession('someKey', 'some data') 492 | .then(() => { 493 | return $.getUserSession('someKey') 494 | }) 495 | .then(data => { 496 | console.log(data) 497 | }) 498 | ``` 499 | 500 | For chat: 501 | 502 | ```js 503 | $.setChatSession('someKey', 'some data') 504 | .then(() => { 505 | return $.getChatSession('someKey') 506 | }) 507 | .then(data => { 508 | console.log(data) 509 | }) 510 | ``` 511 | 512 | 513 | By default sessions are stored in memory, but you can store them anywhere, you need to extend `BaseStorage` and pass instance of your storage to `Telegram`: 514 | 515 | ```js 516 | const tg = new Telegram.Telegram('YOUR_TOKEN',{ 517 | storage: new MyStorage() 518 | }) 519 | ``` 520 | 521 | ## Logging 522 | 523 | Module makes some logs during work, by default logs are written to console, but you can create your own logger if you want, you must extend `BaseLogger` and pass instance of your logger to `Telegram`: 524 | 525 | ```js 526 | const tg = new Telegram.Telegram('YOUR_TOKEN', { 527 | logger: new MyLogger() 528 | }) 529 | ``` 530 | 531 | ## Localization 532 | 533 | To use localization you need to pass your localization files to `Telegram`, they must be like this: 534 | 535 | ```js 536 | { 537 | "lang": "Ru", 538 | "phrases": { 539 | "startMessage": "тест" 540 | } 541 | } 542 | ``` 543 | 544 | after creating your files you need to pass them to `Telegram`: 545 | 546 | ```js 547 | const tg = new Telegram.Telegram('YOUR_TOKEN', { 548 | localization: [require('./Ru.json')] 549 | }) 550 | ``` 551 | 552 | Now you can use them in controllers like this: 553 | ```js 554 | console.log(this._localization.Ru.startMessage) 555 | ``` 556 | 557 | You can even set the language for specific user: 558 | ```js 559 | this._localization.setLanguageForUser(123456, 'Ru') 560 | ``` 561 | 562 | Or get phrases for user: 563 | ```js 564 | this._localization.forUser(123456) 565 | ``` 566 | 567 | ## Scope extensions: 568 | 569 | Lets say you have some function that you want to be in scope, now you can do that like this: 570 | 571 | 572 | ```js 573 | 'use strict' 574 | 575 | const Telegram = require('telegram-node-bot') 576 | const TelegramBaseController = Telegram.TelegramBaseController 577 | const BaseScopeExtension = Telegram.BaseScopeExtension 578 | const tg = new Telegram.Telegram('YOUR_TOKEN') 579 | 580 | class SumScopeExtension extends BaseScopeExtension { 581 | process(num1, num2) { 582 | return num1 + num2 583 | } 584 | 585 | get name() { 586 | return 'sum' 587 | } 588 | } 589 | 590 | class SumController extends TelegramBaseController { 591 | /** 592 | * @param {Scope} $ 593 | */ 594 | sumHandler($) { 595 | $.sendMessage($.sum($.query.num1, $.query.num2)) 596 | } 597 | 598 | get routes() { 599 | return { 600 | '/sum :num1 :num2': 'sumHandler' 601 | } 602 | } 603 | } 604 | 605 | tg.router 606 | .when(['/sum :num1 :num2'], new SumController()) 607 | 608 | tg.addScopeExtension(SumScopeExtension) 609 | ``` 610 | 611 | ## Sending files 612 | 613 | From file id: 614 | 615 | ```js 616 | $.sendPhoto(InputFile.byId('ID')) or $.sendPhoto('ID') 617 | ``` 618 | 619 | From url: 620 | 621 | ```js 622 | $.sendPhoto(InputFile.byUrl('URL', 'image.jpg')) or $.sendPhoto({ url: 'URL', filename: 'image.jpg'}) 623 | ``` 624 | 625 | By path: 626 | 627 | ```js 628 | $.sendPhoto(InputFile.byFilePath('path/to/file')) or $.sendPhoto({ path: 'path/to/file'}) 629 | ``` 630 | 631 | [Full API reference](http://nabovyan.xyz/telegram-node-bot/) 632 | 633 | ## License 634 | 635 | Copyright (c) 2016 Narek Abovyan 636 | 637 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 638 | 639 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 640 | 641 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 642 | -------------------------------------------------------------------------------- /ScreenShot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naltox/telegram-node-bot/f295c6e32040325ae83d11031812c1490577da80/ScreenShot.png -------------------------------------------------------------------------------- /api-scheme/GenerateTGModels.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ModelsGenerator = require('./models/ModelsGenerator') 4 | const fs = require('fs') 5 | const net = require('tiny_request') 6 | 7 | net.get('https://core.telegram.org/bots/api', data => { 8 | const generator = new ModelsGenerator(data) 9 | 10 | const models = generator.generateModels() 11 | 12 | console.info(`${models.length} models generated\n`) 13 | 14 | models.forEach(model => { 15 | fs.writeFile(`./lib/models/${model.name}.js`, model.modelCode, err => { 16 | if(err) { 17 | return console.error(`Failed to save model: ${model.name}`, err) 18 | } 19 | console.info(`Saved model: ${model.name}`) 20 | }) 21 | }) 22 | 23 | fs.writeFile(`./lib/models/Models.js`, generateAllModelsExport(models), err => { 24 | if(err) { 25 | return console.error(`Failed to save models export file:`, err) 26 | } 27 | console.info(`Saved models export file`) 28 | }) 29 | 30 | }) 31 | 32 | function generateAllModelsExport(models) { 33 | let code = `` 34 | code += `module.exports = {\n` 35 | 36 | models.forEach(model => code += ` ${model.name}: require('./${model.name}'),\n`) 37 | code += ` InputMessageContent: require('./InputMessageContent'),\n` 38 | code += ` InlineQueryResult: require('./InlineQueryResult'),\n` 39 | 40 | code += '}' 41 | 42 | return code 43 | } -------------------------------------------------------------------------------- /api-scheme/models/ModelsGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const SchemeClassField = require('./SchemeClassField') 4 | const SchemeClass = require('./SchemeClass') 5 | const cheerio = require('cheerio') 6 | 7 | const JS_TYPES = { 8 | Integer: 'number', 9 | String: 'string', 10 | Float: 'number', 11 | 'Float number': 'number', 12 | Boolean: 'boolean', 13 | True: 'boolean', 14 | False: 'boolean' 15 | } 16 | 17 | class ModelsGenerator { 18 | /** 19 | * 20 | * @param {string} docPageData 21 | */ 22 | constructor(docPageData) { 23 | this._docPageData = docPageData 24 | } 25 | 26 | /** 27 | * 28 | * @returns {SchemeClass[]} 29 | */ 30 | generateModels() { 31 | let models = [] 32 | 33 | let scheme = this._generateScheme() 34 | 35 | scheme.forEach(model => { 36 | models.push(new SchemeClass( 37 | model.name, 38 | this._prepareFields(model.fields), 39 | model.desc 40 | )) 41 | }) 42 | 43 | return models 44 | } 45 | 46 | /** 47 | * 48 | * @param {string} table 49 | * @returns {SchemeClassField[]} 50 | * @private 51 | */ 52 | _prepareFields(raw) { 53 | let fields = [] 54 | 55 | 56 | 57 | raw.forEach(item => { 58 | let type = this._prepareType(item.type) 59 | 60 | fields.push(new SchemeClassField( 61 | item.field, 62 | type, 63 | item.type.indexOf('Array of Array of') > -1 ? '2d array' : (item.type.indexOf('Array of') > -1 ? 'array' : ''), 64 | this._isStandart(item.type), 65 | !item.required, 66 | item.desc 67 | )) 68 | }) 69 | 70 | return fields 71 | } 72 | 73 | _prepareType(type) { 74 | type = type.replace('Array of Array of ', '').replace('Array of ', '') 75 | 76 | if (JS_TYPES[type]) { 77 | return JS_TYPES[type] 78 | } 79 | 80 | return type 81 | } 82 | 83 | _isStandart(type) { 84 | type = type.replace('Array of Array of ', '').replace('Array of ', '') 85 | 86 | if (JS_TYPES[type]) { 87 | return true 88 | } 89 | 90 | return false 91 | } 92 | 93 | 94 | _generateScheme() { 95 | let $ = cheerio.load(this._docPageData) 96 | 97 | const apiScheme = [] 98 | 99 | $("h4").each((index, el) => { 100 | const nextTag = $(el).next().prop("tagName") 101 | const nextNextTag = $(el).next().next().prop("tagName") 102 | const nextNextNextTag = $(el).next().next().next().prop("tagName") 103 | 104 | if ( 105 | nextTag == 'P' && 106 | ( nextNextTag == 'TABLE' 107 | || nextNextTag == 'BLOCKQUOTE' && nextNextNextTag == 'TABLE') 108 | ) { 109 | let isModel = true 110 | var model = {} 111 | 112 | model.name = $(el).text() 113 | model.desc = $(el).next().text() 114 | model.fields = [] 115 | 116 | if (nextNextTag == 'TABLE') var table = $(el).next().next().children().children() 117 | if (nextNextTag == 'BLOCKQUOTE') var table = $(el).next().next().next().children().children() 118 | 119 | 120 | table.each((i, item) => { 121 | let fieldRaw = [] 122 | 123 | $(item).children().each((i, line) => fieldRaw.push($(line).text())) 124 | 125 | if (i === 0) { 126 | isModel = fieldRaw[0] == "Field" 127 | return 128 | } 129 | 130 | let field = {} 131 | field.field = fieldRaw[0] 132 | field.type = fieldRaw[1] 133 | 134 | if (isModel) { 135 | const optionalRegexp = fieldRaw[2].match(/^Optional. (.*)$/) 136 | 137 | if (optionalRegexp != null) { 138 | fieldRaw[3] = optionalRegexp[1] 139 | } else { 140 | fieldRaw[3] = fieldRaw[2] 141 | fieldRaw[2] = true 142 | } 143 | } 144 | 145 | field.required = fieldRaw[2] == true 146 | field.desc = fieldRaw[3] 147 | 148 | model.fields.push(field) 149 | }) 150 | 151 | if (isModel) apiScheme.push(model) 152 | } 153 | }) 154 | 155 | return apiScheme 156 | } 157 | } 158 | 159 | module.exports = ModelsGenerator -------------------------------------------------------------------------------- /api-scheme/models/SchemeClass.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class SchemeClass { 4 | /** 5 | * 6 | * @param {string} name 7 | * @param {SchemeClassField[]} fields 8 | * @param {string} description 9 | */ 10 | constructor(name, fields, description) { 11 | this._name = name 12 | this._fields = fields 13 | this._description = description 14 | } 15 | 16 | /** 17 | * 18 | * @returns {string} 19 | */ 20 | get name() { 21 | return this._name 22 | } 23 | 24 | /** 25 | * 26 | * @returns {SchemeClassField[]} 27 | */ 28 | get fields() { 29 | return this._fields 30 | } 31 | 32 | /** 33 | * 34 | * @returns {string} 35 | */ 36 | get description() { 37 | return this._description 38 | } 39 | 40 | /** 41 | * 42 | * @returns {string} 43 | */ 44 | get modelCode() { 45 | let code = '' 46 | 47 | let extend = '' 48 | 49 | if (this.name.indexOf('MessageContent') > -1) { 50 | extend = `extends InputMessageContent ` 51 | } 52 | if (this.name.indexOf('InlineQueryResult') > -1) { 53 | extend = `extends InlineQueryResult ` 54 | } 55 | 56 | code += "'use strict'\n" 57 | code += `\n${this._generateRequirements()}` 58 | 59 | code += `/**\n` 60 | code += ` * ${this.description}\n` 61 | code += ` */\n\n` 62 | 63 | code += `class ${this.name} ${extend}{\n` 64 | code += `${this._generateConstructor()}\n` 65 | 66 | this.fields.forEach(field => code += `\n${this._generateGetter(field)}\n`) 67 | 68 | code += `\n${this._generateDeserializeMethod()}\n` 69 | 70 | code += `\n${this._generateSerializeMethod()}\n` 71 | 72 | code += `\n${this._generateToJSONMethod()}\n` 73 | 74 | code += `}\n\n` 75 | 76 | code += `module.exports = ${this.name}` 77 | 78 | return code 79 | } 80 | 81 | /** 82 | * 83 | * @returns {string} 84 | * @private 85 | */ 86 | _generateConstructor() { 87 | let fieldsNames = this.fields.map(field => field.nameCamelCase) 88 | 89 | let code = '' 90 | 91 | code += ` /**\n` 92 | code += ` *\n` 93 | 94 | this.fields.forEach(field => { 95 | let paramType = field.isOptional ? `${field.renderedType}|null` : field.renderedType 96 | code += ` * @param {${paramType}} ${field.isOptional ? '[' + field.nameCamelCase + ']' : field.nameCamelCase }\n` 97 | }) 98 | 99 | code += ` */\n` 100 | 101 | if (this.fields.length > 6) { 102 | code += ` constructor(\n ${fieldsNames.join(', \n ')}\n ) {\n` 103 | } 104 | else { 105 | code += ` constructor(${fieldsNames.join(', ')}) {\n` 106 | } 107 | 108 | if(this.name.indexOf('MessageContent') > -1 || this.name.indexOf('InlineQueryResult') > -1) 109 | code += ` super()\n` 110 | 111 | fieldsNames.forEach(name => code += ` this._${name} = ${name}\n`) 112 | code += ' }' 113 | 114 | return code 115 | } 116 | 117 | /** 118 | * 119 | * @param {SchemeClassField} field 120 | * @returns {string} 121 | * @private 122 | */ 123 | _generateGetter(field) { 124 | let code = '' 125 | 126 | let returnType = field.isOptional ? `${field.renderedType}|null` : `${field.renderedType}` 127 | 128 | code += ` /**\n` 129 | code += ` * ${field.description}\n` 130 | code += ` * @returns {${returnType}}\n` 131 | code += ` */\n` 132 | 133 | code += ` get ${field.nameCamelCase}() {\n` 134 | code += ` return this._${field.nameCamelCase}\n` 135 | code += ` }` 136 | 137 | return code 138 | } 139 | 140 | /** 141 | * 142 | * @returns {string} 143 | * @private 144 | */ 145 | _generateDeserializeMethod() { 146 | let code = '' 147 | 148 | code += ` /**\n` 149 | code += ` *\n` 150 | code += ` * @param {Object} raw\n` 151 | code += ` * @returns {${this.name}}\n` 152 | code += ` */\n` 153 | code += ` static deserialize(raw) {\n` 154 | 155 | let args = this.fields.map(field => { 156 | let raw = `raw['${field.name}']` 157 | 158 | if (!field.isStandartType) { 159 | if (field.type == '2d array') { 160 | return `${raw} ? ${raw}.map(arr => arr.map(item => ${field.typeName}.deserialize(item))) : null` 161 | } 162 | else if(field.type == 'array') { 163 | return `${raw} ? ${raw}.map(item => ${field.typeName}.deserialize(item)) : null` 164 | } 165 | 166 | return `${raw} ? ${field.typeName}.deserialize(${raw}) : null` 167 | } 168 | 169 | if (field.isOptional) { 170 | return `${raw} ? ${raw} : null` 171 | } 172 | 173 | return raw 174 | }) 175 | 176 | if (this.fields.length > 4) { 177 | code += ` return new ${this.name}(\n ${args.join(', \n ')}\n )` 178 | } 179 | else { 180 | code += ` return new ${this.name}(${args.join(', ')})` 181 | } 182 | 183 | code += `\n }` 184 | 185 | return code 186 | } 187 | 188 | /** 189 | * 190 | * @returns {string} 191 | * @private 192 | */ 193 | _generateSerializeMethod() { 194 | let code = '' 195 | 196 | code += ` /**\n` 197 | code += ` *\n` 198 | code += ` * @returns {Object}\n` 199 | code += ` */\n` 200 | code += ` serialize() {\n` 201 | 202 | let obj = this.fields.map(field => { 203 | let getter = `this.${field.nameCamelCase}` 204 | 205 | 206 | if (!field.isStandartType) { 207 | if (field.type == '') { 208 | return `${field.name}: ${getter} ? ${getter}.serialize() : undefined` 209 | } 210 | if (field.type == '2d array') { 211 | return `${field.name}: ${getter} ? ${getter}.map(arr => arr.map(item => item.serialize())) : undefined` 212 | } 213 | else if(field.type == 'array') { 214 | return `${field.name}: ${getter} ? ${getter}.map(item => item.serialize()) : undefined` 215 | } 216 | } 217 | 218 | return `${field.name}: ${getter} ? ${getter} : undefined` 219 | }) 220 | 221 | 222 | code += ` return { \n ${obj.join(', \n ')}\n }` 223 | 224 | 225 | code += `\n }` 226 | 227 | return code 228 | } 229 | 230 | /** 231 | * @returns {string} 232 | * @private 233 | */ 234 | _generateRequirements() { 235 | let code = '' 236 | let renderedTypes = [] 237 | 238 | this.fields.forEach(field => { 239 | let typeName = field.typeName 240 | 241 | if (!field.isStandartType && renderedTypes.indexOf(typeName) == -1 && typeName != this.name) { 242 | renderedTypes.push(typeName) 243 | 244 | //because of typo here: https://core.telegram.org/bots/api#inlinequeryresultgif 245 | const name = typeName[0].toUpperCase() + typeName.slice(1, typeName.length) 246 | 247 | code += `const ${name} = require('./${name}')\n` 248 | } 249 | }) 250 | 251 | if (this.name.indexOf('MessageContent') > -1) { 252 | code += `const InputMessageContent = require('./InputMessageContent')` 253 | } 254 | if (this.name.indexOf('InlineQueryResult') > -1) { 255 | code += `const InlineQueryResult = require('./InlineQueryResult')` 256 | } 257 | 258 | return code == '' ? '' : `${code}\n` 259 | } 260 | 261 | /** 262 | * 263 | * @returns {string} 264 | * @private 265 | */ 266 | _generateToJSONMethod() { 267 | let code = '' 268 | 269 | code += ` /**\n` 270 | code += ` *\n` 271 | code += ` * @returns {string}\n` 272 | code += ` */\n` 273 | code += ` toJSON() {\n` 274 | code += ` return this.serialize()` 275 | code += `\n }` 276 | 277 | return code 278 | } 279 | } 280 | 281 | module.exports = SchemeClass -------------------------------------------------------------------------------- /api-scheme/models/SchemeClassField.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class SchemeClassField { 4 | /** 5 | * 6 | * @param {string} name 7 | * @param {string} typeName 8 | * @param {string} type 9 | * @param {boolean} isStandartType 10 | * @param {boolean} isOptional 11 | * @param {string} description 12 | */ 13 | constructor(name, typeName, type, isStandartType, isOptional, description) { 14 | this._name = name 15 | this._typeName = typeName 16 | this._type = type 17 | this._isStandartType = isStandartType 18 | this._isOptional = isOptional 19 | this._nameCamelCase = this._toCamelCase(this.name) 20 | this._renderedType = this._renderType(typeName, type) 21 | this._description = description 22 | } 23 | 24 | /** 25 | * 26 | * @returns {string} 27 | */ 28 | get name() { 29 | return this._name 30 | } 31 | 32 | /** 33 | * 34 | * @returns {string} 35 | */ 36 | get nameCamelCase() { 37 | return this._nameCamelCase 38 | } 39 | 40 | /** 41 | * 42 | * @returns {string} 43 | */ 44 | get type() { 45 | return this._type 46 | } 47 | 48 | /** 49 | * 50 | * @returns {string} 51 | */ 52 | get typeName() { 53 | return this._typeName 54 | } 55 | 56 | /** 57 | * 58 | * @returns {boolean} 59 | */ 60 | get isOptional() { 61 | return this._isOptional 62 | } 63 | 64 | /** 65 | * 66 | * @returns {boolean} 67 | */ 68 | get isStandartType() { 69 | return this._isStandartType 70 | } 71 | 72 | /** 73 | * 74 | * @returns {string} 75 | */ 76 | get renderedType() { 77 | return this._renderedType 78 | } 79 | 80 | /** 81 | * 82 | * @returns {string} 83 | */ 84 | get description() { 85 | return this._description 86 | } 87 | 88 | /** 89 | * 90 | * @param {string} str 91 | * @returns {string} 92 | * @private 93 | */ 94 | _toCamelCase(str) { 95 | const parts = str.split('_') 96 | 97 | if (!parts.length) return str 98 | 99 | const capitalized = parts.slice(1).map(part => part[0].toUpperCase() + part.substr(1)) 100 | 101 | capitalized.unshift(parts[0]); 102 | 103 | return capitalized.join('') 104 | } 105 | 106 | /** 107 | * 108 | * @param {string} typeName 109 | * @param {string} type 110 | * @returns {string} 111 | * @private 112 | */ 113 | _renderType(typeName, type) { 114 | if (type !== '') { 115 | switch (type) { 116 | case 'array': 117 | return `${typeName}[]` 118 | break 119 | 120 | case '2d array': 121 | return `${typeName}[][]` 122 | break 123 | } 124 | } 125 | 126 | return typeName 127 | } 128 | } 129 | 130 | module.exports = SchemeClassField -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": [ "../telegram-node-bot/lib", "../telegram-node-bot/README.md" ], 4 | "exclude": [ "../telegram-node-bot/api-scheme", "../telegram-node-bot/node_modules" ] 5 | }, 6 | 7 | "opts": { 8 | "destination": "./docs/" 9 | }, 10 | 11 | "templates": { 12 | "theme": "lumen", 13 | "outputSourceFiles": true 14 | } 15 | } -------------------------------------------------------------------------------- /lib/BaseTelegramDataSource.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class BaseTelegramDataSource { 4 | /** 5 | * @returns {TelegramApi} 6 | */ 7 | get api() { throw 'Not implemented' } 8 | 9 | /** 10 | * @returns {TelegramRouter} 11 | */ 12 | get router() { throw 'Not implemented' } 13 | 14 | /** 15 | * @returns {BaseLogger} 16 | */ 17 | get logger() { throw 'Not implemented' } 18 | 19 | /** 20 | * @returns {BaseScopeExtension[]} 21 | */ 22 | get scopeExtensions() { throw 'Not implemented' } 23 | 24 | /** 25 | * @returns {TelegramSessionStorage} 26 | */ 27 | get sessionStorage() { throw 'Not implemented' } 28 | 29 | /** 30 | * @returns {Ivan} 31 | */ 32 | get localization() { throw 'Not implemented' } 33 | 34 | /** 35 | * @returns {TelegramIPC} 36 | */ 37 | get ipc() { throw 'Not implemented' } 38 | } 39 | 40 | module.exports = BaseTelegramDataSource -------------------------------------------------------------------------------- /lib/Telegram.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TelegramApi = require('./api/TelegramApi') 4 | const TelegramRouter = require('./routing/TelegramRouter') 5 | const ConsoleLogger = require('./logger/ConsoleLogger') 6 | const TelegramDataSource = require('./TelegramDataSource') 7 | const UpdateProcessorsManager = require('./updateProcessors/UpdateProcessorsManager') 8 | const InMemoryStorage = require('./storage/session/InMemoryStorage') 9 | const TelegramSessionStorage = require('./storage/session/TelegramSessionStorage') 10 | const TelegramBaseController = require('./mvc/TelegramBaseController') 11 | const TelegramBaseCallbackQueryController = require('./mvc/TelegramBaseCallbackQueryController') 12 | const TelegramBaseInlineQueryController = require('./mvc/TelegramBaseInlineQueryController') 13 | const Models = require('./models/Models') 14 | const Update = require('./models/Update') 15 | const Ivan = require('./localization/Ivan') 16 | const Scope = require('./mvc/Scope') 17 | const InputFile = require('./api/InputFile') 18 | const InlineScope = require('./mvc/InlineScope') 19 | const BaseStorage = require('./storage/BaseStorage') 20 | const BaseLogger = require('./logger/BaseLogger') 21 | const BaseScopeExtension = require('./mvc/BaseScopeExtension') 22 | const BaseUpdateProcessor = require('./updateProcessors/BaseUpdateProcessor') 23 | const BaseUpdateFetcher = require('./updateFetchers/BaseUpdateFetcher') 24 | 25 | const cluster = require('cluster') 26 | const os = require('os') 27 | const SharedStorage = require('./storage/sharedStorage/SharedStorage') 28 | const TelegramIPC = require('./ipc/TelegramIPC') 29 | 30 | const WebAdmin = require('./webAdmin/server/WebAdmin') 31 | 32 | const WebhookUpdateFetcher = require('./updateFetchers/WebhookUpdateFetcher') 33 | const LongPoolingUpdateFetcher = require('./updateFetchers/LongPoolingUpdateFetcher') 34 | 35 | const WebAdminLogger = require('./logger/WebAdminLogger') 36 | const Statistics = require('./statistics/Statistics') 37 | 38 | const BaseCommand = require('./routing/commands/BaseCommand') 39 | const TextCommand = require('./routing/commands/TextCommand') 40 | const RegexpCommand = require('./routing/commands/RegexpCommand') 41 | 42 | class Telegram { 43 | /** 44 | * 45 | * @param {string} token 46 | * @param {{ 47 | * logger: BaseLogger, 48 | * storage: BaseStorage, 49 | * localization: Object[], 50 | * workers: number, 51 | * webhook: {url: string, port: number, host: string } 52 | * updateFetcher: BaseUpdateFetcher 53 | * webAdmin: {port: number, host: string} 54 | * }} options 55 | */ 56 | constructor(token, options) { 57 | options = options || {} 58 | 59 | this._token = token 60 | this._logger = options.logger || new WebAdminLogger() 61 | this._storage = options.storage || new InMemoryStorage() 62 | this._sharedStorage = new SharedStorage(this._storage) 63 | this._localization = new Ivan(this._sharedStorage, (options.localization || [])) 64 | this._webAdminPort = options.webAdmin ? options.webAdmin.port : 7777 65 | this._webAdminHost = options.webAdmin ? options.webAdmin.host : 'localhost' 66 | 67 | this._cpus = os.cpus() 68 | this._workersCount = options.workers || this._cpus.length 69 | 70 | this._ipc = new TelegramIPC() 71 | 72 | this._telegramDataSource = new TelegramDataSource( 73 | new TelegramApi(token, this._logger), 74 | new TelegramRouter(), 75 | this._logger, 76 | new TelegramSessionStorage(this._sharedStorage), 77 | this._localization, 78 | this._ipc 79 | ) 80 | 81 | this._beforeUpdateFunction = null 82 | 83 | this._checkNodeVersion() 84 | 85 | this._updatesFetcher = null 86 | 87 | if (options.updateFetcher) 88 | this._updatesFetcher = options.updateFetcher 89 | else if (options.webhook) { 90 | this._updatesFetcher = new WebhookUpdateFetcher( 91 | this._telegramDataSource.api, 92 | this._logger, 93 | options.webhook.url, 94 | options.webhook.host, 95 | options.webhook.port, 96 | token 97 | ) 98 | } 99 | else { 100 | this._updatesFetcher = new LongPoolingUpdateFetcher( 101 | this._telegramDataSource.api, 102 | this._logger 103 | ) 104 | } 105 | 106 | this._setup() 107 | } 108 | 109 | _checkNodeVersion() { 110 | if (process.version.replace('v', '').split('.')[0] < 6) { 111 | this._logger.error({ 112 | 'Fatal error': 'Node version must be 6 or greater, please update your Node.js' 113 | }) 114 | 115 | process.exit() 116 | } 117 | } 118 | 119 | _setup() { 120 | if (cluster.isMaster) 121 | this._master() 122 | 123 | if (cluster.isWorker) 124 | this._worker() 125 | } 126 | 127 | _master() { 128 | this._logger.log({ 129 | 'Telegram': `Master started, ${this._cpus.length} CPUs found, ${this._workersCount} workers will start` 130 | }) 131 | 132 | this._waitingUpdates = {} // each worker can ask master to send him next update from specific chat 133 | this._waitingCallbacks = {} 134 | this._workers = {} 135 | this.statistics = new Statistics() 136 | 137 | new WebAdmin( 138 | this._webAdminHost, 139 | this._webAdminPort, 140 | __dirname + '/webAdmin/client', 141 | this._logger, 142 | this 143 | ) 144 | 145 | this._runWorkers() 146 | 147 | this._updatesFetcher.fetch(updates => { 148 | this._processUpdates(updates) 149 | }) 150 | } 151 | 152 | _worker() { 153 | this._updateProcessor = new UpdateProcessorsManager(this._telegramDataSource) 154 | 155 | process.on('message', msg => { 156 | if (msg.type == 'update') { 157 | this._processUpdates([Update.deserialize(msg.update)]) 158 | return 159 | } 160 | 161 | this._sharedStorage.handleMessageFromMaster(msg) 162 | }) 163 | } 164 | 165 | _fork() { 166 | return cluster.fork() 167 | } 168 | 169 | restartWorkers() { 170 | this._logger.log({ 'Telegram': 'restarting workers' }) 171 | 172 | for (const pid in this._workers) { 173 | if (this._workers[pid]) 174 | this._workers[pid].kill() 175 | } 176 | } 177 | 178 | /** 179 | * This callback will be called from master process 180 | * 181 | * @param {Function} callback 182 | */ 183 | onMaster(callback) { 184 | if (cluster.isMaster) 185 | callback() 186 | } 187 | 188 | _runWorkers() { 189 | for(var i = 0; i < this._workersCount; i++) { 190 | this._runWorker() 191 | } 192 | 193 | cluster.on('online', w => this._logger.log({ 'Telegram': `Worker started at ${w.process.pid} PID`})) 194 | 195 | cluster.on('exit', (worker, code, signal) => { 196 | this._workers[worker.process.pid] = null 197 | 198 | this._logger.log({ 199 | 'Telegram': `Worker ${worker.process.pid} died with code: ${code}, and signal: ${signal}, Starting a new worker` 200 | }) 201 | this._runWorker() 202 | this.statistics.workerDied(worker.process.pid) 203 | }) 204 | } 205 | 206 | _runWorker() { 207 | let worker = this._fork() 208 | this._workers[worker.process.pid] = worker 209 | this.statistics.addWorker(worker.process.pid) 210 | 211 | let self = this 212 | 213 | worker.on('message', function(msg) { 214 | if (msg.type == 'waitForUpdate') { 215 | self._waitingUpdates[msg.chatId] = worker 216 | return 217 | } 218 | 219 | if (msg.type == 'waitForCallbackQuery') { 220 | self._waitingCallbacks[msg.data] = worker 221 | } 222 | 223 | self._sharedStorage.handleMessageFromWorkers(msg, this) 224 | }) 225 | } 226 | 227 | /** 228 | * Pass child of BaseScopeExtension or array of children to use that extensions 229 | * 230 | * @param {BaseScopeExtension|BaseScopeExtension[]} extension 231 | */ 232 | addScopeExtension(extension) { 233 | this._telegramDataSource.addScopeExtension(extension) 234 | } 235 | 236 | /** 237 | * @param {Update} update 238 | */ 239 | emulateUpdate(update) { 240 | this._updateProcessor.process(update) 241 | } 242 | 243 | /** 244 | * 245 | * @returns {TelegramApi} 246 | */ 247 | get api() { 248 | return this._telegramDataSource.api 249 | } 250 | 251 | /** 252 | * 253 | * @returns {TelegramRouter} 254 | */ 255 | get router() { 256 | return this._telegramDataSource.router 257 | } 258 | 259 | /** 260 | * 261 | * @returns {BaseLogger} 262 | */ 263 | get logger() { 264 | return this._telegramDataSource.logger 265 | } 266 | 267 | /** 268 | * 269 | * @returns {TelegramSessionStorage} 270 | */ 271 | get sessionStorage() { 272 | return this._telegramDataSource.sessionStorage 273 | } 274 | 275 | /** 276 | * @callback continueCallback 277 | * @param {boolean} handle 278 | */ 279 | 280 | /** 281 | * @callback beforeHandler 282 | * @param {Update} update 283 | * @param {continueCallback} callback 284 | */ 285 | 286 | /** 287 | * Your handler function passed to this method will be called after getting 288 | * any update, but before it's processing. 289 | * 290 | * Also to your function will be passed callback function, 291 | * if you call that function with 'true' argument, then update handling will be continued, 292 | * else the update will not be handled. 293 | * 294 | * @param {beforeHandler} handler 295 | */ 296 | before(handler) { 297 | this._beforeUpdateFunction = handler 298 | } 299 | 300 | /** 301 | * @param {Update[]} updates 302 | * @private 303 | */ 304 | _processUpdates(updates) { 305 | if (cluster.isMaster) { 306 | updates.forEach(u => { 307 | let worker 308 | 309 | 310 | if (u.message && this._waitingUpdates[u.message.chat.id] != null) 311 | worker = this._waitingUpdates[u.message.chat.id] 312 | else if (u.callbackQuery && this._waitingCallbacks[u.callbackQuery.data] != null) 313 | worker = this._waitingCallbacks[u.callbackQuery.data] 314 | else 315 | worker = this._pickRandomWorker() //pick random worker for update 316 | 317 | this.statistics.registrateRequest(worker.process.pid) 318 | worker.send({ type: 'update', update: u.serialize() }) 319 | 320 | if (u.message) 321 | this._waitingUpdates[u.message.chat.id] = null 322 | 323 | }) 324 | 325 | return 326 | } 327 | 328 | updates.forEach(update => { 329 | if (!this._beforeUpdateFunction) { 330 | this._updateProcessor.process(update) 331 | return 332 | } 333 | 334 | this._beforeUpdateFunction(update, handle => { 335 | if (handle === true) { 336 | this._updateProcessor.process(update) 337 | } 338 | }) 339 | }) 340 | } 341 | 342 | _pickRandomWorker() { 343 | const pids = Object.keys(this._workers).filter(pid => this._workers[pid] != null) 344 | return this._workers[pids[Math.floor(Math.random() * pids.length)]] 345 | } 346 | } 347 | 348 | module.exports = { 349 | TelegramApi, 350 | Telegram, 351 | TelegramBaseController, 352 | TelegramBaseCallbackQueryController, 353 | TelegramBaseInlineQueryController, 354 | Scope, 355 | BaseLogger, 356 | BaseScopeExtension, 357 | InputFile, 358 | InlineScope, 359 | BaseStorage, 360 | BaseUpdateFetcher, 361 | BaseCommand, 362 | TextCommand, 363 | RegexpCommand, 364 | Models 365 | } 366 | -------------------------------------------------------------------------------- /lib/TelegramDataSource.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseTelegramDataSource = require('./BaseTelegramDataSource') 4 | 5 | class TelegramDataSource extends BaseTelegramDataSource { 6 | /** 7 | * 8 | * @param {TelegramApi} api 9 | * @param {TelegramRouter} router 10 | * @param {BaseLogger} logger 11 | * @param {TelegramSessionStorage} sessionStorage 12 | * @param {Ivan} localization 13 | * @param {TelegramIPC} ipc 14 | */ 15 | constructor(api, router, logger, sessionStorage, localization, ipc) { 16 | super() 17 | 18 | this._api = api 19 | this._router = router 20 | this._logger = logger 21 | this._scopeExtensions = [] 22 | this._sessionStorage = sessionStorage 23 | this._localization = localization 24 | this._ipc = ipc 25 | } 26 | 27 | /** 28 | * 29 | * @returns {TelegramApi} 30 | */ 31 | get api() { 32 | return this._api 33 | } 34 | 35 | /** 36 | * 37 | * @returns {TelegramRouter} 38 | */ 39 | get router() { 40 | return this._router 41 | } 42 | 43 | /** 44 | * 45 | * @returns {BaseLogger} 46 | */ 47 | get logger() { 48 | return this._logger 49 | } 50 | 51 | /** 52 | * 53 | * @returns {BaseScopeExtension[]} 54 | */ 55 | get scopeExtensions() { 56 | return this._scopeExtensions 57 | } 58 | 59 | /** 60 | * 61 | * @returns {TelegramSessionStorage} 62 | */ 63 | get sessionStorage() { 64 | return this._sessionStorage 65 | } 66 | 67 | /** 68 | * 69 | * @returns {Ivan} 70 | */ 71 | get localization() { 72 | return this._localization 73 | } 74 | 75 | /** 76 | * @returns {TelegramIPC} 77 | */ 78 | get ipc() { 79 | return this._ipc 80 | } 81 | 82 | /** 83 | * 84 | * @param {BaseScopeExtension|BaseScopeExtension[]} extension 85 | */ 86 | addScopeExtension(extension) { 87 | if (Array.isArray(extension)) { 88 | extension.forEach(this._scopeExtensions.push) 89 | 90 | return 91 | } 92 | 93 | this._scopeExtensions.push(extension) 94 | } 95 | } 96 | 97 | module.exports = TelegramDataSource -------------------------------------------------------------------------------- /lib/api/InputFile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require("fs") 4 | const req = require('tiny_request') 5 | const path = require('path') 6 | 7 | const STANDARD_TYPES = { 8 | photo: { 9 | filename: 'photo.png', 10 | type: 'image/png' 11 | }, 12 | audio: { 13 | filename: 'audio.mp3', 14 | type: 'audio/mpeg' 15 | }, 16 | document: { 17 | filename: 'data.dat', 18 | type: '' 19 | }, 20 | sticker: { 21 | filename: 'sticker.webp', 22 | type: '' 23 | }, 24 | video: { 25 | filename: 'video.mp4', 26 | type: 'audio/mp4' 27 | } 28 | } 29 | 30 | let dir = __dirname + '/temp/' 31 | 32 | if (!fs.existsSync(dir)) { 33 | fs.mkdirSync(dir) 34 | } 35 | 36 | /** 37 | * This class represents any file that's going be send to Telegram 38 | */ 39 | class InputFile { 40 | /** 41 | * @param {string|null} fileId 42 | * @param {string|null} filePath 43 | * @param {string|null} fileUrl 44 | * @param {string|null} fileName 45 | * @private 46 | */ 47 | constructor(fileId, filePath, fileUrl, fileName) { 48 | this._fileId = fileId 49 | this._filePath = filePath 50 | this._fileUrl = fileUrl 51 | this._fileName = fileName 52 | } 53 | 54 | /** 55 | * 56 | * @param {string} type 57 | * @param {Object} params 58 | * @returns {Promise} 59 | */ 60 | prepareRequest(type, params) { 61 | return new Promise((resolve) => { 62 | if (this._fileId) { 63 | params[type] = this._fileId 64 | 65 | resolve({ params: params, multipart: null }) 66 | } 67 | 68 | if (this._fileUrl) { 69 | const filePath = __dirname + '/temp/' + Math.random().toString(36).substring(7) + '.dat' 70 | const wstream = fs.createWriteStream(filePath) 71 | 72 | const sendedCallback = () => { 73 | fs.unlink(filePath) 74 | } 75 | 76 | wstream.on('finish', () => { 77 | const multipart = { } 78 | 79 | multipart[type] = { 80 | value: fs.createReadStream(filePath), 81 | filename: this._fileName || STANDARD_TYPES[type].filename, 82 | contentType: STANDARD_TYPES[type].type 83 | } 84 | 85 | resolve({ params: params, multipart: multipart, callback: sendedCallback}) 86 | }) 87 | 88 | req.get({ 89 | url: this._fileUrl, 90 | pipe: wstream 91 | }) 92 | } 93 | 94 | if (this._filePath) { 95 | const multipart = { } 96 | 97 | multipart[type] = { 98 | value: fs.createReadStream(this._filePath), 99 | filename: path.basename(this._filePath) || STANDARD_TYPES[type].filename, 100 | contentType: STANDARD_TYPES[type].type 101 | } 102 | 103 | resolve({ params: params, multipart: multipart }) 104 | } 105 | }) 106 | } 107 | 108 | /** 109 | * Creates InputFile from plain Object 110 | * 111 | * @param {Object|string} raw 112 | * @returns {InputFile} 113 | */ 114 | static deserialize(raw) { 115 | if (typeof raw == 'string') { 116 | return InputFile.byId(raw) 117 | } 118 | 119 | if (raw.url) { 120 | return InputFile.byUrl(raw.url, raw.filename) 121 | } 122 | 123 | if (raw.path) { 124 | return InputFile.byFilePath(raw.path) 125 | } 126 | } 127 | 128 | /** 129 | * Creates InputFile by file id 130 | * 131 | * @param {string} id 132 | * @returns {InputFile} 133 | */ 134 | static byId(id) { 135 | return new InputFile(id, null, null, null) 136 | } 137 | 138 | /** 139 | * Creates InputFile by file path 140 | * 141 | * @param {string} path 142 | * @returns {InputFile} 143 | */ 144 | static byFilePath(path) { 145 | return new InputFile(null, path, null, null) 146 | } 147 | 148 | /** 149 | * Creates InputFile by url 150 | * 151 | * @param {string} url 152 | * @param {string} [fileName] 153 | * @returns {InputFile} 154 | */ 155 | static byUrl(url, fileName) { 156 | return new InputFile(null, null, url, fileName) 157 | } 158 | } 159 | 160 | module.exports = InputFile -------------------------------------------------------------------------------- /lib/api/TelegramApi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const req = require('tiny_request') 4 | const CallbackQueue = require('../utils/CallbackQueue') 5 | const TelegramApiRequest = require('./TelegramApiRequest') 6 | const Models = require('../models/Models') 7 | const Message = require('../models/Message') 8 | const File = require('../models/File') 9 | const UserProfilePhotos = require('../models/UserProfilePhotos') 10 | const User = require('../models/User') 11 | const Update = require('../models/Update') 12 | const Chat = require('../models/Chat') 13 | const ChatMember = require('../models/ChatMember') 14 | const InputFile = require('./InputFile') 15 | const TelegramApiError = require('./TelegramApiError') 16 | const GameHighScore = require('../models/GameHighScore') 17 | 18 | const REQUESTS_PER_SECOND = 30 19 | const REQUEST_RETRY_TIMEOUT = 1000 //ms 20 | 21 | /** 22 | * Telegram API class 23 | */ 24 | class TelegramApi { 25 | /** 26 | * 27 | * @param {string} token 28 | * @param {BaseLogger} logger 29 | */ 30 | constructor(token, logger) { 31 | this._token = token 32 | this._url = `https://api.telegram.org/bot${this._token}/` 33 | this._queue = new CallbackQueue(REQUESTS_PER_SECOND) 34 | this._logger = logger 35 | } 36 | 37 | /** 38 | * 39 | * @param {string} method 40 | * @returns {string} 41 | * @private 42 | */ 43 | _urlForMethod(method) { 44 | return this._url + method 45 | } 46 | 47 | /** 48 | * 49 | * @param {string} method 50 | * @param {object} params 51 | * @param {object} [multipart] 52 | * @returns {Promise} 53 | */ 54 | call(method, params, multipart) { 55 | return new Promise((resolve, reject) => { 56 | const request = new TelegramApiRequest(method, params, multipart) 57 | 58 | this._queue.push(() => { 59 | this._handleRequest(request, resolve, reject) 60 | }) 61 | }) 62 | } 63 | 64 | /** 65 | * 66 | * @param {string} method 67 | * @param {Object} params 68 | * @param {function} type 69 | * @param {object} [multipart] 70 | * @returns {Promise} 71 | * @private 72 | */ 73 | _callWithReturnType(method, params, type, multipart) { 74 | return this.call(method, params, multipart) 75 | .then(response => { 76 | return type.deserialize(response.result) 77 | }) 78 | } 79 | 80 | /** 81 | * 82 | * @param {TelegramApiRequest }request 83 | * @param {function} resolve 84 | * @param {function} reject 85 | * @private 86 | */ 87 | _handleRequest(request, resolve, reject) { 88 | req.post({ 89 | url: this._urlForMethod(request.method), 90 | form: request.multipart ? null : request.params, 91 | query: request.multipart ? request.params : null, 92 | multipart: request.multipart, 93 | json: true 94 | }, (body, response, err) => { 95 | if (!err && response.statusCode == 200 && body) { 96 | resolve(body) 97 | return 98 | } 99 | 100 | if (err && err.code) { 101 | this._logger.error({'Network error:': err, 'request': request }) 102 | this._retryRequest(request, resolve, reject) 103 | 104 | return 105 | } 106 | 107 | if (body && body.error_code) { 108 | const error = TelegramApiError.fromResponse(body) 109 | 110 | if (error.code == 500) { 111 | this._logger.warn({ 'Got Internal server error from Telegram. Body:': body }) 112 | this._retryRequest(request, resolve, reject) 113 | 114 | return 115 | } 116 | 117 | reject(error) 118 | this._logger.warn({ 'Api error: Body:': body }) 119 | 120 | return 121 | } 122 | 123 | if (err.message === 'Unexpected token < in JSON at position 0') { 124 | this._logger.error({ 125 | 'api request error: Telegram returned some html instead of json. Body:': body, 126 | 'Error:': err 127 | }) 128 | this._retryRequest(request, resolve, reject) 129 | 130 | return 131 | } 132 | 133 | this._logger.error({'api request error: Body:': body, 'Error:': err }) 134 | reject(err) 135 | }) 136 | } 137 | 138 | /** 139 | * 140 | * @param {TelegramApiRequest }request 141 | * @param {function} resolve 142 | * @param {function} reject 143 | * @private 144 | */ 145 | _retryRequest(request, resolve, reject) { 146 | setTimeout(() => { 147 | this._queue.push(() => { 148 | this._logger.log({ 'Retry request': request }) 149 | this._handleRequest(request, resolve, reject) 150 | }) 151 | }, REQUEST_RETRY_TIMEOUT) 152 | } 153 | 154 | /** 155 | * 156 | * @param {string} method 157 | * @param {InputFile|Object} inputFile 158 | * @param {string} type 159 | * @param {Object} params 160 | * @returns {Promise} 161 | * @private 162 | */ 163 | _callWithInputFile(method, inputFile, type, params) { 164 | const file = inputFile instanceof InputFile ? inputFile : InputFile.deserialize(inputFile) 165 | let sentCallback = Function() 166 | 167 | return file.prepareRequest(type, params) 168 | .then(prepared => { 169 | sentCallback = prepared.callback || Function() 170 | 171 | return this._callWithReturnType( 172 | method, 173 | prepared.params, 174 | Message, 175 | prepared.multipart 176 | ) 177 | }) 178 | .then(r => { 179 | sentCallback() 180 | 181 | return r 182 | }) 183 | } 184 | 185 | /** 186 | * 187 | * @param {Object} [options] 188 | * @returns {Promise} 189 | */ 190 | getUpdates(options) { 191 | return this.call('getUpdates', options) 192 | .then(r => r.result.map(u => Update.deserialize(u))) 193 | } 194 | 195 | /** 196 | * 197 | * @param options 198 | * @returns {Promise} 199 | */ 200 | setWebhook(options) { 201 | return this._callWithReturnType('setWebhook', { 202 | url: options.url ? options.url + '/' + this._token : '' 203 | }, Models.Update) 204 | } 205 | 206 | /** 207 | * 208 | * @returns {Promise} 209 | */ 210 | getMe() { 211 | return this._callWithReturnType('getMe', {}, User) 212 | } 213 | 214 | /** 215 | * 216 | * @param {number|string} chatId 217 | * @param {string} text 218 | * @param {Object} [options] 219 | * @returns {Promise} 220 | */ 221 | sendMessage(chatId, text, options) { 222 | const params = { 223 | chat_id: chatId, 224 | text: text 225 | } 226 | 227 | if (text.length > 4096) { 228 | this.sendMessage(chatId, text.slice(0, 4096), options) 229 | .then(() => { 230 | this.sendMessage(chatId, text.slice(4096, text.length), options) 231 | }) 232 | } else { 233 | return this._callWithReturnType('sendMessage', Object.assign(params, options), Message) 234 | } 235 | } 236 | 237 | /** 238 | * 239 | * @param {number|string} chatId 240 | * @param {number} fromChatId 241 | * @param {number} messageId 242 | * @param {Object} [options] 243 | * @returns {Promise} 244 | */ 245 | forwardMessage(chatId, fromChatId, messageId, options) { 246 | const params = { 247 | chat_id: chatId, 248 | from_chat_id: fromChatId, 249 | message_id: messageId 250 | } 251 | 252 | return this._callWithReturnType('forwardMessage', Object.assign(params, options), Message) 253 | } 254 | 255 | /** 256 | * 257 | * @param {number|string} chatId 258 | * @param {InputFile|Object} photo 259 | * @param {Object} [options] 260 | * @returns {Promise} 261 | */ 262 | sendPhoto(chatId, photo, options) { 263 | return this._callWithInputFile( 264 | 'sendPhoto', 265 | photo, 266 | 'photo', 267 | Object.assign( 268 | { chat_id: chatId }, 269 | options 270 | ) 271 | ) 272 | } 273 | 274 | /** 275 | * 276 | * @param {number|string} chatId 277 | * @param {InputFile|Object} audio 278 | * @param {Object} [options] 279 | * @returns {Promise} 280 | */ 281 | sendAudio(chatId, audio, options) { 282 | return this._callWithInputFile( 283 | 'sendAudio', 284 | audio, 285 | 'audio', 286 | Object.assign( 287 | { chat_id: chatId }, 288 | options 289 | ) 290 | ) 291 | } 292 | 293 | /** 294 | * 295 | * @param {number|string} chatId 296 | * @param {InputFile|Object} document 297 | * @param {Object} [options] 298 | * @returns {Promise} 299 | */ 300 | sendDocument(chatId, document, options) { 301 | return this._callWithInputFile( 302 | 'sendDocument', 303 | document, 304 | 'document', 305 | Object.assign( 306 | { chat_id: chatId }, 307 | options 308 | ) 309 | ) 310 | } 311 | 312 | /** 313 | * 314 | * @param {number|string} chatId 315 | * @param {InputFile|Object} sticker 316 | * @param {Object} [options] 317 | * @returns {Promise} 318 | */ 319 | sendSticker(chatId, sticker, options) { 320 | return this._callWithInputFile( 321 | 'sendSticker', 322 | sticker, 323 | 'sticker', 324 | Object.assign( 325 | { chat_id: chatId }, 326 | options 327 | ) 328 | ) 329 | } 330 | 331 | /** 332 | * 333 | * @param {number|string} chatId 334 | * @param {InputFile|Object} video 335 | * @param {Object} [options] 336 | * @returns {Promise} 337 | */ 338 | sendVideo(chatId, video, options) { 339 | return this._callWithInputFile( 340 | 'sendVideo', 341 | video, 342 | 'video', 343 | Object.assign( 344 | { chat_id: chatId }, 345 | options 346 | ) 347 | ) 348 | } 349 | 350 | /** 351 | * 352 | * @param {number|string} chatId 353 | * @param {InputFile|Object} voice 354 | * @param {Object} [options] 355 | * @returns {Promise} 356 | */ 357 | sendVoice(chatId, voice, options) { 358 | return this._callWithInputFile( 359 | 'sendVoice', 360 | voice, 361 | 'voice', 362 | Object.assign( 363 | { chat_id: chatId }, 364 | options 365 | ) 366 | ) 367 | } 368 | 369 | /** 370 | * 371 | * @param {number|string} chatId 372 | * @param {number} latitude 373 | * @param {number} longitude 374 | * @param {Object} [options] 375 | * @returns {Promise} 376 | */ 377 | sendLocation(chatId, latitude, longitude, options) { 378 | const params = { 379 | chat_id: chatId, 380 | latitude: latitude, 381 | longitude: longitude 382 | } 383 | 384 | return this._callWithReturnType('sendLocation', Object.assign(params, options), Message) 385 | } 386 | 387 | /** 388 | * 389 | * @param {number|string} chatId 390 | * @param {number} latitude 391 | * @param {number} longitude 392 | * @param {string} title 393 | * @param {string}address 394 | * @param {Object} [options] 395 | * @returns {Promise} 396 | */ 397 | sendVenue(chatId, latitude, longitude, title, address, options) { 398 | const params = { 399 | chat_id: chatId, 400 | latitude: latitude, 401 | longitude: longitude, 402 | title: title, 403 | address: address 404 | } 405 | 406 | return this._callWithReturnType('sendVenue', Object.assign(params, options), Message) 407 | } 408 | 409 | /** 410 | * 411 | * @param {number|string} chatId 412 | * @param {string} phoneNumber 413 | * @param {string} firstName 414 | * @param {Object} [options] 415 | * @returns {Promise} 416 | */ 417 | sendContact(chatId, phoneNumber, firstName, options) { 418 | const params = { 419 | chat_id: chatId, 420 | phone_number: phoneNumber, 421 | first_name: firstName 422 | } 423 | 424 | return this._callWithReturnType('sendContact', Object.assign(params, options), Message) 425 | } 426 | 427 | /** 428 | * 429 | * @param {number|string} chatId 430 | * @param {string} action 431 | * @returns {Promise} 432 | */ 433 | sendChatAction(chatId, action) { 434 | return this.call('sendChatAction', { 435 | chat_id: chatId, 436 | action: action 437 | }) 438 | } 439 | 440 | /** 441 | * 442 | * @param {number} userId 443 | * @param {number} offset 444 | * @param {number} limit 445 | * @returns {Promise} 446 | */ 447 | getUserProfilePhotos(userId, offset, limit) { 448 | return this._callWithReturnType('getUserProfilePhotos', { 449 | user_id: userId, 450 | ofsset: offset, 451 | limit: limit 452 | }, UserProfilePhotos) 453 | } 454 | 455 | /** 456 | * 457 | * @param {number} fileId 458 | * @returns {Promise} 459 | */ 460 | getFile(fileId) { 461 | return this._callWithReturnType('getFile', { file_id: fileId }, File) 462 | } 463 | 464 | /** 465 | * 466 | * @param {number|string} chatId 467 | * @param {number} userId 468 | * @returns {Promise} 469 | */ 470 | kickChatMember(chatId, userId) { 471 | const params = { 472 | chat_id: chatId, 473 | user_id: userId 474 | } 475 | 476 | return this.call('kickChatMember', params) 477 | .then(r => r.result) 478 | } 479 | 480 | /** 481 | * 482 | * @param {number|string} chatId 483 | * @returns {Promise} 484 | */ 485 | leaveChat(chatId) { 486 | return this.call('leaveChat', { chat_id: chatId }) 487 | .then(r => r.result) 488 | } 489 | 490 | /** 491 | * 492 | * @param {number|string} chatId 493 | * @param {number} userId 494 | * @returns {Promise} 495 | */ 496 | unbanChatMember(chatId, userId) { 497 | const params = { 498 | chat_id: chatId, 499 | user_id: userId 500 | } 501 | 502 | return this.call('unbanChatMember', params) 503 | .then(r => r.result) 504 | } 505 | 506 | /** 507 | * 508 | * @param {number|string} chatId 509 | * @returns {Promise} 510 | */ 511 | getChat(chatId) { 512 | return this._callWithReturnType('getChat', { chat_id: chatId }, Chat) 513 | } 514 | 515 | /** 516 | * 517 | * @param {number|string} chatId 518 | * @returns {Promise} 519 | */ 520 | getChatAdministrators(chatId) { 521 | return this.call('getChatAdministrators', { chat_id: chatId }) 522 | .then(r => r.result.map(m => ChatMember.deserialize(m))) 523 | } 524 | 525 | /** 526 | * 527 | * @param {number|string} chatId 528 | * @returns {Promise} 529 | */ 530 | getChatMembersCount(chatId) { 531 | return this.call('getChatMembersCount', { chat_id: chatId }) 532 | .then(r => r.result) 533 | } 534 | 535 | /** 536 | * 537 | * @param {number|string} chatId 538 | * @param {number} userId 539 | * @returns {Promise} 540 | */ 541 | getChatMember(chatId, userId) { 542 | const params = { 543 | chat_id: chatId, 544 | user_id: userId 545 | } 546 | 547 | return this._callWithReturnType('getChatMember', params, ChatMember) 548 | } 549 | 550 | /** 551 | * 552 | * @param {string} callbackQueryId 553 | * @param {Object} [options] 554 | * @returns {Promise} 555 | */ 556 | answerCallbackQuery(callbackQueryId, options) { 557 | const params = { 558 | callback_query_id: callbackQueryId 559 | } 560 | 561 | return this.call('answerCallbackQuery', Object.assign(params, options)) 562 | .then(r => r.result) 563 | } 564 | 565 | /** 566 | * 567 | * @param {string} text 568 | * @param {Object} options 569 | * @returns {Promise} 570 | */ 571 | editMessageText(text, options) { 572 | const params = { 573 | text: text 574 | } 575 | 576 | return this.call('editMessageText', Object.assign(params, options)) 577 | .then(r => typeof r.result == 'boolean' ? r.require : Message.deserialize(r.result) ) 578 | } 579 | 580 | /** 581 | * 582 | * @param {Object} options 583 | * @returns {Promise} 584 | */ 585 | editMessageCaption(options) { 586 | return this.call('editMessageCaption', options) 587 | .then(r => typeof r.result == 'boolean' ? r.require : Message.deserialize(r.result)) 588 | } 589 | 590 | /** 591 | * 592 | * @param {Object} options 593 | * @returns {Promise} 594 | */ 595 | editMessageReplyMarkup(options) { 596 | return this.call('editMessageReplyMarkup', options) 597 | .then(r => typeof r.result == 'boolean' ? r.require : Message.deserialize(r.result)) 598 | } 599 | 600 | /** 601 | * 602 | * @param {string} inlineQueryId 603 | * @param {InlineQueryResult[]} results 604 | * @param {Object} [options] 605 | * @returns {Promise} 606 | */ 607 | answerInlineQuery(inlineQueryId, results, options) { 608 | const params = { 609 | inline_query_id: inlineQueryId, 610 | results: JSON.stringify(results) 611 | } 612 | 613 | return this.call('answerInlineQuery', Object.assign(params, options)) 614 | .then(r => r.result) 615 | } 616 | 617 | /** 618 | * @param {number|string} chatId 619 | * @param {string} gameShortName 620 | * @param {object} [options] 621 | * @returns {Promise} 622 | */ 623 | sendGame(chatId, gameShortName, options) { 624 | const params = { 625 | chat_id: chatId, 626 | game_short_name: gameShortName 627 | } 628 | 629 | return this._callWithReturnType('sendGame', Object.assign(params, options), Message) 630 | } 631 | 632 | /** 633 | * @param {number} userId 634 | * @param {number} score 635 | * @param {Object} [options] 636 | * @returns {Promise} 637 | */ 638 | setGameScore(userId, score, options) { 639 | const params = { 640 | chat_id: chatId, 641 | score: score 642 | } 643 | 644 | return this.call('setGameScore', Object.assign(params, options)) 645 | .then(r => typeof r.result == 'boolean' ? r.require : Message.deserialize(r.result)) 646 | } 647 | 648 | getGameHighScores(userId, options) { 649 | return this.call('getGameHighScores', Object.assign({ user_id: userId }, options)) 650 | .then(r => r.result.map(m => ChatMember.deserialize(m))) 651 | } 652 | } 653 | 654 | module.exports = TelegramApi -------------------------------------------------------------------------------- /lib/api/TelegramApiError.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * TelegramApiError 5 | */ 6 | class TelegramApiError { 7 | /** 8 | * 9 | * @param {number} code 10 | * @param {string} description 11 | */ 12 | constructor(code, description) { 13 | this._code = code 14 | this._description = description 15 | } 16 | 17 | /** 18 | * 19 | * @returns {number} 20 | */ 21 | get code() { 22 | return this._code 23 | } 24 | 25 | /** 26 | * 27 | * @returns {string} 28 | */ 29 | get description() { 30 | return this._description 31 | } 32 | 33 | /** 34 | * 35 | * @param {Object} raw 36 | * @returns {TelegramApiError} 37 | */ 38 | static fromResponse(raw) { 39 | return new TelegramApiError(raw.error_code, raw.description) 40 | } 41 | } 42 | 43 | module.exports = TelegramApiError -------------------------------------------------------------------------------- /lib/api/TelegramApiRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * TelegramApiRequest 5 | */ 6 | class TelegramApiRequest { 7 | /** 8 | * 9 | * @param {string} method 10 | * @param {Object} params 11 | * @param {Object} [multipart] 12 | */ 13 | constructor(method, params, multipart) { 14 | this._method = method 15 | this._params = params 16 | this._multipart = multipart 17 | } 18 | 19 | /** 20 | * 21 | * @returns {string} 22 | */ 23 | get method() { 24 | return this._method 25 | } 26 | 27 | /** 28 | * 29 | * @returns {Object} 30 | */ 31 | get params() { 32 | return this._params 33 | } 34 | 35 | /** 36 | * 37 | * @returns {Object} 38 | */ 39 | get multipart() { 40 | return this._multipart 41 | } 42 | } 43 | 44 | module.exports = TelegramApiRequest -------------------------------------------------------------------------------- /lib/ipc/TelegramIPC.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class TelegramIPC { 4 | /** 5 | * Worker can ask master to send him next update from specific chat 6 | * 7 | * @param {number} chatId 8 | */ 9 | askForNextUpdate(chatId) { 10 | process.send({ type: 'waitForUpdate', chatId: chatId }) 11 | } 12 | 13 | /** 14 | * Worker can ask master to send him next callback query for specific callback data 15 | * 16 | * @param {string} chatId 17 | */ 18 | askForNextCallbackQuery(data) { 19 | process.send({ type: 'waitForCallbackQuery', data: data }) 20 | } 21 | } 22 | 23 | module.exports = TelegramIPC -------------------------------------------------------------------------------- /lib/localization/Ivan.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Localization = require('./Localization') 4 | 5 | const LOCALIZATION_STORAGE = 'localizationStorage' 6 | 7 | /** 8 | * Localization class 9 | */ 10 | class Ivan { 11 | /** 12 | * 13 | * @param {BaseStorage} storage 14 | * @param {Object[]} localizations 15 | */ 16 | constructor(storage, localizations) { 17 | this._storage = storage 18 | this._localizations = [] 19 | this._loc = {} 20 | 21 | const locHandler = { 22 | set: () => { 23 | throw 'Cant set value for localization' 24 | }, 25 | get: (target, key, receiver) => { 26 | let loc = this.localizationForLanguage(key) 27 | if (loc) { 28 | return this.localizationForLanguage(key) 29 | } 30 | else { 31 | return Reflect.get(target, key, receiver) 32 | } 33 | } 34 | } 35 | localizations.forEach(localization => { 36 | if (!this._checkLocalization(localization)) throw `Wrong localization: ${localization}` 37 | 38 | this._localizations.push(Localization.deserialize(localization)) 39 | }) 40 | 41 | return new Proxy(this, locHandler) 42 | } 43 | 44 | /** 45 | * Translates localized string to other language 46 | * 47 | * @param {string} localizedString 48 | * @param {string} toLang 49 | * @returns {string} 50 | */ 51 | translate(localizedString, toLang) { 52 | return this.loc[toLang][this.getPhraseKey(localizedString)] 53 | } 54 | 55 | /** 56 | * Returns phrases for user by userId 57 | * 58 | * @param {number} userId 59 | * @returns {Promise} 60 | */ 61 | forUser(userId) { 62 | return this.getLanguageForUser(userId) 63 | .then(lang => { 64 | return this.localizationForLanguage(lang) 65 | }) 66 | } 67 | 68 | /** 69 | * Sets language for user by userId 70 | * 71 | * @param {number} userId 72 | * @param {string} lang 73 | */ 74 | setLanguageForUser(userId, lang) { 75 | this._storage.set(LOCALIZATION_STORAGE, userId, { lang: lang }) 76 | } 77 | 78 | /** 79 | * Returns stored language for user by userId 80 | * 81 | * @param {number} userId 82 | * @returns {Promise} 83 | */ 84 | getLanguageForUser(userId) { 85 | return this._storage.get(LOCALIZATION_STORAGE, userId) 86 | .then(user => { 87 | if (user.lang) { 88 | return user.lang 89 | } 90 | else { 91 | throw 'No data for that user' 92 | } 93 | }) 94 | } 95 | 96 | /** 97 | * Returns phrases for language 98 | * 99 | * @param {string} lang 100 | * @returns {Object|null} 101 | */ 102 | localizationForLanguage(lang) { 103 | let loc = this._localizations.find(localization => localization.lang === lang) 104 | return loc ? loc.phrases : null 105 | } 106 | 107 | /** 108 | * Returns language by phrase 109 | * 110 | * @param inputPhrase 111 | * @returns {string|null} 112 | */ 113 | languageByPhrase(inputPhrase) { 114 | for (const loc of this._localizations) { 115 | for (const phrase in loc.phrases) { 116 | if (loc.phrases[phrase] === inputPhrase) { 117 | return loc.lang 118 | } 119 | } 120 | } 121 | 122 | return null 123 | } 124 | 125 | /** 126 | * Returns the key name of phrase 127 | * 128 | * @param {string} inputPhrase 129 | * @returns {string|null} 130 | */ 131 | getPhraseKey(inputPhrase) { 132 | for (const loc of this._localizations) { 133 | for (const phrase in loc.phrases) { 134 | if (loc.phrases[phrase] === inputPhrase) { 135 | return phrase 136 | } 137 | } 138 | } 139 | 140 | return null 141 | } 142 | 143 | /** 144 | * 145 | * @param {Object} rawLocalization 146 | * @returns {Boolean} 147 | * @private 148 | */ 149 | _checkLocalization(rawLocalization) { 150 | return rawLocalization.lang && rawLocalization.phrases 151 | } 152 | } 153 | 154 | module.exports = Ivan -------------------------------------------------------------------------------- /lib/localization/Localization.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Represents Localization file 5 | */ 6 | class Localization { 7 | /** 8 | * 9 | * @param {String} lang 10 | * @param {Object} phrases 11 | */ 12 | constructor(lang, phrases) { 13 | this._lang = lang 14 | this._phrases = phrases 15 | } 16 | 17 | /** 18 | * 19 | * @returns {String} 20 | */ 21 | get lang() { 22 | return this._lang 23 | } 24 | 25 | /** 26 | * 27 | * @returns {Object} 28 | */ 29 | get phrases() { 30 | return this._phrases 31 | } 32 | 33 | /** 34 | * 35 | * @param {Object} raw 36 | * @returns {Localization} 37 | */ 38 | static deserialize(raw) { 39 | return new Localization(raw.lang, raw.phrases) 40 | } 41 | } 42 | 43 | module.exports = Localization -------------------------------------------------------------------------------- /lib/logger/BaseLogger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Represents any logger class 5 | * 6 | * if you want to create your own logger, you must extend BaseLogger 7 | * and override all methods 8 | */ 9 | class BaseLogger { 10 | /** 11 | * Any log 12 | * 13 | * @param {Object} data 14 | */ 15 | log(data) { throw 'Not implemented' } 16 | 17 | /** 18 | * Warning log 19 | * 20 | * @param {Object} data 21 | */ 22 | warn(data) { throw 'Not implemented' } 23 | 24 | /** 25 | * Error log 26 | * 27 | * @param {Object} data 28 | */ 29 | error(data) { throw 'Not implemented' } 30 | } 31 | 32 | module.exports = BaseLogger -------------------------------------------------------------------------------- /lib/logger/ConsoleLogger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseLogger = require('./BaseLogger') 4 | 5 | const COLOR_CODES = { 6 | red: '\x1b[31m', 7 | cyan: '\x1b[36m', 8 | yellow: '\x1b[33m', 9 | reset: '\x1b[0m' 10 | } 11 | 12 | /** 13 | * Standard ConsoleLogger, will be used if no logger passed to Telegram 14 | */ 15 | class ConsoleLogger extends BaseLogger { 16 | /** 17 | * 18 | * @param {Object} data 19 | */ 20 | log(data) { 21 | this._prepareLog('cyan', 'log', data) 22 | } 23 | 24 | /** 25 | * 26 | * @param {Object} data 27 | */ 28 | warn(data) { 29 | this._prepareLog('yellow', 'warn', data) 30 | } 31 | 32 | /** 33 | * 34 | * @param {Object} data 35 | */ 36 | error(data) { 37 | this._prepareLog('red', 'error', data) 38 | } 39 | 40 | /** 41 | * 42 | * @param {string} color 43 | * @param {string} prefix 44 | * @param {Object} data 45 | * @private 46 | */ 47 | _prepareLog(color, prefix, data) { 48 | Object.keys(data).forEach(key => { 49 | if (data[key] instanceof Error) { 50 | data[key] = data[key].stack || data[key] 51 | } 52 | }) 53 | 54 | console.log(`${COLOR_CODES[color]}[${prefix}] ${COLOR_CODES.reset}`) 55 | Object.keys(data).forEach(key => console.log(key, data[key])) 56 | console.log('\n') 57 | } 58 | } 59 | 60 | module.exports = ConsoleLogger -------------------------------------------------------------------------------- /lib/logger/WebAdminLogger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ConsoleLogger = require('./ConsoleLogger') 4 | 5 | class WebAdminLogger extends ConsoleLogger { 6 | constructor() { 7 | super() 8 | 9 | this._logs = '' 10 | } 11 | 12 | /** 13 | * @returns {string} 14 | */ 15 | getAllLogs() { 16 | return this._logs 17 | } 18 | 19 | /** 20 | * 21 | * @param {string} color 22 | * @param {string} prefix 23 | * @param {Object} data 24 | * @private 25 | */ 26 | _prepareLog(color, prefix, data) { 27 | super._prepareLog(color, prefix, data) 28 | 29 | Object.keys(data).forEach(key => { 30 | if (data[key] instanceof Error) { 31 | data[key] = data[key].stack || data[key] 32 | } 33 | }) 34 | 35 | this._logs += `${new Date().toString().split(' ')[4]} [${prefix}] - ` 36 | 37 | Object.keys(data).forEach(key => { 38 | if (typeof data[key] == 'object') 39 | this._logs += `${key} ${JSON.stringify(data[key], null, 2)}` 40 | else 41 | this._logs += `${key} ${data[key]}` 42 | }) 43 | this._logs += '\n' 44 | } 45 | } 46 | 47 | module.exports = WebAdminLogger -------------------------------------------------------------------------------- /lib/models/CallbackGame.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class CallbackGame { 4 | //placeholder 5 | } 6 | 7 | module.exports = CallbackGame -------------------------------------------------------------------------------- /lib/models/InlineQueryResult.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class InlineQueryResult { 4 | toJSON() { } 5 | 6 | get id() { } 7 | } 8 | 9 | module.exports = InlineQueryResult -------------------------------------------------------------------------------- /lib/models/InputMessageContent.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class InputMessageContent { 4 | /** 5 | * 6 | * @param {Object} raw 7 | * @returns {InputTextMessageContent|InputVenueMessageContent|InputContactMessageContent|InputLocationMessageContent} 8 | */ 9 | static deserialize(raw) { 10 | switch (raw) { 11 | case raw.message_text: 12 | return require('./InputTextMessageContent').deserialize(raw) 13 | case raw.latitude && raw.title: 14 | return require('./InputVenueMessageContent').deserialize(raw) 15 | case raw.phone_number: 16 | return require('./InputContactMessageContent').deserialize(raw) 17 | case raw.latitude: 18 | return require('./InputLocationMessageContent').deserialize(raw) 19 | } 20 | } 21 | } 22 | 23 | module.exports = InputMessageContent -------------------------------------------------------------------------------- /lib/mvc/BaseScopeExtension.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Represents any scope extension 5 | * 6 | * if you want to create your own scope extension, 7 | * you must extend BaseScopeExtension 8 | * and override all methods 9 | */ 10 | class BaseScopeExtension { 11 | /** 12 | * 13 | * @param {Scope} scope 14 | */ 15 | constructor(scope) { } 16 | 17 | /** 18 | * This method will be called by your extension user ( $.yourExtension(args...) ) 19 | * @param {...*} 20 | */ 21 | process() { throw 'Not implemented' } 22 | 23 | /** 24 | * You should return your extension name here. That name will be in scope. ( $.yourExtensionName ) 25 | * @returns {string} 26 | */ 27 | get name() { throw 'Not implemented' } 28 | } 29 | 30 | module.exports = BaseScopeExtension -------------------------------------------------------------------------------- /lib/mvc/InlineScope.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class InlineScope { 4 | /** 5 | * 6 | * @param {Update} update 7 | * @param {TelegramApi} api 8 | */ 9 | constructor(update, api, waitingChosenResults, waitingQueries) { 10 | this._update = update 11 | this._api = api 12 | this._waitingChosenResults = waitingChosenResults 13 | this._waitingQueries = waitingQueries 14 | this._inlineQuery = update.inlineQuery 15 | this._userId = update.inlineQuery.from.id 16 | } 17 | 18 | /** 19 | * 20 | * @returns {Update} 21 | */ 22 | get update() { 23 | return this._update 24 | } 25 | 26 | /** 27 | * 28 | * @returns {TelegramApi} 29 | */ 30 | get api() { 31 | return this._api 32 | } 33 | 34 | /** 35 | * 36 | * @returns {InlineQuery} 37 | */ 38 | get inlineQuery() { 39 | return this._inlineQuery 40 | } 41 | 42 | /** 43 | * 44 | * @returns {number} 45 | */ 46 | get userId() { 47 | return this._userId 48 | } 49 | 50 | /** 51 | * 52 | * @callback answerCallback 53 | * @param {InlineQueryResult} chosenResult 54 | */ 55 | 56 | /** 57 | * 58 | * @param {InlineQueryResult[]} results 59 | * @param {Object} [options] 60 | * @param {answerCallback} [callback] 61 | * @returns Promise 62 | */ 63 | answer(results, options, callback) { 64 | results = results.map(result => { 65 | if (!result.id) 66 | result._id = Math.random().toString(36).substring(7) 67 | 68 | return result 69 | }) 70 | 71 | this._api.answerInlineQuery(this._inlineQuery.id, results, options) 72 | .then(() => { 73 | results.forEach(result => { 74 | this._waitingChosenResults[result.id] = () => { 75 | callback(result) 76 | } 77 | }) 78 | }) 79 | } 80 | 81 | /** 82 | * 83 | * @param {InlineQueryResult[]} results 84 | * @param {number} answersPerPage 85 | * @param {answerCallback} callback 86 | */ 87 | answerPaginated(results, answersPerPage, callback) { 88 | let slicedData = results.slice(0, answersPerPage) 89 | 90 | this.answer(slicedData, { next_offset: results.length.toString() }, callback) 91 | 92 | this._waitingQueries[this._inlineQuery.query + ':' + this._inlineQuery.from.id] = ($) => { 93 | $.answerPaginated(results.slice(answersPerPage), answersPerPage, callback) 94 | } 95 | } 96 | 97 | //api methods starts here 98 | 99 | /** 100 | * 101 | * @param {string} text 102 | * @param {Object} [options] 103 | * @returns {Promise} 104 | */ 105 | sendMessage(text, options) { 106 | return this._api.sendMessage(this.userId, text, options) 107 | } 108 | 109 | /** 110 | * 111 | * @param {number} fromChatId 112 | * @param {number} messageId 113 | * @param {Object} [options] 114 | * @returns {Promise} 115 | */ 116 | forwardMessage(fromChatId, messageId, options) { 117 | return this._api.forwardMessage(this.userId, fromChatId, messageId, options) 118 | } 119 | 120 | /** 121 | * 122 | * @param {InputFile|Object} photo 123 | * @param {Object} [options] 124 | * @returns {Promise} 125 | */ 126 | sendPhoto(photo, options) { 127 | return this._api.sendPhoto(this.userId, photo, options) 128 | } 129 | 130 | /** 131 | * 132 | * @param {InputFile|Object} audio 133 | * @param {Object} [options] 134 | * @returns {Promise} 135 | */ 136 | sendAudio(audio, options) { 137 | return this._api.sendAudio(this.userId, audio, options) 138 | } 139 | 140 | /** 141 | * 142 | * @param {InputFile|Object} document 143 | * @param {Object} [options] 144 | * @returns {Promise} 145 | */ 146 | sendDocument(document, options) { 147 | return this._api.sendDocument(this.userId, document, options) 148 | } 149 | 150 | /** 151 | * 152 | * @param {InputFile|Object} sticker 153 | * @param {Object} [options] 154 | * @returns {Promise} 155 | */ 156 | sendSticker(sticker, options) { 157 | return this._api.sendSticker(this.userId, sticker, options) 158 | } 159 | 160 | /** 161 | * 162 | * @param {InputFile|Object} video 163 | * @param {Object} [options] 164 | * @returns {Promise} 165 | */ 166 | sendVideo(video, options) { 167 | return this._api.sendVideo(this.userId, video, options) 168 | } 169 | 170 | /** 171 | * 172 | * @param {InputFile|Object} voice 173 | * @param {Object} [options] 174 | * @returns {Promise} 175 | */ 176 | sendVoice(voice, options) { 177 | return this._api.sendVoice(this.userId, voice, options) 178 | } 179 | 180 | /** 181 | * 182 | * @param {number} latitude 183 | * @param {number} longitude 184 | * @param {Object} [options] 185 | * @returns {Promise} 186 | */ 187 | sendLocation(latitude, longitude, options) { 188 | return this._api.sendLocation(this.userId, latitude, longitude, options) 189 | } 190 | 191 | /** 192 | * 193 | * @param {number} latitude 194 | * @param {number} longitude 195 | * @param {string} title 196 | * @param {string}address 197 | * @param {Object} [options] 198 | * @returns {Promise} 199 | */ 200 | sendVenue(latitude, longitude, title, address, options) { 201 | return this._api.sendVenue(this.userId, latitude, longitude, title, address, options) 202 | } 203 | 204 | /** 205 | * 206 | * @param {string} phoneNumber 207 | * @param {string} firstName 208 | * @param {Object} [options] 209 | * @returns {Promise} 210 | */ 211 | sendContact(phoneNumber, firstName, options) { 212 | return this._api.sendContact(this.userId, phoneNumber, firstName, options) 213 | } 214 | 215 | /** 216 | * 217 | * @param {string} action 218 | * @returns {Promise} 219 | */ 220 | sendChatAction(action) { 221 | return this._api.sendChatAction(this.userId, action) 222 | } 223 | 224 | /** 225 | * 226 | * @param {number} offset 227 | * @param {number} limit 228 | * @returns {Promise} 229 | */ 230 | getUserProfilePhotos(offset, limit) { 231 | return this._api.getUserProfilePhotos(userId, offset, limit) 232 | } 233 | 234 | /** 235 | * 236 | * @param {number} userId 237 | * @returns {Promise} 238 | */ 239 | kickChatMember(userId) { 240 | return this._api.kickChatMember(this.userId, userId) 241 | } 242 | 243 | /** 244 | * 245 | * @returns {Promise} 246 | */ 247 | leaveChat() { 248 | return this._api.leaveChat(this.userId) 249 | } 250 | 251 | /** 252 | * 253 | * @param {number} userId 254 | * @returns {Promise} 255 | */ 256 | unbanChatMember(userId) { 257 | return this._api.unbanChatMember(this.userId, userId) 258 | } 259 | 260 | /** 261 | * 262 | * @returns {Promise} 263 | */ 264 | getChat() { 265 | return this._api.getChat(this.userId) 266 | } 267 | 268 | /** 269 | * 270 | * @returns {Promise} 271 | */ 272 | getChatAdministrators() { 273 | return this._api.getChatAdministrators(this.userId) 274 | } 275 | 276 | /** 277 | * 278 | * @returns {Promise} 279 | */ 280 | getChatMembersCount() { 281 | return this._api.getChatMembersCount(this.userId) 282 | } 283 | 284 | /** 285 | * 286 | * @param {number} userId 287 | * @returns {Promise.} 288 | */ 289 | getChatMember(userId) { 290 | return this._api.getChatMember(this.userId, userId) 291 | } 292 | } 293 | 294 | module.exports = InlineScope -------------------------------------------------------------------------------- /lib/mvc/Scope.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const InlineKeyboardButton = require('../models/InlineKeyboardButton') 4 | const InlineKeyboardMarkup = require('../models/InlineKeyboardMarkup') 5 | const ReplyKeyboardMarkup = require('../models/ReplyKeyboardMarkup') 6 | const KeyboardButton = require('../models/KeyboardButton') 7 | 8 | class Scope { 9 | /** 10 | * 11 | * @param {Update} update 12 | * @param {TelegramApi} api 13 | * @param {BaseScopeExtension[]} extensions 14 | * @param {Function[]} waitingRequests 15 | * @param {Object} waitingCallbackQueries 16 | * @param {BaseLogger} logger 17 | * @param {Function} processUpdate 18 | * @param {TelegramSessionStorage} sessionStorage 19 | * @param {Function} waitForUpdate 20 | * @param {Function} waitForCallback 21 | */ 22 | constructor( 23 | update, 24 | api, 25 | extensions, 26 | waitingRequests, 27 | waitingCallbackQueries, 28 | logger, 29 | sessionStorage, 30 | waitForUpdate, 31 | waitForCallback 32 | ) { 33 | this._api = api 34 | this._update = update 35 | /** 36 | * 37 | * @type {BaseScopeExtension[]} 38 | * @private 39 | */ 40 | this._extensions = extensions 41 | this._waitingRequests = waitingRequests 42 | this._waitingCallbackQueries = waitingCallbackQueries 43 | 44 | this._isEditedMessage = update.editedMessage ? true : false 45 | 46 | this._message = update.message || update.editedMessage 47 | this._chatId = this._message.chat.id 48 | this._userId = this._message.from.id 49 | this._fromGroupChat = !(this._userId === this._chatId) 50 | 51 | this._logger = logger 52 | this._sessionStorage = sessionStorage 53 | this._waitForUpdate = waitForUpdate 54 | this._waitForCallback = waitForCallback 55 | 56 | this._extensions.forEach(extension => { 57 | const extensionInstance = new extension(this) 58 | this[extensionInstance.name] = extensionInstance.process 59 | }) 60 | } 61 | 62 | /** 63 | * @returns {TelegramSessionStorage} 64 | */ 65 | get sessionStorage() { 66 | return this._sessionStorage 67 | } 68 | 69 | /** 70 | * @returns {BaseStorage} 71 | */ 72 | get storage() { 73 | return this._sessionStorage 74 | } 75 | 76 | /** 77 | * 78 | * @returns {Update} 79 | */ 80 | get update() { 81 | return this._update 82 | } 83 | 84 | /** 85 | * 86 | * @returns {Message} 87 | */ 88 | get message() { 89 | return this._message 90 | } 91 | 92 | /** 93 | * 94 | * @returns {number} 95 | */ 96 | get chatId() { 97 | return this._chatId 98 | } 99 | 100 | /** 101 | * 102 | * @returns {number} 103 | */ 104 | get userId() { 105 | return this._userId 106 | } 107 | 108 | /** 109 | * 110 | * @returns {boolean} 111 | */ 112 | get idFromGroupChat() { 113 | return this._fromGroupChat 114 | } 115 | 116 | /** 117 | * 118 | * @returns {TelegramApi} 119 | */ 120 | get api() { 121 | return this._api 122 | } 123 | 124 | /** 125 | * @param {string} key 126 | * @returns {Promise.<*>} 127 | */ 128 | getUserSession(key) { 129 | return this._sessionStorage.getUserSession(this.userId, key) 130 | } 131 | 132 | /** 133 | * @param {string} key 134 | * @param {*} value 135 | * @returns {Promise} 136 | */ 137 | setUserSession(key, value) { 138 | return this._sessionStorage.setUserSession(this.userId, key, value) 139 | } 140 | 141 | /** 142 | * @param {string} key 143 | * @returns {Promise.<*>} 144 | */ 145 | getChatSession(key) { 146 | return this._sessionStorage.getChatSession(this.chatId, key) 147 | } 148 | 149 | /** 150 | * @param {string} key 151 | * @param {*} value 152 | * @returns {Promise} 153 | */ 154 | setChatSession(key, value) { 155 | return this._sessionStorage.setChatSession(this.chatId, key, value) 156 | } 157 | 158 | /** 159 | * 160 | * @returns {BaseLogger} 161 | */ 162 | get logger() { 163 | return this._logger 164 | } 165 | 166 | /** 167 | * 168 | * @returns {boolean} 169 | */ 170 | get isEditedMessage() { 171 | return this._isEditedMessage 172 | } 173 | 174 | /** 175 | * After calling this the next update 176 | * from current user will be passed to promise 177 | * 178 | * @returns {Promise} 179 | */ 180 | get waitForRequest() { 181 | return new Promise(resolve => { 182 | this._waitingRequests[this.chatId] = resolve 183 | this._waitForUpdate(this.chatId) 184 | }) 185 | } 186 | 187 | /** 188 | * @callback waitForCallbackQueryCallback 189 | * @param {CallbackQuery} query 190 | */ 191 | 192 | /** 193 | * If you send some inline keyboard after that you can call this method, 194 | * pass to it string callback data or array of string or your InlineKeyboardMarkup 195 | * and then when user press button CallbackQuery will be passed to callback 196 | * 197 | * @param {string|string[]|InlineKeyboardMarkup} data 198 | * @param {waitForCallbackQueryCallback} callback 199 | */ 200 | waitForCallbackQuery(data, callback) { 201 | if (typeof data === 'string') { 202 | this._waitForCallback(data) 203 | this._waitingCallbackQueries[data] = callback 204 | } 205 | 206 | if (Array.isArray(data)) { 207 | data.forEach(item => { 208 | this._waitForCallback(item) 209 | this._waitingCallbackQueries[item] = callback 210 | }) 211 | } 212 | 213 | if (data instanceof InlineKeyboardMarkup) { 214 | data.inlineKeyboard.forEach(line => { 215 | line.forEach(key => { 216 | this._waitForCallback(key.callbackData) 217 | this._waitingCallbackQueries[key.callbackData] = callback 218 | }) 219 | }) 220 | } 221 | } 222 | 223 | /** 224 | * 225 | * @param {Object} menuData 226 | */ 227 | runMenu(menuData) { 228 | const startMessage = menuData.message 229 | 230 | const ignoredKeys = [ 231 | 'message', 232 | 'layout', 233 | 'options', 234 | 'resizeKeyboard', 235 | 'oneTimeKeyboard', 236 | 'anyMatch' 237 | ] 238 | 239 | const keys = Object.keys(menuData) 240 | let keyboard = [] 241 | 242 | if (menuData.layout) { 243 | let lineIndex = 0 244 | 245 | keys.forEach(key => { 246 | if (ignoredKeys.indexOf(key) === -1) { 247 | if (!keyboard[lineIndex]) 248 | keyboard[lineIndex] = [] 249 | 250 | keyboard[lineIndex].push(new KeyboardButton(key)) 251 | 252 | if (typeof menuData.layout === 'number') { 253 | if (keyboard[lineIndex].length === menuData.layout) { 254 | lineIndex++ 255 | } 256 | } else { 257 | if (keyboard[lineIndex].length === menuData.layout[lineIndex]) { 258 | lineIndex++ 259 | } 260 | } 261 | 262 | } 263 | }) 264 | } else { 265 | keys.forEach(key => { 266 | if (ignoredKeys.indexOf(key) === -1) { 267 | keyboard.push([new KeyboardButton(key)]) 268 | } 269 | }) 270 | } 271 | 272 | const resizeKeyboard = (menuData.resizeKeyboard && menuData.resizeKeyboard === true) 273 | const oneTimeKeyboard = (menuData.oneTimeKeyboard && menuData.oneTimeKeyboard === true) 274 | 275 | let replyMarkup = new ReplyKeyboardMarkup(keyboard, resizeKeyboard, oneTimeKeyboard) 276 | 277 | let options = { 278 | reply_markup: JSON.stringify(replyMarkup) 279 | } 280 | 281 | if (menuData.options) options = Object.assign(options, menuData.options) 282 | 283 | this.sendMessage(startMessage, options) 284 | 285 | this.waitForRequest 286 | .then($ => { 287 | if (keys.indexOf($.message.text) > -1 && 288 | ignoredKeys.indexOf($.message.text) === -1) { 289 | if (typeof menuData[$.message.text] === 'object') { 290 | $.runMenu(menuData[$.message.text]) 291 | } else { 292 | menuData[$.message.text]($) 293 | } 294 | } else if (menuData.anyMatch) { 295 | menuData.anyMatch($) 296 | } else { 297 | $.runMenu(menuData) 298 | } 299 | }) 300 | } 301 | 302 | /** 303 | * 304 | * @callback runFormCallback 305 | * @param {Object} response 306 | */ 307 | 308 | /** 309 | * 310 | * @param {Object} formData 311 | * @param {runFormCallback} callback 312 | */ 313 | runForm(formData, callback) { 314 | let i = 0 315 | 316 | const run = () => { 317 | const key = keys[i] 318 | 319 | this.sendMessage(formData[key].q, { 320 | disable_web_page_preview: true, 321 | reply_markup: formData[key].keyboard ? JSON.stringify({ 322 | one_time_keyboard: true, 323 | resize_keyboard: formData[key].resize_keyboard || false, 324 | keyboard: formData[key].keyboard 325 | }) : '' 326 | }) 327 | 328 | this.waitForRequest 329 | .then($ => { 330 | formData[key].validator($.message, (valid, value) => { 331 | if (valid === true) { 332 | result[key] = value 333 | i++ 334 | 335 | if (i === Object.keys(formData).length) { 336 | try { 337 | callback(result) 338 | } 339 | catch (e) { 340 | this.logger.error({ 'error in user callback:': e }) 341 | } 342 | 343 | return 344 | } 345 | 346 | run() 347 | } else { 348 | this.sendMessage(formData[key].error, { 349 | disable_web_page_preview: true 350 | }) 351 | .then(() => { 352 | run() 353 | }) 354 | } 355 | }) 356 | }) 357 | } 358 | 359 | let result = {} 360 | const keys = Object.keys(formData) 361 | 362 | run() 363 | } 364 | 365 | /** 366 | * 367 | * @param {Object} menuData 368 | */ 369 | runInlineMenu(menuData, prevMessage) { 370 | const method = menuData.method 371 | const params = menuData.params || [] 372 | const layout = menuData.layout 373 | const menu = menuData.menu 374 | 375 | let keyboard = [] 376 | 377 | let callbackData = [] 378 | 379 | if (!layout) { 380 | keyboard = menu.map(item => { 381 | callbackData.push(Math.random().toString(36).substring(7)) 382 | 383 | return [new InlineKeyboardButton( 384 | item.text, 385 | item.url, 386 | callbackData[callbackData.length - 1] 387 | )] 388 | }) 389 | } 390 | else { 391 | let line = 0 392 | menu.forEach(item => { 393 | if (!keyboard[line]) keyboard[line] = [] 394 | 395 | callbackData.push(Math.random().toString(36).substring(7)) 396 | 397 | keyboard[line].push(new InlineKeyboardButton( 398 | item.text, 399 | item.url, 400 | callbackData[callbackData.length - 1] 401 | )) 402 | 403 | let goToNextLine = Array.isArray(layout) ? keyboard[line].length === 404 | layout[line] : keyboard[line].length === layout 405 | 406 | if (goToNextLine) 407 | line++ 408 | }) 409 | } 410 | 411 | if (typeof params[params.length - 1] === 'object') { 412 | params[params.length - 1] = Object.assign(params[params.length - 1], { 413 | reply_markup: JSON.stringify(new InlineKeyboardMarkup(keyboard)) 414 | }) 415 | } 416 | else { 417 | params.push({ 418 | reply_markup: JSON.stringify(new InlineKeyboardMarkup(keyboard)) 419 | }) 420 | } 421 | 422 | var prepareCallback = (response) => { 423 | callbackData.forEach((data, index) => { 424 | this.waitForCallbackQuery(data, (query) => { 425 | if (menu[index].callback) 426 | try { 427 | menu[index].callback(query, response) 428 | } 429 | catch (e) { 430 | this.logger.error({ 'error in user callback:': e }) 431 | } 432 | else { 433 | this.runInlineMenu(menu[index], response) 434 | } 435 | }) 436 | }) 437 | } 438 | 439 | if (!prevMessage) { 440 | this[method].apply(this, params) 441 | .then(response => { 442 | prepareCallback(response) 443 | }) 444 | } 445 | else { 446 | params[0].chat_id = prevMessage.chat.id 447 | params[0].message_id = prevMessage.messageId 448 | 449 | this.api.editMessageText(menuData.message, params[0]) 450 | .then(response => { 451 | prepareCallback(response) 452 | }) 453 | } 454 | } 455 | 456 | //api methods starts here 457 | 458 | /** 459 | * 460 | * @param {string} text 461 | * @param {Object} [options] 462 | * @returns {Promise} 463 | */ 464 | sendMessage(text, options) { 465 | return this._api.sendMessage(this.chatId, text, options) 466 | } 467 | 468 | /** 469 | * 470 | * @param {number} fromChatId 471 | * @param {number} messageId 472 | * @param {Object} [options] 473 | * @returns {Promise} 474 | */ 475 | forwardMessage(fromChatId, messageId, options) { 476 | return this._api.forwardMessage(this.chatId, fromChatId, messageId, options) 477 | } 478 | 479 | /** 480 | * 481 | * @param {InputFile|Object} photo 482 | * @param {Object} [options] 483 | * @returns {Promise} 484 | */ 485 | sendPhoto(photo, options) { 486 | return this._api.sendPhoto(this.chatId, photo, options) 487 | } 488 | 489 | /** 490 | * 491 | * @param {InputFile|Object} audio 492 | * @param {Object} [options] 493 | * @returns {Promise} 494 | */ 495 | sendAudio(audio, options) { 496 | return this._api.sendAudio(this.chatId, audio, options) 497 | } 498 | 499 | /** 500 | * 501 | * @param {InputFile|Object} document 502 | * @param {Object} [options] 503 | * @returns {Promise} 504 | */ 505 | sendDocument(document, options) { 506 | return this._api.sendDocument(this.chatId, document, options) 507 | } 508 | 509 | /** 510 | * 511 | * @param {InputFile|Object} sticker 512 | * @param {Object} [options] 513 | * @returns {Promise} 514 | */ 515 | sendSticker(sticker, options) { 516 | return this._api.sendSticker(this.chatId, sticker, options) 517 | } 518 | 519 | /** 520 | * 521 | * @param {InputFile|Object} video 522 | * @param {Object} [options] 523 | * @returns {Promise} 524 | */ 525 | sendVideo(video, options) { 526 | return this._api.sendVideo(this.chatId, video, options) 527 | } 528 | 529 | /** 530 | * 531 | * @param {InputFile|Object} voice 532 | * @param {Object} [options] 533 | * @returns {Promise} 534 | */ 535 | sendVoice(voice, options) { 536 | return this._api.sendVoice(this.chatId, voice, options) 537 | } 538 | 539 | /** 540 | * 541 | * @param {number} latitude 542 | * @param {number} longitude 543 | * @param {Object} [options] 544 | * @returns {Promise} 545 | */ 546 | sendLocation(latitude, longitude, options) { 547 | return this._api.sendLocation(this.chatId, latitude, longitude, options) 548 | } 549 | 550 | /** 551 | * 552 | * @param {number} latitude 553 | * @param {number} longitude 554 | * @param {string} title 555 | * @param {string}address 556 | * @param {Object} [options] 557 | * @returns {Promise} 558 | */ 559 | sendVenue(latitude, longitude, title, address, options) { 560 | return this._api.sendVenue(this.chatId, latitude, longitude, title, address, options) 561 | } 562 | 563 | /** 564 | * 565 | * @param {string} phoneNumber 566 | * @param {string} firstName 567 | * @param {Object} [options] 568 | * @returns {Promise} 569 | */ 570 | sendContact(phoneNumber, firstName, options) { 571 | return this._api.sendContact(this.chatId, phoneNumber, firstName, options) 572 | } 573 | 574 | /** 575 | * 576 | * @param {string} action 577 | * @returns {Promise} 578 | */ 579 | sendChatAction(action) { 580 | return this._api.sendChatAction(this.chatId, action) 581 | } 582 | 583 | /** 584 | * 585 | * @param {number} offset 586 | * @param {number} limit 587 | * @returns {Promise} 588 | */ 589 | getUserProfilePhotos(offset, limit) { 590 | return this._api.getUserProfilePhotos(userId, offset, limit) 591 | } 592 | 593 | /** 594 | * 595 | * @param {number} userId 596 | * @returns {Promise.} 597 | */ 598 | kickChatMember(userId) { 599 | return this._api.kickChatMember(this.chatId, userId) 600 | } 601 | 602 | /** 603 | * 604 | * @returns {Promise.} 605 | */ 606 | leaveChat() { 607 | return this._api.leaveChat(this.chatId) 608 | } 609 | 610 | /** 611 | * 612 | * @param {number} userId 613 | * @returns {Promise.} 614 | */ 615 | unbanChatMember(userId) { 616 | return this._api.unbanChatMember(this.chatId, userId) 617 | } 618 | 619 | /** 620 | * 621 | * @returns {Promise} 622 | */ 623 | getChat() { 624 | return this._api.getChat(this.chatId) 625 | } 626 | 627 | /** 628 | * 629 | * @returns {Promise} 630 | */ 631 | getChatAdministrators() { 632 | return this._api.getChatAdministrators(this.chatId) 633 | } 634 | 635 | /** 636 | * 637 | * @returns {Promise} 638 | */ 639 | getChatMembersCount() { 640 | return this._api.getChatMembersCount(this.chatId) 641 | } 642 | 643 | /** 644 | * 645 | * @param {number} userId 646 | * @returns {Promise.} 647 | */ 648 | getChatMember(userId) { 649 | return this._api.getChatMember(this.chatId, userId) 650 | } 651 | } 652 | 653 | module.exports = Scope 654 | -------------------------------------------------------------------------------- /lib/mvc/TelegramBaseCallbackQueryController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Base Callback Query Controller 5 | * you must extend TelegramBaseCallbackQueryController 6 | * to create callback query controller. 7 | */ 8 | class TelegramBaseCallbackQueryController { 9 | /** 10 | * This method of your controller will be called to handle callbackQuery. 11 | * 12 | * @param {CallbackQuery} query 13 | */ 14 | handle(query) { throw 'Not implemented' } 15 | } 16 | 17 | module.exports = TelegramBaseCallbackQueryController -------------------------------------------------------------------------------- /lib/mvc/TelegramBaseController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Represents any TelegramController 5 | * you must extend TelegramBaseController 6 | * and override at least the handle method to create controller 7 | */ 8 | class TelegramBaseController { 9 | constructor() { 10 | this._api = null 11 | this._localization = null 12 | } 13 | 14 | /** 15 | * This method of your controller will be called to handle command. 16 | * 17 | * @param {Scope} scope 18 | */ 19 | handle(scope) { throw 'Not implemented' } 20 | 21 | /** 22 | * If you want a specific methods of your controller be called for specific commands, 23 | * you should return here an plain object where key is a route and value is name of your method. 24 | * In that case handle method will not be called and scope will be passed to your method. 25 | * Return example: { '/start': 'startMethod' } 26 | * 27 | * @returns {Object} 28 | */ 29 | get routes() { return {} } 30 | 31 | /** 32 | * This method will be called before any command handler or handle method. 33 | * You can modify incoming scope and must return it. 34 | * Your modified scope will be passed to controller. 35 | * 36 | * @param {Scope} scope 37 | * @returns {Scope} 38 | */ 39 | before(scope) { return scope } 40 | 41 | /** 42 | * 43 | * @param {TelegramApi} api 44 | */ 45 | set api(api) { 46 | this._api = api 47 | } 48 | 49 | /** 50 | * 51 | * @param {Ivan} localization 52 | */ 53 | set localization(localization) { 54 | this._localization = localization 55 | } 56 | } 57 | 58 | module.exports = TelegramBaseController -------------------------------------------------------------------------------- /lib/mvc/TelegramBaseInlineQueryController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Base inline query controller 5 | * you must extend TelegramBaseInlineQueryController 6 | * and override at least the handle method to create controller 7 | */ 8 | class TelegramBaseInlineQueryController { 9 | constructor() { 10 | this._api = null 11 | this._localization = null 12 | } 13 | 14 | /** 15 | * This method of your controller will be called to handle inline query. 16 | * 17 | * @param {InlineScope} scope 18 | */ 19 | handle(scope) { throw 'Not implemented' } 20 | 21 | /** 22 | * 23 | * @param {ChosenInlineResult} result 24 | */ 25 | chosenResult(result) { } 26 | 27 | /** 28 | * 29 | * @param {TelegramApi} api 30 | */ 31 | set api(api) { 32 | this._api = api 33 | } 34 | 35 | /** 36 | * 37 | * @param {Ivan} localization 38 | */ 39 | set localization(localization) { 40 | this._localization = localization 41 | } 42 | } 43 | 44 | module.exports = TelegramBaseInlineQueryController -------------------------------------------------------------------------------- /lib/routing/TelegramRoute.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class TelegramRoute { 4 | /** 5 | * @param {BaseCommand|BaseCommand[]} commands 6 | * @param {TelegramBaseController} controller 7 | */ 8 | constructor(commands, controller) { 9 | this._commands = Array.isArray(commands) ? commands : [commands] 10 | this._controller = controller 11 | } 12 | 13 | /** 14 | * @returns {BaseCommand[]} 15 | */ 16 | get commands() { 17 | return this._commands 18 | } 19 | 20 | /** 21 | * @returns {TelegramBaseController} 22 | */ 23 | get controller() { 24 | return this._controller 25 | } 26 | 27 | /** 28 | * @param {Scope} scope 29 | * @returns {boolean|BaseCommand} 30 | */ 31 | test(scope) { 32 | for (let command of this._commands) { 33 | if (command.test(scope) == true) { 34 | return command 35 | } 36 | } 37 | 38 | return false 39 | } 40 | } 41 | 42 | module.exports = TelegramRoute -------------------------------------------------------------------------------- /lib/routing/TelegramRouter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TelegramRoute = require('./TelegramRoute') 4 | const AnyCommand = require('./commands/AnyCommand') 5 | 6 | class TelegramRouter { 7 | constructor() { 8 | /** 9 | * @type {TelegramRoute[]} 10 | * @private 11 | */ 12 | this._routes = [] 13 | 14 | /** 15 | * @type {TelegramBaseController} 16 | * @private 17 | */ 18 | this._otherwiseController = null 19 | 20 | this._callbackQueryController = null 21 | this._inlineQueryController = null 22 | } 23 | 24 | 25 | /** 26 | * You can pass your command pattern or array of patterns 27 | * and some child of TelegramBaseController 28 | * 29 | * After that any update that satisfies your command 30 | * will be passed to your controller 31 | * 32 | * @param {BaseCommand|BaseCommand[]} commands 33 | * @param {TelegramBaseController} controller 34 | * @returns {TelegramRouter} 35 | */ 36 | when(commands, controller) { 37 | this._routes.push(new TelegramRoute(commands, controller)) 38 | 39 | return this 40 | } 41 | 42 | /** 43 | * This child of TelegramBaseController will be called for all updates 44 | * 45 | * @param {TelegramBaseController} controller 46 | * @returns {TelegramRouter} 47 | */ 48 | any(controller) { 49 | this._routes.push(new TelegramRoute(new AnyCommand(), controller)) 50 | 51 | return this 52 | } 53 | 54 | /** 55 | * This child of TelegramBaseController will be called 56 | * if there is no controller for that update (except controller passed to 'any' method) 57 | * 58 | * @param {TelegramBaseController} controller 59 | * @returns {TelegramRouter} 60 | */ 61 | otherwise(controller) { 62 | this._otherwiseController = controller 63 | 64 | return this 65 | } 66 | 67 | 68 | /** 69 | * This child of TelegramBaseCallbackQueryController will be called for all callback queries 70 | * 71 | * @param {TelegramBaseCallbackQueryController} controller 72 | */ 73 | callbackQuery(controller) { 74 | this._callbackQueryController = controller 75 | return this 76 | } 77 | 78 | /** 79 | * This child of TelegramBaseCallbackQueryController will be called for all inline queries 80 | * 81 | * @param {TelegramBaseInlineQueryController} controller 82 | * @returns {TelegramRouter} 83 | */ 84 | inlineQuery(controller) { 85 | this._inlineQueryController = controller 86 | return this 87 | } 88 | 89 | /** 90 | * 91 | * @returns {TelegramBaseCallbackQueryController|null} 92 | */ 93 | get callbackQueryController() { 94 | return this._callbackQueryController 95 | } 96 | 97 | /** 98 | * 99 | * @returns {TelegramBaseInlineQueryController|null} 100 | */ 101 | get inlineQueryController() { 102 | return this._inlineQueryController 103 | } 104 | 105 | /** 106 | * @param {Scope} scope 107 | * @returns { { controller: TelegramBaseController, handler: string }[] } 108 | */ 109 | controllersForScope(scope) { 110 | let controllers = [] 111 | 112 | this._routes.forEach(route => { 113 | let command = route.test(scope) 114 | 115 | if (command !== false) { 116 | let controllerRoutes = route.controller.routes 117 | let controllerHandler 118 | 119 | if (controllerRoutes && controllerRoutes[command.handlerName]) { 120 | controllerHandler = controllerRoutes[command.handlerName] 121 | } 122 | else { 123 | controllerHandler = 'handle' 124 | } 125 | 126 | controllers.push({ 127 | controller: route.controller, 128 | handler: controllerHandler 129 | }) 130 | } 131 | }) 132 | 133 | if (controllers.length === 0 && this._otherwiseController !== null) { 134 | controllers.push({ controller: this._otherwiseController, handler: 'handle'}) 135 | } 136 | 137 | return controllers 138 | } 139 | } 140 | 141 | module.exports = TelegramRouter 142 | -------------------------------------------------------------------------------- /lib/routing/commands/AnyCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseCommand = require('./BaseCommand') 4 | 5 | class AnyCommand { 6 | /** 7 | * @param {Scope} scope 8 | * @returns {boolean} 9 | */ 10 | test(scope) { 11 | return true 12 | } 13 | 14 | /** 15 | * @returns {string} 16 | */ 17 | get handlerName() { 18 | return 'handle' 19 | } 20 | } 21 | 22 | module.exports = AnyCommand -------------------------------------------------------------------------------- /lib/routing/commands/BaseCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class BaseCommand { 4 | /** 5 | * Tests message 6 | * 7 | * @param {Scope} scope 8 | * @returns {boolean} 9 | */ 10 | test(scope) { 11 | throw 'Not implemented' 12 | } 13 | 14 | /** 15 | * Returns handler method name in controller 16 | * 17 | * @returns {string} 18 | */ 19 | get handlerName() { throw 'Not implemented' } 20 | } 21 | 22 | 23 | module.exports = BaseCommand -------------------------------------------------------------------------------- /lib/routing/commands/CustomFilterCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseCommand = require('./BaseCommand') 4 | 5 | class CustomFilterCommand extends BaseCommand { 6 | /** 7 | * @param {function} filterCallback 8 | * @param {string} [handler] 9 | */ 10 | constructor(filterCallback, handler) { 11 | super() 12 | this._filterCallback = filterCallback 13 | this._handler = handler 14 | } 15 | 16 | /** 17 | * @param {Scope} scope 18 | * @returns {boolean} 19 | */ 20 | test(scope) { 21 | console.log(this._filterCallback) 22 | return this._filterCallback(scope) 23 | } 24 | 25 | /** 26 | * @returns {string} 27 | */ 28 | get handlerName() { 29 | return this._handler 30 | } 31 | } 32 | 33 | module.exports = CustomFilterCommand -------------------------------------------------------------------------------- /lib/routing/commands/RegexpCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseCommand = require('./BaseCommand') 4 | 5 | class RegexpCommand extends BaseCommand { 6 | /** 7 | * @param {RegExp} regexp 8 | * @param {string} [handler] 9 | */ 10 | constructor(regexp, handler) { 11 | super() 12 | this._regexp = regexp 13 | this._handler = handler 14 | } 15 | 16 | /** 17 | * @param {Scope} scope 18 | * @returns {boolean} 19 | */ 20 | test(scope) { 21 | return scope.message.text && this._regexp.test(scope.message.text) 22 | } 23 | 24 | /** 25 | * @returns {string} 26 | */ 27 | get handlerName() { 28 | return this._handler 29 | } 30 | } 31 | 32 | module.exports = RegexpCommand -------------------------------------------------------------------------------- /lib/routing/commands/TextCommand.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseCommand = require('./BaseCommand') 4 | 5 | class TextCommand extends BaseCommand { 6 | /** 7 | * @param {string} textPattern 8 | * @param {string} [handler] 9 | */ 10 | constructor(textPattern, handler) { 11 | super() 12 | this._textPattern = textPattern 13 | this._handler = handler 14 | } 15 | 16 | /** 17 | * @param {Scope} scope 18 | * @returns {boolean} 19 | */ 20 | test(scope) { 21 | return scope.message.text && 22 | scope.message.text.indexOf(this._textPattern) > -1 23 | } 24 | 25 | /** 26 | * @returns {string} 27 | */ 28 | get handlerName() { 29 | return this._handler 30 | } 31 | } 32 | 33 | module.exports = TextCommand -------------------------------------------------------------------------------- /lib/statistics/Statistics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Statistics { 4 | constructor() { 5 | this._requestsCount = 0 6 | this._workersRequests = {} 7 | this._workers = {} 8 | } 9 | 10 | /** 11 | * @param {number} workerPid 12 | */ 13 | registrateRequest(workerPid) { 14 | this._requestsCount++ 15 | 16 | if (!this._workersRequests[workerPid]) 17 | this._workersRequests[workerPid] = 1 18 | else 19 | this._workersRequests[workerPid] += 1 20 | } 21 | 22 | /** 23 | * @returns {number} 24 | */ 25 | getTotalRequestsCount() { 26 | return this._requestsCount 27 | } 28 | 29 | /** 30 | * @returns {{}} 31 | */ 32 | getWorkersRequests() { 33 | return this._workersRequests 34 | } 35 | 36 | /** 37 | * @returns {string} 38 | */ 39 | getUptime() { 40 | return this._secondsToHms(process.uptime()) 41 | } 42 | 43 | /** 44 | * @returns {{}} 45 | */ 46 | getWorkersStatus() { 47 | return this._workers 48 | } 49 | 50 | /** 51 | * @param {number} workerPid 52 | */ 53 | addWorker(workerPid) { 54 | this._workers[workerPid] = { 55 | status: 'live' 56 | } 57 | } 58 | 59 | /** 60 | * @param {number} workerPid 61 | */ 62 | workerDied(workerPid) { 63 | this._workers[workerPid] = { 64 | status: 'died' 65 | } 66 | } 67 | 68 | _secondsToHms(d) { 69 | d = Number(d) 70 | var h = Math.floor(d / 3600) 71 | var m = Math.floor(d % 3600 / 60) 72 | var s = Math.floor(d % 3600 % 60) 73 | return ((h > 0 ? h + ":" + (m < 10 ? "0" : "") : "") + m + ":" + (s < 10 ? "0" : "") + s) 74 | } 75 | } 76 | 77 | module.exports = Statistics -------------------------------------------------------------------------------- /lib/storage/BaseStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Represents some abstract storage 5 | * you must extend BaseStorage and override all methods to create your own storage 6 | */ 7 | class BaseStorage { 8 | /** 9 | * @param {string} storage 10 | * @param {string} key 11 | * @returns {Promise} 12 | */ 13 | get(storage, key) { throw 'Not implemented' } 14 | 15 | /** 16 | * @param {string} storage 17 | * @param {string} key 18 | * @param {Object} data 19 | * @returns {Promise<>} 20 | */ 21 | set(storage, key, data) { throw 'Not implemented' } 22 | 23 | /** 24 | * @param {string} storage 25 | * @param {string} key 26 | * @returns {Promise<>} 27 | */ 28 | remove(storage, key) { throw 'Not implemented' } 29 | } 30 | 31 | module.exports = BaseStorage -------------------------------------------------------------------------------- /lib/storage/session/InMemoryStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseStorage = require('../BaseStorage') 4 | 5 | /** 6 | * Standard in memory storage, will be used if there is no custom storage passed Telegram 7 | */ 8 | class InMemoryStorage extends BaseStorage { 9 | constructor() { 10 | super() 11 | 12 | this._storage = {} 13 | } 14 | 15 | /** 16 | * @param {string} storage 17 | * @param {string} key 18 | * @returns {Promise} 19 | */ 20 | get(storage, key) { 21 | return new Promise(resolve => { 22 | if (!this._storage[storage]) this._storage[storage] = {} 23 | 24 | resolve(this._storage[storage][key] || {}) 25 | }) 26 | } 27 | 28 | /** 29 | * @param {string} storage 30 | * @param {key} key 31 | * @param {Object} data 32 | */ 33 | set(storage, key, data) { 34 | return new Promise(resolve => { 35 | if (!this._storage[storage]) this._storage[storage] = {} 36 | 37 | this._storage[storage][key] = data 38 | 39 | resolve() 40 | }) 41 | } 42 | 43 | /** 44 | * @param {string} storage 45 | * @param {string} key 46 | */ 47 | remove(storage, key) { 48 | return new Promise(resolve => { 49 | if (this._storage[storage] && this._storage[storage][key]) { 50 | this._storage[storage][key] = null 51 | } 52 | 53 | resolve() 54 | }) 55 | } 56 | } 57 | 58 | module.exports = InMemoryStorage -------------------------------------------------------------------------------- /lib/storage/session/TelegramSession.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class TelegramSession { 4 | /** 5 | * @param {Object} userSession 6 | * @param {Object} chatSession 7 | */ 8 | constructor(userSession, chatSession) { 9 | this._userSession = userSession 10 | this._chatSession = chatSession 11 | } 12 | 13 | /** 14 | * @returns {Object} 15 | */ 16 | get userSession() { 17 | return this._userSession 18 | } 19 | 20 | /** 21 | * @returns {Object} 22 | */ 23 | get chatSession() { 24 | return this._chatSession 25 | } 26 | } 27 | 28 | module.exports = TelegramSession -------------------------------------------------------------------------------- /lib/storage/session/TelegramSessionStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const CHAT_STORAGE = 'chatStorage' 4 | const USER_STORAGE = 'userStorage' 5 | 6 | class TelegramSessionStorage { 7 | /** 8 | * 9 | * @param {BaseStorage} storage 10 | */ 11 | constructor(storage) { 12 | this._storage = storage 13 | this._cache = {} 14 | } 15 | 16 | /** 17 | * @param {number} userId 18 | * @param {string} key 19 | * @returns {Promise<*>} 20 | */ 21 | getUserSession(userId, key) { 22 | return this._storage.get( 23 | USER_STORAGE, 24 | this._generateKeyForUserSession(userId, key) 25 | ) 26 | } 27 | 28 | /** 29 | * @param {number} userId 30 | * @param {string} key 31 | * @param {*} value 32 | * @returns {Promise} 33 | */ 34 | setUserSession(userId, key, value) { 35 | return this._storage.set( 36 | USER_STORAGE, 37 | this._generateKeyForUserSession(userId, key), 38 | value 39 | ) 40 | } 41 | 42 | /** 43 | * @param {number} chatId 44 | * @param {string} key 45 | * @returns {Promise<*>} 46 | */ 47 | getChatSession(chatId, key) { 48 | return this._storage.get( 49 | CHAT_STORAGE, 50 | this._generateKeyForUserSession(chatId, key) 51 | ) 52 | } 53 | 54 | /** 55 | * @param {number} chatId 56 | * @param {string} key 57 | * @param {*} value 58 | * @returns {Promise} 59 | */ 60 | setChatSession(chatId, key, value) { 61 | return this._storage.set( 62 | CHAT_STORAGE, 63 | this._generateKeyForUserSession(chatId, key), 64 | value 65 | ) 66 | } 67 | 68 | /** 69 | * @param {number} userId 70 | * @param {string} key 71 | * @returns {string} 72 | * @private 73 | */ 74 | _generateKeyForUserSession(userId, key) { 75 | return `USER_${userId}_${key}` 76 | } 77 | 78 | /** 79 | * @param {number} chatId 80 | * @param {string} key 81 | * @returns {string} 82 | * @private 83 | */ 84 | _generateKeyForChatSession(chatId, key) { 85 | return `CHAT_${chatId}_${key}` 86 | } 87 | } 88 | 89 | module.exports = TelegramSessionStorage 90 | -------------------------------------------------------------------------------- /lib/storage/sharedStorage/SharedStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseStorage = require('../BaseStorage') 4 | const Message = require('./models/Message') 5 | const GetMessage = require('./models/GetMessage') 6 | const SetMessage = require('./models/SetMessage') 7 | const RemoveMessage = require('./models/RemoveMessage') 8 | const ResponseMessage = require('./models/ResponseMessage') 9 | 10 | /** 11 | * SharedStorage used to sync data between workers 12 | */ 13 | class SharedStorage extends BaseStorage { 14 | /** 15 | * @param {BaseStorage} storage 16 | */ 17 | constructor(storage) { 18 | super() 19 | 20 | this._storage = storage 21 | this._callbacks = {} 22 | } 23 | 24 | /** 25 | * @param {object} msg 26 | * @param {Worker} worker 27 | */ 28 | handleMessageFromWorkers(msg, worker) { 29 | msg = Message.deserialize(msg) 30 | 31 | if (msg instanceof GetMessage) { 32 | this._storage.get(msg.storage, msg.key) 33 | .then(data => { 34 | worker.send(Message.response(data, msg.id).serialize()) 35 | }) 36 | } 37 | 38 | if (msg instanceof SetMessage) { 39 | this._storage.set(msg.storage, msg.key, msg.value) 40 | .then(() => { 41 | worker.send(Message.response(null, msg.id).serialize()) 42 | }) 43 | } 44 | 45 | if (msg instanceof RemoveMessage) { 46 | this._storage.remove(msg.storage, msg.key) 47 | .then(() => { 48 | worker.send(Message.response(null, msg.id).serialize()) 49 | }) 50 | } 51 | } 52 | 53 | /** 54 | * @param {object} msg 55 | */ 56 | handleMessageFromMaster(msg) { 57 | msg = Message.deserialize(msg) 58 | 59 | if (msg instanceof ResponseMessage) { 60 | this._callbacks[msg.id](msg.data) 61 | } 62 | } 63 | 64 | /** 65 | * 66 | * @param {string} storage 67 | * @param {string} key 68 | * @returns {Promise} 69 | */ 70 | get(storage, key) { 71 | return new Promise(resolve => { 72 | let id = this._genId() 73 | 74 | this._callbacks[id] = resolve 75 | 76 | process.send(Message.get(storage, key, id).serialize()) 77 | }) 78 | } 79 | 80 | /** 81 | * 82 | * @param {string} storage 83 | * @param {key} key 84 | * @param {Object} data 85 | * @returns {Promise<>} 86 | */ 87 | set(storage, key, data) { 88 | return new Promise(resolve => { 89 | let id = this._genId() 90 | 91 | this._callbacks[id] = resolve 92 | 93 | process.send(Message.set(storage, key, data, id).serialize()) 94 | }) 95 | } 96 | 97 | /** 98 | * 99 | * @param {string} storage 100 | * @param {string} key 101 | */ 102 | remove(storage, key) { 103 | return new Promise(resolve => { 104 | let id = this._genId() 105 | 106 | this._callbacks[id] = resolve 107 | 108 | process.send(Message.remove(storage, key, id).serialize()) 109 | }) 110 | } 111 | 112 | /** 113 | * @returns {string} 114 | * @private 115 | */ 116 | _genId() { 117 | return Math.random().toString(36).substring(7) 118 | } 119 | } 120 | 121 | module.exports = SharedStorage -------------------------------------------------------------------------------- /lib/storage/sharedStorage/models/GetMessage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class GetMessage { 4 | /** 5 | * @param {string} storage 6 | * @param {string} key 7 | * @param {string} id 8 | */ 9 | constructor(storage, key, id) { 10 | this._storage = storage 11 | this._key = key 12 | this._id = id 13 | } 14 | 15 | /** 16 | * @returns {string} 17 | */ 18 | get storage() { 19 | return this._storage 20 | } 21 | 22 | /** 23 | * @returns {string} 24 | */ 25 | get key() { 26 | return this._key 27 | } 28 | 29 | /** 30 | * @returns {string} 31 | */ 32 | get id() { 33 | return this._id 34 | } 35 | 36 | /** 37 | * @returns {object} 38 | */ 39 | serialize() { 40 | return { 41 | storage: this._storage, 42 | key: this._key, 43 | id: this._id 44 | } 45 | } 46 | 47 | /** 48 | * @param {object} raw 49 | * @returns {GetMessage} 50 | */ 51 | static deserialize(raw) { 52 | return new GetMessage( 53 | raw.storage, 54 | raw.key, 55 | raw.id 56 | ) 57 | } 58 | } 59 | 60 | module.exports = GetMessage -------------------------------------------------------------------------------- /lib/storage/sharedStorage/models/Message.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GetMessage = require('./GetMessage') 4 | const SetMessage = require('./SetMessage') 5 | const RemoveMessage = require('./RemoveMessage') 6 | const ResponseMessage = require('./ResponseMessage') 7 | 8 | class Message { 9 | constructor(type, payload) { 10 | this._type = type 11 | this._payload = payload 12 | } 13 | 14 | /** 15 | * @param {string} storage 16 | * @param {string} key 17 | * @param {string} id 18 | */ 19 | static get(storage, key, id) { 20 | return new Message('get', new GetMessage(storage, key, id)) 21 | } 22 | 23 | /** 24 | * @param {string} storage 25 | * @param {string} key 26 | * @param {object} value 27 | * @param {string} id 28 | */ 29 | static set(storage, key, value, id) { 30 | return new Message('set', new SetMessage(storage, key, value, id)) 31 | } 32 | 33 | /** 34 | * @param {string} storage 35 | * @param {string} key 36 | * @param {string} id 37 | */ 38 | static remove(storage, key, id) { 39 | return new Message('remove', new RemoveMessage(storage, key, id)) 40 | } 41 | 42 | /** 43 | * @param {object} data 44 | * @param {string} id 45 | * @returns {Message} 46 | */ 47 | static response(data, id) { 48 | return new Message('response', new ResponseMessage(data, id)) 49 | } 50 | 51 | /** 52 | * @returns {object} 53 | */ 54 | serialize() { 55 | return { 56 | type: this._type, 57 | payload: this._payload.serialize() 58 | } 59 | } 60 | 61 | static deserialize(raw) { 62 | switch (raw.type) { 63 | case 'get': 64 | return GetMessage.deserialize(raw.payload) 65 | break 66 | 67 | case 'set': 68 | return SetMessage.deserialize(raw.payload) 69 | break 70 | 71 | case 'remove': 72 | return RemoveMessage.deserialize(raw.payload) 73 | break 74 | 75 | case 'response': 76 | return ResponseMessage.deserialize(raw.payload) 77 | break 78 | 79 | default: 80 | return null 81 | break 82 | } 83 | } 84 | } 85 | 86 | module.exports = Message -------------------------------------------------------------------------------- /lib/storage/sharedStorage/models/RemoveMessage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class RemoveMessage { 4 | /** 5 | * @param {string} storage 6 | * @param {string} key 7 | * @param {string} id 8 | */ 9 | constructor(storage, key, id) { 10 | this._storage = storage 11 | this._key = key 12 | this._id = id 13 | } 14 | 15 | /** 16 | * @returns {string} 17 | */ 18 | get storage() { 19 | return this._storage 20 | } 21 | 22 | /** 23 | * @returns {string} 24 | */ 25 | get key() { 26 | return this._key 27 | } 28 | 29 | /** 30 | * @returns {string} 31 | */ 32 | get id() { 33 | return this._id 34 | } 35 | 36 | /** 37 | * @returns {object} 38 | */ 39 | serialize() { 40 | return { 41 | storage: this._storage, 42 | key: this._key, 43 | id: this._id 44 | } 45 | } 46 | 47 | /** 48 | * @param {object} raw 49 | * @returns {RemoveMessage} 50 | */ 51 | static deserialize(raw) { 52 | return new RemoveMessage( 53 | raw.storage, 54 | raw.key, 55 | raw.id 56 | ) 57 | } 58 | } 59 | 60 | module.exports = RemoveMessage -------------------------------------------------------------------------------- /lib/storage/sharedStorage/models/ResponseMessage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ResponseMessage { 4 | /** 5 | * @param {object} data 6 | * @param {string} id 7 | */ 8 | constructor(data, id) { 9 | this._data = data 10 | this._id = id 11 | } 12 | 13 | /** 14 | * @returns {object} 15 | */ 16 | get data() { 17 | return this._data 18 | } 19 | 20 | /** 21 | * @returns {string} 22 | */ 23 | get id() { 24 | return this._id 25 | } 26 | 27 | /** 28 | * @returns {object} 29 | */ 30 | serialize() { 31 | return { 32 | data: this._data, 33 | id: this._id 34 | } 35 | } 36 | 37 | /** 38 | * @param {object} raw 39 | * @returns {ResponseMessage} 40 | */ 41 | static deserialize(raw) { 42 | return new ResponseMessage( 43 | raw.data, 44 | raw.id 45 | ) 46 | } 47 | } 48 | 49 | module.exports = ResponseMessage -------------------------------------------------------------------------------- /lib/storage/sharedStorage/models/SetMessage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class SetMessage { 4 | /** 5 | * @param {string} storage 6 | * @param {string} key 7 | * @param {object} value 8 | * @param {string} id 9 | */ 10 | constructor(storage, key, value, id) { 11 | this._storage = storage 12 | this._key = key 13 | this._value = value 14 | this._id = id 15 | } 16 | 17 | /** 18 | * @returns {string} 19 | */ 20 | get storage() { 21 | return this._storage 22 | } 23 | 24 | /** 25 | * @returns {string} 26 | */ 27 | get key() { 28 | return this._key 29 | } 30 | 31 | /** 32 | * @returns {object} 33 | */ 34 | get value() { 35 | return this._value 36 | } 37 | 38 | /** 39 | * @returns {string} 40 | */ 41 | get id() { 42 | return this._id 43 | } 44 | 45 | /** 46 | * @returns {object} 47 | */ 48 | serialize() { 49 | return { 50 | storage: this._storage, 51 | key: this._key, 52 | value: this._value, 53 | id: this._id 54 | } 55 | } 56 | 57 | /** 58 | * @param {object} raw 59 | * @returns {SetMessage} 60 | */ 61 | static deserialize(raw) { 62 | return new SetMessage( 63 | raw.storage, 64 | raw.key, 65 | raw.value, 66 | raw.id 67 | ) 68 | } 69 | } 70 | 71 | module.exports = SetMessage -------------------------------------------------------------------------------- /lib/updateFetchers/BaseUpdateFetcher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class BaseUpdateFetcher { 4 | /** 5 | * @param {TelegramApi} api 6 | * @param {BaseLogger} logger 7 | */ 8 | constructor(api, logger) { 9 | this._api = api 10 | this._logger = logger 11 | } 12 | 13 | /** 14 | * @callback fetchUpdatesCallback 15 | * @param {Update[]} updates 16 | */ 17 | 18 | /** 19 | * @param {fetchUpdatesCallback} callback 20 | */ 21 | fetch(callback) { throw 'Not implemented' } 22 | } 23 | 24 | module.exports = BaseUpdateFetcher -------------------------------------------------------------------------------- /lib/updateFetchers/LongPoolingUpdateFetcher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseUpdateFetcher = require('./BaseUpdateFetcher') 4 | 5 | class LongPoolingUpdateFetcher extends BaseUpdateFetcher { 6 | /** 7 | * @param {TelegramApi} api 8 | * @param {BaseLogger} logger 9 | */ 10 | constructor(api, logger) { 11 | super(api, logger) 12 | 13 | this._callback = null 14 | } 15 | 16 | /** 17 | * @callback fetchUpdatesCallback 18 | * @param {Update[]} updates 19 | */ 20 | 21 | /** 22 | * @param {fetchUpdatesCallback} callback 23 | */ 24 | fetch(callback) { 25 | this._callback = callback 26 | this._api.setWebhook({ url: '' }) 27 | this._getUpdates() 28 | } 29 | 30 | /** 31 | * @param {number} [offset] 32 | * @private 33 | */ 34 | _getUpdates(offset) { 35 | offset = offset || 0 36 | 37 | this._api.getUpdates({ timeout: 50, offset }) 38 | .then(updates => { 39 | if (updates.length > 0) 40 | this._callback(updates) 41 | 42 | const nextOffset = updates.length > 0 ? updates[updates.length - 1].updateId + 1 : offset 43 | 44 | this._getUpdates(nextOffset) 45 | }) 46 | .catch(error => { 47 | this._logger.error({ fetchUpdate: error }) 48 | this._getUpdates() 49 | }) 50 | } 51 | } 52 | 53 | module.exports = LongPoolingUpdateFetcher -------------------------------------------------------------------------------- /lib/updateFetchers/WebhookUpdateFetcher.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseUpdateFetcher = require('./BaseUpdateFetcher') 4 | const http = require('http') 5 | const Update = require('../models/Update') 6 | 7 | class WebhookUpdateFetcher extends BaseUpdateFetcher { 8 | /** 9 | * @param {TelegramApi} api 10 | * @param {BaseLogger} logger 11 | * @param {string} url 12 | * @param {string} host 13 | * @param {number} port 14 | * @param {string} apiToken 15 | */ 16 | constructor(api, logger, url, host, port, apiToken) { 17 | super(api, logger) 18 | 19 | this._url = url 20 | this._host = host 21 | this._port = port 22 | this._apiToken = apiToken 23 | 24 | this._server = http.createServer((req, res) => this._handleRequest(req, res)) 25 | } 26 | 27 | /** 28 | * @param {fetchUpdatesCallback} callback 29 | */ 30 | fetch(callback) { 31 | this._callback = callback 32 | this._getUpdates() 33 | } 34 | 35 | /** 36 | * @private 37 | */ 38 | _getUpdates() { 39 | this._api.setWebhook({ url: this._url }) 40 | .then(() => { 41 | this._server.listen(this._port, this._host, () => { 42 | this._logger.log({ WebhookUpdateFetcher: `Server started at ${this._host}:${this._port}` }) 43 | }) 44 | }) 45 | } 46 | 47 | /** 48 | * @param req 49 | * @param res 50 | * @private 51 | */ 52 | _handleRequest(req, res) { 53 | const validateRegExp = new RegExp(this._apiToken) 54 | 55 | if (!validateRegExp.test(req.url)) { 56 | this._logger.error({ webhook: 'Not authorized request from Telegram' }) 57 | res.statusCode = 401 58 | res.end() 59 | } else if (req.method === 'POST') { 60 | let chunks = [] 61 | 62 | req.on('data', chunk => { 63 | chunks.push(chunk) 64 | }) 65 | 66 | req.on('end', () => { 67 | res.end('OK') 68 | 69 | const data = Buffer.concat(chunks).toString('utf-8') 70 | let parsedUpdate 71 | 72 | try { 73 | parsedUpdate = JSON.parse(data) 74 | } 75 | catch (e) { 76 | this._logger.error({ 'Error parsing webhook update from json': e }) 77 | } 78 | 79 | if(!parsedUpdate) 80 | return 81 | 82 | this._logger.log({ webhook: 'Got ', update: parsedUpdate }) 83 | 84 | const update = Update.deserialize(parsedUpdate) 85 | 86 | this._callback([update]) 87 | }) 88 | } else { 89 | this._logger.error({ webhook: 'Authorized request from Telegram but not a POST' }) 90 | 91 | res.statusCode = 400 92 | res.end() 93 | } 94 | } 95 | } 96 | 97 | module.exports = WebhookUpdateFetcher -------------------------------------------------------------------------------- /lib/updateProcessors/BaseUpdateProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class BaseUpdateProcessor { 4 | /** 5 | * 6 | * @param {BaseTelegramDataSource} delegate 7 | */ 8 | constructor(dataSource) { 9 | /** 10 | * @var {BaseTelegramDataSource} this._dataSource 11 | */ 12 | this._dataSource = dataSource 13 | } 14 | 15 | /** 16 | * 17 | * @param {Update} update 18 | */ 19 | process(update) { throw 'Not implemented' } 20 | 21 | /** 22 | * 23 | * @param {Update} update 24 | * @returns {boolean} 25 | */ 26 | supports(update) { throw 'Not implemented' } 27 | } 28 | 29 | module.exports = BaseUpdateProcessor -------------------------------------------------------------------------------- /lib/updateProcessors/InlineQueryUpdateProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseUpdateProcessor = require('./BaseUpdateProcessor') 4 | const InlineScope = require('../mvc/InlineScope') 5 | 6 | class InlineQueryUpdateProcessor extends BaseUpdateProcessor { 7 | /** 8 | * 9 | * @param {BaseTelegramDataSource} dataSource 10 | */ 11 | constructor(dataSource) { 12 | super(dataSource) 13 | 14 | this._waitingQueries = {} 15 | this._waitingChosenResults = {} 16 | } 17 | 18 | /** 19 | * 20 | * @param {Update} update 21 | */ 22 | process(update) { 23 | if (!this._dataSource.router.inlineQueryController) return 24 | 25 | if (update.inlineQuery) { 26 | let scope = new InlineScope( 27 | update, 28 | this._dataSource.api, 29 | this._waitingChosenResults, 30 | this._waitingQueries 31 | ) 32 | 33 | let queryText = update.inlineQuery.query 34 | let userId = update.inlineQuery.from.id 35 | 36 | if (this._waitingQueries[queryText + ':' + userId] && 37 | this._waitingQueries[queryText + ':' + userId] !== null) { 38 | const callback = this._waitingQueries[queryText + ':' + userId] 39 | callback(scope) 40 | 41 | if (this._waitingQueries[queryText + ':' + userId] == callback) 42 | this._waitingQueries[queryText + ':' + userId] = null 43 | 44 | return 45 | } 46 | 47 | try { 48 | this._dataSource.router.inlineQueryController.handle(scope) 49 | } 50 | catch (e) { 51 | this._dataSource.logger.error({ 52 | 'error': e, 53 | 'in controller': this._dataSource.router.inlineQueryController, 54 | 'for update': update 55 | }) 56 | } 57 | } 58 | 59 | if (update.chosenInlineResult) { 60 | let resultId = update.chosenInlineResult.resultId 61 | 62 | if (this._waitingChosenResults[resultId] && this._waitingChosenResults[resultId] !== null) { 63 | const callback = this._waitingChosenResults[resultId] 64 | 65 | callback(update.chosenInlineResult) 66 | 67 | if (this._waitingChosenResults[resultId] == callback) 68 | this._waitingChosenResults[resultId] = null 69 | 70 | return 71 | } 72 | 73 | try { 74 | this._dataSource.router.inlineQueryController.chosenResult(update.chosenInlineResult) 75 | } 76 | catch (e) { 77 | this._dataSource.logger.error({ 78 | 'error': e, 79 | 'in controller': this._dataSource.router.inlineQueryController, 80 | 'for update': update 81 | }) 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * 88 | * @param {Update} update 89 | * @returns {boolean} 90 | */ 91 | supports(update) { 92 | return !!(update.inlineQuery || update.chosenInlineResult) 93 | } 94 | } 95 | 96 | module.exports = InlineQueryUpdateProcessor -------------------------------------------------------------------------------- /lib/updateProcessors/MessageUpdateProcessor.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseUpdateProcessor = require('./BaseUpdateProcessor') 4 | const Scope = require('../mvc/Scope') 5 | const TelegramSession = require('../storage/session/TelegramSession') 6 | 7 | class MessageUpdateProcessor extends BaseUpdateProcessor { 8 | /** 9 | * 10 | * @param {BaseTelegramDataSource} dataSource 11 | */ 12 | constructor(dataSource) { 13 | super(dataSource) 14 | 15 | this._waitingRequests = {} 16 | this._waitingCallbackQueries = {} 17 | } 18 | 19 | /** 20 | * 21 | * @param {Update} update 22 | */ 23 | process(update) { 24 | if (update.message || update.editedMessage) { 25 | let message = update.message || update.editedMessage 26 | 27 | let scope = new Scope( 28 | update, 29 | this._dataSource.api, 30 | this._dataSource.scopeExtensions, 31 | this._waitingRequests, 32 | this._waitingCallbackQueries, 33 | this._dataSource.logger, 34 | this._dataSource.sessionStorage, 35 | chatId => this._waitForUpdate(chatId), 36 | data => this._waitForCallback(data) 37 | ) 38 | 39 | const chatId = message.chat.id 40 | 41 | if (this._waitingRequests[chatId] && this._waitingRequests[chatId] !== null) { 42 | const callback = this._waitingRequests[chatId] 43 | callback(scope) 44 | 45 | if (this._waitingRequests[chatId] == callback) this._waitingRequests[chatId] = null 46 | scope = null 47 | 48 | return 49 | } 50 | const controllers = this._dataSource.router.controllersForScope(scope) 51 | 52 | controllers.forEach(controller => { 53 | controller.controller.api = this._dataSource.api 54 | controller.controller.localization = this._dataSource.localization 55 | 56 | try { 57 | controller.controller[controller.handler](controller.controller.before(scope)) 58 | } 59 | catch (e) { 60 | this._dataSource.logger.error({ 61 | 'error': e, 62 | 'in controller': controller, 63 | 'for update': update 64 | }) 65 | } 66 | }) 67 | 68 | if (controllers.length === 0) { 69 | this._dataSource.logger.warn({ 70 | 'Cant find controller for update': update 71 | }) 72 | } 73 | 74 | scope = null 75 | 76 | return 77 | } 78 | 79 | if (update.callbackQuery) { 80 | if (this._waitingCallbackQueries[update.callbackQuery.data]) { 81 | this._waitingCallbackQueries[update.callbackQuery.data](update.callbackQuery) 82 | 83 | return 84 | } 85 | 86 | if (this._dataSource.router.callbackQueryController) { 87 | try { 88 | this._dataSource.router.callbackQueryController.handle(update.callbackQuery) 89 | } 90 | catch (e) { 91 | this._dataSource.logger.error({ 92 | 'error': e, 93 | 'in controller': this._dataSource.router.callbackQueryController, 94 | 'for update': update 95 | }) 96 | } 97 | } 98 | 99 | return 100 | } 101 | 102 | this._dataSource.logger.warn({ 'Update was not handled': update }) 103 | } 104 | 105 | /** 106 | * 107 | * @param {Update} update 108 | */ 109 | supports(update) { 110 | return !!(update.message || update.editedMessage || update.callbackQuery) 111 | } 112 | 113 | /** 114 | * @param {number} chatId 115 | * @private 116 | */ 117 | _waitForUpdate(chatId) { 118 | this._dataSource.ipc.askForNextUpdate(chatId) 119 | } 120 | 121 | /** 122 | * @param {string} data 123 | * @private 124 | */ 125 | _waitForCallback(data) { 126 | this._dataSource.ipc.askForNextCallbackQuery(data) 127 | } 128 | } 129 | 130 | module.exports = MessageUpdateProcessor -------------------------------------------------------------------------------- /lib/updateProcessors/UpdateProcessorDelegate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class UpdateProcessorDelegate { //Delegate in js, LOL :D 4 | /** 5 | * @returns {TelegramApi} 6 | */ 7 | get api() { throw 'Not implemented' } 8 | 9 | /** 10 | * @returns {TelegramRouter} 11 | */ 12 | get router() { throw 'Not implemented' } 13 | 14 | /** 15 | * @returns {BaseLogger} 16 | */ 17 | get logger() { throw 'Not implemented' } 18 | 19 | /** 20 | * @returns {BaseScopeExtension[]} 21 | */ 22 | get scopeExtensions() { throw 'Not implemented' } 23 | 24 | /** 25 | * @returns {TelegramSessionStorage} 26 | */ 27 | get sessionStorage() { throw 'Not implemented' } 28 | } 29 | 30 | module.exports = UpdateProcessorDelegate -------------------------------------------------------------------------------- /lib/updateProcessors/UpdateProcessorsManager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const BaseUpdateProcessor = require('./BaseUpdateProcessor') 4 | const MessageUpdateProcessor = require('./MessageUpdateProcessor') 5 | const InlineQueryUpdateProcessor = require('./InlineQueryUpdateProcessor') 6 | 7 | class UpdateProcessorsManager extends BaseUpdateProcessor { 8 | /** 9 | * 10 | * @param {BaseTelegramDataSource} delegate 11 | */ 12 | constructor(delegate) { 13 | super(delegate) 14 | 15 | /** 16 | * 17 | * @type {BaseUpdateProcessor[]} 18 | * @private 19 | */ 20 | this._processors = [ 21 | new MessageUpdateProcessor(this._dataSource), 22 | new InlineQueryUpdateProcessor(this._dataSource) 23 | ] 24 | } 25 | 26 | /** 27 | * 28 | * @param {Update} update 29 | */ 30 | process(update) { 31 | const processor = this._processorForUpdate(update) 32 | 33 | if (processor) { 34 | processor.process(update) 35 | } 36 | } 37 | 38 | /** 39 | * 40 | * @param update 41 | * @returns {boolean} 42 | */ 43 | supports(update) { 44 | return true 45 | } 46 | 47 | /** 48 | * 49 | * @param {Update} update 50 | * @returns {BaseUpdateProcessor} 51 | * @private 52 | */ 53 | _processorForUpdate(update) { 54 | for (const processor of this._processors) { 55 | if (processor.supports(update)) { 56 | return processor 57 | } 58 | } 59 | 60 | this._dataSource.logger.error({ 'No processor found for update:': update }) 61 | } 62 | } 63 | 64 | module.exports = UpdateProcessorsManager -------------------------------------------------------------------------------- /lib/utils/CallbackQueue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class CallbackQueue { 4 | /** 5 | * 6 | * @param {number} countPerSec 7 | */ 8 | constructor(countPerSec){ 9 | this._delay = 1000 / countPerSec 10 | this._queue = [] 11 | this._queueStarted = false 12 | 13 | this._prepareQueue() 14 | } 15 | 16 | /** 17 | * 18 | * @param {function} func 19 | */ 20 | push(func){ 21 | this._queue.push(func) 22 | 23 | if(!this._queueStarted) 24 | this._prepareQueue() 25 | } 26 | 27 | /** 28 | * 29 | * @private 30 | */ 31 | _prepareQueue(){ 32 | if(this._queue.length != 0){ 33 | const func = this._queue.shift() 34 | 35 | setTimeout(() => { 36 | func() 37 | this._prepareQueue() 38 | }, this._queueStarted ? this._delay : 0) 39 | 40 | this._queueStarted = true 41 | return 42 | } 43 | 44 | this._queueStarted = false 45 | } 46 | } 47 | 48 | module.exports = CallbackQueue -------------------------------------------------------------------------------- /lib/webAdmin/client/client.js: -------------------------------------------------------------------------------- 1 | var logsTextArea, logsHeight, health, content 2 | 3 | window.onload = function () { 4 | logsTextArea = document.getElementById('logs') 5 | logsHeight = window.getComputedStyle(logsTextArea).height.replace('px', '') 6 | health = document.getElementById('health') 7 | content = document.getElementsByClassName('content')[0] 8 | 9 | updateData() 10 | setInterval(updateData, 1000) 11 | } 12 | 13 | function updateData() { 14 | getRequest('/logs', function (logs) { 15 | if (logsTextArea.value != logs) { 16 | var willScroll = (logsTextArea.scrollHeight - logsTextArea.scrollTop <= logsHeight) 17 | logsTextArea.value = logs 18 | if (willScroll) 19 | logsTextArea.scrollTop = logsTextArea.scrollHeight 20 | } 21 | }) 22 | 23 | getRequest('/statistics', function (stats) { 24 | health.innerHTML = renderHealth(JSON.parse(stats)) 25 | }) 26 | } 27 | 28 | function getRequest(url, callback) { 29 | var xmlhttp = new XMLHttpRequest() 30 | xmlhttp.open('GET', url, true) 31 | xmlhttp.onreadystatechange = function() { 32 | if (xmlhttp.readyState == 4) { 33 | if(xmlhttp.status == 200) { 34 | callback(xmlhttp.responseText) 35 | } 36 | } 37 | } 38 | xmlhttp.send(null) 39 | } 40 | 41 | function restartWorkers() { 42 | getRequest('/restartWorkers', function () { 43 | 44 | }) 45 | } 46 | 47 | function renderHealth(stats) { 48 | return ( 49 | '
Uptime: ' + stats.uptime + '
' + 50 | '
' + 51 | '
Total requests: ' + stats.totalRequests + '
' + 52 | '
' + 53 | '
Requests by workers: ' + renderRequestsByWorkers(stats.requestsForWorkers) + '
' + 54 | '
' + 55 | '
Workers status: ' + renderWorkersStatus(stats.workersStatus) + '
' 56 | ) 57 | } 58 | 59 | function renderRequestsByWorkers(reqs) { 60 | var html = '' 61 | 62 | Object.keys(reqs).forEach(function (pid) { 63 | html += '
PID #' + pid + ' - ' + reqs[pid] 64 | }) 65 | 66 | return html 67 | } 68 | 69 | function renderWorkersStatus(stats) { 70 | var html = '' 71 | 72 | Object.keys(stats).forEach(function (pid) { 73 | html += '
PID ' + pid + ' - ' + stats[pid].status 74 | }) 75 | 76 | return html 77 | } -------------------------------------------------------------------------------- /lib/webAdmin/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | telegram-node-bot 9 | 10 | 11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 | 20 | 21 | 22 | 23 |

24 | docs 25 |

26 |

27 | help chat 28 |

29 |

30 | Narek Abovyan 2016 31 |

32 |
33 | 34 | -------------------------------------------------------------------------------- /lib/webAdmin/client/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 42 | 45 | 46 | 49 | 53 | 55 | 58 | 61 | 62 | 64 | 68 | 71 | 74 | 75 | 78 | 82 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /lib/webAdmin/client/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | background-color: #080808; 4 | color: #ffffff; 5 | animation: start 0.5s; 6 | font-family: 'Open Sans', sans-serif; 7 | } 8 | 9 | .logs { 10 | width: 100%; 11 | height: 500px; 12 | display: block; 13 | margin-bottom: 25px; 14 | } 15 | 16 | textarea { 17 | margin-top: 25px; 18 | background-color: #080808; 19 | border: 1px #2F9ED9 solid; 20 | border-radius: 5px; 21 | padding: 10px; 22 | height: 50px; 23 | display: block; 24 | color: #ffffff; 25 | outline: none; 26 | } 27 | 28 | button { 29 | color: #ffffff; 30 | background-color: #00b7ff; 31 | padding: 10px 15px; 32 | border: 1px solid #00b7ff; 33 | border-radius: 50px; 34 | cursor: pointer; 35 | outline: none; 36 | } 37 | 38 | @keyframes start { 39 | 0% { 40 | transform: scale(0.7, 0.7); 41 | } 42 | 100% { 43 | transform: scale(1, 1); 44 | } 45 | } 46 | 47 | #health { 48 | margin-top: 25px; 49 | text-align: center; 50 | border: solid 1px #2F9ED9; 51 | border-radius: 5px; 52 | margin-bottom: 25px; 53 | font-size: small; 54 | padding: 10px; 55 | } 56 | 57 | .content { 58 | width: 100%; 59 | max-width: 500px; 60 | margin: 0 auto; 61 | text-align: center; 62 | padding: 10px; 63 | padding-top: 20px; 64 | } 65 | 66 | * { 67 | box-sizing: border-box; 68 | } 69 | 70 | .health-wrap { 71 | margin: 0 auto; 72 | max-width: 300px; 73 | } 74 | 75 | a { 76 | color: #ffffff; 77 | } -------------------------------------------------------------------------------- /lib/webAdmin/server/WebAdmin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const path = require('path') 5 | const fs = require('fs') 6 | 7 | class WebAdmin { 8 | /** 9 | * @param {string} host 10 | * @param {number} port 11 | * @param {string} path 12 | * @param {WebAdminLogger} logger 13 | * @param {Telegram} telegram 14 | */ 15 | constructor(host, port, path, logger, telegram) { 16 | this._host = host 17 | this._port = port 18 | this._path = path 19 | this._logger = logger 20 | this._telegram = telegram 21 | 22 | this._server = http.createServer((request, response) => { 23 | this._handleRequest(request, response) 24 | }).listen(this._port, this._host) 25 | 26 | this._logger.log({ 'WebAdmin ': `started at ${host}:${port}`}) 27 | } 28 | 29 | /** 30 | * @param request 31 | * @param response 32 | * @private 33 | */ 34 | _handleRequest(request, response) { 35 | let reqPath = path.join(this._path, request.url == '/' ? '/index.html' : request.url) 36 | 37 | switch (request.url) { 38 | case '/logs': 39 | response.end(this._logger.getAllLogs()) 40 | break 41 | case '/restartWorkers': 42 | this._telegram.restartWorkers() 43 | response.end('ok') 44 | break 45 | case '/downloadLogs': 46 | response.writeHead(200, { 47 | 'Content-Type': 'text/plain', 48 | 'Content-Disposition': 'attachment; filename=logs.txt' 49 | }) 50 | response.end(this._logger.getAllLogs()) 51 | break 52 | case '/statistics': 53 | response.end(JSON.stringify({ 54 | totalRequests: this._telegram.statistics.getTotalRequestsCount(), 55 | requestsForWorkers: this._telegram.statistics.getWorkersRequests(), 56 | uptime: this._telegram.statistics.getUptime(), 57 | workersStatus: this._telegram.statistics.getWorkersStatus() 58 | })) 59 | 60 | } 61 | 62 | fs.lstat(reqPath, (err, stats) => { 63 | if (stats && stats.isFile()) { 64 | this._sendFile(response, reqPath, stats.size) 65 | } 66 | else { 67 | this._sendNotFound(response) 68 | } 69 | }) 70 | } 71 | 72 | /** 73 | * @param response 74 | * @param {string} filePath 75 | * @param {number} size 76 | * @private 77 | */ 78 | _sendFile(response, filePath, size) { 79 | const fileStream = fs.createReadStream(filePath) 80 | const ext = path.parse(filePath).ext 81 | 82 | response.writeHead(200, { 83 | 'Content-Type': this._getMimeType(ext), 84 | 'Content-Length': size 85 | }) 86 | 87 | fileStream.pipe(response) 88 | } 89 | 90 | /** 91 | * @param extension 92 | * @returns {*} 93 | * @private 94 | */ 95 | _getMimeType(extension) { 96 | switch (extension) { 97 | case '.html': 98 | return 'text/html' 99 | case '.js': 100 | return 'application/javascript' 101 | case '.css': 102 | return 'text/css' 103 | case '.svg': 104 | return 'image/svg+xml' 105 | default: 106 | return 'text/plain' 107 | } 108 | } 109 | 110 | /** 111 | * @param response 112 | * @private 113 | */ 114 | _sendNotFound(response) { 115 | response.writeHead(404) 116 | response.end('404 Not found') 117 | } 118 | } 119 | 120 | module.exports = WebAdmin -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "telegram-node-bot", 3 | "version": "4.0.5", 4 | "description": "Module for creating Telegram bots.", 5 | "main": "lib/Telegram.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prepublish": "node api-scheme/GenerateTGModels.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Naltox/telegram-node-bot.git" 13 | }, 14 | "keywords": [ 15 | "telegram", 16 | "bot", 17 | "API" 18 | ], 19 | "author": "Narek Abovyan ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/Naltox/telegram-bot/issues" 23 | }, 24 | "homepage": "https://github.com/Naltox/telegram-node-bot", 25 | "dependencies": { 26 | "tiny_request": "latest" 27 | }, 28 | "devDependencies": { 29 | "cheerio": "^0.20.0", 30 | "jsdoc": "latest", 31 | "ink-docstrap": "latest" 32 | }, 33 | "engines": { 34 | "node": ">=6.0.0" 35 | } 36 | } 37 | --------------------------------------------------------------------------------