diff --git a/README.md b/README.md
index 3b3262c..b3e62f2 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,7 @@ TelegramBot
* [TelegramBot](#TelegramBot)
* [new TelegramBot(token, [options])](#new_TelegramBot_new)
+ * [.initPolling()](#TelegramBot+initPolling)
* [.stopPolling()](#TelegramBot+stopPolling) ⇒ Promise
* [.getMe()](#TelegramBot+getMe) ⇒ Promise
* [.setWebHook(url, [cert])](#TelegramBot+setWebHook)
@@ -122,14 +123,21 @@ 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 |
-| [options.polling.timeout] | String
| Number
| 10
| Polling time in seconds |
-| [options.polling.interval] | String
| Number
| 2000
| Interval between requests in miliseconds |
+| [options.polling.timeout] | String
| Number
| 10
| Timeout in seconds for long polling |
+| [options.polling.interval] | String
| Number
| 300
| Interval between requests in miliseconds |
| [options.webHook] | Boolean
| Object
| false
| Set true to enable WebHook or set options |
-| [options.webHook.key] | String
| | PEM private key to webHook server. |
-| [options.webHook.cert] | String
| | PEM certificate (public) to webHook server. |
+| [options.webHook.port] | Number
| 8443
| Port to bind to |
+| [options.webHook.key] | String
| | Path to file with PEM private key for webHook server. (Read synchronously!) |
+| [options.webHook.cert] | String
| | Path to file with PEM certificate (public) for webHook server. (Read synchronously!) |
| [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.request] | Object
| | Options which will be added for all requests to telegram api. See https://github.com/request/request#requestoptions-callback for more information. |
+
+
+### telegramBot.initPolling()
+Start polling
+
+**Kind**: instance method of [TelegramBot](#TelegramBot)
### telegramBot.stopPolling() ⇒ Promise
diff --git a/package.json b/package.json
index c314626..6f23db0 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,8 @@
"bot"
],
"scripts": {
- "prepublish": "babel -d ./lib src",
+ "build": "babel -d ./lib src",
+ "prepublish": "npm run build",
"test": "istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec --timeout 10000",
"prepublish:test": "npm run prepublish && npm run test",
"gen-doc": "jsdoc2md --src src/telegram.js -t README.hbs > README.md",
diff --git a/src/telegram.js b/src/telegram.js
index 1f65f0c..419b3d1 100644
--- a/src/telegram.js
+++ b/src/telegram.js
@@ -20,6 +20,11 @@ const _messageTypes = [
'new_chat_photo', 'delete_chat_photo', 'group_chat_created'
];
+// enable cancellation
+Promise.config({
+ cancellation: true,
+});
+
class TelegramBot extends EventEmitter {
static get messageTypes() {
@@ -36,22 +41,22 @@ class TelegramBot extends EventEmitter {
* @param {String} token Bot Token
* @param {Object} [options]
* @param {Boolean|Object} [options.polling=false] Set true to enable polling or set options
- * @param {String|Number} [options.polling.timeout=10] Polling time in seconds
- * @param {String|Number} [options.polling.interval=2000] Interval between requests in miliseconds
+ * @param {String|Number} [options.polling.timeout=10] Timeout in seconds for long polling
+ * @param {String|Number} [options.polling.interval=300] Interval between requests in miliseconds
* @param {Boolean|Object} [options.webHook=false] Set true to enable WebHook or set options
- * @param {String} [options.webHook.key] PEM private key to webHook server.
- * @param {String} [options.webHook.cert] PEM certificate (public) to webHook server.
+ * @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. (Read synchronously!)
+ * @param {String} [options.webHook.cert] Path to file with PEM certificate (public) for webHook server. (Read synchronously!)
* @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.
+ * @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://core.telegram.org/bots/api
*/
constructor(token, options = {}) {
super();
- this.options = options;
this.token = token;
- this.textRegexpCallbacks = [];
- this.onReplyToMessages = [];
+ this.options = options;
+ this._textRegexpCallbacks = [];
+ this._onReplyToMessages = [];
if (options.polling) {
this.initPolling();
@@ -62,27 +67,13 @@ class TelegramBot extends EventEmitter {
}
}
- initPolling() {
- if (this._polling) {
- this._polling.abort = true;
- this._polling.lastRequest.cancel('Polling restart');
- }
- this._polling = new TelegramBotPolling(this.token, this.options.polling, this.processUpdate.bind(this));
- }
-
/**
- * Stops polling after the last polling request resolves
- *
- * @return {Promise} promise Promise, of last polling request
+ * Process an update; emitting the proper events and executing regexp
+ * callbacks
+ * @param {Object} update
+ * @private
*/
- stopPolling() {
- if (this._polling) {
- return this._polling.stopPolling();
- }
- return Promise.resolve();
- }
-
- processUpdate(update) {
+ _processUpdate(update) {
debug('Process Update %j', update);
const message = update.message;
const editedMessage = update.edited_message;
@@ -97,27 +88,28 @@ class TelegramBot extends EventEmitter {
this.emit('message', message);
const processMessageType = messageType => {
if (message[messageType]) {
- debug('Emtting %s: %j', messageType, message);
+ debug('Emitting %s: %j', messageType, message);
this.emit(messageType, message);
}
};
TelegramBot.messageTypes.forEach(processMessageType);
if (message.text) {
debug('Text message');
- this.textRegexpCallbacks.some(reg => {
+ this._textRegexpCallbacks.some(reg => {
debug('Matching %s with %s', message.text, reg.regexp);
const result = reg.regexp.exec(message.text);
- if (result) {
- debug('Matches %s', reg.regexp);
- reg.callback(message, result);
- // returning truthy value exits .some
- return this.options.onlyFirstMatch;
+ if (!result) {
+ return false;
}
+ debug('Matches %s', reg.regexp);
+ reg.callback(message, result);
+ // returning truthy value exits .some
+ return this.options.onlyFirstMatch;
});
}
if (message.reply_to_message) {
// Only callbacks waiting for this message
- this.onReplyToMessages.forEach(reply => {
+ this._onReplyToMessages.forEach(reply => {
// Message from the same chat
if (reply.chatId === message.chat.id) {
// Responding to that message
@@ -139,7 +131,7 @@ class TelegramBot extends EventEmitter {
}
} else if (channelPost) {
debug('Process Update channel_post %j', channelPost);
- this.emit('channel_post', channelPost);
+ this.emit('channel_post', channelPost);
} else if (editedChannelPost) {
debug('Process Update edited_channel_post %j', editedChannelPost);
this.emit('edited_channel_post', editedChannelPost);
@@ -148,7 +140,7 @@ class TelegramBot extends EventEmitter {
}
if (editedChannelPost.caption) {
this.emit('edited_channel_post_caption', editedChannelPost);
- }
+ }
} else if (inlineQuery) {
debug('Process Update inline_query %j', inlineQuery);
this.emit('inline_query', inlineQuery);
@@ -161,24 +153,42 @@ class TelegramBot extends EventEmitter {
}
}
- // used so that other funcs are not non-optimizable
- _safeParse(json) {
- try {
- return JSON.parse(json);
- } catch (err) {
- throw new Error(`Error parsing Telegram response: ${String(json)}`);
- }
+ /**
+ * Generates url with bot token and provided path/method you want to be got/executed by bot
+ * @param {String} path
+ * @return {String} url
+ * @private
+ * @see https://core.telegram.org/bots/api#making-requests
+ */
+ _buildURL(_path) {
+ return URL.format({
+ protocol: 'https',
+ host: 'api.telegram.org',
+ pathname: `/bot${this.token}/${_path}`
+ });
}
+ /**
+ * Fix 'reply_markup' parameter by making it JSON-serialized, as
+ * required by the Telegram Bot API
+ * @param {Object} obj Object; either 'form' or 'qs'
+ * @private
+ * @see https://core.telegram.org/bots/api#sendmessage
+ */
_fixReplyMarkup(obj) {
const replyMarkup = obj.reply_markup;
if (replyMarkup && typeof replyMarkup !== 'string') {
- // reply_markup must be passed as JSON stringified to Telegram
obj.reply_markup = JSON.stringify(replyMarkup);
}
}
- // request-promise
+ /**
+ * Make request against the API
+ * @param {String} _path API endpoint
+ * @param {Object} [options]
+ * @private
+ * @return {Promise}
+ */
_request(_path, options = {}) {
if (!this.token) {
throw new Error('Telegram Bot Token not provided!');
@@ -194,6 +204,7 @@ class TelegramBot extends EventEmitter {
if (options.qs) {
this._fixReplyMarkup(options.qs);
}
+
options.url = this._buildURL(_path);
options.simple = false;
options.resolveWithFullResponse = true;
@@ -202,31 +213,108 @@ class TelegramBot extends EventEmitter {
return request(options)
.then(resp => {
if (resp.statusCode !== 200) {
- throw new Error(`${resp.statusCode} ${resp.body}`);
+ const error = new Error(`${resp.statusCode} ${resp.body}`);
+ error.response = resp;
+ throw error;
+ }
+
+ let data;
+
+ try {
+ data = JSON.parse(resp.body);
+ } catch (err) {
+ const error = new Error(`Error parsing Telegram response: ${resp.body}`);
+ error.response = resp;
+ throw error;
}
- const data = this._safeParse(resp.body);
if (data.ok) {
return data.result;
}
- throw new Error(`${data.error_code} ${data.description}`);
+ const error = new Error(`${data.error_code} ${data.description}`);
+ error.response = resp;
+ error.response.body = data;
+ throw error;
});
}
/**
- * Generates url with bot token and provided path/method you want to be got/executed by bot
- * @return {String} url
- * @param {String} path
+ * Format data to be uploaded; handles file paths, streams and buffers
+ * @param {String} type
+ * @param {String|stream.Stream|Buffer} data
+ * @return {Array} formatted
+ * @return {Object} formatted[0] formData
+ * @return {String} formatted[1] fileId
* @private
- * @see https://core.telegram.org/bots/api#making-requests
*/
- _buildURL(_path) {
- return URL.format({
- protocol: 'https',
- host: 'api.telegram.org',
- pathname: `/bot${this.token}/${_path}`
- });
+ _formatSendData(type, data) {
+ let formData;
+ let fileName;
+ let fileId;
+ if (data instanceof stream.Stream) {
+ fileName = URL.parse(path.basename(data.path.toString())).pathname;
+ formData = {};
+ formData[type] = {
+ value: data,
+ options: {
+ filename: qs.unescape(fileName),
+ contentType: mime.lookup(fileName)
+ }
+ };
+ } else if (Buffer.isBuffer(data)) {
+ const filetype = fileType(data);
+ if (!filetype) {
+ throw new Error('Unsupported Buffer file type');
+ }
+ formData = {};
+ formData[type] = {
+ value: data,
+ options: {
+ filename: `data.${filetype.ext}`,
+ contentType: filetype.mime
+ }
+ };
+ } else if (fs.existsSync(data)) {
+ fileName = path.basename(data);
+ formData = {};
+ formData[type] = {
+ value: fs.createReadStream(data),
+ options: {
+ filename: fileName,
+ contentType: mime.lookup(fileName)
+ }
+ };
+ } else {
+ fileId = data;
+ }
+ return [formData, fileId];
+ }
+
+ /**
+ * Start polling
+ */
+ initPolling() {
+ if (this._polling) {
+ this._polling.stopPolling({
+ cancel: true,
+ reason: 'Polling restart',
+ });
+ }
+ this._polling = new TelegramBotPolling(this._request.bind(this), this.options.polling, this._processUpdate.bind(this));
+ }
+
+ /**
+ * Stops polling after the last polling request resolves
+ * @return {Promise} promise Promise, of last polling request
+ */
+ stopPolling() {
+ if (!this._polling) {
+ return Promise.resolve();
+ }
+ const polling = this._polling;
+ delete this._polling;
+ return polling.stopPolling();
}
/**
@@ -330,49 +418,6 @@ class TelegramBot extends EventEmitter {
return this._request('forwardMessage', { form });
}
- _formatSendData(type, data) {
- let formData;
- let fileName;
- let fileId;
- if (data instanceof stream.Stream) {
- fileName = URL.parse(path.basename(data.path.toString())).pathname;
- formData = {};
- formData[type] = {
- value: data,
- options: {
- filename: qs.unescape(fileName),
- contentType: mime.lookup(fileName)
- }
- };
- } else if (Buffer.isBuffer(data)) {
- const filetype = fileType(data);
- if (!filetype) {
- throw new Error('Unsupported Buffer file type');
- }
- formData = {};
- formData[type] = {
- value: data,
- options: {
- filename: `data.${filetype.ext}`,
- contentType: filetype.mime
- }
- };
- } else if (fs.existsSync(data)) {
- fileName = path.basename(data);
- formData = {};
- formData[type] = {
- value: fs.createReadStream(data),
- options: {
- filename: fileName,
- contentType: mime.lookup(fileName)
- }
- };
- } else {
- fileId = data;
- }
- return [formData, fileId];
- }
-
/**
* Send photo
* @param {Number|String} chatId Unique identifier for the message recipient
@@ -776,7 +821,7 @@ class TelegramBot extends EventEmitter {
* the `msg` and the result of executing `regexp.exec` on message text.
*/
onText(regexp, callback) {
- this.textRegexpCallbacks.push({ regexp, callback });
+ this._textRegexpCallbacks.push({ regexp, callback });
}
/**
@@ -787,7 +832,7 @@ class TelegramBot extends EventEmitter {
* message.
*/
onReplyToMessage(chatId, messageId, callback) {
- this.onReplyToMessages.push({
+ this._onReplyToMessages.push({
chatId,
messageId,
callback
diff --git a/src/telegramPolling.js b/src/telegramPolling.js
index fa0bd44..fd679f5 100644
--- a/src/telegramPolling.js
+++ b/src/telegramPolling.js
@@ -1,48 +1,69 @@
-const Promise = require('bluebird');
const debug = require('debug')('node-telegram-bot-api');
-const request = require('request-promise');
-const URL = require('url');
const ANOTHER_WEB_HOOK_USED = 409;
+
class TelegramBotPolling {
-
- constructor(token, options = {}, callback) {
- // enable cancellation
- Promise.config({
- cancellation: true,
- });
-
+ /**
+ * 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
+ */
+ constructor(request, options = {}, callback) {
+ /* eslint-disable no-param-reassign */
if (typeof options === 'function') {
- callback = options; // eslint-disable-line no-param-reassign
- options = {}; // eslint-disable-line no-param-reassign
+ callback = options;
+ options = {};
+ } else if (typeof options === 'boolean') {
+ options = {};
}
+ /* eslint-enable no-param-reassign */
- this.offset = 0;
- this.token = token;
+ this.request = request;
+ this.options = options;
+ this.options.timeout = options.timeout || 10;
+ this.options.interval = (typeof options.interval === 'number') ? options.interval : 300;
this.callback = callback;
- this.timeout = options.timeout || 10;
- this.interval = (typeof options.interval === 'number') ? options.interval : 300;
- this.lastUpdate = 0;
- this.lastRequest = null;
- this.abort = false;
+ this._offset = 0;
+ this._lastUpdate = 0;
+ this._lastRequest = null;
+ this._abort = false;
this._polling();
}
- stopPolling() {
- this.abort = true;
+ /**
+ * Stop polling
+ * @param {Object} [options]
+ * @param {Boolean} [options.cancel] Cancel current request
+ * @param {String} [options.reason] Reason for stopping polling
+ */
+ stopPolling(options = {}) {
+ this._abort = true;
+ if (options.cancel) {
+ const reason = options.reason || 'Polling stop';
+ return this._lastRequest.cancel(reason);
+ }
// wait until the last request is fulfilled
- return this.lastRequest;
+ return this._lastRequest;
}
+ /**
+ * Invokes polling (with recursion!)
+ * @private
+ */
_polling() {
- this.lastRequest = this
+ this._lastRequest = this
._getUpdates()
.then(updates => {
- this.lastUpdate = Date.now();
+ this._lastUpdate = Date.now();
debug('polling data %j', updates);
updates.forEach(update => {
- this.offset = update.update_id;
- debug('updated offset: %s', this.offset);
+ this._offset = update.update_id;
+ debug('updated offset: %s', this._offset);
this.callback(update);
});
})
@@ -51,83 +72,46 @@ class TelegramBotPolling {
throw err;
})
.finally(() => {
- if (this.abort) {
+ if (this._abort) {
debug('Polling is aborted!');
} else {
- debug('setTimeout for %s miliseconds', this.interval);
- setTimeout(() => this._polling(), this.interval);
+ debug('setTimeout for %s miliseconds', this.options.interval);
+ setTimeout(() => this._polling(), this.options.interval);
}
});
}
- // used so that other funcs are not non-optimizable
- _safeParse(json) {
- try {
- return JSON.parse(json);
- } catch (err) {
- throw new Error(`Error parsing Telegram response: ${String(json)}`);
- }
- }
-
+ /**
+ * Unset current webhook. Used when we detect that a webhook has been set
+ * and we are trying to poll. Polling and WebHook are mutually exclusive.
+ * @see https://core.telegram.org/bots/api#getting-updates
+ * @private
+ */
_unsetWebHook() {
- return request({
- url: URL.format({
- protocol: 'https',
- host: 'api.telegram.org',
- pathname: `/bot${this.token}/setWebHook`
- }),
- simple: false,
- resolveWithFullResponse: true
- })
- .promise()
- .then(resp => {
- if (!resp) {
- throw new Error(resp);
- }
- return [];
- });
+ return this.request('setWebHook');
}
+ /**
+ * Retrieve updates
+ */
_getUpdates() {
const opts = {
qs: {
- offset: this.offset + 1,
- limit: this.limit,
- timeout: this.timeout
+ offset: this._offset + 1,
+ limit: this.options.limit,
+ timeout: this.options.timeout
},
- url: URL.format({
- protocol: 'https',
- host: 'api.telegram.org',
- pathname: `/bot${this.token}/getUpdates`
- }),
- simple: false,
- resolveWithFullResponse: true,
- forever: true,
};
debug('polling with options: %j', opts);
- return request(opts)
- .promise()
- .timeout((10 + this.timeout) * 1000)
- .then(resp => {
- if (resp.statusCode === ANOTHER_WEB_HOOK_USED) {
+ return this.request('getUpdates', opts)
+ .catch(err => {
+ if (err.response.statusCode === ANOTHER_WEB_HOOK_USED) {
return this._unsetWebHook();
}
-
- if (resp.statusCode !== 200) {
- throw new Error(`${resp.statusCode} ${resp.body}`);
- }
-
- const data = this._safeParse(resp.body);
-
- if (data.ok) {
- return data.result;
- }
-
- throw new Error(`${data.error_code} ${data.description}`);
+ throw err;
});
}
-
}
module.exports = TelegramBotPolling;
diff --git a/src/telegramWebHook.js b/src/telegramWebHook.js
index 9239481..5efa3c1 100644
--- a/src/telegramWebHook.js
+++ b/src/telegramWebHook.js
@@ -4,18 +4,28 @@ const http = require('http');
const fs = require('fs');
const bl = require('bl');
+
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
+ */
constructor(token, options, callback) {
- this.token = token;
- this.callback = callback;
- this.regex = new RegExp(this.token);
-
// define opts
if (typeof options === 'boolean') {
options = {}; // eslint-disable-line no-param-reassign
}
- options.port = options.port || 8443;
+
+ this.token = token;
+ this.options = options;
+ this.options.port = options.port || 8443;
+ this.callback = callback;
+ this._regex = new RegExp(this.token);
+ this._webServer = null;
if (options.key && options.cert) { // HTTPS Server
debug('HTTPS WebHook enabled');
@@ -44,7 +54,10 @@ class TelegramBotWebHook {
}
}
- // pipe+parse body
+ /**
+ * Handle request body by passing it to 'callback'
+ * @private
+ */
_parseBody = (err, body) => {
if (err) {
return debug(err);
@@ -58,13 +71,18 @@ class TelegramBotWebHook {
return null;
}
- // bound req listener
+ /**
+ * Listener for 'request' event on server
+ * @private
+ * @see https://nodejs.org/docs/latest/api/http.html#http_http_createserver_requestlistener
+ * @see https://nodejs.org/docs/latest/api/https.html#https_https_createserver_options_requestlistener
+ */
_requestListener = (req, res) => {
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 (!this._regex.test(req.url)) {
debug('WebHook request unauthorized');
res.statusCode = 401;
res.end();