diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..531f1a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,68 @@ + + + + + + +> Bug Report + +I have read: + +* [Usage information](https://github.com/yagop/node-telegram-bot-api/tree/master/doc/usage.md) +* [Help information](https://github.com/yagop/node-telegram-bot-api/tree/master/doc/help.md) + +I am using the latest version of the library. + +### Expected Behavior + + + +### Actual Behavior + + + +### Steps to reproduce the Behavior + + + + + + + +> Feature Request + +I have: + +* searched for such a feature request (https://github.com/yagop/node-telegram-bot-api/labels/enhancement) and found none + +### Introduction + + + +### Example + + + + + + + +> Question + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e35ebd9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + +- [ ] All tests pass +- [ ] I have run `npm run gen-doc` + +### Description + + + +### References + + diff --git a/CHANGELOG.md b/CHANGELOG.md index dc873c3..bf2a7c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,46 @@ This project adheres to [Semantic Versioning](http://semver.org/). + +* * * + +## [0.27.0][0.27.0] - 2016-02-10 + +Added: + +1. Add constructor options: + * (#243) `options.polling.params` (by @GochoMugo, requested-by @sidelux) +1. Add methods: + * (#74) *TelegramBot#removeReplyListener()* (by @githugger) +1. (#283) Add proper error handling (by @GochoMugo) +1. (#272) Add health-check endpoint (by @mironov) + * `options.webHook.healthEndpoint` +1. (#152) Add test for TelegramBot#sendDocument() using 'fileOpts' + param (by @evolun) +1. Document `options.webHook.host` (by @GochoMugo) +1. (#264) Add Bot API version to README (by @kamikazechaser) +1. Add examples: + - (#271) WebHook on Heroku (by @TheBeastOfCaerbannog) + - (#274) WebHook on Zeit Now (by @Ferrari) + +Changed: + +1. (#147) Use *String#indexOf()*, instead of *RegExp#test()*, to + find token in webhook request (by @AVVS) + +Fixed: + +* Fix bug: + - (#275, #280) fix es6 syntax error on Node.js v4.x (by @crazyabdul) + - (#276) promise.warning from `request-promise` (by @GochoMugo, + reported-by @preco21) + - (#281) fix handling error during polling (by @GochoMugo, + reported-by @dimawebmaker) + - (#284) fix error during deletion of already-set webhook, during + polling (by @GochoMugo, reported-by @dcparga) +1. Fix links in documentation (by @Ni2c2k) + + * * * ## [0.26.0][0.26.0] - 2016-01-20 @@ -67,4 +107,5 @@ Fixed: [0.25.0]:https://github.com/yagop/node-telegram-bot-api/releases/tag/v0.25.0 [0.26.0]:https://github.com/yagop/node-telegram-bot-api/releases/tag/v0.26.0 -[Unreleased]:https://github.com/yagop/node-telegram-bot-api/compare/v0.26.0...master +[0.27.0]:https://github.com/yagop/node-telegram-bot-api/releases/tag/v0.27.0 +[Unreleased]:https://github.com/yagop/node-telegram-bot-api/compare/v0.27.0...master diff --git a/README.md b/README.md index d62ab21..e62e46c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Build Status](https://travis-ci.org/yagop/node-telegram-bot-api.svg?branch=master)](https://travis-ci.org/yagop/node-telegram-bot-api) + [![Bot API](http://img.shields.io/badge/Bot API-v2.3.1-00aced.svg)](https://core.telegram.org/bots/api) + [![Build Status](https://travis-ci.org/yagop/node-telegram-bot-api.svg?branch=master)](https://travis-ci.org/yagop/node-telegram-bot-api) [![Build status](https://ci.appveyor.com/api/projects/status/ujko6bsum3g5msjh/branch/master?svg=true)](https://ci.appveyor.com/project/yagop/node-telegram-bot-api/branch/master) [![Coverage Status](https://coveralls.io/repos/yagop/node-telegram-bot-api/badge.svg?branch=master)](https://coveralls.io/r/yagop/node-telegram-bot-api?branch=master) [![bitHound Score](https://www.bithound.io/github/yagop/node-telegram-bot-api/badges/score.svg)](https://www.bithound.io/github/yagop/node-telegram-bot-api) @@ -53,24 +54,21 @@ bot.on('message', function (msg) { ## Documentation - * Usage ([release][usage-release] / [development][usage-dev]) - * Examples ([release][examples-release] / [development][examples-dev]) - * Help Information ([release][help-release] / [development][help-dev]) + * [Usage][usage] + * [Examples][examples] + * [Help Information][help] * API Reference ([release][api-release] / [development][api-dev]) * [Contributing to the Project][contributing] _**Note**: Development is done against the **master** branch. Code for the latest release resides on the **release** branch._ -[usage-release]:https://github.com/yagop/node-telegram-bot-api/tree/release/doc/usage.md -[examples-release]:https://github.com/yagop/node-telegram-bot-api/tree/release/doc/help.md -[help-release]:https://github.com/yagop/node-telegram-bot-api/tree/release/examples -[api-release]:https://github.com/yagop/node-telegram-bot-api/tree/release/doc/api.md -[usage-dev]:https://github.com/yagop/node-telegram-bot-api/tree/master/doc/usage.md -[examples-dev]:https://github.com/yagop/node-telegram-bot-api/tree/master/doc/help.md -[help-dev]:https://github.com/yagop/node-telegram-bot-api/tree/master/examples +[usage]:https://github.com/yagop/node-telegram-bot-api/tree/master/doc/usage.md +[examples]:https://github.com/yagop/node-telegram-bot-api/tree/master/examples +[help]:https://github.com/yagop/node-telegram-bot-api/tree/master/doc/help.md [api-dev]:https://github.com/yagop/node-telegram-bot-api/tree/master/doc/api.md +[api-release]:https://github.com/yagop/node-telegram-bot-api/tree/release/doc/api.md [contributing]:https://github.com/yagop/node-telegram-bot-api/tree/master/CONTRIBUTING.md @@ -86,6 +84,7 @@ Some things built using this library, and might interest you: * [tgfancy](https://github.com/GochoMugo/tgfancy): A Fancy, Higher-Level Wrapper for Telegram Bot API * [node-telegram-bot-api-middleware](https://github.com/idchlife/node-telegram-bot-api-middleware): Middleware for node-telegram-bot-api +* [teleirc](https://github.com/FruitieX/teleirc): A simple Telegram ↔ IRC gateway * * * diff --git a/doc/api.md b/doc/api.md index 9fa7798..e29e744 100644 --- a/doc/api.md +++ b/doc/api.md @@ -47,7 +47,8 @@ TelegramBot * [.getFileLink(fileId)](#TelegramBot+getFileLink) ⇒ Promise * [.downloadFile(fileId, downloadDir)](#TelegramBot+downloadFile) ⇒ Promise * [.onText(regexp, callback)](#TelegramBot+onText) - * [.onReplyToMessage(chatId, messageId, callback)](#TelegramBot+onReplyToMessage) + * [.onReplyToMessage(chatId, messageId, callback)](#TelegramBot+onReplyToMessage) ⇒ Number + * [.removeReplyListener(replyListenerId)](#TelegramBot+removeReplyListener) ⇒ Object * [.getChat(chatId)](#TelegramBot+getChat) ⇒ Promise * [.getChatAdministrators(chatId)](#TelegramBot+getChatAdministrators) ⇒ Promise * [.getChatMembersCount(chatId)](#TelegramBot+getChatMembersCount) ⇒ Promise @@ -70,16 +71,20 @@ Emits `message` when a message arrives. | token | String | | Bot Token | | [options] | Object | | | | [options.polling] | Boolean | Object | false | Set true to enable polling or set options. If a WebHook has been set, it will be deleted automatically. | -| [options.polling.timeout] | String | Number | 10 | Timeout in seconds for long polling | +| [options.polling.timeout] | String | Number | 10 | *Deprecated. Use `options.polling.params` instead*. Timeout in seconds for long polling. | | [options.polling.interval] | String | Number | 300 | Interval between requests in miliseconds | | [options.polling.autoStart] | Boolean | true | Start polling immediately | +| [options.polling.params] | Object | | Parameters to be used in polling API requests. See https://core.telegram.org/bots/api#getupdates for more information. | +| [options.polling.params.timeout] | Number | 10 | Timeout in seconds for long polling. | | [options.webHook] | Boolean | Object | false | Set true to enable WebHook or set options | +| [options.webHook.host] | String | 0.0.0.0 | Host to bind to | | [options.webHook.port] | Number | 8443 | Port to bind to | | [options.webHook.key] | String | | Path to file with PEM private key for webHook server. The file is read **synchronously**! | | [options.webHook.cert] | String | | Path to file with PEM certificate (public) for webHook server. The file is read **synchronously**! | | [options.webHook.pfx] | String | | Path to file with PFX private key and certificate chain for webHook server. The file is read **synchronously**! | | [options.webHook.autoOpen] | Boolean | true | Open webHook immediately | | [options.webHook.https] | Object | | Options to be passed to `https.createServer()`. Note that `options.webHook.key`, `options.webHook.cert` and `options.webHook.pfx`, if provided, will be used to override `key`, `cert` and `pfx` in this object, respectively. See https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener for more information. | +| [options.webHook.healthEndpoint] | String | /healthz | An endpoint for health checks that always responds with 200 OK | | [options.onlyFirstMatch] | Boolean | false | Set to true to stop after first match. Otherwise, all regexps are executed | | [options.request] | Object | | Options which will be added for all requests to telegram api. See https://github.com/request/request#requestoptions-callback for more information. | | [options.baseApiUrl] | String | https://api.telegram.org | API Base URl; useful for proxying and testing | @@ -592,16 +597,31 @@ Register a RegExp to test against an incomming text message. -### telegramBot.onReplyToMessage(chatId, messageId, callback) +### telegramBot.onReplyToMessage(chatId, messageId, callback) ⇒ Number Register a reply to wait for a message response. **Kind**: instance method of [TelegramBot](#TelegramBot) +**Returns**: Number - id The ID of the inserted reply listener. | Param | Type | Description | | --- | --- | --- | | chatId | Number | String | The chat id where the message cames from. | | messageId | Number | String | The message id to be replied. | -| callback | function | Callback will be called with the reply message. | +| callback | function | Callback will be called with the reply message. | + + + +### telegramBot.removeReplyListener(replyListenerId) ⇒ Object +Removes a reply that has been prev. registered for a message response. + +**Kind**: instance method of [TelegramBot](#TelegramBot) +**Returns**: Object - deletedListener The removed reply listener if + found. This object has `id`, `chatId`, `messageId` and `callback` + properties. If not found, returns `null`. + +| Param | Type | Description | +| --- | --- | --- | +| replyListenerId | Number | The ID of the reply listener. | diff --git a/doc/help.md b/doc/help.md index 6759bb2..a634f01 100644 --- a/doc/help.md +++ b/doc/help.md @@ -172,4 +172,9 @@ Sources: *Not Done. Send PR please!* +Sources: + + * Issue #219: https://github.com/yagop/node-telegram-bot-api/issues/219 + + --- diff --git a/doc/usage.md b/doc/usage.md index c2fb4f0..78043b0 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -3,6 +3,7 @@ 1. [Events](#events) 1. [WebHooks](#WebHooks) 1. [Sending files](#sending-files) +1. [Error handling](#error-handling) * * * @@ -29,6 +30,8 @@ 1. `edited_channel_post`: Received a new version of a channel post that is known to the bot and was edited 1. `edited_channel_post_text` 1. `edited_channel_post_caption` +1. `polling_error`: Error occurred during polling. See [polling errors](#polling-errors). +1. `webhook_error`: Error occurred handling a webhook request. See [webhook errors](#webhook-errors). **Tip:** Its much better to listen a specific event rather than on `message` in order to stay safe from the content. @@ -145,3 +148,60 @@ const bot = new TelegramBot(token, { filepath: false, }); ``` + + + +## Error handling + +Every `Error` object we pass back has the properties: + +* `code` (String): + * value is `EFATAL` if error was fatal e.g. network error + * value is `EPARSE` if response body could **not** be parsed + * value is `ETELEGRAM` if error was returned from Telegram servers +* `response` ([http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)): + * available if `error.code` is **not** `EFATAL` +* `response.body` (String|Object): Error response from Telegram + * type is `String` if `error.code` is `EPARSE` + * type is `Object` if `error.code` is `ETELEGRAM` + +For example, sending message to a non-existent user: + +```js +bot.sendMessage(nonExistentUserId, 'text').catch(error => { + console.log(error.code); // => 'ETELEGRAM' + console.log(error.response.body); // => { ok: false, error_code: 400, description: 'Bad Request: chat not found' } +}); +``` + + +#### Polling errors + +An error may occur during polling. It is up to you to handle it +as you see fit. You may decide to crash your bot after a maximum number +of polling errors occurring. **It is all up to you.** + +By default, the polling error is just logged to stderr, if you do +**not** handle this event yourself. + +Listen on the `'polling_error'` event. For example, + +```js +bot.on('polling_error', (error) => { + console.log(error.code); // => 'EFATAL' +}); +``` + + +#### WebHook errors + +Just like with [polling errors](#polling-errors), you decide on how to +handle it. By default, the error is logged to stderr. + +Listen on the `'webhook_error'` event. For example, + +```js +bot.on('webhook_error', (error) => { + console.log(error.code); // => 'EPARSE' +}); +``` diff --git a/examples/herokuWebHook.js b/examples/herokuWebHook.js new file mode 100644 index 0000000..a348c32 --- /dev/null +++ b/examples/herokuWebHook.js @@ -0,0 +1,35 @@ +/** + * This example demonstrates setting up webhook + * on the Heroku platform. + */ + + +const TOKEN = process.env.TELEGRAM_TOKEN || 'YOUR_TELEGRAM_BOT_TOKEN'; +const TelegramBot = require('..'); +const options = { + webHook: { + // Port to which you should bind is assigned to $PORT variable + // See: https://devcenter.heroku.com/articles/dynos#local-environment-variables + port: process.env.PORT + // you do NOT need to set up certificates since Heroku provides + // the SSL certs already (https://.herokuapp.com) + // Also no need to pass IP because on Heroku you need to bind to 0.0.0.0 + } +}; +// Heroku routes from port :443 to $PORT +// Add URL of your app to env variable or enable Dyno Metadata +// to get this automatically +// See: https://devcenter.heroku.com/articles/dyno-metadata +const url = process.env.APP_URL || 'https://.herokuapp.com:443'; +const bot = new TelegramBot(TOKEN, options); + + +// This informs the Telegram servers of the new webhook. +// Note: we do not need to pass in the cert, as it already provided +bot.setWebHook(`${url}/bot${TOKEN}`); + + +// Just to ping! +bot.on('message', function onMessage(msg) { + bot.sendMessage(msg.chat.id, 'I am alive on Heroku!'); +}); diff --git a/examples/httpsWebHook.js b/examples/httpsWebHook.js index ad0f40a..c2a27ef 100644 --- a/examples/httpsWebHook.js +++ b/examples/httpsWebHook.js @@ -26,5 +26,5 @@ bot.setWebHook(`${url}/bot${TOKEN}`, { // Just to ping! bot.on('message', function onMessage(msg) { - bot.sendMessage(msg.chat.id, "I'm alive!"); + bot.sendMessage(msg.chat.id, 'I am alive!'); }); diff --git a/examples/nowWebHook.js b/examples/nowWebHook.js new file mode 100644 index 0000000..9a94ef1 --- /dev/null +++ b/examples/nowWebHook.js @@ -0,0 +1,32 @@ +/** + * This example demonstrates setting up webhook on Zeit Now platform. + * Attention: You have to use webhook with Zeit Now only, polling doesn't + * work. + */ + + +const TOKEN = process.env.TELEGRAM_TOKEN || 'YOUR_TELEGRAM_BOT_TOKEN'; +const TelegramBot = require('..'); +const options = { + webHook: { + // Just use 443 directly + port: 443 + } +}; +// You can use 'now alias ' to assign fixed +// domain. +// See: https://zeit.co/blog/now-alias +// Or just use NOW_URL to get deployment url from env. +const url = 'YOUR_DOMAIN_ALIAS' || process.env.NOW_URL; +const bot = new TelegramBot(TOKEN, options); + + +// This informs the Telegram servers of the new webhook. +// Note: we do not need to pass in the cert, as it already provided +bot.setWebHook(`${url}/bot${TOKEN}`); + + +// Just to ping! +bot.on('message', function onMessage(msg) { + bot.sendMessage(msg.chat.id, 'I am alive on Zeit Now!'); +}); diff --git a/examples/openShiftWebHook.js b/examples/openShiftWebHook.js index 90b15fa..162d02a 100644 --- a/examples/openShiftWebHook.js +++ b/examples/openShiftWebHook.js @@ -28,5 +28,5 @@ bot.setWebHook(`${url}/bot${TOKEN}`); // Just to ping! bot.on('message', function onMessage(msg) { - bot.sendMessage(msg.chat.id, "I'm alive on OpenShift!"); + bot.sendMessage(msg.chat.id, 'I am alive on OpenShift!'); }); diff --git a/index.js b/index.js index 6165386..8e0c99b 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,12 @@ /** - * If running on Nodejs 0.12, we load the transpiled code. + * If running on Nodejs 4.x and below, we load the transpiled code. * Otherwise, we use the ES6 code. - * We are deprecating support for Node.js v0.x + * We are deprecating support for Node.js v4.x and below. */ -const majorVersion = process.versions.node.split('.')[0]; -if (majorVersion === '0') { +const majorVersion = parseInt(process.versions.node.split('.')[0], 10); +if (majorVersion <= 4) { const deprecate = require('depd')('node-telegram-bot-api'); - deprecate('Node.js v0.12 and below will no longer be supported in the future'); + deprecate('Node.js v4.x and below will no longer be supported in the future'); module.exports = require('./lib/telegram'); } else { module.exports = require('./src/telegram'); diff --git a/package.json b/package.json index 3441979..821f3a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-telegram-bot-api", - "version": "0.26.0", + "version": "0.27.0", "description": "Telegram Bot API", "main": "./index.js", "directories": { @@ -24,7 +24,11 @@ }, "author": "Yago Pérez ", "license": "MIT", + "engines": { + "node": ">=0.12" + }, "dependencies": { + "array.prototype.findindex": "^2.0.0", "bl": "^1.1.2", "bluebird": "^3.3.4", "debug": "^2.2.0", @@ -69,14 +73,32 @@ "homepage": "https://github.com/yagop/node-telegram-bot-api", "contributors": [ { - "name": "Mohammed Sohail", - "email": "sohailsameja@gmail.com", - "url": "https://github.com/kamikazechaser", + "name": "Anton Mironov", + "email": "ant.mironov@gmail.com", + "url": "https://github.com/mironov", "contributions": 1, - "additions": 18, - "deletions": 4, + "additions": 51, + "deletions": 15, + "hireable": true + }, + { + "name": "Daniil Yastremskiy", + "email": "Catharsis@post.cz", + "url": "https://github.com/TheBeastOfCaerbannog", + "contributions": 1, + "additions": 36, + "deletions": 0, "hireable": true }, + { + "name": null, + "email": null, + "url": "https://github.com/Ni2c2k", + "contributions": 1, + "additions": 4, + "deletions": 4, + "hireable": null + }, { "name": "Alexander Tarmolov", "email": "tarmolov@gmail.com", @@ -95,6 +117,15 @@ "deletions": 0, "hireable": null }, + { + "name": "Ola Flisbäck", + "email": null, + "url": "https://github.com/oflisback", + "contributions": 1, + "additions": 3, + "deletions": 3, + "hireable": true + }, { "name": null, "email": null, @@ -122,15 +153,6 @@ "deletions": 5, "hireable": null }, - { - "name": "Ola Flisbäck", - "email": null, - "url": "https://github.com/oflisback", - "contributions": 1, - "additions": 3, - "deletions": 3, - "hireable": true - }, { "name": "Horus Lugo", "email": "horusgoul@gmail.com", @@ -158,6 +180,15 @@ "deletions": 1, "hireable": null }, + { + "name": "Aleksandr L.", + "email": "w.siteee@gmail.com", + "url": "https://github.com/w-site", + "contributions": 1, + "additions": 24, + "deletions": 0, + "hireable": null + }, { "name": "Matthew Brandly", "email": "matt@brandly.me", @@ -185,15 +216,6 @@ "deletions": 2, "hireable": true }, - { - "name": "Aleksandr L.", - "email": "w.siteee@gmail.com", - "url": "https://github.com/w-site", - "contributions": 1, - "additions": 24, - "deletions": 0, - "hireable": null - }, { "name": "Guido García", "email": "palmerabollo@gmail.com", @@ -212,6 +234,24 @@ "deletions": 1, "hireable": null }, + { + "name": "Mohammed Sohail", + "email": "sohail@forfuture.tech", + "url": "https://github.com/kamikazechaser", + "contributions": 2, + "additions": 20, + "deletions": 5, + "hireable": true + }, + { + "name": "Jishnu Mohan", + "email": "jishnu7@gmail.com", + "url": "https://github.com/jishnu7", + "contributions": 2, + "additions": 84, + "deletions": 0, + "hireable": true + }, { "name": "Jérémy Gotteland", "email": null, @@ -230,15 +270,6 @@ "deletions": 2, "hireable": true }, - { - "name": "Iiro Jäppinen", - "email": null, - "url": "https://github.com/iiroj", - "contributions": 2, - "additions": 40, - "deletions": 0, - "hireable": null - }, { "name": "Dardan Neziri", "email": "dard.ne@gmail.com", @@ -249,22 +280,22 @@ "hireable": true }, { - "name": "Jishnu Mohan", - "email": "jishnu7@gmail.com", - "url": "https://github.com/jishnu7", + "name": "Cristian Baldi", + "email": "bld.cris.96@gmail.com", + "url": "https://github.com/crisbal", "contributions": 2, - "additions": 84, - "deletions": 0, + "additions": 26, + "deletions": 1, "hireable": true }, { - "name": "TJ Horner", - "email": "me@tjhorner.com", - "url": "https://github.com/tjhorner", + "name": "Vitaly Aminev", + "email": null, + "url": "https://github.com/AVVS", "contributions": 2, - "additions": 223, - "deletions": 1, - "hireable": null + "additions": 1065, + "deletions": 1001, + "hireable": true }, { "name": null, @@ -276,13 +307,31 @@ "hireable": null }, { - "name": "Vitaly Aminev", + "name": "Iiro Jäppinen", "email": null, - "url": "https://github.com/AVVS", + "url": "https://github.com/iiroj", "contributions": 2, - "additions": 1065, - "deletions": 1001, - "hireable": true + "additions": 40, + "deletions": 0, + "hireable": null + }, + { + "name": "TJ Horner", + "email": "me@tjhorner.com", + "url": "https://github.com/tjhorner", + "contributions": 2, + "additions": 223, + "deletions": 1, + "hireable": null + }, + { + "name": "Rafael Kr", + "email": null, + "url": "https://github.com/RafaelKr", + "contributions": 3, + "additions": 3, + "deletions": 2, + "hireable": null }, { "name": "Vítor Augusto da Silva Vasconcellos", @@ -302,15 +351,6 @@ "deletions": 25, "hireable": null }, - { - "name": "Rafael Kr", - "email": null, - "url": "https://github.com/RafaelKr", - "contributions": 3, - "additions": 3, - "deletions": 2, - "hireable": null - }, { "name": "Ivan Skorokhodov", "email": "iskorokhodov@gmail.com", @@ -339,13 +379,13 @@ "hireable": true }, { - "name": "Chris54721", - "email": null, - "url": "https://github.com/chris54721", - "contributions": 5, - "additions": 22, - "deletions": 6, - "hireable": null + "name": "Yago", + "email": "yago@yago.me", + "url": "https://github.com/yagop", + "contributions": 194, + "additions": 3014, + "deletions": 1173, + "hireable": true }, { "name": "Ilias Ismanalijev", @@ -356,32 +396,23 @@ "deletions": 10, "hireable": true }, + { + "name": "Chris54721", + "email": null, + "url": "https://github.com/chris54721", + "contributions": 5, + "additions": 22, + "deletions": 6, + "hireable": null + }, { "name": "Gocho Mugo", "email": "mugo@forfuture.co.ke", "url": "https://github.com/GochoMugo", - "contributions": 56, - "additions": 3779, - "deletions": 2167, - "hireable": true - }, - { - "name": "Cristian Baldi", - "email": "bld.cris.96@gmail.com", - "url": "https://github.com/crisbal", - "contributions": 2, - "additions": 26, - "deletions": 1, - "hireable": true - }, - { - "name": "Yago", - "email": "yago@yago.me", - "url": "https://github.com/yagop", - "contributions": 194, - "additions": 3014, - "deletions": 1173, + "contributions": 80, + "additions": 4590, + "deletions": 2377, "hireable": true } ] -} +} \ No newline at end of file diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 0000000..a4bffd3 --- /dev/null +++ b/src/errors.js @@ -0,0 +1,59 @@ +exports.BaseError = class BaseError extends Error { + /** + * @class BaseError + * @constructor + * @private + * @param {String} code Error code + * @param {String} message Error message + */ + constructor(code, message) { + super(`${code}: ${message}`); + this.code = code; + } +}; + + +exports.FatalError = class FatalError extends exports.BaseError { + /** + * Fatal Error. Error code is `"EFATAL"`. + * @class FatalError + * @constructor + * @param {String|Error} data Error object or message + */ + constructor(data) { + const error = (typeof data === 'string') ? null : data; + const message = error ? error.message : data; + super('EFATAL', message); + if (error) this.stack = error.stack; + } +}; + + +exports.ParseError = class ParseError extends exports.BaseError { + /** + * Error during parsing. Error code is `"EPARSE"`. + * @class ParseError + * @constructor + * @param {String} message Error message + * @param {http.IncomingMessage} response Server response + */ + constructor(message, response) { + super('EPARSE', message); + this.response = response; + } +}; + + +exports.TelegramError = class TelegramError extends exports.BaseError { + /** + * Error returned from Telegram. Error code is `"ETELEGRAM"`. + * @class TelegramError + * @constructor + * @param {String} message Error message + * @param {http.IncomingMessage} response Server response + */ + constructor(message, response) { + super('ETELEGRAM', message); + this.response = response; + } +}; diff --git a/src/telegram.js b/src/telegram.js index f454c27..e30d4a0 100644 --- a/src/telegram.js +++ b/src/telegram.js @@ -1,3 +1,7 @@ +// shims +require('array.prototype.findindex').shim(); // for Node.js v0.x + +const errors = require('./errors'); const TelegramBotWebHook = require('./telegramWebHook'); const TelegramBotPolling = require('./telegramPolling'); const debug = require('debug')('node-telegram-bot-api'); @@ -28,6 +32,10 @@ Promise.config({ class TelegramBot extends EventEmitter { + static get errors() { + return errors; + } + static get messageTypes() { return _messageTypes; } @@ -43,10 +51,15 @@ class TelegramBot extends EventEmitter { * @param {Object} [options] * @param {Boolean|Object} [options.polling=false] Set true to enable polling or set options. * If a WebHook has been set, it will be deleted automatically. - * @param {String|Number} [options.polling.timeout=10] Timeout in seconds for long polling + * @param {String|Number} [options.polling.timeout=10] *Deprecated. Use `options.polling.params` instead*. + * Timeout in seconds for long polling. * @param {String|Number} [options.polling.interval=300] Interval between requests in miliseconds * @param {Boolean} [options.polling.autoStart=true] Start polling immediately + * @param {Object} [options.polling.params] Parameters to be used in polling API requests. + * See https://core.telegram.org/bots/api#getupdates for more information. + * @param {Number} [options.polling.params.timeout=10] Timeout in seconds for long polling. * @param {Boolean|Object} [options.webHook=false] Set true to enable WebHook or set options + * @param {String} [options.webHook.host=0.0.0.0] Host to bind to * @param {Number} [options.webHook.port=8443] Port to bind to * @param {String} [options.webHook.key] Path to file with PEM private key for webHook server. * The file is read **synchronously**! @@ -59,6 +72,7 @@ class TelegramBot extends EventEmitter { * Note that `options.webHook.key`, `options.webHook.cert` and `options.webHook.pfx`, if provided, will be * used to override `key`, `cert` and `pfx` in this object, respectively. * See https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener for more information. + * @param {String} [options.webHook.healthEndpoint=/healthz] An endpoint for health checks that always responds with 200 OK * @param {Boolean} [options.onlyFirstMatch=false] Set to true to stop after first match. Otherwise, all regexps are executed * @param {Object} [options.request] Options which will be added for all requests to telegram api. * See https://github.com/request/request#requestoptions-callback for more information. @@ -77,7 +91,8 @@ class TelegramBot extends EventEmitter { this.options.baseApiUrl = options.baseApiUrl || 'https://api.telegram.org'; this.options.filepath = (typeof options.filepath === 'undefined') ? true : options.filepath; this._textRegexpCallbacks = []; - this._onReplyToMessages = []; + this._replyListenerId = 0; + this._replyListeners = []; this._polling = null; this._webHook = null; @@ -130,7 +145,7 @@ class TelegramBot extends EventEmitter { */ _request(_path, options = {}) { if (!this.token) { - throw new Error('Telegram Bot Token not provided!'); + return Promise.reject(new errors.FatalError('Telegram Bot Token not provided!')); } if (this.options.request) { @@ -152,30 +167,22 @@ class TelegramBot extends EventEmitter { debug('HTTP request: %j', options); return request(options) .then(resp => { - if (resp.statusCode !== 200) { - const error = new Error(`${resp.statusCode} ${resp.body}`); - error.response = resp; - throw error; - } - let data; - try { - data = JSON.parse(resp.body); + data = resp.body = JSON.parse(resp.body); } catch (err) { - const error = new Error(`Error parsing Telegram response: ${resp.body}`); - error.response = resp; - throw error; + throw new errors.ParseError(`Error parsing Telegram response: ${resp.body}`, resp); } if (data.ok) { return data.result; } - const error = new Error(`${data.error_code} ${data.description}`); - error.response = resp; - error.response.body = data; - throw error; + throw new errors.TelegramError(`${data.error_code} ${data.description}`, resp); + }).catch(error => { + // TODO: why can't we do `error instanceof errors.BaseError`? + if (error.response) throw error; + throw new errors.FatalError(error); }); } @@ -195,7 +202,9 @@ class TelegramBot extends EventEmitter { let fileName; let fileId; if (data instanceof stream.Stream) { - fileName = URL.parse(path.basename(data.path.toString())).pathname; + // Will be 'null' if could not be parsed. Default to 'filename'. + // For example, 'data.path' === '/?id=123' from 'request("https://example.com/?id=123")' + fileName = URL.parse(path.basename(data.path.toString())).pathname || 'filename'; formData = {}; formData[type] = { value: data, @@ -207,7 +216,7 @@ class TelegramBot extends EventEmitter { } else if (Buffer.isBuffer(data)) { const filetype = fileType(data); if (!filetype) { - throw new Error('Unsupported Buffer file type'); + throw new errors.FatalError('Unsupported Buffer file type'); } formData = {}; formData[type] = { @@ -248,11 +257,11 @@ class TelegramBot extends EventEmitter { */ startPolling(options = {}) { if (this.hasOpenWebHook()) { - return Promise.reject(new Error('Polling and WebHook are mutually exclusive')); + return Promise.reject(new errors.FatalError('Polling and WebHook are mutually exclusive')); } options.restart = typeof options.restart === 'undefined' ? true : options.restart; if (!this._polling) { - this._polling = new TelegramBotPolling(this._request.bind(this), this.options.polling, this.processUpdate.bind(this)); + this._polling = new TelegramBotPolling(this); } return this._polling.start(options); } @@ -297,10 +306,10 @@ class TelegramBot extends EventEmitter { */ openWebHook() { if (this.isPolling()) { - return Promise.reject(new Error('WebHook and Polling are mutually exclusive')); + return Promise.reject(new errors.FatalError('WebHook and Polling are mutually exclusive')); } if (!this._webHook) { - this._webHook = new TelegramBotWebHook(this.token, this.options.webHook, this.processUpdate.bind(this)); + this._webHook = new TelegramBotWebHook(this); } return this._webHook.open(); } @@ -377,14 +386,7 @@ class TelegramBot extends EventEmitter { } } - return this._request('setWebHook', opts) - .then(resp => { - if (!resp) { - throw new Error(resp); - } - - return resp; - }); + return this._request('setWebHook', opts); } /** @@ -479,7 +481,7 @@ class TelegramBot extends EventEmitter { } if (message.reply_to_message) { // Only callbacks waiting for this message - this._onReplyToMessages.forEach(reply => { + this._replyListeners.forEach(reply => { // Message from the same chat if (reply.chatId === message.chat.id) { // Responding to that message @@ -1007,14 +1009,35 @@ class TelegramBot extends EventEmitter { * @param {Number|String} chatId The chat id where the message cames from. * @param {Number|String} messageId The message id to be replied. * @param {Function} callback Callback will be called with the reply - * message. + * message. + * @return {Number} id The ID of the inserted reply listener. */ onReplyToMessage(chatId, messageId, callback) { - this._onReplyToMessages.push({ + const id = ++this._replyListenerId; + this._replyListeners.push({ + id, chatId, messageId, callback }); + return id; + } + + /** + * Removes a reply that has been prev. registered for a message response. + * @param {Number} replyListenerId The ID of the reply listener. + * @return {Object} deletedListener The removed reply listener if + * found. This object has `id`, `chatId`, `messageId` and `callback` + * properties. If not found, returns `null`. + */ + removeReplyListener(replyListenerId) { + const index = this._replyListeners.findIndex((replyListener) => { + return replyListener.id === replyListenerId; + }); + if (index === -1) { + return null; + } + return this._replyListeners.splice(index, 1)[0]; } /** diff --git a/src/telegramPolling.js b/src/telegramPolling.js index 91d6b45..abd4a7c 100644 --- a/src/telegramPolling.js +++ b/src/telegramPolling.js @@ -1,34 +1,26 @@ const debug = require('debug')('node-telegram-bot-api'); +const deprecate = require('depd')('node-telegram-bot-api'); const ANOTHER_WEB_HOOK_USED = 409; class TelegramBotPolling { /** * Handles polling against the Telegram servers. - * - * @param {Function} request Function used to make HTTP requests - * @param {Boolean|Object} options Polling options - * @param {Number} [options.timeout=10] Timeout in seconds for long polling - * @param {Number} [options.interval=300] Interval between requests in milliseconds - * @param {Function} callback Function for processing a new update - * @see https://core.telegram.org/bots/api#getupdates + * @param {TelegramBot} bot + * @see https://core.telegram.org/bots/api#getting-updates */ - constructor(request, options = {}, callback) { - /* eslint-disable no-param-reassign */ - if (typeof options === 'function') { - callback = options; - options = {}; - } else if (typeof options === 'boolean') { - options = {}; + constructor(bot) { + this.bot = bot; + this.options = (typeof bot.options.polling === 'boolean') ? {} : bot.options.polling; + this.options.interval = (typeof this.options.interval === 'number') ? this.options.interval : 300; + this.options.params = (typeof this.options.params === 'object') ? this.options.params : {}; + this.options.params.offset = (typeof this.options.params.offset === 'number') ? this.options.params.offset : 0; + if (typeof this.options.timeout === 'number') { + deprecate('`options.polling.timeout` is deprecated. Use `options.polling.params` instead.'); + this.options.params.timeout = this.options.timeout; + } else { + this.options.params.timeout = 10; } - /* eslint-enable no-param-reassign */ - - this.request = request; - this.options = options; - this.options.timeout = (typeof options.timeout === 'number') ? options.timeout : 10; - this.options.interval = (typeof options.interval === 'number') ? options.interval : 300; - this.callback = callback; - this._offset = 0; this._lastUpdate = 0; this._lastRequest = null; this._abort = false; @@ -100,14 +92,20 @@ class TelegramBotPolling { this._lastUpdate = Date.now(); debug('polling data %j', updates); updates.forEach(update => { - this._offset = update.update_id; - debug('updated offset: %s', this._offset); - this.callback(update); + this.options.params.offset = update.update_id + 1; + debug('updated offset: %s', this.options.params.offset); + this.bot.processUpdate(update); }); + return null; }) .catch(err => { debug('polling error: %s', err.message); - throw err; + if (this.bot.listeners('polling_error').length) { + this.bot.emit('polling_error', err); + } else { + console.error(err); // eslint-disable-line no-console + } + return null; }) .finally(() => { if (this._abort) { @@ -127,26 +125,21 @@ class TelegramBotPolling { * @private */ _unsetWebHook() { - return this.request('setWebHook'); + debug('unsetting webhook'); + return this.bot._request('setWebHook'); } /** * Retrieve updates */ _getUpdates() { - const opts = { - qs: { - offset: this._offset + 1, - limit: this.options.limit, - timeout: this.options.timeout - }, - }; - debug('polling with options: %j', opts); - - return this.request('getUpdates', opts) + debug('polling with options: %j', this.options.params); + return this.bot.getUpdates(this.options.params) .catch(err => { - if (err.response.statusCode === ANOTHER_WEB_HOOK_USED) { - return this._unsetWebHook(); + if (err.response && err.response.statusCode === ANOTHER_WEB_HOOK_USED) { + return this._unsetWebHook().then(() => { + return this.bot.getUpdates(this.options.params); + }); } throw err; }); diff --git a/src/telegramWebHook.js b/src/telegramWebHook.js index 6ee33f2..2ca20a3 100644 --- a/src/telegramWebHook.js +++ b/src/telegramWebHook.js @@ -1,3 +1,4 @@ +const errors = require('./errors'); const debug = require('debug')('node-telegram-bot-api'); const https = require('https'); const http = require('http'); @@ -9,24 +10,17 @@ const Promise = require('bluebird'); class TelegramBotWebHook { /** * Sets up a webhook to receive updates - * - * @param {String} token Telegram API token - * @param {Boolean|Object} options WebHook options - * @param {Number} [options.port=8443] Port to bind to - * @param {Function} callback Function for process a new update + * @param {TelegramBot} bot + * @see https://core.telegram.org/bots/api#getting-updates */ - constructor(token, options, callback) { - // define opts - if (typeof options === 'boolean') { - options = {}; // eslint-disable-line no-param-reassign - } - - this.token = token; - this.options = options; - this.options.port = options.port || 8443; - this.options.https = options.https || {}; - this.callback = callback; - this._regex = new RegExp(this.token); + constructor(bot) { + this.bot = bot; + this.options = (typeof bot.options.webHook === 'boolean') ? {} : bot.options.webHook; + this.options.host = this.options.host || '0.0.0.0'; + this.options.port = this.options.port || 8443; + this.options.https = this.options.https || {}; + this.options.healthEndpoint = this.options.healthEndpoint || '/healthz'; + this._healthRegex = new RegExp(this.options.healthEndpoint); this._webServer = null; this._open = false; this._requestListener = this._requestListener.bind(this); @@ -96,31 +90,35 @@ class TelegramBotWebHook { return this._open; } - // used so that other funcs are not non-optimizable - _safeParse(json) { - try { - return JSON.parse(json); - } catch (err) { - debug(err); - return null; + /** + * Handle error thrown during processing of webhook request. + * @private + * @param {Error} error + */ + _error(error) { + if (!this.bot.listeners('webhook_error').length) { + return console.error(error); // eslint-disable-line no-console } + return this.bot.emit('webhook_error', error); } /** * Handle request body by passing it to 'callback' * @private */ - _parseBody(err, body) { - if (err) { - return debug(err); + _parseBody(error, body) { + if (error) { + return this._error(new errors.FatalError(error)); } - const data = this._safeParse(body); - if (data) { - return this.callback(data); + let data; + try { + data = JSON.parse(body.toString()); + } catch (parseError) { + return this._error(new errors.ParseError(parseError.message)); } - return null; + return this.bot.processUpdate(data); } /** @@ -133,20 +131,24 @@ class TelegramBotWebHook { debug('WebHook request URL: %s', req.url); debug('WebHook request headers: %j', req.headers); - // If there isn't token on URL - if (!this._regex.test(req.url)) { + if (req.url.indexOf(this.bot.token) !== -1) { + if (req.method !== 'POST') { + debug('WebHook request isn\'t a POST'); + res.statusCode = 418; // I'm a teabot! + res.end(); + } else { + req + .pipe(bl(this._parseBody)) + .on('finish', () => res.end('OK')); + } + } else if (this._healthRegex.test(req.url)) { + debug('WebHook health check passed'); + res.statusCode = 200; + res.end('OK'); + } else { debug('WebHook request unauthorized'); res.statusCode = 401; res.end(); - } else if (req.method === 'POST') { - req - .pipe(bl(this._parseBody)) - .on('finish', () => res.end('OK')); - } else { - // Authorized but not a POST - debug('WebHook request isn\'t a POST'); - res.statusCode = 418; // I'm a teabot! - res.end(); } } } diff --git a/test/telegram.js b/test/telegram.js index 40c5ad5..288fe02 100644 --- a/test/telegram.js +++ b/test/telegram.js @@ -27,8 +27,10 @@ const pollingPort = portindex++; const webHookPort = portindex++; const pollingPort2 = portindex++; const webHookPort2 = portindex++; +const badTgServerPort = portindex++; const staticUrl = `http://127.0.0.1:${staticPort}`; const key = `${__dirname}/../examples/key.pem`; +const ip = '216.58.210.174'; // Google IP ¯\_(ツ)_/¯ const cert = `${__dirname}/../examples/crt.pem`; let FILE_ID; let GAME_CHAT_ID; @@ -39,17 +41,20 @@ before(function beforeAll() { return utils.startMockServer(pollingPort) .then(() => { return utils.startMockServer(pollingPort2); + }).then(() => { + return utils.startMockServer(badTgServerPort, { bad: true }); }); }); describe('module.exports', function moduleExportsSuite() { - it('is loaded from src/ if NOT on Node.js 0.12', function test() { - if (process.versions.node.split('.')[0] === '0') this.skip(); // skip on Node.js v0.12 + const nodeVersion = parseInt(process.versions.node.split('.')[0], 10); + it('is loaded from src/ on Node.js v5+ and above', function test() { + if (nodeVersion <= 4) this.skip(); // skip on Node.js v4 and below assert.equal(TelegramBot, require('../src/telegram')); }); - it('is loaded from lib/ if on Node.js 0.12', function test() { - if (process.versions.node.split('.')[0] !== '0') this.skip(); // skip on newer versions + it('is loaded from lib/ on Node.js v4 and below', function test() { + if (nodeVersion > 4) this.skip(); // skip on newer versions assert.equal(TelegramBot, require('../lib/telegram')); }); }); @@ -118,21 +123,58 @@ describe('TelegramBot', function telegramSuite() { return utils.hasOpenWebHook(webHookPort, true); }); + it('correctly deletes the webhook if polling', function test() { + const myBot = new TelegramBot(TOKEN, { + polling: { autoStart: false, params: { timeout: 0 } }, + }); + utils.handleRatelimit(myBot, 'setWebHook', this); + myBot.on('polling_error', (error) => { + assert.ifError(error); + }); + return myBot.setWebHook(ip).then(() => { + return myBot.startPolling(); + }).then(() => { + return myBot.stopPolling(); + }); + }); + describe('Events', function eventsSuite() { it('(polling) emits "message" on receiving message', function test(done) { botPolling.once('message', () => { return done(); }); }); + it('(polling) emits "polling_error" if error occurs during polling', function test(done) { + const myBot = new TelegramBot(12345, { polling: true }); + myBot.once('polling_error', (error) => { + assert.ok(error); + assert.equal(error.code, 'ETELEGRAM'); + return myBot.stopPolling().then(() => { done(); }).catch(done); + }); + }); it('(webhook) emits "message" on receiving message', function test(done) { botWebHook.once('message', () => { return done(); }); utils.sendWebHookMessage(webHookPort2, TOKEN); }); + it('(webhook) emits "webhook_error" if could not parse webhook request body', function test(done) { + botWebHook.once('webhook_error', (error) => { + assert.ok(error); + assert.equal(error.code, 'EPARSE'); + return done(); + }); + utils.sendWebHookMessage(webHookPort2, TOKEN, { update: 'unparseable!', json: false }); + }); }); describe('WebHook', function webHookSuite() { + it('returns 200 OK for health endpoint', function test(done) { + utils.sendWebHookRequest(webHookPort2, '/healthz').then(resp => { + assert.equal(resp, 'OK'); + return done(); + }); + }); it('returns 401 error if token is wrong', function test(done) { utils.sendWebHookMessage(webHookPort2, 'wrong-token').catch(resp => { assert.equal(resp.statusCode, 401); @@ -179,6 +221,58 @@ describe('TelegramBot', function telegramSuite() { }); }); + describe('errors', function errorsSuite() { + const botParse = new TelegramBot('useless-token', { + baseApiUrl: `http://localhost:${badTgServerPort}`, + }); + it('FatalError is thrown if token is missing', function test() { + const myBot = new TelegramBot(null); + return myBot.sendMessage(USERID, 'text').catch(error => { + // FIX: assert.ok(error instanceof TelegramBot.errors.FatalError); + assert.equal(error.code, 'EFATAL'); + assert.ok(error.message.indexOf('not provided') > -1); + }); + }); + it('FatalError is thrown if file-type of Buffer could not be determined', function test() { + let buffer; + try { + buffer = Buffer.from('12345'); + } catch (ex) { + buffer = new Buffer('12345'); + } + return bot.sendPhoto(USERID, buffer).catch(error => { + // FIX: assert.ok(error instanceof TelegramBot.errors.FatalError); + assert.equal(error.code, 'EFATAL'); + assert.ok(error.message.indexOf('Unsupported') > -1); + }); + }); + it('FatalError is thrown on network error', function test() { + const myBot = new TelegramBot('useless-token', { + baseApiUrl: 'http://localhost:23', // are we sure this port is not bound to? + }); + return myBot.getMe().catch(error => { + // FIX: assert.ok(error instanceof TelegramBot.errors.FatalError); + assert.equal(error.code, 'EFATAL'); + }); + }); + it('ParseError is thrown if response body could not be parsed', function test() { + botParse.sendMessage(USERID, 'text').catch(error => { + // FIX: assert.ok(error instanceof TelegramBot.errors.ParseError); + assert.equal(error.code, 'EPARSE'); + assert.ok(typeof error.response === 'object'); + assert.ok(typeof error.response.body === 'string'); + }); + }); + it('TelegramError is thrown if error is from Telegram', function test() { + return bot.sendMessage('404', 'text').catch(error => { + // FIX: assert.ok(error instanceof TelegramBot.errors.TelegramError); + assert.equal(error.code, 'ETELEGRAM'); + assert.ok(typeof error.response === 'object'); + assert.ok(typeof error.response.body === 'object'); + }); + }); + }); + describe('#startPolling', function initPollingSuite() { it('initiates polling', function test() { return testbot.startPolling().then(() => { @@ -188,6 +282,8 @@ describe('TelegramBot', function telegramSuite() { it('returns error if using webhook', function test() { return botWebHook.startPolling().catch((err) => { // TODO: check for error in a better way + // FIX: assert.ok(err instanceof TelegramBot.errors.FatalError); + assert.equal(err.code, 'EFATAL'); assert.ok(err.message.indexOf('mutually exclusive') !== -1); }); }); @@ -228,6 +324,8 @@ describe('TelegramBot', function telegramSuite() { it('returns error if using polling', function test() { return botPolling.openWebHook().catch((err) => { // TODO: check for error in a better way + // FIX: assert.ok(err instanceof TelegramBot.errors.FatalError); + assert.equal(err.code, 'EFATAL'); assert.ok(err.message.indexOf('mutually exclusive') !== -1); }); }); @@ -271,12 +369,10 @@ describe('TelegramBot', function telegramSuite() { }); describe('#setWebHook', function setWebHookSuite() { - const ip = '216.58.210.174'; before(function before() { utils.handleRatelimit(bot, 'setWebHook', this); }); it('should set a webHook', function test() { - // Google IP ¯\_(ツ)_/¯ return bot .setWebHook(ip) .then(resp => { @@ -516,6 +612,13 @@ describe('TelegramBot', function telegramSuite() { assert.ok(is.object(resp.document)); }); }); + it('should send a document with custom file options', function test() { + const document = fs.createReadStream(`${__dirname}/data/photo.gif`); + const fileOpts = { filename: 'customfilename.gif' }; + return bot.sendDocument(USERID, document, {}, fileOpts).then(resp => { + assert.equal(resp.document.file_name, fileOpts.filename); + }); + }); }); describe('#sendSticker', function sendStickerSuite() { @@ -876,6 +979,24 @@ describe('TelegramBot', function telegramSuite() { describe.skip('#onReplyToMessage', function onReplyToMessageSuite() {}); + describe('#removeReplyListener', function removeReplyListenerSuite() { + const chatId = -1234; + const messageId = 1; + const callback = function noop() {}; + it('returns the right reply-listener', function test() { + const id = bot.onReplyToMessage(chatId, messageId, callback); + const replyListener = bot.removeReplyListener(id); + assert.equal(id, replyListener.id); + assert.equal(chatId, replyListener.chatId); + assert.equal(messageId, replyListener.messageId); + assert.equal(callback, replyListener.callback); + }); + it('returns `null` if missing', function test() { + // NOTE: '0' is never a valid reply listener ID :) + assert.equal(null, bot.removeReplyListener(0)); + }); + }); + describe('#getChat', function getChatSuite() { before(function before() { utils.handleRatelimit(bot, 'getChat', this); @@ -988,8 +1109,13 @@ describe('TelegramBot', function telegramSuite() { const photo = `${__dirname}/data/photo.gif`; return tgbot.sendPhoto(USERID, photo).catch(err => { // TODO: check for error in a better way - assert.ok(err.response.body.indexOf('Bad Request') !== -1); + assert.ok(err.response.body.description.indexOf('Bad Request') !== -1); }); }); + it('should allow stream.path that can not be parsed', function test() { + const stream = fs.createReadStream(`${__dirname}/data/photo.gif`); + stream.path = '/?id=123'; // for example, 'http://example.com/?id=666' + return bot.sendPhoto(USERID, stream); + }); }); }); // End Telegram diff --git a/test/utils.js b/test/utils.js index c63c58e..23df9d8 100644 --- a/test/utils.js +++ b/test/utils.js @@ -30,12 +30,25 @@ exports = module.exports = { * @return {Promise} */ isPollingMockServer, + /** + * Send a message to the webhook at the specified port and path. + * @param {Number} port + * @param {String} path + * @param {Object} [options] + * @param {String} [options.method=POST] Method to use + * @param {Object} [options.update] Update object to send. + * @param {Object} [options.message] Message to send. Default to a generic text message + * @param {Boolean} [options.https=false] Use https + * @return {Promise} + */ + sendWebHookRequest, /** * Send a message to the webhook at the specified port. * @param {Number} port * @param {String} token * @param {Object} [options] * @param {String} [options.method=POST] Method to use + * @param {Object} [options.update] Update object to send. * @param {Object} [options.message] Message to send. Default to a generic text message * @param {Boolean} [options.https=false] Use https * @return {Promise} @@ -44,6 +57,9 @@ exports = module.exports = { /** * Start a mock server at the specified port. * @param {Number} port + * @param {Object} [options] + * @param {Boolean} [options.bad=false] Bad Mock Server; responding with + * unparseable messages * @return {Promise} */ startMockServer, @@ -65,10 +81,13 @@ const statics = require('node-static'); const servers = {}; -function startMockServer(port) { +function startMockServer(port, options = {}) { assert.ok(port); const server = http.Server((req, res) => { servers[port].polling = true; + if (options.bad) { + return res.end('can not be parsed with JSON.parse()'); + } return res.end(JSON.stringify({ ok: true, result: [{ @@ -134,23 +153,31 @@ function hasOpenWebHook(port, reverse) { } -function sendWebHookMessage(port, token, options = {}) { +function sendWebHookRequest(port, path, options = {}) { assert.ok(port); - assert.ok(token); + assert.ok(path); const protocol = options.https ? 'https' : 'http'; - const url = `${protocol}://127.0.0.1:${port}/bot${token}`; + const url = `${protocol}://127.0.0.1:${port}${path}`; return request({ url, method: options.method || 'POST', - body: { + body: options.update || { update_id: 1, message: options.message || { text: 'test' } }, - json: true, + json: (typeof options.json === 'undefined') ? true : options.json, }); } +function sendWebHookMessage(port, token, options = {}) { + assert.ok(port); + assert.ok(token); + const path = `/bot${token}`; + return sendWebHookRequest(port, path, options); +} + + function handleRatelimit(bot, methodName, suite) { const backupMethodName = `__${methodName}`; if (!bot[backupMethodName]) bot[backupMethodName] = bot[methodName];