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)
v0.27.0
GochoMugo 9 years ago
commit 7304265278
No known key found for this signature in database
GPG Key ID: 7B6A01CB57AA39E4
  1. 68
      .github/ISSUE_TEMPLATE.md
  2. 23
      .github/PULL_REQUEST_TEMPLATE.md
  3. 43
      CHANGELOG.md
  4. 21
      README.md
  5. 28
      doc/api.md
  6. 5
      doc/help.md
  7. 60
      doc/usage.md
  8. 35
      examples/herokuWebHook.js
  9. 2
      examples/httpsWebHook.js
  10. 32
      examples/nowWebHook.js
  11. 2
      examples/openShiftWebHook.js
  12. 10
      index.js
  13. 205
      package.json
  14. 59
      src/errors.js
  15. 93
      src/telegram.js
  16. 71
      src/telegramPolling.js
  17. 86
      src/telegramWebHook.js
  18. 140
      test/telegram.js
  19. 39
      test/utils.js

@ -0,0 +1,68 @@
<!--
This template includes two sections:
1. Bug reporting
2. Feature request
3. Question
Please remove whichever section that does not apply to your issue
-->
<!--********************************************************************
Reporting a Bug.
*********************************************************************-->
> 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
<!-- Explain what you are trying to achieve -->
### Actual Behavior
<!-- Explain what happens, contrary to what you expected -->
### Steps to reproduce the Behavior
<!-- Explain how we can reproduce the bug -->
<!--********************************************************************
Feature Request.
*********************************************************************-->
> Feature Request
I have:
* searched for such a feature request (https://github.com/yagop/node-telegram-bot-api/labels/enhancement) and found none
### Introduction
<!-- Describe what value this feature would add, and in which use case,
or scenario -->
### Example
<!-- A code snippet of how this feature would work, were it already
implemented -->
<!--********************************************************************
Question.
*********************************************************************-->
> Question
<!-- Ask your question here. Please be precise, adding as much detail
as necessary. Also, add a code snippet(s) if possible. -->

@ -0,0 +1,23 @@
<!--
Mark whichever option below applies to this PR.
For example, if your PR passes all tests, you would mark the option as so:
- [x] All tests pass
Note the 'x' in between the square brackets '[]'
-->
- [ ] All tests pass
- [ ] I have run `npm run gen-doc`
### Description
<!-- Explain what you are trying to achieve with this PR -->
### References
<!--
Add references to other documents/pages that are relevant to this
PR, such as related issues, documentation, etc.
For example,
* Issue #1: https://github.com/yagop/node-telegram-bot-api/issues/1
* Telegram Bot API - Getting updates: https://core.telegram.org/bots/api#getting-updates
-->

@ -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 ## [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.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 [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

@ -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) [![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) [![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) [![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 ## Documentation
* Usage ([release][usage-release] / [development][usage-dev]) * [Usage][usage]
* Examples ([release][examples-release] / [development][examples-dev]) * [Examples][examples]
* Help Information ([release][help-release] / [development][help-dev]) * [Help Information][help]
* API Reference ([release][api-release] / [development][api-dev]) * API Reference ([release][api-release] / [development][api-dev])
* [Contributing to the Project][contributing] * [Contributing to the Project][contributing]
_**Note**: Development is done against the **master** branch. Code for the latest release _**Note**: Development is done against the **master** branch. Code for the latest release
resides on the **release** branch._ 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 [usage]: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 [examples]:https://github.com/yagop/node-telegram-bot-api/tree/master/examples
[help-dev]: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-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 [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 * [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 * [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
* * * * * *

@ -47,7 +47,8 @@ TelegramBot
* [.getFileLink(fileId)](#TelegramBot+getFileLink) ⇒ <code>Promise</code> * [.getFileLink(fileId)](#TelegramBot+getFileLink) ⇒ <code>Promise</code>
* [.downloadFile(fileId, downloadDir)](#TelegramBot+downloadFile) ⇒ <code>Promise</code> * [.downloadFile(fileId, downloadDir)](#TelegramBot+downloadFile) ⇒ <code>Promise</code>
* [.onText(regexp, callback)](#TelegramBot+onText) * [.onText(regexp, callback)](#TelegramBot+onText)
* [.onReplyToMessage(chatId, messageId, callback)](#TelegramBot+onReplyToMessage) * [.onReplyToMessage(chatId, messageId, callback)](#TelegramBot+onReplyToMessage) ⇒ <code>Number</code>
* [.removeReplyListener(replyListenerId)](#TelegramBot+removeReplyListener) ⇒ <code>Object</code>
* [.getChat(chatId)](#TelegramBot+getChat) ⇒ <code>Promise</code> * [.getChat(chatId)](#TelegramBot+getChat) ⇒ <code>Promise</code>
* [.getChatAdministrators(chatId)](#TelegramBot+getChatAdministrators) ⇒ <code>Promise</code> * [.getChatAdministrators(chatId)](#TelegramBot+getChatAdministrators) ⇒ <code>Promise</code>
* [.getChatMembersCount(chatId)](#TelegramBot+getChatMembersCount) ⇒ <code>Promise</code> * [.getChatMembersCount(chatId)](#TelegramBot+getChatMembersCount) ⇒ <code>Promise</code>
@ -70,16 +71,20 @@ Emits `message` when a message arrives.
| token | <code>String</code> | | Bot Token | | token | <code>String</code> | | Bot Token |
| [options] | <code>Object</code> | | | | [options] | <code>Object</code> | | |
| [options.polling] | <code>Boolean</code> &#124; <code>Object</code> | <code>false</code> | Set true to enable polling or set options. If a WebHook has been set, it will be deleted automatically. | | [options.polling] | <code>Boolean</code> &#124; <code>Object</code> | <code>false</code> | Set true to enable polling or set options. If a WebHook has been set, it will be deleted automatically. |
| [options.polling.timeout] | <code>String</code> &#124; <code>Number</code> | <code>10</code> | Timeout in seconds for long polling | | [options.polling.timeout] | <code>String</code> &#124; <code>Number</code> | <code>10</code> | *Deprecated. Use `options.polling.params` instead*. Timeout in seconds for long polling. |
| [options.polling.interval] | <code>String</code> &#124; <code>Number</code> | <code>300</code> | Interval between requests in miliseconds | | [options.polling.interval] | <code>String</code> &#124; <code>Number</code> | <code>300</code> | Interval between requests in miliseconds |
| [options.polling.autoStart] | <code>Boolean</code> | <code>true</code> | Start polling immediately | | [options.polling.autoStart] | <code>Boolean</code> | <code>true</code> | Start polling immediately |
| [options.polling.params] | <code>Object</code> | | Parameters to be used in polling API requests. See https://core.telegram.org/bots/api#getupdates for more information. |
| [options.polling.params.timeout] | <code>Number</code> | <code>10</code> | Timeout in seconds for long polling. |
| [options.webHook] | <code>Boolean</code> &#124; <code>Object</code> | <code>false</code> | Set true to enable WebHook or set options | | [options.webHook] | <code>Boolean</code> &#124; <code>Object</code> | <code>false</code> | Set true to enable WebHook or set options |
| [options.webHook.host] | <code>String</code> | <code>0.0.0.0</code> | Host to bind to |
| [options.webHook.port] | <code>Number</code> | <code>8443</code> | Port to bind to | | [options.webHook.port] | <code>Number</code> | <code>8443</code> | Port to bind to |
| [options.webHook.key] | <code>String</code> | | Path to file with PEM private key for webHook server. The file is read **synchronously**! | | [options.webHook.key] | <code>String</code> | | Path to file with PEM private key for webHook server. The file is read **synchronously**! |
| [options.webHook.cert] | <code>String</code> | | Path to file with PEM certificate (public) for webHook server. The file is read **synchronously**! | | [options.webHook.cert] | <code>String</code> | | Path to file with PEM certificate (public) for webHook server. The file is read **synchronously**! |
| [options.webHook.pfx] | <code>String</code> | | Path to file with PFX private key and certificate chain for webHook server. The file is read **synchronously**! | | [options.webHook.pfx] | <code>String</code> | | Path to file with PFX private key and certificate chain for webHook server. The file is read **synchronously**! |
| [options.webHook.autoOpen] | <code>Boolean</code> | <code>true</code> | Open webHook immediately | | [options.webHook.autoOpen] | <code>Boolean</code> | <code>true</code> | Open webHook immediately |
| [options.webHook.https] | <code>Object</code> | | 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.https] | <code>Object</code> | | 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] | <code>String</code> | <code>/healthz</code> | An endpoint for health checks that always responds with 200 OK |
| [options.onlyFirstMatch] | <code>Boolean</code> | <code>false</code> | Set to true to stop after first match. Otherwise, all regexps are executed | | [options.onlyFirstMatch] | <code>Boolean</code> | <code>false</code> | Set to true to stop after first match. Otherwise, all regexps are executed |
| [options.request] | <code>Object</code> | | Options which will be added for all requests to telegram api. See https://github.com/request/request#requestoptions-callback for more information. | | [options.request] | <code>Object</code> | | Options which will be added for all requests to telegram api. See https://github.com/request/request#requestoptions-callback for more information. |
| [options.baseApiUrl] | <code>String</code> | <code>https://api.telegram.org</code> | API Base URl; useful for proxying and testing | | [options.baseApiUrl] | <code>String</code> | <code>https://api.telegram.org</code> | API Base URl; useful for proxying and testing |
@ -592,16 +597,31 @@ Register a RegExp to test against an incomming text message.
<a name="TelegramBot+onReplyToMessage"></a> <a name="TelegramBot+onReplyToMessage"></a>
### telegramBot.onReplyToMessage(chatId, messageId, callback) ### telegramBot.onReplyToMessage(chatId, messageId, callback)<code>Number</code>
Register a reply to wait for a message response. Register a reply to wait for a message response.
**Kind**: instance method of <code>[TelegramBot](#TelegramBot)</code> **Kind**: instance method of <code>[TelegramBot](#TelegramBot)</code>
**Returns**: <code>Number</code> - id The ID of the inserted reply listener.
| Param | Type | Description | | Param | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| chatId | <code>Number</code> &#124; <code>String</code> | The chat id where the message cames from. | | chatId | <code>Number</code> &#124; <code>String</code> | The chat id where the message cames from. |
| messageId | <code>Number</code> &#124; <code>String</code> | The message id to be replied. | | messageId | <code>Number</code> &#124; <code>String</code> | The message id to be replied. |
| callback | <code>function</code> | Callback will be called with the reply message. | | callback | <code>function</code> | Callback will be called with the reply message. |
<a name="TelegramBot+removeReplyListener"></a>
### telegramBot.removeReplyListener(replyListenerId) ⇒ <code>Object</code>
Removes a reply that has been prev. registered for a message response.
**Kind**: instance method of <code>[TelegramBot](#TelegramBot)</code>
**Returns**: <code>Object</code> - 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 | <code>Number</code> | The ID of the reply listener. |
<a name="TelegramBot+getChat"></a> <a name="TelegramBot+getChat"></a>

@ -172,4 +172,9 @@ Sources:
*Not Done. Send PR please!* *Not Done. Send PR please!*
Sources:
* Issue #219: https://github.com/yagop/node-telegram-bot-api/issues/219
--- ---

@ -3,6 +3,7 @@
1. [Events](#events) 1. [Events](#events)
1. [WebHooks](#WebHooks) 1. [WebHooks](#WebHooks)
1. [Sending files](#sending-files) 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`: 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_text`
1. `edited_channel_post_caption` 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 **Tip:** Its much better to listen a specific event rather than on
`message` in order to stay safe from the content. `message` in order to stay safe from the content.
@ -145,3 +148,60 @@ const bot = new TelegramBot(token, {
filepath: false, filepath: false,
}); });
``` ```
<a name="error-handling"></a>
## 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' }
});
```
<a name="polling-errors"></a>
#### 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'
});
```
<a name="webhook-errors"></a>
#### 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'
});
```

@ -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://<app-name>.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://<app-name>.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!');
});

@ -26,5 +26,5 @@ bot.setWebHook(`${url}/bot${TOKEN}`, {
// Just to ping! // Just to ping!
bot.on('message', function onMessage(msg) { bot.on('message', function onMessage(msg) {
bot.sendMessage(msg.chat.id, "I'm alive!"); bot.sendMessage(msg.chat.id, 'I am alive!');
}); });

@ -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 <your deployment url> <custom url>' 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!');
});

@ -28,5 +28,5 @@ bot.setWebHook(`${url}/bot${TOKEN}`);
// Just to ping! // Just to ping!
bot.on('message', function onMessage(msg) { 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!');
}); });

@ -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. * 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]; const majorVersion = parseInt(process.versions.node.split('.')[0], 10);
if (majorVersion === '0') { if (majorVersion <= 4) {
const deprecate = require('depd')('node-telegram-bot-api'); 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'); module.exports = require('./lib/telegram');
} else { } else {
module.exports = require('./src/telegram'); module.exports = require('./src/telegram');

@ -1,6 +1,6 @@
{ {
"name": "node-telegram-bot-api", "name": "node-telegram-bot-api",
"version": "0.26.0", "version": "0.27.0",
"description": "Telegram Bot API", "description": "Telegram Bot API",
"main": "./index.js", "main": "./index.js",
"directories": { "directories": {
@ -24,7 +24,11 @@
}, },
"author": "Yago Pérez <yagoperezs@gmail.com>", "author": "Yago Pérez <yagoperezs@gmail.com>",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=0.12"
},
"dependencies": { "dependencies": {
"array.prototype.findindex": "^2.0.0",
"bl": "^1.1.2", "bl": "^1.1.2",
"bluebird": "^3.3.4", "bluebird": "^3.3.4",
"debug": "^2.2.0", "debug": "^2.2.0",
@ -69,14 +73,32 @@
"homepage": "https://github.com/yagop/node-telegram-bot-api", "homepage": "https://github.com/yagop/node-telegram-bot-api",
"contributors": [ "contributors": [
{ {
"name": "Mohammed Sohail", "name": "Anton Mironov",
"email": "sohailsameja@gmail.com", "email": "ant.mironov@gmail.com",
"url": "https://github.com/kamikazechaser", "url": "https://github.com/mironov",
"contributions": 1, "contributions": 1,
"additions": 18, "additions": 51,
"deletions": 4, "deletions": 15,
"hireable": true
},
{
"name": "Daniil Yastremskiy",
"email": "Catharsis@post.cz",
"url": "https://github.com/TheBeastOfCaerbannog",
"contributions": 1,
"additions": 36,
"deletions": 0,
"hireable": true "hireable": true
}, },
{
"name": null,
"email": null,
"url": "https://github.com/Ni2c2k",
"contributions": 1,
"additions": 4,
"deletions": 4,
"hireable": null
},
{ {
"name": "Alexander Tarmolov", "name": "Alexander Tarmolov",
"email": "tarmolov@gmail.com", "email": "tarmolov@gmail.com",
@ -95,6 +117,15 @@
"deletions": 0, "deletions": 0,
"hireable": null "hireable": null
}, },
{
"name": "Ola Flisbäck",
"email": null,
"url": "https://github.com/oflisback",
"contributions": 1,
"additions": 3,
"deletions": 3,
"hireable": true
},
{ {
"name": null, "name": null,
"email": null, "email": null,
@ -122,15 +153,6 @@
"deletions": 5, "deletions": 5,
"hireable": null "hireable": null
}, },
{
"name": "Ola Flisbäck",
"email": null,
"url": "https://github.com/oflisback",
"contributions": 1,
"additions": 3,
"deletions": 3,
"hireable": true
},
{ {
"name": "Horus Lugo", "name": "Horus Lugo",
"email": "horusgoul@gmail.com", "email": "horusgoul@gmail.com",
@ -158,6 +180,15 @@
"deletions": 1, "deletions": 1,
"hireable": null "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", "name": "Matthew Brandly",
"email": "matt@brandly.me", "email": "matt@brandly.me",
@ -185,15 +216,6 @@
"deletions": 2, "deletions": 2,
"hireable": true "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", "name": "Guido García",
"email": "palmerabollo@gmail.com", "email": "palmerabollo@gmail.com",
@ -212,6 +234,24 @@
"deletions": 1, "deletions": 1,
"hireable": null "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", "name": "Jérémy Gotteland",
"email": null, "email": null,
@ -230,15 +270,6 @@
"deletions": 2, "deletions": 2,
"hireable": true "hireable": true
}, },
{
"name": "Iiro Jäppinen",
"email": null,
"url": "https://github.com/iiroj",
"contributions": 2,
"additions": 40,
"deletions": 0,
"hireable": null
},
{ {
"name": "Dardan Neziri", "name": "Dardan Neziri",
"email": "dard.ne@gmail.com", "email": "dard.ne@gmail.com",
@ -249,22 +280,22 @@
"hireable": true "hireable": true
}, },
{ {
"name": "Jishnu Mohan", "name": "Cristian Baldi",
"email": "jishnu7@gmail.com", "email": "bld.cris.96@gmail.com",
"url": "https://github.com/jishnu7", "url": "https://github.com/crisbal",
"contributions": 2, "contributions": 2,
"additions": 84, "additions": 26,
"deletions": 0, "deletions": 1,
"hireable": true "hireable": true
}, },
{ {
"name": "TJ Horner", "name": "Vitaly Aminev",
"email": "me@tjhorner.com", "email": null,
"url": "https://github.com/tjhorner", "url": "https://github.com/AVVS",
"contributions": 2, "contributions": 2,
"additions": 223, "additions": 1065,
"deletions": 1, "deletions": 1001,
"hireable": null "hireable": true
}, },
{ {
"name": null, "name": null,
@ -276,13 +307,31 @@
"hireable": null "hireable": null
}, },
{ {
"name": "Vitaly Aminev", "name": "Iiro Jäppinen",
"email": null, "email": null,
"url": "https://github.com/AVVS", "url": "https://github.com/iiroj",
"contributions": 2, "contributions": 2,
"additions": 1065, "additions": 40,
"deletions": 1001, "deletions": 0,
"hireable": true "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", "name": "Vítor Augusto da Silva Vasconcellos",
@ -302,15 +351,6 @@
"deletions": 25, "deletions": 25,
"hireable": null "hireable": null
}, },
{
"name": "Rafael Kr",
"email": null,
"url": "https://github.com/RafaelKr",
"contributions": 3,
"additions": 3,
"deletions": 2,
"hireable": null
},
{ {
"name": "Ivan Skorokhodov", "name": "Ivan Skorokhodov",
"email": "iskorokhodov@gmail.com", "email": "iskorokhodov@gmail.com",
@ -339,13 +379,13 @@
"hireable": true "hireable": true
}, },
{ {
"name": "Chris54721", "name": "Yago",
"email": null, "email": "yago@yago.me",
"url": "https://github.com/chris54721", "url": "https://github.com/yagop",
"contributions": 5, "contributions": 194,
"additions": 22, "additions": 3014,
"deletions": 6, "deletions": 1173,
"hireable": null "hireable": true
}, },
{ {
"name": "Ilias Ismanalijev", "name": "Ilias Ismanalijev",
@ -356,32 +396,23 @@
"deletions": 10, "deletions": 10,
"hireable": true "hireable": true
}, },
{
"name": "Chris54721",
"email": null,
"url": "https://github.com/chris54721",
"contributions": 5,
"additions": 22,
"deletions": 6,
"hireable": null
},
{ {
"name": "Gocho Mugo", "name": "Gocho Mugo",
"email": "mugo@forfuture.co.ke", "email": "mugo@forfuture.co.ke",
"url": "https://github.com/GochoMugo", "url": "https://github.com/GochoMugo",
"contributions": 56, "contributions": 80,
"additions": 3779, "additions": 4590,
"deletions": 2167, "deletions": 2377,
"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,
"hireable": true "hireable": true
} }
] ]
} }

@ -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;
}
};

@ -1,3 +1,7 @@
// shims
require('array.prototype.findindex').shim(); // for Node.js v0.x
const errors = require('./errors');
const TelegramBotWebHook = require('./telegramWebHook'); const TelegramBotWebHook = require('./telegramWebHook');
const TelegramBotPolling = require('./telegramPolling'); const TelegramBotPolling = require('./telegramPolling');
const debug = require('debug')('node-telegram-bot-api'); const debug = require('debug')('node-telegram-bot-api');
@ -28,6 +32,10 @@ Promise.config({
class TelegramBot extends EventEmitter { class TelegramBot extends EventEmitter {
static get errors() {
return errors;
}
static get messageTypes() { static get messageTypes() {
return _messageTypes; return _messageTypes;
} }
@ -43,10 +51,15 @@ class TelegramBot extends EventEmitter {
* @param {Object} [options] * @param {Object} [options]
* @param {Boolean|Object} [options.polling=false] Set true to enable polling or set 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. * 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 {String|Number} [options.polling.interval=300] Interval between requests in miliseconds
* @param {Boolean} [options.polling.autoStart=true] Start polling immediately * @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 {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 {Number} [options.webHook.port=8443] Port to bind to
* @param {String} [options.webHook.key] Path to file with PEM private key for webHook server. * @param {String} [options.webHook.key] Path to file with PEM private key for webHook server.
* The file is read **synchronously**! * 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 * 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. * 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. * 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 {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. * @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. * 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.baseApiUrl = options.baseApiUrl || 'https://api.telegram.org';
this.options.filepath = (typeof options.filepath === 'undefined') ? true : options.filepath; this.options.filepath = (typeof options.filepath === 'undefined') ? true : options.filepath;
this._textRegexpCallbacks = []; this._textRegexpCallbacks = [];
this._onReplyToMessages = []; this._replyListenerId = 0;
this._replyListeners = [];
this._polling = null; this._polling = null;
this._webHook = null; this._webHook = null;
@ -130,7 +145,7 @@ class TelegramBot extends EventEmitter {
*/ */
_request(_path, options = {}) { _request(_path, options = {}) {
if (!this.token) { 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) { if (this.options.request) {
@ -152,30 +167,22 @@ class TelegramBot extends EventEmitter {
debug('HTTP request: %j', options); debug('HTTP request: %j', options);
return request(options) return request(options)
.then(resp => { .then(resp => {
if (resp.statusCode !== 200) {
const error = new Error(`${resp.statusCode} ${resp.body}`);
error.response = resp;
throw error;
}
let data; let data;
try { try {
data = JSON.parse(resp.body); data = resp.body = JSON.parse(resp.body);
} catch (err) { } catch (err) {
const error = new Error(`Error parsing Telegram response: ${resp.body}`); throw new errors.ParseError(`Error parsing Telegram response: ${resp.body}`, resp);
error.response = resp;
throw error;
} }
if (data.ok) { if (data.ok) {
return data.result; return data.result;
} }
const error = new Error(`${data.error_code} ${data.description}`); throw new errors.TelegramError(`${data.error_code} ${data.description}`, resp);
error.response = resp; }).catch(error => {
error.response.body = data; // TODO: why can't we do `error instanceof errors.BaseError`?
throw error; if (error.response) throw error;
throw new errors.FatalError(error);
}); });
} }
@ -195,7 +202,9 @@ class TelegramBot extends EventEmitter {
let fileName; let fileName;
let fileId; let fileId;
if (data instanceof stream.Stream) { 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 = {};
formData[type] = { formData[type] = {
value: data, value: data,
@ -207,7 +216,7 @@ class TelegramBot extends EventEmitter {
} else if (Buffer.isBuffer(data)) { } else if (Buffer.isBuffer(data)) {
const filetype = fileType(data); const filetype = fileType(data);
if (!filetype) { if (!filetype) {
throw new Error('Unsupported Buffer file type'); throw new errors.FatalError('Unsupported Buffer file type');
} }
formData = {}; formData = {};
formData[type] = { formData[type] = {
@ -248,11 +257,11 @@ class TelegramBot extends EventEmitter {
*/ */
startPolling(options = {}) { startPolling(options = {}) {
if (this.hasOpenWebHook()) { 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; options.restart = typeof options.restart === 'undefined' ? true : options.restart;
if (!this._polling) { 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); return this._polling.start(options);
} }
@ -297,10 +306,10 @@ class TelegramBot extends EventEmitter {
*/ */
openWebHook() { openWebHook() {
if (this.isPolling()) { 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) { 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(); return this._webHook.open();
} }
@ -377,14 +386,7 @@ class TelegramBot extends EventEmitter {
} }
} }
return this._request('setWebHook', opts) return this._request('setWebHook', opts);
.then(resp => {
if (!resp) {
throw new Error(resp);
}
return resp;
});
} }
/** /**
@ -479,7 +481,7 @@ class TelegramBot extends EventEmitter {
} }
if (message.reply_to_message) { if (message.reply_to_message) {
// Only callbacks waiting for this message // Only callbacks waiting for this message
this._onReplyToMessages.forEach(reply => { this._replyListeners.forEach(reply => {
// Message from the same chat // Message from the same chat
if (reply.chatId === message.chat.id) { if (reply.chatId === message.chat.id) {
// Responding to that message // 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} chatId The chat id where the message cames from.
* @param {Number|String} messageId The message id to be replied. * @param {Number|String} messageId The message id to be replied.
* @param {Function} callback Callback will be called with the reply * @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) { onReplyToMessage(chatId, messageId, callback) {
this._onReplyToMessages.push({ const id = ++this._replyListenerId;
this._replyListeners.push({
id,
chatId, chatId,
messageId, messageId,
callback 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];
} }
/** /**

@ -1,34 +1,26 @@
const debug = require('debug')('node-telegram-bot-api'); const debug = require('debug')('node-telegram-bot-api');
const deprecate = require('depd')('node-telegram-bot-api');
const ANOTHER_WEB_HOOK_USED = 409; const ANOTHER_WEB_HOOK_USED = 409;
class TelegramBotPolling { class TelegramBotPolling {
/** /**
* Handles polling against the Telegram servers. * Handles polling against the Telegram servers.
* * @param {TelegramBot} bot
* @param {Function} request Function used to make HTTP requests * @see https://core.telegram.org/bots/api#getting-updates
* @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
*/ */
constructor(request, options = {}, callback) { constructor(bot) {
/* eslint-disable no-param-reassign */ this.bot = bot;
if (typeof options === 'function') { this.options = (typeof bot.options.polling === 'boolean') ? {} : bot.options.polling;
callback = options; this.options.interval = (typeof this.options.interval === 'number') ? this.options.interval : 300;
options = {}; this.options.params = (typeof this.options.params === 'object') ? this.options.params : {};
} else if (typeof options === 'boolean') { this.options.params.offset = (typeof this.options.params.offset === 'number') ? this.options.params.offset : 0;
options = {}; 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._lastUpdate = 0;
this._lastRequest = null; this._lastRequest = null;
this._abort = false; this._abort = false;
@ -100,14 +92,20 @@ class TelegramBotPolling {
this._lastUpdate = Date.now(); this._lastUpdate = Date.now();
debug('polling data %j', updates); debug('polling data %j', updates);
updates.forEach(update => { updates.forEach(update => {
this._offset = update.update_id; this.options.params.offset = update.update_id + 1;
debug('updated offset: %s', this._offset); debug('updated offset: %s', this.options.params.offset);
this.callback(update); this.bot.processUpdate(update);
}); });
return null;
}) })
.catch(err => { .catch(err => {
debug('polling error: %s', err.message); 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(() => { .finally(() => {
if (this._abort) { if (this._abort) {
@ -127,26 +125,21 @@ class TelegramBotPolling {
* @private * @private
*/ */
_unsetWebHook() { _unsetWebHook() {
return this.request('setWebHook'); debug('unsetting webhook');
return this.bot._request('setWebHook');
} }
/** /**
* Retrieve updates * Retrieve updates
*/ */
_getUpdates() { _getUpdates() {
const opts = { debug('polling with options: %j', this.options.params);
qs: { return this.bot.getUpdates(this.options.params)
offset: this._offset + 1,
limit: this.options.limit,
timeout: this.options.timeout
},
};
debug('polling with options: %j', opts);
return this.request('getUpdates', opts)
.catch(err => { .catch(err => {
if (err.response.statusCode === ANOTHER_WEB_HOOK_USED) { if (err.response && err.response.statusCode === ANOTHER_WEB_HOOK_USED) {
return this._unsetWebHook(); return this._unsetWebHook().then(() => {
return this.bot.getUpdates(this.options.params);
});
} }
throw err; throw err;
}); });

@ -1,3 +1,4 @@
const errors = require('./errors');
const debug = require('debug')('node-telegram-bot-api'); const debug = require('debug')('node-telegram-bot-api');
const https = require('https'); const https = require('https');
const http = require('http'); const http = require('http');
@ -9,24 +10,17 @@ const Promise = require('bluebird');
class TelegramBotWebHook { class TelegramBotWebHook {
/** /**
* Sets up a webhook to receive updates * Sets up a webhook to receive updates
* * @param {TelegramBot} bot
* @param {String} token Telegram API token * @see https://core.telegram.org/bots/api#getting-updates
* @param {Boolean|Object} options WebHook options
* @param {Number} [options.port=8443] Port to bind to
* @param {Function} callback Function for process a new update
*/ */
constructor(token, options, callback) { constructor(bot) {
// define opts this.bot = bot;
if (typeof options === 'boolean') { this.options = (typeof bot.options.webHook === 'boolean') ? {} : bot.options.webHook;
options = {}; // eslint-disable-line no-param-reassign this.options.host = this.options.host || '0.0.0.0';
} this.options.port = this.options.port || 8443;
this.options.https = this.options.https || {};
this.token = token; this.options.healthEndpoint = this.options.healthEndpoint || '/healthz';
this.options = options; this._healthRegex = new RegExp(this.options.healthEndpoint);
this.options.port = options.port || 8443;
this.options.https = options.https || {};
this.callback = callback;
this._regex = new RegExp(this.token);
this._webServer = null; this._webServer = null;
this._open = false; this._open = false;
this._requestListener = this._requestListener.bind(this); this._requestListener = this._requestListener.bind(this);
@ -96,31 +90,35 @@ class TelegramBotWebHook {
return this._open; return this._open;
} }
// used so that other funcs are not non-optimizable /**
_safeParse(json) { * Handle error thrown during processing of webhook request.
try { * @private
return JSON.parse(json); * @param {Error} error
} catch (err) { */
debug(err); _error(error) {
return null; 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' * Handle request body by passing it to 'callback'
* @private * @private
*/ */
_parseBody(err, body) { _parseBody(error, body) {
if (err) { if (error) {
return debug(err); return this._error(new errors.FatalError(error));
} }
const data = this._safeParse(body); let data;
if (data) { try {
return this.callback(data); 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 URL: %s', req.url);
debug('WebHook request headers: %j', req.headers); debug('WebHook request headers: %j', req.headers);
// If there isn't token on URL if (req.url.indexOf(this.bot.token) !== -1) {
if (!this._regex.test(req.url)) { 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'); debug('WebHook request unauthorized');
res.statusCode = 401; res.statusCode = 401;
res.end(); 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();
} }
} }
} }

@ -27,8 +27,10 @@ const pollingPort = portindex++;
const webHookPort = portindex++; const webHookPort = portindex++;
const pollingPort2 = portindex++; const pollingPort2 = portindex++;
const webHookPort2 = portindex++; const webHookPort2 = portindex++;
const badTgServerPort = portindex++;
const staticUrl = `http://127.0.0.1:${staticPort}`; const staticUrl = `http://127.0.0.1:${staticPort}`;
const key = `${__dirname}/../examples/key.pem`; const key = `${__dirname}/../examples/key.pem`;
const ip = '216.58.210.174'; // Google IP ¯\_(ツ)_/¯
const cert = `${__dirname}/../examples/crt.pem`; const cert = `${__dirname}/../examples/crt.pem`;
let FILE_ID; let FILE_ID;
let GAME_CHAT_ID; let GAME_CHAT_ID;
@ -39,17 +41,20 @@ before(function beforeAll() {
return utils.startMockServer(pollingPort) return utils.startMockServer(pollingPort)
.then(() => { .then(() => {
return utils.startMockServer(pollingPort2); return utils.startMockServer(pollingPort2);
}).then(() => {
return utils.startMockServer(badTgServerPort, { bad: true });
}); });
}); });
describe('module.exports', function moduleExportsSuite() { describe('module.exports', function moduleExportsSuite() {
it('is loaded from src/ if NOT on Node.js 0.12', function test() { const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
if (process.versions.node.split('.')[0] === '0') this.skip(); // skip on Node.js v0.12 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')); assert.equal(TelegramBot, require('../src/telegram'));
}); });
it('is loaded from lib/ if on Node.js 0.12', function test() { it('is loaded from lib/ on Node.js v4 and below', function test() {
if (process.versions.node.split('.')[0] !== '0') this.skip(); // skip on newer versions if (nodeVersion > 4) this.skip(); // skip on newer versions
assert.equal(TelegramBot, require('../lib/telegram')); assert.equal(TelegramBot, require('../lib/telegram'));
}); });
}); });
@ -118,21 +123,58 @@ describe('TelegramBot', function telegramSuite() {
return utils.hasOpenWebHook(webHookPort, true); 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() { describe('Events', function eventsSuite() {
it('(polling) emits "message" on receiving message', function test(done) { it('(polling) emits "message" on receiving message', function test(done) {
botPolling.once('message', () => { botPolling.once('message', () => {
return done(); 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) { it('(webhook) emits "message" on receiving message', function test(done) {
botWebHook.once('message', () => { botWebHook.once('message', () => {
return done(); return done();
}); });
utils.sendWebHookMessage(webHookPort2, TOKEN); 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() { 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) { it('returns 401 error if token is wrong', function test(done) {
utils.sendWebHookMessage(webHookPort2, 'wrong-token').catch(resp => { utils.sendWebHookMessage(webHookPort2, 'wrong-token').catch(resp => {
assert.equal(resp.statusCode, 401); 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() { describe('#startPolling', function initPollingSuite() {
it('initiates polling', function test() { it('initiates polling', function test() {
return testbot.startPolling().then(() => { return testbot.startPolling().then(() => {
@ -188,6 +282,8 @@ describe('TelegramBot', function telegramSuite() {
it('returns error if using webhook', function test() { it('returns error if using webhook', function test() {
return botWebHook.startPolling().catch((err) => { return botWebHook.startPolling().catch((err) => {
// TODO: check for error in a better way // 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); assert.ok(err.message.indexOf('mutually exclusive') !== -1);
}); });
}); });
@ -228,6 +324,8 @@ describe('TelegramBot', function telegramSuite() {
it('returns error if using polling', function test() { it('returns error if using polling', function test() {
return botPolling.openWebHook().catch((err) => { return botPolling.openWebHook().catch((err) => {
// TODO: check for error in a better way // 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); assert.ok(err.message.indexOf('mutually exclusive') !== -1);
}); });
}); });
@ -271,12 +369,10 @@ describe('TelegramBot', function telegramSuite() {
}); });
describe('#setWebHook', function setWebHookSuite() { describe('#setWebHook', function setWebHookSuite() {
const ip = '216.58.210.174';
before(function before() { before(function before() {
utils.handleRatelimit(bot, 'setWebHook', this); utils.handleRatelimit(bot, 'setWebHook', this);
}); });
it('should set a webHook', function test() { it('should set a webHook', function test() {
// Google IP ¯\_(ツ)_/¯
return bot return bot
.setWebHook(ip) .setWebHook(ip)
.then(resp => { .then(resp => {
@ -516,6 +612,13 @@ describe('TelegramBot', function telegramSuite() {
assert.ok(is.object(resp.document)); 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() { describe('#sendSticker', function sendStickerSuite() {
@ -876,6 +979,24 @@ describe('TelegramBot', function telegramSuite() {
describe.skip('#onReplyToMessage', function onReplyToMessageSuite() {}); 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() { describe('#getChat', function getChatSuite() {
before(function before() { before(function before() {
utils.handleRatelimit(bot, 'getChat', this); utils.handleRatelimit(bot, 'getChat', this);
@ -988,8 +1109,13 @@ describe('TelegramBot', function telegramSuite() {
const photo = `${__dirname}/data/photo.gif`; const photo = `${__dirname}/data/photo.gif`;
return tgbot.sendPhoto(USERID, photo).catch(err => { return tgbot.sendPhoto(USERID, photo).catch(err => {
// TODO: check for error in a better way // 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 }); // End Telegram

@ -30,12 +30,25 @@ exports = module.exports = {
* @return {Promise} * @return {Promise}
*/ */
isPollingMockServer, 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. * Send a message to the webhook at the specified port.
* @param {Number} port * @param {Number} port
* @param {String} token * @param {String} token
* @param {Object} [options] * @param {Object} [options]
* @param {String} [options.method=POST] Method to use * @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 {Object} [options.message] Message to send. Default to a generic text message
* @param {Boolean} [options.https=false] Use https * @param {Boolean} [options.https=false] Use https
* @return {Promise} * @return {Promise}
@ -44,6 +57,9 @@ exports = module.exports = {
/** /**
* Start a mock server at the specified port. * Start a mock server at the specified port.
* @param {Number} port * @param {Number} port
* @param {Object} [options]
* @param {Boolean} [options.bad=false] Bad Mock Server; responding with
* unparseable messages
* @return {Promise} * @return {Promise}
*/ */
startMockServer, startMockServer,
@ -65,10 +81,13 @@ const statics = require('node-static');
const servers = {}; const servers = {};
function startMockServer(port) { function startMockServer(port, options = {}) {
assert.ok(port); assert.ok(port);
const server = http.Server((req, res) => { const server = http.Server((req, res) => {
servers[port].polling = true; servers[port].polling = true;
if (options.bad) {
return res.end('can not be parsed with JSON.parse()');
}
return res.end(JSON.stringify({ return res.end(JSON.stringify({
ok: true, ok: true,
result: [{ result: [{
@ -134,23 +153,31 @@ function hasOpenWebHook(port, reverse) {
} }
function sendWebHookMessage(port, token, options = {}) { function sendWebHookRequest(port, path, options = {}) {
assert.ok(port); assert.ok(port);
assert.ok(token); assert.ok(path);
const protocol = options.https ? 'https' : 'http'; 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({ return request({
url, url,
method: options.method || 'POST', method: options.method || 'POST',
body: { body: options.update || {
update_id: 1, update_id: 1,
message: options.message || { text: 'test' } 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) { function handleRatelimit(bot, methodName, suite) {
const backupMethodName = `__${methodName}`; const backupMethodName = `__${methodName}`;
if (!bot[backupMethodName]) bot[backupMethodName] = bot[methodName]; if (!bot[backupMethodName]) bot[backupMethodName] = bot[methodName];

Loading…
Cancel
Save