├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── index.js ├── package.json └── tests.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "comma-dangle": 0 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 idchlife 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/idchlife/node-telegram-bot-api-middleware.svg?branch=master)](https://travis-ci.org/idchlife/node-telegram-bot-api-middleware) 2 | 3 | Using middleware for node-telegram-bot-api from yagop: https://github.com/yagop/node-telegram-bot-api 4 | 5 | ## Why? 6 | So, you happened to be here. And I assume you like writing telegram bots using **yagop**-s library node-telegram-bot-api. 7 | But sometimes you end making multiple promises, generators by hand, async/await constructions and all that. 8 | I present you the way of using power of generators and **co** library by **tj** by adding middleware before executing bot message callback 9 | 10 | ## Installation 11 | 12 | npm i node-telegram-bot-api-middleware --save 13 | 14 | Then you can use it like this: 15 | 16 | ```js 17 | const TelegramBot = require('node-telegram-bot-api'); 18 | const bot = new TelegramBot(TOKEN, { polling: true }); 19 | const use = require('node-telegram-bot-api-middleware').use; 20 | 21 | // Simple middleware that adds random method to your context 22 | function randomMiddleware() { 23 | this.random = (number) => Math.floor(Math.random() * number) 24 | } 25 | 26 | let response = use(randomMiddleware); 27 | 28 | bot.onText(/\/command/, response(function() { 29 | bot.sendMessage(this.chatId, this.random(10)); 30 | }); 31 | 32 | // or 33 | response = response.use(function() { 34 | bot.sendMessage(this.chatId, this.random()); 35 | }); 36 | 37 | bot.onText(/\/command/, response); 38 | 39 | // or 40 | 41 | response = response(function() { 42 | bot.sendMessage(this.chatId, this.random()); 43 | }); 44 | 45 | bot.onText(/\/command/, response); 46 | ``` 47 | 48 | ## Available at the moment middleware 49 | - **simpleauth** - https://github.com/idchlife/node-telegram-bot-api-middleware-simpleauth 50 | 51 | ## Usage 52 | 53 | ```js 54 | const use = require('node-telegram-bot-api-middleware').use; 55 | // Your configured bot 56 | const bot = require('./bot'); 57 | 58 | // You can use simple functions 59 | function middleware2() { 60 | // You will already have msg, chatId in context. This is achieved by added already inside 61 | // library default middleware, that populates context with those things, as well as method .stop() 62 | // (see further down about .stop) 63 | 64 | this.quickResponse = function* (text) { 65 | yield bot.sendMessage(this.chatId, text); 66 | }.bind(this); 67 | } 68 | 69 | // You can use generators 70 | function* middleware2() { 71 | yield this.quickResponse('You wrote something to this bot!'); 72 | 73 | console.log('Answer sent'); 74 | } 75 | 76 | // You cannot use short functions if you're going to use context passed from 77 | // previous middleware. `This wont't work: ` 78 | const notWorkingResponse = use(() => { console.log(this.msg.chat.id); }); 79 | 80 | // Be aware of adding middlewares in proper order, 81 | // because they will be executed in order in which you added them 82 | const response = use(middleware).use(middleware2); 83 | 84 | // You can also add more middleware to variable that already has set of middlewares 85 | // Adding more, it will create new set of middleware, not affecting old set of 86 | // middlewares. response still will have 2 middlewares. 87 | const checkAuth = response.use(function() { 88 | // Imagine, this also can be method from other middleware, 89 | // e.g.: this.isAuthenticated() 90 | const userIsAuthenticated = false; 91 | 92 | if (!userIsAuthenticated) { 93 | this.quickResponse('You are not authenticated to do that'); 94 | 95 | // If you want to prevent executing next middlewares, use .stop() 96 | this.stop(); 97 | } 98 | }); 99 | 100 | bot.onText(/\/need_auth/, checkAuth(function() { 101 | // Give some info only to authenticated user 102 | })); 103 | ``` 104 | 105 | ### Handling errors. 106 | 107 | Default error handler built in will console.error all errors from middleware and proceed to next one. 108 | 109 | You can also set your own global error handler like this (WARNING: this will replace default error handler, if you need to combine default handler with your - use middleware.getDefaultErrorHandler()): 110 | 111 | ```js 112 | const middleware = require('middleware'); 113 | const use = middleware.use; 114 | 115 | middleware.setErrorHandler(err => { 116 | // Your own logic for handling errors 117 | }); 118 | ``` 119 | 120 | Sometime you might need to set custom error handler to your middleware. 121 | This done line this: 122 | 123 | ```js 124 | function yourCustomMiddleware() { 125 | this.methodWithPossibilityOfErrorOccuring(); 126 | } 127 | 128 | yourCustomMiddleware.onErrorHandler = err => { 129 | // Log or do some other things with error 130 | } 131 | ``` 132 | 133 | ## How does it work 134 | 135 | **use** - is just a function, that returns another function, that accepts middleware as arguments or object with 136 | message data on bot.onText executiong. It also has .use method, that is just copy of function itself. Useful when 137 | writing code like use(middleware).use(middleware)(yourCallbackFunction) 138 | 139 | 140 | Basically you can write even like this: 141 | 142 | ```js 143 | use(middleware)(middleware).use(middleware)(botCallbackArguments); // botCallbackArguments will be passed by bot, and executed function will be also by bot. 144 | ``` 145 | 146 | For more information on this topic look into index.js file. There are many comments explaining how does it work. 147 | 148 | 149 | ## Help yourself and everyone build better bots! 150 | 151 | As you can see, this is cool opportunity to create many different middleware libraries for different purposes. For database connection, for special file uploaders or many other things. Join the opensource! (if you did not yet) Create middleware for some other libraries or for your special case and share with everyone! Create an issue or send pull request and I will add link to your middleware in the list. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | 5 | /** 6 | * Checking if fn is generator 7 | */ 8 | function isGenerator(fn) { 9 | return Object.getPrototypeOf(fn) === Object.getPrototypeOf(function* () {}); 10 | } 11 | 12 | /** 13 | * This is default middleware, that adds handy functionality to context for 14 | * other middlewares to use 15 | */ 16 | function defaultMiddleware(msg) { 17 | this.msg = msg; 18 | this.chatId = msg.chat.id; 19 | 20 | this.shouldStop = false; 21 | 22 | /** 23 | * Function that stops stepping into another middleware. Useful for stopping 24 | * if e.g. there is logical error or auth error etc. 25 | */ 26 | this.stop = () => { 27 | this.shouldStop = true; 28 | }; 29 | } 30 | 31 | /** 32 | * This is the main function that is used for creating context with .use and 33 | * returning function that also can add middleware or execute middleware if 34 | * bot object passed in arguments 35 | * 36 | * @param middleware 37 | * @returns {function(this:{middlewares})} 38 | */ 39 | const use = function use(middleware) { 40 | /** 41 | * Copying context, so middleware will be added only in returned callback, 42 | * and won't be added to global context of this function 43 | */ 44 | const copy = { 45 | middlewares: this.middlewares.slice() 46 | }; 47 | 48 | /** 49 | * Adding new middlewares 50 | */ 51 | copy.middlewares.push(middleware); 52 | 53 | /** 54 | * Binding callback to new context, so middlewares will be available 55 | */ 56 | const callback = botCallback.bind(copy); 57 | 58 | /** 59 | * Adding use method and binding it co copy context, so context will 60 | * be saved for further usage 61 | */ 62 | callback.use = use.bind(copy); 63 | 64 | return callback; 65 | }.bind({ 66 | middlewares: [] 67 | }); 68 | /** 69 | * First time binding to object with middlewares, so `this` context will be as 70 | * we expected 71 | */ 72 | 73 | const botCallback = function botCallback() { 74 | const args = arguments; 75 | 76 | /** 77 | * If arguments length is positive and first arguments 78 | * is function, then callback for bot message is added 79 | */ 80 | if (args.length && typeof args[0] === 'function') { 81 | const copy = { 82 | middlewares: this.middlewares.slice() 83 | }; 84 | 85 | copy.middlewares.push(args[0]); 86 | 87 | const callback = botCallback.bind(copy); 88 | callback.use = use.bind(copy); 89 | 90 | return callback; 91 | } 92 | 93 | /** 94 | * Check if there is msg.chat and msg.chat.id to ensure that everything is ok 95 | * when bot is executing function with arguments 96 | */ 97 | if (!args.length) { 98 | console.error( 99 | '[node-telegram-bot-api-middleware]: No arguments passed ' + 100 | 'to a function used in bot event callback or adding new middleware' 101 | ); 102 | 103 | return; 104 | } 105 | 106 | if (typeof args[0].chat === 'undefined') { 107 | console.error( 108 | '[node-telegram-bot-api-middleware]: Chat id not ' + 109 | 'defined in arguments. Not executing middlewares' 110 | ); 111 | 112 | return; 113 | } 114 | 115 | if (typeof args[0].chat.id === 'undefined') { 116 | console.error( 117 | '[node-telegram-bot-api-middleware]: There is no ' + 118 | 'visible chat id in chat in arguments. Not executing middlewares' 119 | ); 120 | return; 121 | } 122 | 123 | /** 124 | * Context object, that will be modified by all middlewares 125 | */ 126 | const context = {}; 127 | 128 | /** 129 | * Adding default middleware before other middlewares 130 | */ 131 | this.middlewares.unshift(defaultMiddleware); 132 | 133 | /** 134 | * Using co for executing generator, so we can use yield keyword 135 | */ 136 | return co(function* executeMiddlewares() { 137 | for (let i = 0, size = this.middlewares.length; i < size; i++) { 138 | /** 139 | * Somewhere in middleware was activated .stop method. No need to execute 140 | * middlewares further 141 | */ 142 | if (context.shouldStop) { 143 | return; 144 | } 145 | 146 | /** 147 | * Creating callback 148 | */ 149 | const middleware = this.middlewares[i]; 150 | 151 | /** 152 | * If middleware just a function, execute it without yield 153 | * 154 | * 155 | * Executing middleware with context object and args. 156 | * Args by this point is arguments passed by node-telegram-bot-api when 157 | * receiving new message from recipient 158 | */ 159 | try { 160 | if (!isGenerator(middleware)) { 161 | middleware.apply(context, args); 162 | } else { 163 | yield middleware.apply(context, args); 164 | } 165 | } catch (err) { 166 | /** 167 | * If middleware that had error has onErrorHandler in it, 168 | * pass error to it also 169 | */ 170 | if (typeof middleware.onErrorHandler === 'function') { 171 | middleware.onErrorHandler(err); 172 | } 173 | 174 | /** 175 | * We want error to pass to main handler also, 176 | * so it won't be lost 177 | */ 178 | throw err; 179 | } 180 | } 181 | }.bind(this)).catch(onErrorHandler); 182 | }; 183 | 184 | /** 185 | * Default error handler to handle errors in middleware. 186 | * It will only console.errors 187 | * 188 | * @param error 189 | */ 190 | const onErrorHandlerDefault = (error) => { 191 | console.error('[node-telegram-bot-api-middleware]: Error occurred in the middleware: '); 192 | console.error(error); 193 | }; 194 | 195 | let onErrorHandler = onErrorHandlerDefault; 196 | 197 | /** 198 | * You can set your own handler of all errors 199 | * @param onError 200 | */ 201 | exports.setOnErrorHandler = (onError) => { 202 | /** 203 | * OnError must be a function that does something 204 | */ 205 | if (typeof onError !== 'function') { 206 | console.error( 207 | '[node-telegram-bot-api-middleware]: ' + 208 | 'onErrorHandler must be function. Not setting.' 209 | ); 210 | } else { 211 | onErrorHandler = onError; 212 | } 213 | }; 214 | 215 | exports.getDefaultErrorHandler = () => onErrorHandlerDefault; 216 | 217 | /** 218 | * Reset error handler to default. For testing and various purposes 219 | */ 220 | exports.resetErrorHandler = () => { 221 | onErrorHandler = onErrorHandlerDefault; 222 | }; 223 | 224 | exports.use = use; 225 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-telegram-bot-api-middleware", 3 | "version": "0.1.3", 4 | "description": "Package that helps using middleware with generators in node-telegram-bot-api", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha tests.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "chai": "^3.5.0", 13 | "co-mocha": "^1.1.2", 14 | "eslint": "^2.10.2", 15 | "eslint-config-airbnb": "^9.0.1", 16 | "eslint-plugin-import": "^1.8.0", 17 | "eslint-plugin-jsx-a11y": "^1.2.0", 18 | "eslint-plugin-react": "^5.1.1", 19 | "mocha": "^2.4.5", 20 | "node-telegram-bot-api": "^0.21.1" 21 | }, 22 | "dependencies": { 23 | "co": "^4.6.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const middleware = require('./index'); 5 | const use = middleware.use; 6 | require('co-mocha'); 7 | 8 | const botArgumentsMock = { 9 | chat: { 10 | id: 1 11 | } 12 | }; 13 | 14 | describe('basic middleware usage', () => { 15 | it('should return function', () => { 16 | expect(use(() => {})).to.be.a('function'); 17 | }); 18 | 19 | const integratedValues = []; 20 | 21 | it('should execute middlewares in proper order', function* () { 22 | yield use(() => { 23 | integratedValues.push('A'); 24 | }).use(() => { 25 | integratedValues.push('B'); 26 | }).use(function* gen1() { 27 | integratedValues.push('C'); 28 | }).use(function* gen2() { 29 | integratedValues.push('D'); 30 | })(botArgumentsMock); 31 | 32 | expect(integratedValues.join('')).to.equal('ABCD'); 33 | }); 34 | 35 | it('should have properties from default middleware', function* () { 36 | let thereIsMsg = false; 37 | yield use(function () { 38 | thereIsMsg = typeof this.msg !== 'undefined'; 39 | })(botArgumentsMock); 40 | 41 | expect(thereIsMsg).to.equal(true); 42 | }); 43 | 44 | it('should have new middlewares and not have old used, when using use() from scratch', function* () { 45 | yield use(() => { 46 | integratedValues.push('E'); 47 | })(botArgumentsMock); 48 | 49 | // If test result would be false - value would be ABCDABCDE 50 | expect(integratedValues.join('')).to.equal('ABCDE'); 51 | }); 52 | 53 | it('should stop executing middleware when .stop() is used', function* () { 54 | const values = []; 55 | 56 | yield use(function() { 57 | this.stop(); 58 | }).use(() => { 59 | values.push('A'); 60 | })(botArgumentsMock); 61 | 62 | expect(values).to.be.empty; 63 | 64 | yield use(function () { 65 | values.push('A'); 66 | }).use(function () { 67 | this.stop(); 68 | }).use(function () { 69 | values.push('B'); 70 | })(botArgumentsMock); 71 | 72 | expect(values.join('')).to.equal('A'); 73 | }); 74 | 75 | it('should work properly with alternative syntax', function* () { 76 | const v = []; 77 | 78 | yield use(() => v.push('A')).use(() => v.push('B'))(() => v.push('C'))(botArgumentsMock); 79 | 80 | expect(v.join('')).to.equal('ABC'); 81 | 82 | yield use(() => v.push('D'))(function() { this.stop(); })(() => v.push('E'))(botArgumentsMock); 83 | 84 | expect(v.join('')).to.equal('ABCD'); 85 | }); 86 | 87 | it('should properly throw errors and also pass it to middleware error handler', function* () { 88 | let wasThereError = false; 89 | let middlewareErrorHandlerWorked = false; 90 | 91 | function customErrorHandler(err) { 92 | throw err; 93 | } 94 | 95 | function middlewareWithErrorHandler() { 96 | this.undefinedMethod(); 97 | } 98 | 99 | middlewareWithErrorHandler.onErrorHandler = function() { 100 | middlewareErrorHandlerWorked = true; 101 | }; 102 | 103 | middleware.setOnErrorHandler(customErrorHandler); 104 | 105 | try { 106 | yield use(middlewareWithErrorHandler)(botArgumentsMock); 107 | } catch (err) { 108 | wasThereError = true; 109 | } finally { 110 | expect(wasThereError).to.equal(true); 111 | expect(middlewareErrorHandlerWorked).to.equal(true); 112 | } 113 | }); 114 | 115 | it('should properly pass copied context and not affect previous middlewares', function* () { 116 | const values = []; 117 | 118 | const def = use(() => { 119 | values.push('Z'); 120 | }); 121 | 122 | const a = def.use(() => { 123 | values.push('A'); 124 | }); 125 | 126 | const b = def.use(() => { 127 | values.push('B'); 128 | }); 129 | 130 | const c = b.use(() => { 131 | values.push('C'); 132 | }); 133 | 134 | yield a(botArgumentsMock); 135 | yield b(botArgumentsMock); 136 | 137 | expect(values.join('')).to.equal('ZAZB'); 138 | 139 | yield c(botArgumentsMock); 140 | 141 | expect(values.join('')).to.equal('ZAZBZBC'); 142 | }); 143 | }); 144 | --------------------------------------------------------------------------------