diff --git a/.travis.yml b/.travis.yml
index ec786d8..e3efadb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,9 @@
language: node_js
node_js:
+ - "6"
- "5"
- "4"
- - "0.12.12"
+ - "0.12"
after_success:
- bash <(curl -s https://codecov.io/bash)
cache:
diff --git a/README.md b/README.md
index 3b3262c..1ea1f16 100644
--- a/README.md
+++ b/README.md
@@ -71,10 +71,16 @@ TelegramBot
* [TelegramBot](#TelegramBot)
* [new TelegramBot(token, [options])](#new_TelegramBot_new)
+ * [.initPolling()](#TelegramBot+initPolling)
* [.stopPolling()](#TelegramBot+stopPolling) ⇒ Promise
+ * [.isPolling()](#TelegramBot+isPolling) ⇒ Boolean
+ * [.openWebHook()](#TelegramBot+openWebHook)
+ * [.closeWebHook()](#TelegramBot+closeWebHook) ⇒ Promise
+ * [.hasOpenWebHook()](#TelegramBot+hasOpenWebHook) ⇒ Boolean
* [.getMe()](#TelegramBot+getMe) ⇒ Promise
* [.setWebHook(url, [cert])](#TelegramBot+setWebHook)
* [.getUpdates([timeout], [limit], [offset])](#TelegramBot+getUpdates) ⇒ Promise
+ * [.processUpdate(update)](#TelegramBot+processUpdate)
* [.sendMessage(chatId, text, [options])](#TelegramBot+sendMessage) ⇒ Promise
* [.answerInlineQuery(inlineQueryId, results, [options])](#TelegramBot+answerInlineQuery) ⇒ Promise
* [.forwardMessage(chatId, fromChatId, messageId)](#TelegramBot+forwardMessage) ⇒ Promise
@@ -122,14 +128,24 @@ 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.polling.autoStart] | Boolean
| true
| Start polling immediately |
| [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.webHook.autoOpen] | Boolean
| true
| Open webHook immediately |
| [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 |
+
+
+### telegramBot.initPolling()
+Start polling
+
+**Kind**: instance method of [TelegramBot](#TelegramBot)
### telegramBot.stopPolling() ⇒ Promise
@@ -137,6 +153,32 @@ Stops polling after the last polling request resolves
**Kind**: instance method of [TelegramBot](#TelegramBot)
**Returns**: Promise
- promise Promise, of last polling request
+
+
+### telegramBot.isPolling() ⇒ Boolean
+Return true if polling. Otherwise, false.
+
+**Kind**: instance method of [TelegramBot](#TelegramBot)
+
+
+### telegramBot.openWebHook()
+Open webhook
+
+**Kind**: instance method of [TelegramBot](#TelegramBot)
+
+
+### telegramBot.closeWebHook() ⇒ Promise
+Close webhook after closing all current connections
+
+**Kind**: instance method of [TelegramBot](#TelegramBot)
+**Returns**: Promise
- promise
+
+
+### telegramBot.hasOpenWebHook() ⇒ Boolean
+Return true if using webhook and it is open i.e. accepts connections.
+Otherwise, false.
+
+**Kind**: instance method of [TelegramBot](#TelegramBot)
### telegramBot.getMe() ⇒ Promise
@@ -172,6 +214,20 @@ Use this method to receive incoming updates using long polling
| [limit] | Number
| String
| Limits the number of updates to be retrieved. |
| [offset] | Number
| String
| Identifier of the first update to be returned. |
+
+
+### telegramBot.processUpdate(update)
+Process an update; emitting the proper events and executing regexp
+callbacks. This method is useful should you be using a different
+way to fetch updates, other than those provided by TelegramBot.
+
+**Kind**: instance method of [TelegramBot](#TelegramBot)
+**See**: https://core.telegram.org/bots/api#update
+
+| Param | Type |
+| --- | --- |
+| update | Object
|
+
### telegramBot.sendMessage(chatId, text, [options]) ⇒ Promise
diff --git a/package.json b/package.json
index c314626..8a10d0a 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,13 @@
"bot"
],
"scripts": {
- "prepublish": "babel -d ./lib src",
- "test": "istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec --timeout 10000",
+ "build": "babel -d ./lib src",
+ "prepublish": "npm run build",
+ "test": "istanbul cover ./node_modules/mocha/bin/_mocha",
"prepublish:test": "npm run prepublish && npm run test",
"gen-doc": "jsdoc2md --src src/telegram.js -t README.hbs > README.md",
- "eslint": "eslint ./src"
+ "eslint": "eslint ./src ./test",
+ "pretest": "npm run eslint && npm run build"
},
"author": "Yago Pérez ",
"license": "MIT",
@@ -53,7 +55,8 @@
"istanbul": "^1.1.0-alpha.1",
"jsdoc-to-markdown": "^1.3.3",
"mocha": "^2.4.5",
- "mocha-lcov-reporter": "^1.2.0"
+ "mocha-lcov-reporter": "^1.2.0",
+ "node-static": "^0.7.9"
},
"repository": {
"type": "git",
diff --git a/src/telegram.js b/src/telegram.js
index 1f65f0c..8d9be09 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,149 +41,75 @@ 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} [options.polling.autoStart=true] Start polling immediately
* @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.webHook.autoOpen=true] Open webHook immediately
* @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 {String} [options.baseApiUrl=https://api.telegram.org] API Base URl; useful for proxying and testing
* @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.options.baseApiUrl = options.baseApiUrl || 'https://api.telegram.org';
+ this._textRegexpCallbacks = [];
+ this._onReplyToMessages = [];
if (options.polling) {
- this.initPolling();
+ const autoStart = options.polling.autoStart;
+ if (typeof autoStart === 'undefined' || autoStart === true) {
+ this.initPolling();
+ }
}
if (options.webHook) {
- this._WebHook = new TelegramBotWebHook(token, options.webHook, this.processUpdate.bind(this));
+ const autoOpen = options.webHook.autoOpen;
+ if (typeof autoOpen === 'undefined' || autoOpen === true) {
+ this.openWebHook();
+ }
}
}
- 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
+ * 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
*/
- stopPolling() {
- if (this._polling) {
- return this._polling.stopPolling();
- }
- return Promise.resolve();
- }
-
- processUpdate(update) {
- debug('Process Update %j', update);
- const message = update.message;
- const editedMessage = update.edited_message;
- const channelPost = update.channel_post;
- const editedChannelPost = update.edited_channel_post;
- const inlineQuery = update.inline_query;
- const chosenInlineResult = update.chosen_inline_result;
- const callbackQuery = update.callback_query;
-
- if (message) {
- debug('Process Update message %j', message);
- this.emit('message', message);
- const processMessageType = messageType => {
- if (message[messageType]) {
- debug('Emtting %s: %j', messageType, message);
- this.emit(messageType, message);
- }
- };
- TelegramBot.messageTypes.forEach(processMessageType);
- if (message.text) {
- debug('Text message');
- 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 (message.reply_to_message) {
- // Only callbacks waiting for this message
- this.onReplyToMessages.forEach(reply => {
- // Message from the same chat
- if (reply.chatId === message.chat.id) {
- // Responding to that message
- if (reply.messageId === message.reply_to_message.message_id) {
- // Resolve the promise
- reply.callback(message);
- }
- }
- });
- }
- } else if (editedMessage) {
- debug('Process Update edited_message %j', editedMessage);
- this.emit('edited_message', editedMessage);
- if (editedMessage.text) {
- this.emit('edited_message_text', editedMessage);
- }
- if (editedMessage.caption) {
- this.emit('edited_message_caption', editedMessage);
- }
- } else if (channelPost) {
- debug('Process Update channel_post %j', channelPost);
- this.emit('channel_post', channelPost);
- } else if (editedChannelPost) {
- debug('Process Update edited_channel_post %j', editedChannelPost);
- this.emit('edited_channel_post', editedChannelPost);
- if (editedChannelPost.text) {
- this.emit('edited_channel_post_text', editedChannelPost);
- }
- 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);
- } else if (chosenInlineResult) {
- debug('Process Update chosen_inline_result %j', chosenInlineResult);
- this.emit('chosen_inline_result', chosenInlineResult);
- } else if (callbackQuery) {
- debug('Process Update callback_query %j', callbackQuery);
- this.emit('callback_query', callbackQuery);
- }
- }
-
- // 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)}`);
- }
+ _buildURL(_path) {
+ return `${this.options.baseApiUrl}/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 +125,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 +134,148 @@ 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();
+ }
+
+ /**
+ * Return true if polling. Otherwise, false.
+ * @return {Boolean}
+ */
+ isPolling() {
+ return !!this._polling;
+ }
+
+ /**
+ * Open webhook
+ */
+ openWebHook() {
+ if (this._webHook) {
+ return;
+ }
+ this._webHook = new TelegramBotWebHook(this.token, this.options.webHook, this.processUpdate.bind(this));
+ }
+
+ /**
+ * Close webhook after closing all current connections
+ * @return {Promise} promise
+ */
+ closeWebHook() {
+ if (!this._webHook) {
+ return Promise.resolve();
+ }
+ const webHook = this._webHook;
+ delete this._webHook;
+ return webHook.close();
+ }
+
+ /**
+ * Return true if using webhook and it is open i.e. accepts connections.
+ * Otherwise, false.
+ * @return {Boolean}
+ */
+ hasOpenWebHook() {
+ return !!this._webHook;
}
/**
@@ -284,6 +333,93 @@ class TelegramBot extends EventEmitter {
return this._request('getUpdates', { form });
}
+ /**
+ * Process an update; emitting the proper events and executing regexp
+ * callbacks. This method is useful should you be using a different
+ * way to fetch updates, other than those provided by TelegramBot.
+ * @param {Object} update
+ * @see https://core.telegram.org/bots/api#update
+ */
+ processUpdate(update) {
+ debug('Process Update %j', update);
+ const message = update.message;
+ const editedMessage = update.edited_message;
+ const channelPost = update.channel_post;
+ const editedChannelPost = update.edited_channel_post;
+ const inlineQuery = update.inline_query;
+ const chosenInlineResult = update.chosen_inline_result;
+ const callbackQuery = update.callback_query;
+
+ if (message) {
+ debug('Process Update message %j', message);
+ this.emit('message', message);
+ const processMessageType = messageType => {
+ if (message[messageType]) {
+ debug('Emitting %s: %j', messageType, message);
+ this.emit(messageType, message);
+ }
+ };
+ TelegramBot.messageTypes.forEach(processMessageType);
+ if (message.text) {
+ debug('Text message');
+ this._textRegexpCallbacks.some(reg => {
+ debug('Matching %s with %s', message.text, reg.regexp);
+ const result = reg.regexp.exec(message.text);
+ 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 => {
+ // Message from the same chat
+ if (reply.chatId === message.chat.id) {
+ // Responding to that message
+ if (reply.messageId === message.reply_to_message.message_id) {
+ // Resolve the promise
+ reply.callback(message);
+ }
+ }
+ });
+ }
+ } else if (editedMessage) {
+ debug('Process Update edited_message %j', editedMessage);
+ this.emit('edited_message', editedMessage);
+ if (editedMessage.text) {
+ this.emit('edited_message_text', editedMessage);
+ }
+ if (editedMessage.caption) {
+ this.emit('edited_message_caption', editedMessage);
+ }
+ } else if (channelPost) {
+ debug('Process Update channel_post %j', channelPost);
+ this.emit('channel_post', channelPost);
+ } else if (editedChannelPost) {
+ debug('Process Update edited_channel_post %j', editedChannelPost);
+ this.emit('edited_channel_post', editedChannelPost);
+ if (editedChannelPost.text) {
+ this.emit('edited_channel_post_text', editedChannelPost);
+ }
+ 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);
+ } else if (chosenInlineResult) {
+ debug('Process Update chosen_inline_result %j', chosenInlineResult);
+ this.emit('chosen_inline_result', chosenInlineResult);
+ } else if (callbackQuery) {
+ debug('Process Update callback_query %j', callbackQuery);
+ this.emit('callback_query', callbackQuery);
+ }
+ }
+
/**
* Send text message.
* @param {Number|String} chatId Unique identifier for the message recipient
@@ -330,49 +466,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
@@ -737,11 +830,7 @@ class TelegramBot extends EventEmitter {
*/
getFileLink(fileId) {
return this.getFile(fileId)
- .then(resp => URL.format({
- protocol: 'https',
- host: 'api.telegram.org',
- pathname: `/file/bot${this.token}/${resp.file_path}`
- }));
+ .then(resp => `${this.options.baseApiUrl}/file/bot${this.token}/${resp.file_path}`);
}
/**
@@ -776,7 +865,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 +876,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..70b2516 100644
--- a/src/telegramPolling.js
+++ b/src/telegramPolling.js
@@ -1,48 +1,71 @@
-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._pollingTimeout = null;
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;
+ clearTimeout(this._pollingTimeout);
+ 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 +74,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);
+ this._pollingTimeout = 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..87feed6 100644
--- a/src/telegramWebHook.js
+++ b/src/telegramWebHook.js
@@ -3,19 +3,30 @@ const https = require('https');
const http = require('http');
const fs = require('fs');
const bl = require('bl');
+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
+ */
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 +55,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 +72,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();
@@ -80,6 +99,19 @@ class TelegramBotWebHook {
}
}
+ /**
+ * Close the webHook
+ * @return {Promise}
+ */
+ close() {
+ const self = this;
+ return new Promise(function closePromise(resolve, reject) {
+ self._webServer.close(function closeCb(error) {
+ if (error) return reject(error);
+ return resolve();
+ });
+ });
+ }
}
module.exports = TelegramBotWebHook;
diff --git a/test/README.md b/test/README.md
index 4e8f583..72321d3 100644
--- a/test/README.md
+++ b/test/README.md
@@ -1,10 +1,18 @@
Running the tests:
```bash
+# Token to be used
export TEST_TELEGRAM_TOKEN=
+
# User Id which you want to send the messages.
export TEST_USER_ID=
+
# Group Id which to use in some of the tests, e.g. for TelegramBot#getChat()
export TEST_GROUP_ID=
+
+# Game short name which to use in some of the tests, e.g. TelegramBot#sendGame()
+export TEST_GAME_SHORT_NAME=
+
+# Run tests
npm run test
```
diff --git a/test/data/audio.mp3 b/test/data/audio.mp3
new file mode 100644
index 0000000..11cd785
Binary files /dev/null and b/test/data/audio.mp3 differ
diff --git a/test/bot.gif b/test/data/photo.gif
similarity index 100%
rename from test/bot.gif
rename to test/data/photo.gif
diff --git a/test/sticker.webp b/test/data/sticker.webp
similarity index 100%
rename from test/sticker.webp
rename to test/data/sticker.webp
diff --git a/test/data/video.mp4 b/test/data/video.mp4
new file mode 100644
index 0000000..8b2dbd8
Binary files /dev/null and b/test/data/video.mp4 differ
diff --git a/test/data/voice.ogg b/test/data/voice.ogg
new file mode 100644
index 0000000..0d7f43e
Binary files /dev/null and b/test/data/voice.ogg differ
diff --git a/test/index.js b/test/index.js
deleted file mode 100644
index 7f189aa..0000000
--- a/test/index.js
+++ /dev/null
@@ -1,673 +0,0 @@
-const TelegramPolling = require('../lib/telegramPolling');
-const Telegram = require('../lib/telegram');
-const Promise = require('bluebird');
-const request = require('request-promise');
-const assert = require('assert');
-const fs = require('fs');
-const is = require('is');
-
-process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
-const TOKEN = process.env.TEST_TELEGRAM_TOKEN;
-if (!TOKEN) {
- throw new Error('Bot token not provided');
-}
-
-// Telegram service if not User Id
-const USERID = process.env.TEST_USER_ID || 777000;
-const GROUPID = process.env.TEST_GROUP_ID || -1001075450562;
-
-describe('Telegram', function telegramSuite() {
- describe('#setWebHook', function setWebHookSuite() {
- it('should set a webHook', function test() {
- const bot = new Telegram(TOKEN);
- // Google IP ¯\_(ツ)_/¯
- return bot
- .setWebHook('216.58.210.174')
- .then(resp => {
- assert.equal(resp, true);
- });
- });
-
- it('should set a webHook with certificate', function test() {
- const bot = new Telegram(TOKEN);
- const cert = `${__dirname}/../examples/crt.pem`;
- return bot
- .setWebHook('216.58.210.174', cert)
- .then(resp => {
- assert.equal(resp, true);
- });
- });
-
- it('should delete the webHook', function test() {
- const bot = new Telegram(TOKEN);
- return bot
- .setWebHook('')
- .then(resp => {
- assert.equal(resp, true);
- });
- });
- });
-
- describe('#WebHook', function WebHookSuite() {
- it('should reject request if same token not provided', function test() {
- const bot = new Telegram(TOKEN, { webHook: true });
-
- return request({
- url: 'http://localhost:8443/NOT_REAL_TOKEN',
- method: 'POST',
- simple: false,
- resolveWithFullResponse: true
- }).then(response => {
- assert.notEqual(response.statusCode, 200);
- bot._WebHook._webServer.close();
- });
- });
-
- it('should reject request if authorized but not a POST', function test() {
- const bot = new Telegram(TOKEN, { webHook: true });
- return request({
- url: `http://localhost:8443/bot${TOKEN}`,
- method: 'GET',
- simple: false,
- resolveWithFullResponse: true
- })
- .then(response => {
- assert.notEqual(response.statusCode, 200);
- bot._WebHook._webServer.close();
- });
- });
-
- it('should emit a `message` on HTTP WebHook', function test(done) {
- const bot = new Telegram(TOKEN, { webHook: true });
- bot.on('message', () => {
- bot._WebHook._webServer.close();
- done();
- });
-
- const url = `http://localhost:8443/bot${TOKEN}`;
- request({
- url,
- method: 'POST',
- json: true,
- headers: {
- 'content-type': 'application/json',
- },
- body: { update_id: 0, message: { text: 'test' } }
- });
- });
-
- it('should emit a `message` on HTTPS WebHook', function test(done) {
- const opts = {
- webHook: {
- port: 8443,
- key: `${__dirname}/../examples/key.pem`,
- cert: `${__dirname}/../examples/crt.pem`
- }
- };
- const bot = new Telegram(TOKEN, opts);
- bot.on('message', () => {
- bot._WebHook._webServer.close();
- done();
- });
- const url = `https://localhost:8443/bot${TOKEN}`;
- request({
- url,
- method: 'POST',
- json: true,
- headers: {
- 'content-type': 'application/json',
- },
- rejectUnhauthorized: false,
- body: { update_id: 0, message: { text: 'test' } }
- });
- });
- });
-
- describe('#getMe', function getMeSuite() {
- it('should return an User object', function test() {
- const bot = new Telegram(TOKEN);
- return bot.getMe().then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#getChat', function getChatSuite() {
- it('should return a Chat object', function test() {
- const bot = new Telegram(TOKEN);
- return bot.getChat(USERID).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#getChatAdministrators', function getChatAdministratorsSuite() {
- it('should return an Array', function test() {
- const bot = new Telegram(TOKEN);
- return bot.getChatAdministrators(GROUPID).then(resp => {
- assert.ok(Array.isArray(resp));
- });
- });
- });
-
- describe('#getChatMembersCount', function getChatMembersCountSuite() {
- it('should return an Integer', function test() {
- const bot = new Telegram(TOKEN);
- return bot.getChatMembersCount(GROUPID).then(resp => {
- assert.ok(Number.isInteger(resp));
- });
- });
- });
-
- describe('#getChatMember', function getChatMemberSuite() {
- it('should return a ChatMember', function test() {
- const bot = new Telegram(TOKEN);
- return bot.getChatMember(GROUPID, USERID).then(resp => {
- assert.ok(is.object(resp.user));
- assert.ok(is.string(resp.status));
- });
- });
- });
-
- describe('#getUpdates', function getUpdatesSuite() {
- it('should return an Array', function test() {
- const bot = new Telegram(TOKEN);
- return bot.getUpdates().then(resp => {
- assert.equal(Array.isArray(resp), true);
- });
- });
- });
-
- describe('#sendMessage', function sendMessageSuite() {
- it('should send a message', function test() {
- const bot = new Telegram(TOKEN);
- return bot.sendMessage(USERID, 'test').then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#forwardMessage', function forwardMessageSuite() {
- it('should forward a message', function test() {
- const bot = new Telegram(TOKEN);
- return bot.sendMessage(USERID, 'test').then(resp => {
- const messageId = resp.message_id;
- return bot.forwardMessage(USERID, USERID, messageId)
- .then(forwarded => {
- assert.ok(is.object(forwarded));
- });
- });
- });
- });
-
- describe('#_formatSendData', function _formatSendData() {
- it('should handle buffer path from fs.readStream', function test() {
- const bot = new Telegram(TOKEN);
- let photo;
- try {
- photo = fs.createReadStream(Buffer.from(`${__dirname}/bot.gif`));
- } catch(ex) {
- // Older Node.js versions do not support passing a Buffer
- // representation of the path to fs.createReadStream()
- if (ex instanceof TypeError) return;
- }
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#sendPhoto', function sendPhotoSuite() {
- let photoId;
- it('should send a photo from file', function test() {
- const bot = new Telegram(TOKEN);
- const photo = `${__dirname}/bot.gif`;
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- photoId = resp.photo[0].file_id;
- });
- });
-
- it('should send a photo from id', function test() {
- const bot = new Telegram(TOKEN);
- // Send the same photo as before
- const photo = photoId;
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a photo from fs.readStream', function test() {
- const bot = new Telegram(TOKEN);
- const photo = fs.createReadStream(`${__dirname}/bot.gif`);
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a photo from request Stream', function test() {
- const bot = new Telegram(TOKEN);
- const photo = request('https://telegram.org/img/t_logo.png');
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a photo from a Buffer', function test() {
- const bot = new Telegram(TOKEN);
- const photo = fs.readFileSync(`${__dirname}/bot.gif`);
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a photo along with reply_markup', function test() {
- const bot = new Telegram(TOKEN);
- const photo = fs.readFileSync(`${__dirname}/bot.gif`);
- return bot.sendPhoto(USERID, photo, {
- reply_markup: {
- hide_keyboard: true
- }
- }).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#sendChatAction', function sendChatActionSuite() {
- it('should send a chat action', function test() {
- const bot = new Telegram(TOKEN);
- const action = 'typing';
- return bot.sendChatAction(USERID, action).then(resp => {
- assert.equal(resp, true);
- });
- });
- });
-
- describe('#editMessageText', function editMessageTextSuite() {
- it('should edit a message sent by the bot', function test() {
- const bot = new Telegram(TOKEN);
- return bot.sendMessage(USERID, 'test').then(resp => {
- assert.equal(resp.text, 'test');
- const opts = {
- chat_id: USERID,
- message_id: resp.message_id
- };
- return bot.editMessageText('edit test', opts).then(msg => {
- assert.equal(msg.text, 'edit test');
- });
- });
- });
- });
-
- describe('#editMessageCaption', function editMessageCaptionSuite() {
- it('should edit a caption sent by the bot', function test() {
- const bot = new Telegram(TOKEN);
- const photo = `${__dirname}/bot.gif`;
- const options = { caption: 'test caption' };
- return bot.sendPhoto(USERID, photo, options).then(resp => {
- assert.equal(resp.caption, 'test caption');
- const opts = {
- chat_id: USERID,
- message_id: resp.message_id
- };
- return bot.editMessageCaption('new test caption', opts).then(msg => {
- assert.equal(msg.caption, 'new test caption');
- });
- });
- });
- });
-
- describe('#editMessageReplyMarkup', function editMessageReplyMarkupSuite() {
- it('should edit previously-set reply markup', function test() {
- const bot = new Telegram(TOKEN);
- return bot.sendMessage(USERID, 'test').then(resp => {
- const replyMarkup = JSON.stringify({
- inline_keyboard: [[{
- text: 'Test button',
- callback_data: 'test'
- }]]
- });
- const opts = {
- chat_id: USERID,
- message_id: resp.message_id
- };
- return bot.editMessageReplyMarkup(replyMarkup, opts).then(msg => {
- // Keyboard markup is not returned, do a simple object check
- assert.ok(is.object(msg));
- });
- });
- });
- });
-
- describe('#sendAudio', function sendAudioSuite() {
- it('should send an OGG audio', function test() {
- const bot = new Telegram(TOKEN);
- const audio = request('https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg');
- return bot.sendAudio(USERID, audio).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#sendDocument', function sendDocumentSuite() {
- let documentId;
- it('should send a document from file', function test() {
- const bot = new Telegram(TOKEN);
- const document = `${__dirname}/bot.gif`;
- return bot.sendDocument(USERID, document).then(resp => {
- assert.ok(is.object(resp));
- documentId = resp.document.file_id;
- });
- });
-
- it('should send a document from id', function test() {
- const bot = new Telegram(TOKEN);
- // Send the same photo as before
- const document = documentId;
- return bot.sendDocument(USERID, document).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a document from fs.readStream', function test() {
- const bot = new Telegram(TOKEN);
- const document = fs.createReadStream(`${__dirname}/bot.gif`);
- return bot.sendDocument(USERID, document).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a document from request Stream', function test() {
- const bot = new Telegram(TOKEN);
- const document = request('https://telegram.org/img/t_logo.png');
- return bot.sendDocument(USERID, document).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a document from a Buffer', function test() {
- const bot = new Telegram(TOKEN);
- const document = fs.readFileSync(`${__dirname}/bot.gif`);
- return bot.sendDocument(USERID, document).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#sendSticker', function sendStickerSuite() {
- let stickerId;
- it('should send a sticker from file', function test() {
- const bot = new Telegram(TOKEN);
- const sticker = `${__dirname}/sticker.webp`;
- return bot.sendSticker(USERID, sticker).then(resp => {
- assert.ok(is.object(resp));
- stickerId = resp.sticker.file_id;
- });
- });
-
- it('should send a sticker from id', function test() {
- const bot = new Telegram(TOKEN);
- // Send the same photo as before
- return bot.sendSticker(USERID, stickerId).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a sticker from fs.readStream', function test() {
- const bot = new Telegram(TOKEN);
- const sticker = fs.createReadStream(`${__dirname}/sticker.webp`);
- return bot.sendSticker(USERID, sticker).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a sticker from request Stream', function test() {
- const bot = new Telegram(TOKEN);
- const sticker = request('https://www.gstatic.com/webp/gallery3/1_webp_ll.webp');
- return bot.sendSticker(USERID, sticker).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a sticker from a Buffer', function test() {
- const bot = new Telegram(TOKEN);
- const sticker = fs.readFileSync(`${__dirname}/sticker.webp`);
- return bot.sendDocument(USERID, sticker).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#sendVideo', function sendVideoSuite() {
- let videoId;
- it('should send a video from file', function test() {
- const bot = new Telegram(TOKEN);
- const video = `${__dirname}/video.mp4`;
- return bot.sendVideo(USERID, video).then(resp => {
- assert.ok(is.object(resp));
- videoId = resp.video.file_id;
- });
- });
-
- it('should send a video from id', function test() {
- const bot = new Telegram(TOKEN);
- // Send the same photo as before
- return bot.sendVideo(USERID, videoId).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a video from fs.readStream', function test() {
- const bot = new Telegram(TOKEN);
- const video = fs.createReadStream(`${__dirname}/video.mp4`);
- return bot.sendVideo(USERID, video).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a video from request Stream', function test() {
- const bot = new Telegram(TOKEN);
- const sticker = request('http://techslides.com/demos/sample-videos/small.mp4');
- return bot.sendVideo(USERID, sticker).then(resp => {
- assert.ok(is.object(resp));
- });
- });
-
- it('should send a video from a Buffer', function test() {
- const bot = new Telegram(TOKEN);
- const video = fs.readFileSync(`${__dirname}/video.mp4`);
- return bot.sendVideo(USERID, video).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#sendVoice', function sendVoiceSuite() {
- it('should send an OGG audio as voice', function test() {
- const bot = new Telegram(TOKEN);
- const voice = request('https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg');
- return bot.sendVoice(USERID, voice).then(resp => {
- assert.ok(is.object(resp));
- });
- });
- });
-
- describe('#getUserProfilePhotos', function getUserProfilePhotosSuite() {
- it('should get user profile photos', function test() {
- const bot = new Telegram(TOKEN);
- return bot.getUserProfilePhotos(USERID).then(resp => {
- assert.ok(is.object(resp));
- assert.ok(is.number(resp.total_count));
- assert.ok(is.array(resp.photos));
- });
- });
- });
-
- describe('#sendLocation', function sendLocationSuite() {
- it('should send a location', function test() {
- const bot = new Telegram(TOKEN);
- const lat = 47.5351072;
- const long = -52.7508537;
- return bot.sendLocation(USERID, lat, long).then(resp => {
- assert.ok(is.object(resp));
- assert.ok(is.object(resp.location));
- assert.ok(is.number(resp.location.latitude));
- assert.ok(is.number(resp.location.longitude));
- });
- });
- });
-
- describe('#sendVenue', function sendVenueSuite() {
- it('should send a venue', function test() {
- const bot = new Telegram(TOKEN);
- const lat = 47.5351072;
- const long = -52.7508537;
- const title = `The Village Shopping Centre`;
- const address = `430 Topsail Rd,St. John's, NL A1E 4N1, Canada`;
- return bot.sendVenue(USERID, lat, long, title, address).then(resp => {
- assert.ok(is.object(resp));
- assert.ok(is.object(resp.venue));
- assert.ok(is.object(resp.venue.location));
- assert.ok(is.number(resp.venue.location.latitude));
- assert.ok(is.number(resp.venue.location.longitude));
- assert.ok(is.string(resp.venue.title));
- assert.ok(is.string(resp.venue.address));
- });
- });
- });
-
- describe('#getFile', function getFileSuite() {
- let fileId;
-
- // To get a file we have to send any file first
- it('should send a photo from file', function test() {
- const bot = new Telegram(TOKEN);
- const photo = `${__dirname}/bot.gif`;
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- fileId = resp.photo[0].file_id;
- });
- });
-
- it('should get a file', function test() {
- const bot = new Telegram(TOKEN);
-
- return bot
- .getFile(fileId)
- .then(resp => {
- assert.ok(is.object(resp));
- assert.ok(is.string(resp.file_path));
- });
- });
- });
-
- describe('#getFileLink', function getFileLinkSuite() {
- let fileId;
-
- // To get a file we have to send any file first
- it('should send a photo from file', function test() {
- const bot = new Telegram(TOKEN);
- const photo = `${__dirname}/bot.gif`;
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- fileId = resp.photo[0].file_id;
- });
- });
-
- it('should get a file link', function test() {
- const bot = new Telegram(TOKEN);
-
- return bot
- .getFileLink(fileId)
- .then(fileURI => {
- assert.ok(is.string(fileURI));
- assert.equal(fileURI.indexOf('https'), 0);
- // TODO: validate URL with some library or regexp
- });
- });
- });
-
- describe('#downloadFile', function downloadFileSuite() {
- const downloadPath = __dirname;
-
- it('should download a file', function test() {
- const bot = new Telegram(TOKEN);
- const photo = `${__dirname}/bot.gif`;
-
- // Send a file to get the ID
- return bot.sendPhoto(USERID, photo).then(resp => {
- assert.ok(is.object(resp));
- const fileId = resp.photo[0].file_id;
-
- return bot
- .downloadFile(fileId, downloadPath)
- .then(filePath => {
- assert.ok(is.string(filePath));
- assert.ok(fs.existsSync(filePath));
- fs.unlinkSync(filePath); // Delete file after test
- });
- });
- });
- });
-
- it('should call `onText` callback on match', function test(done) {
- const bot = new Telegram(TOKEN, { webHook: true });
- bot.onText(/\/echo (.+)/, (msg, match) => {
- bot._WebHook._webServer.close();
- assert.equal(match[1], 'ECHO ALOHA');
- done();
- });
- const url = `http://localhost:8443/bot${TOKEN}`;
- request({
- url,
- method: 'POST',
- json: true,
- headers: {
- 'content-type': 'application/json',
- },
- body: { update_id: 0, message: { text: '/echo ECHO ALOHA' } }
- });
- });
-}); // End Telegram
-
-describe('#TelegramBotPolling', function TelegramBotPollingSuite() {
-
- it('should call the callback on polling', function test(done) {
- const opts = { interval: 100, timeout: 1 };
- const polling = new TelegramPolling(TOKEN, opts, (msg) => {
- if (msg.update_id === 10) {
- polling.stopPolling().then(() => {
- done();
- });
- }
- });
- // The second time _getUpdates is called it will return a message
- // Really dirty but it works
- polling._getUpdates = () => {
- return new Promise.resolve([{ update_id: 10, message: {} }]);
- };
- });
-
- describe('#stopPolling', function stopPollingSuite() {
- it('should stop polling after last poll request', function test(done) {
- const opts = { interval: 200, timeout: 0.5 };
- const polling = new TelegramPolling(TOKEN, opts, (msg) => {
- // error if message received as only one poll will complete and there should be no more because of stopPolling
- done(msg);
- });
- polling.stopPolling()
- .then(() => {
- setInterval(() => {
- done();
- }, 1000);
- }).catch(done);
- // The second time _getUpdates is called it will return a message
- // Really dirty but it works
- polling._getUpdates = () => {
- return new Promise.resolve([{ update_id: 11, message: {} }]);
- };
- });
- });
-
-});
diff --git a/test/mocha.opts b/test/mocha.opts
index 7903b88..c7f44ab 100644
--- a/test/mocha.opts
+++ b/test/mocha.opts
@@ -1 +1,3 @@
---require babel-register
\ No newline at end of file
+--reporter spec
+--require babel-register
+--timeout 30000
diff --git a/test/telegram.js b/test/telegram.js
new file mode 100644
index 0000000..4f0628a
--- /dev/null
+++ b/test/telegram.js
@@ -0,0 +1,878 @@
+const Telegram = require('../lib/telegram');
+const Promise = require('bluebird');
+const request = require('request-promise');
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const is = require('is');
+const utils = require('./utils');
+
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+const TOKEN = process.env.TEST_TELEGRAM_TOKEN;
+if (!TOKEN) {
+ throw new Error('Bot token not provided');
+}
+
+// Telegram service if not User Id
+const USERID = process.env.TEST_USER_ID || 777000;
+const GROUPID = process.env.TEST_GROUP_ID || -1001075450562;
+const GAME_SHORT_NAME = process.env.TEST_GAME_SHORT_NAME || 'medusalab_test';
+const timeout = 60 * 1000;
+const staticPort = 8091;
+const pollingPort = 8092;
+const webHookPort = 8093;
+const pollingPort2 = 8094;
+const webHookPort2 = 8095;
+const staticUrl = `http://127.0.0.1:${staticPort}`;
+let FILE_ID;
+let GAME_CHAT_ID;
+let GAME_MSG_ID;
+
+before(function beforeAll() {
+ utils.startStaticServer(staticPort);
+ return utils.startMockServer(pollingPort)
+ .then(() => {
+ return utils.startMockServer(pollingPort2);
+ });
+});
+
+describe('Telegram', function telegramSuite() {
+ let bot;
+ let testbot;
+ let botPolling;
+ let botWebHook;
+
+ before(function beforeAll() {
+ this.timeout(timeout);
+ bot = new Telegram(TOKEN);
+ testbot = new Telegram(TOKEN, {
+ baseApiUrl: `http://127.0.0.1:${pollingPort}`,
+ polling: {
+ autoStart: false,
+ },
+ webHook: {
+ autoOpen: false,
+ port: webHookPort,
+ },
+ });
+ botPolling = new Telegram(TOKEN, {
+ baseApiUrl: `http://127.0.0.1:${pollingPort2}`,
+ polling: true,
+ });
+ botWebHook = new Telegram(TOKEN, {
+ webHook: {
+ port: webHookPort2,
+ },
+ });
+
+ utils.handleRatelimit(bot, 'sendPhoto', this);
+ utils.handleRatelimit(bot, 'sendMessage', this);
+ utils.handleRatelimit(bot, 'sendGame', this);
+ return bot.sendPhoto(USERID, `${__dirname}/data/photo.gif`).then(resp => {
+ FILE_ID = resp.photo[0].file_id;
+ return bot.sendMessage(USERID, 'chat');
+ }).then(resp => {
+ GAME_CHAT_ID = resp.chat.id;
+ return bot.sendGame(USERID, GAME_SHORT_NAME);
+ }).then(resp => {
+ GAME_MSG_ID = resp.message_id;
+ });
+ });
+
+ it('automatically starts polling', function test() {
+ assert.equal(botPolling.isPolling(), true);
+ return utils.isPollingMockServer(pollingPort2);
+ });
+
+ it('automatically opens webhook', function test() {
+ assert.equal(botWebHook.hasOpenWebHook(), true);
+ return utils.hasOpenWebHook(webHookPort2);
+ });
+
+ it('does not automatically poll if "autoStart" is false', function test() {
+ assert.equal(testbot.isPolling(), false);
+ return utils.isPollingMockServer(pollingPort, true);
+ });
+
+ it('does not automatically open webhook if "autoOpen" is false', function test() {
+ assert.equal(testbot.hasOpenWebHook(), false);
+ return utils.hasOpenWebHook(webHookPort, true);
+ });
+
+ describe('Events', function eventsSuite() {
+ it('(polling) emits "message" on receiving message', function test(done) {
+ botPolling.once('message', () => {
+ return done();
+ });
+ });
+ it('(webhook) emits "message" on receiving message', function test(done) {
+ botWebHook.once('message', () => {
+ return done();
+ });
+ utils.sendWebHookMessage(webHookPort2, TOKEN);
+ });
+ });
+
+ describe('WebHook', function webHookSuite() {
+ it('returns 401 error if token is wrong', function test(done) {
+ utils.sendWebHookMessage(webHookPort2, 'wrong-token').catch(resp => {
+ assert.equal(resp.statusCode, 401);
+ return done();
+ });
+ });
+ it('only accepts POST method', function test() {
+ const methods = ['GET', 'PUT', 'DELETE', 'OPTIONS'];
+ return Promise.each(methods, (method) => {
+ return utils.sendWebHookMessage(webHookPort2, TOKEN, {
+ method,
+ }).then(() => {
+ throw new Error(`expected error with webhook ${method} request`);
+ }).catch(resp => {
+ if (!resp.statusCode) throw resp;
+ if (resp.statusCode !== 418) throw new Error(`unexpected error: ${resp.body}`);
+ });
+ }); // Promise.each
+ });
+ });
+
+ describe('#initPolling', function initPollingSuite() {
+ it('initiates polling', function test() {
+ testbot.initPolling();
+ return utils.isPollingMockServer(pollingPort);
+ });
+ });
+
+ describe('#isPolling', function isPollingSuite() {
+ it('returns true if bot is polling', function test() {
+ assert.equal(testbot.isPolling(), true);
+ return utils.isPollingMockServer(pollingPort);
+ });
+ it('returns false if bot is not polling', function test() {
+ return testbot.stopPolling().then(() => {
+ assert.equal(testbot.isPolling(), false);
+ utils.clearPollingCheck(pollingPort);
+ return utils.isPollingMockServer(pollingPort, true);
+ });
+ });
+ after(function after() {
+ return testbot.initPolling();
+ });
+ });
+
+ describe('#stopPolling', function stopPollingSuite() {
+ it('stops polling by bot', function test() {
+ return testbot.stopPolling().then(() => {
+ utils.clearPollingCheck(pollingPort);
+ return utils.isPollingMockServer(pollingPort, true);
+ });
+ });
+ });
+
+ describe('#openWebHook', function openWebHookSuite() {
+ it('opens webhook', function test() {
+ testbot.openWebHook();
+ return utils.hasOpenWebHook(webHookPort);
+ });
+ });
+
+ describe('#hasOpenWebHook', function hasOpenWebHookSuite() {
+ it('returns true if webhook is opened', function test() {
+ assert.equal(testbot.hasOpenWebHook(), true);
+ return utils.hasOpenWebHook(webHookPort);
+ });
+ it('returns false if webhook is closed', function test() {
+ testbot.closeWebHook().then(() => {
+ assert.equal(testbot.hasOpenWebHook(), false);
+ return utils.hasOpenWebHook(webHookPort, true);
+ });
+ });
+ after(function after() {
+ return testbot.openWebHook();
+ });
+ });
+
+ describe('#closeWebHook', function closeWebHookSuite() {
+ it('closes webhook', function test() {
+ testbot.closeWebHook().then(() => {
+ return utils.hasOpenWebHook(webHookPort, true);
+ });
+ });
+ });
+
+ describe('#getMe', function getMeSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getMe', this);
+ });
+ it('should return an User object', function test() {
+ return bot.getMe().then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.number(resp.id));
+ assert.ok(is.string(resp.first_name));
+ });
+ });
+ });
+
+ 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 => {
+ assert.equal(resp, true);
+ });
+ });
+ it('should set a webHook with certificate', function test() {
+ const cert = `${__dirname}/../examples/crt.pem`;
+ return bot
+ .setWebHook(ip, cert)
+ .then(resp => {
+ assert.equal(resp, true);
+ });
+ });
+ it('should delete the webHook', function test() {
+ return bot
+ .setWebHook('')
+ .then(resp => {
+ assert.equal(resp, true);
+ });
+ });
+ });
+
+ describe('#getUpdates', function getUpdatesSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getUpdates', this);
+ });
+ it('should return an Array', function test() {
+ return bot.getUpdates().then(resp => {
+ assert.equal(Array.isArray(resp), true);
+ });
+ });
+ });
+
+ describe('#sendMessage', function sendMessageSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendMessage', this);
+ });
+ it('should send a message', function test() {
+ return bot.sendMessage(USERID, 'test').then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.number(resp.message_id));
+ });
+ });
+ });
+
+ describe.skip('#answerInlineQuery', function answerInlineQuerySuite() {});
+
+ describe('#forwardMessage', function forwardMessageSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendMessage', this);
+ utils.handleRatelimit(bot, 'forwardMessage', this);
+ });
+ it('should forward a message', function test() {
+ return bot.sendMessage(USERID, 'test').then(resp => {
+ const messageId = resp.message_id;
+ return bot.forwardMessage(USERID, USERID, messageId)
+ .then(forwarded => {
+ assert.ok(is.object(forwarded));
+ assert.ok(is.number(forwarded.message_id));
+ });
+ });
+ });
+ });
+
+ describe('#sendPhoto', function sendPhotoSuite() {
+ let photoId;
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendPhoto', this);
+ });
+ it('should send a photo from file', function test() {
+ const photo = `${__dirname}/data/photo.gif`;
+ return bot.sendPhoto(USERID, photo).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.array(resp.photo));
+ photoId = resp.photo[0].file_id;
+ });
+ });
+ it('should send a photo from id', function test() {
+ // Send the same photo as before
+ const photo = photoId;
+ return bot.sendPhoto(USERID, photo).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.array(resp.photo));
+ });
+ });
+ it('should send a photo from fs.readStream', function test() {
+ const photo = fs.createReadStream(`${__dirname}/data/photo.gif`);
+ return bot.sendPhoto(USERID, photo).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.array(resp.photo));
+ });
+ });
+ it('should send a photo from request Stream', function test() {
+ const photo = request(`${staticUrl}/photo.gif`);
+ return bot.sendPhoto(USERID, photo).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.array(resp.photo));
+ });
+ });
+ it('should send a photo from a Buffer', function test() {
+ const photo = fs.readFileSync(`${__dirname}/data/photo.gif`);
+ return bot.sendPhoto(USERID, photo).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.array(resp.photo));
+ });
+ });
+ });
+
+ describe('#sendAudio', function sendAudioSuite() {
+ let audioId;
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendAudio', this);
+ });
+ it('should send an MP3 audio', function test() {
+ const audio = `${__dirname}/data/audio.mp3`;
+ return bot.sendAudio(USERID, audio).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.audio));
+ audioId = resp.audio.file_id;
+ });
+ });
+ it('should send an audio from id', function test() {
+ // Send the same audio as before
+ const audio = audioId;
+ return bot.sendAudio(USERID, audio).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.audio));
+ });
+ });
+ it('should send an audio from fs.readStream', function test() {
+ const audio = fs.createReadStream(`${__dirname}/data/audio.mp3`);
+ return bot.sendAudio(USERID, audio).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.audio));
+ });
+ });
+ it('should send an audio from request Stream', function test() {
+ const audio = request(`${staticUrl}/audio.mp3`);
+ return bot.sendAudio(USERID, audio).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.audio));
+ });
+ });
+ it('should send an audio from a Buffer', function test() {
+ const audio = fs.readFileSync(`${__dirname}/data/audio.mp3`);
+ return bot.sendAudio(USERID, audio).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.audio));
+ });
+ });
+ });
+
+ describe('#sendDocument', function sendDocumentSuite() {
+ let documentId;
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendDocument', this);
+ });
+ it('should send a document from file', function test() {
+ const document = `${__dirname}/data/photo.gif`;
+ return bot.sendDocument(USERID, document).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.document));
+ documentId = resp.document.file_id;
+ });
+ });
+ it('should send a document from id', function test() {
+ // Send the same document as before
+ const document = documentId;
+ return bot.sendDocument(USERID, document).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.document));
+ });
+ });
+ it('should send a document from fs.readStream', function test() {
+ const document = fs.createReadStream(`${__dirname}/data/photo.gif`);
+ return bot.sendDocument(USERID, document).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.document));
+ });
+ });
+ it('should send a document from request Stream', function test() {
+ const document = request(`${staticUrl}/photo.gif`);
+ return bot.sendDocument(USERID, document).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.document));
+ });
+ });
+ it('should send a document from a Buffer', function test() {
+ const document = fs.readFileSync(`${__dirname}/data/photo.gif`);
+ return bot.sendDocument(USERID, document).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.document));
+ });
+ });
+ });
+
+ describe('#sendSticker', function sendStickerSuite() {
+ let stickerId;
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendSticker', this);
+ });
+ it('should send a sticker from file', function test() {
+ const sticker = `${__dirname}/data/sticker.webp`;
+ return bot.sendSticker(USERID, sticker).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.sticker));
+ stickerId = resp.sticker.file_id;
+ });
+ });
+ it('should send a sticker from id', function test() {
+ // Send the same photo as before
+ return bot.sendSticker(USERID, stickerId).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.sticker));
+ });
+ });
+ it('should send a sticker from fs.readStream', function test() {
+ const sticker = fs.createReadStream(`${__dirname}/data/sticker.webp`);
+ return bot.sendSticker(USERID, sticker).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.sticker));
+ });
+ });
+ it('should send a sticker from request Stream', function test() {
+ const sticker = request(`${staticUrl}/sticker.webp`);
+ return bot.sendSticker(USERID, sticker).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.sticker));
+ });
+ });
+ it('should send a sticker from a Buffer', function test() {
+ const sticker = fs.readFileSync(`${__dirname}/data/sticker.webp`);
+ return bot.sendSticker(USERID, sticker).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.sticker));
+ });
+ });
+ });
+
+ describe('#sendVideo', function sendVideoSuite() {
+ let videoId;
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendVideo', this);
+ });
+ it('should send a video from file', function test() {
+ const video = `${__dirname}/data/video.mp4`;
+ return bot.sendVideo(USERID, video).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.video));
+ videoId = resp.video.file_id;
+ });
+ });
+ it('should send a video from id', function test() {
+ // Send the same video as before
+ return bot.sendVideo(USERID, videoId).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.video));
+ });
+ });
+ it('should send a video from fs.readStream', function test() {
+ const video = fs.createReadStream(`${__dirname}/data/video.mp4`);
+ return bot.sendVideo(USERID, video).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.video));
+ });
+ });
+ it('should send a video from request Stream', function test() {
+ const video = request(`${staticUrl}/video.mp4`);
+ return bot.sendVideo(USERID, video).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.video));
+ });
+ });
+ it('should send a video from a Buffer', function test() {
+ const video = fs.readFileSync(`${__dirname}/data/video.mp4`);
+ return bot.sendVideo(USERID, video).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.video));
+ });
+ });
+ });
+
+ describe('#sendVoice', function sendVoiceSuite() {
+ let voiceId;
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendVoice', this);
+ });
+ it('should send a voice from file', function test() {
+ const voice = `${__dirname}/data/voice.ogg`;
+ return bot.sendVoice(USERID, voice).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.voice));
+ voiceId = resp.voice.file_id;
+ });
+ });
+ it('should send a voice from id', function test() {
+ // Send the same voice as before
+ return bot.sendVoice(USERID, voiceId).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.voice));
+ });
+ });
+ it('should send a voice from fs.readStream', function test() {
+ const voice = fs.createReadStream(`${__dirname}/data/voice.ogg`);
+ return bot.sendVoice(USERID, voice).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.voice));
+ });
+ });
+ it('should send a voice from request Stream', function test() {
+ const voice = request(`${staticUrl}/voice.ogg`);
+ return bot.sendVoice(USERID, voice).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.voice));
+ });
+ });
+ it('should send a voice from a Buffer', function test() {
+ const voice = fs.readFileSync(`${__dirname}/data/voice.ogg`);
+ return bot.sendVoice(USERID, voice).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.voice));
+ });
+ });
+ });
+
+ describe('#sendChatAction', function sendChatActionSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendChatAction', this);
+ });
+ it('should send a chat action', function test() {
+ const action = 'typing';
+ return bot.sendChatAction(USERID, action).then(resp => {
+ assert.equal(resp, true);
+ });
+ });
+ });
+
+ describe.skip('#kickChatMember', function kickChatMemberSuite() {});
+
+ describe.skip('#unbanChatMember', function unbanChatMemberSuite() {});
+
+ describe.skip('#answerCallbackQuery', function answerCallbackQuerySuite() {});
+
+ describe('#editMessageText', function editMessageTextSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendMessage', this);
+ utils.handleRatelimit(bot, 'editMessageText', this);
+ });
+ it('should edit a message sent by the bot', function test() {
+ return bot.sendMessage(USERID, 'test').then(resp => {
+ assert.equal(resp.text, 'test');
+ const opts = {
+ chat_id: USERID,
+ message_id: resp.message_id
+ };
+ return bot.editMessageText('edit test', opts).then(msg => {
+ assert.equal(msg.text, 'edit test');
+ });
+ });
+ });
+ });
+
+ describe('#editMessageCaption', function editMessageCaptionSuite() {
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendPhoto', this);
+ utils.handleRatelimit(bot, 'editMessageCaption', this);
+ });
+ it('should edit a caption sent by the bot', function test() {
+ const photo = `${__dirname}/data/photo.gif`;
+ const options = { caption: 'test caption' };
+ return bot.sendPhoto(USERID, photo, options).then(resp => {
+ assert.equal(resp.caption, 'test caption');
+ const opts = {
+ chat_id: USERID,
+ message_id: resp.message_id
+ };
+ return bot.editMessageCaption('new test caption', opts).then(msg => {
+ assert.equal(msg.caption, 'new test caption');
+ });
+ });
+ });
+ });
+
+ describe('#editMessageReplyMarkup', function editMessageReplyMarkupSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendMessage', this);
+ utils.handleRatelimit(bot, 'editMessageReplyMarkup', this);
+ });
+ it('should edit previously-set reply markup', function test() {
+ return bot.sendMessage(USERID, 'test').then(resp => {
+ const replyMarkup = JSON.stringify({
+ inline_keyboard: [[{
+ text: 'Test button',
+ callback_data: 'test'
+ }]]
+ });
+ const opts = {
+ chat_id: USERID,
+ message_id: resp.message_id
+ };
+ return bot.editMessageReplyMarkup(replyMarkup, opts).then(msg => {
+ // Keyboard markup is not returned, do a simple object check
+ assert.ok(is.object(msg));
+ });
+ });
+ });
+ });
+
+ describe('#getUserProfilePhotos', function getUserProfilePhotosSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getUserProfilePhotos', this);
+ });
+ it('should get user profile photos', function test() {
+ return bot.getUserProfilePhotos(USERID).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.number(resp.total_count));
+ assert.ok(is.array(resp.photos));
+ });
+ });
+ });
+
+ describe('#sendLocation', function sendLocationSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendLocation', this);
+ });
+ it('should send a location', function test() {
+ const lat = 47.5351072;
+ const long = -52.7508537;
+ return bot.sendLocation(USERID, lat, long).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.location));
+ assert.ok(is.number(resp.location.latitude));
+ assert.ok(is.number(resp.location.longitude));
+ });
+ });
+ });
+
+ describe('#sendVenue', function sendVenueSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendVenue', this);
+ });
+ it('should send a venue', function test() {
+ const lat = 47.5351072;
+ const long = -52.7508537;
+ const title = 'The Village Shopping Centre';
+ const address = '430 Topsail Rd,St. John\'s, NL A1E 4N1, Canada';
+ return bot.sendVenue(USERID, lat, long, title, address).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.venue));
+ assert.ok(is.object(resp.venue.location));
+ assert.ok(is.number(resp.venue.location.latitude));
+ assert.ok(is.number(resp.venue.location.longitude));
+ assert.ok(is.string(resp.venue.title));
+ assert.ok(is.string(resp.venue.address));
+ });
+ });
+ });
+
+ // NOTE: We are skipping TelegramBot#sendContact() as the
+ // corresponding rate-limits enforced by the Telegram servers
+ // are too strict! During our initial tests, we were required
+ // to retry after ~72000 secs (1200 mins / 20 hrs).
+ // We surely can NOT wait for that much time during testing
+ // (or in most practical cases for that matter!)
+ describe.skip('#sendContact', function sendContactSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendContact', this);
+ });
+ it('should send a contact', function test() {
+ const phoneNumber = '+1(000)000-000';
+ const firstName = 'John Doe';
+ return bot.sendContact(USERID, phoneNumber, firstName).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.contact));
+ assert.ok(is.string(resp.contact.phone_number));
+ assert.ok(is.string(resp.contact.first_name));
+ });
+ });
+ });
+
+ describe('#getFile', function getFileSuite() {
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'getFile', this);
+ });
+ it('should get a file', function test() {
+ return bot.getFile(FILE_ID)
+ .then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.string(resp.file_path));
+ });
+ });
+ });
+
+ describe('#getFileLink', function getFileLinkSuite() {
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'getFileLink', this);
+ });
+ it('should get a file link', function test() {
+ return bot.getFileLink(FILE_ID)
+ .then(fileURI => {
+ assert.ok(is.string(fileURI));
+ assert.ok(/https?:\/\/.*\/file\/bot.*\/.*/.test(fileURI));
+ });
+ });
+ });
+
+ describe('#downloadFile', function downloadFileSuite() {
+ const downloadPath = os.tmpdir();
+ this.timeout(timeout);
+ before(function before() {
+ utils.handleRatelimit(bot, 'downloadFile', this);
+ });
+ it('should download a file', function test() {
+ return bot.downloadFile(FILE_ID, downloadPath)
+ .then(filePath => {
+ assert.ok(is.string(filePath));
+ assert.equal(path.dirname(filePath), downloadPath);
+ assert.ok(fs.existsSync(filePath));
+ fs.unlinkSync(filePath); // Delete file after test
+ });
+ });
+ });
+
+ describe('#onText', function onTextSuite() {
+ it('should call `onText` callback on match', function test(done) {
+ botWebHook.onText(/\/onText (.+)/, (msg, match) => {
+ assert.equal(match[1], 'ECHO ALOHA');
+ return done();
+ });
+ utils.sendWebHookMessage(webHookPort2, TOKEN, {
+ message: { text: '/onText ECHO ALOHA' },
+ });
+ });
+ });
+
+ describe.skip('#onReplyToMessage', function onReplyToMessageSuite() {});
+
+ describe('#getChat', function getChatSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getChat', this);
+ });
+ it('should return a Chat object', function test() {
+ return bot.getChat(USERID).then(resp => {
+ assert.ok(is.object(resp));
+ });
+ });
+ });
+
+ describe('#getChatAdministrators', function getChatAdministratorsSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getChatAdministrators', this);
+ });
+ it('should return an Array', function test() {
+ return bot.getChatAdministrators(GROUPID).then(resp => {
+ assert.ok(Array.isArray(resp));
+ });
+ });
+ });
+
+ describe('#getChatMembersCount', function getChatMembersCountSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getChatMembersCount', this);
+ });
+ it('should return an Integer', function test() {
+ return bot.getChatMembersCount(GROUPID).then(resp => {
+ assert.ok(Number.isInteger(resp));
+ });
+ });
+ });
+
+ describe('#getChatMember', function getChatMemberSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getChatMember', this);
+ });
+ it('should return a ChatMember', function test() {
+ return bot.getChatMember(GROUPID, USERID).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.user));
+ assert.ok(is.string(resp.status));
+ });
+ });
+ });
+
+ describe.skip('#leaveChat', function leaveChatSuite() {});
+
+ describe('#sendGame', function sendGameSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'sendGame', this);
+ });
+ it('should send a Game', function test() {
+ return bot.sendGame(USERID, GAME_SHORT_NAME).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.object(resp.game));
+ });
+ });
+ });
+
+ describe('#setGameScore', function setGameScoreSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'setGameScore', this);
+ });
+ it('should set GameScore', function test() {
+ const score = Math.floor(Math.random() * 1000);
+ const opts = {
+ chat_id: GAME_CHAT_ID,
+ message_id: GAME_MSG_ID,
+ force: true
+ };
+ return bot.setGameScore(USERID, score, opts).then(resp => {
+ assert.ok(is.object(resp) || is.boolean(resp));
+ });
+ });
+ });
+
+ describe('#getGameHighScores', function getGameHighScoresSuite() {
+ before(function before() {
+ utils.handleRatelimit(bot, 'getGameHighScores', this);
+ });
+ it('should get GameHighScores', function test() {
+ const opts = {
+ chat_id: GAME_CHAT_ID,
+ message_id: GAME_MSG_ID,
+ };
+ return bot.getGameHighScores(USERID, opts).then(resp => {
+ assert.ok(is.array(resp));
+ });
+ });
+ });
+
+ describe('#_formatSendData', function _formatSendDataSuite() {
+ it('should handle buffer path from fs.readStream', function test() {
+ let photo;
+ try {
+ photo = fs.createReadStream(Buffer.from(`${__dirname}/data/photo.gif`));
+ } catch (ex) {
+ // Older Node.js versions do not support passing a Buffer
+ // representation of the path to fs.createReadStream()
+ if (ex instanceof TypeError) return Promise.resolve();
+ }
+ return bot.sendPhoto(USERID, photo).then(resp => {
+ assert.ok(is.object(resp));
+ assert.ok(is.array(resp.photo));
+ });
+ });
+ });
+}); // End Telegram
diff --git a/test/utils.js b/test/utils.js
new file mode 100644
index 0000000..c4b59a9
--- /dev/null
+++ b/test/utils.js
@@ -0,0 +1,190 @@
+/* eslint-disable no-use-before-define */
+exports = module.exports = {
+ /**
+ * Clear polling check, so that 'isPollingMockServer()' returns false
+ * if the bot stopped polling the mock server.
+ * @param {Number} port
+ */
+ clearPollingCheck,
+ /**
+ * Redefine a bot method to allow us to ignore 429 (rate-limit) errors
+ * @param {TelegramBot} bot
+ * @param {String} methodName
+ * @param {Suite} suite From mocha
+ * @return {TelegramBot}
+ */
+ handleRatelimit,
+ /**
+ * Return true if a webhook has been opened at the specified port.
+ * Otherwise throw an error.
+ * @param {Number} port
+ * @param {Boolean} [reverse] Throw error when it should have returned true (and vice versa)
+ * @return {Promise}
+ */
+ hasOpenWebHook,
+ /**
+ * Return true if the mock server is being polled by a bot.
+ * Otherwise throw an error.
+ * @param {Number} port
+ * @param {Boolean} [reverse] Throw error when it should have returned true (and vice versa)
+ * @return {Promise}
+ */
+ isPollingMockServer,
+ /**
+ * 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.message] Message to send. Default to a generic text message
+ * @return {Promise}
+ */
+ sendWebHookMessage,
+ /**
+ * Start a mock server at the specified port.
+ * @param {Number} port
+ * @return {Promise}
+ */
+ startMockServer,
+ /**
+ * Start the static server, serving files in './data'
+ * @param {Number} port
+ */
+ startStaticServer,
+};
+/* eslint-enable no-use-before-define */
+
+
+const assert = require('assert');
+const http = require('http');
+const Promise = require('bluebird');
+const request = require('request-promise');
+const statics = require('node-static');
+
+const servers = {};
+
+
+function startMockServer(port) {
+ assert.ok(port);
+ const server = http.Server((req, res) => {
+ servers[port].polling = true;
+ return res.end(JSON.stringify({
+ ok: true,
+ result: [{
+ update_id: 0,
+ message: { text: 'test' },
+ }],
+ }));
+ });
+ return new Promise((resolve, reject) => {
+ servers[port] = { server, polling: false };
+ server.on('error', reject).listen(port, resolve);
+ });
+}
+
+
+function startStaticServer(port) {
+ const fileServer = new statics.Server(`${__dirname}/data`);
+ http.Server((req, res) => {
+ req.addListener('end', () => {
+ fileServer.serve(req, res);
+ }).resume();
+ }).listen(port);
+}
+
+
+function isPollingMockServer(port, reverse) {
+ assert.ok(port);
+ return new Promise((resolve, reject) => {
+ // process.nextTick() does not wait until a poll request
+ // is complete!
+ setTimeout(() => {
+ let polling = servers[port] && servers[port].polling;
+ if (reverse) polling = !polling;
+ if (polling) return resolve(true);
+ return reject(new Error('polling-check failed'));
+ }, 1000);
+ });
+}
+
+
+function clearPollingCheck(port) {
+ assert.ok(port);
+ if (servers[port]) servers[port].polling = false;
+}
+
+
+function hasOpenWebHook(port, reverse) {
+ assert.ok(port);
+ const error = new Error('open-webhook-check failed');
+ let connected = false;
+ return request.get(`http://127.0.0.1:${port}`)
+ .then(() => {
+ connected = true;
+ }).catch(e => {
+ if (e.statusCode < 500) connected = true;
+ }).finally(() => {
+ if (reverse) {
+ if (connected) throw error;
+ return;
+ }
+ if (!connected) throw error;
+ });
+}
+
+
+function sendWebHookMessage(port, token, options = {}) {
+ assert.ok(port);
+ assert.ok(token);
+ const url = `http://127.0.0.1:${port}/bot${token}`;
+ return request({
+ url,
+ method: options.method || 'POST',
+ body: {
+ update_id: 1,
+ message: options.message || { text: 'test' }
+ },
+ json: true,
+ });
+}
+
+
+function handleRatelimit(bot, methodName, suite) {
+ const backupMethodName = `__${methodName}`;
+ if (!bot[backupMethodName]) bot[backupMethodName] = bot[methodName];
+
+ const maxRetries = 3;
+ const addSecs = 5;
+ const method = bot[backupMethodName];
+ assert.equal(typeof method, 'function');
+
+ bot[methodName] = (...args) => {
+ let retry = 0;
+ function exec() {
+ return method.call(bot, ...args)
+ .catch(error => {
+ if (!error.response || error.response.statusCode !== 429) {
+ throw error;
+ }
+ retry++;
+ if (retry > maxRetries) {
+ throw error;
+ }
+ if (typeof error.response.body === 'string') {
+ error.response.body = JSON.parse(error.response.body);
+ }
+ const retrySecs = error.response.body.parameters.retry_after;
+ const timeout = (1000 * retrySecs) + (1000 * addSecs);
+ console.error('tests: Handling rate-limit error. Retrying after %d secs', timeout / 1000); // eslint-disable-line no-console
+ suite.timeout(timeout * 2);
+ return new Promise(function timeoutPromise(resolve, reject) {
+ setTimeout(function execTimeout() {
+ return exec().then(resolve).catch(reject);
+ }, timeout);
+ });
+ });
+ }
+ return exec();
+ };
+ return bot;
+}
diff --git a/test/video.mp4 b/test/video.mp4
deleted file mode 100644
index 1fc4788..0000000
Binary files a/test/video.mp4 and /dev/null differ