'use strict' var EventEmitter = require('events').EventEmitter , debug = require('debug')('node-telegram-bot') , util = require('util') , request = require('request') , fs = require('fs') , path = require('path') , qs = require('querystring') , Q = require('q') , mime = require('mime'); /** * Constructor for Telegram Bot API Client. * * @class Bot * @constructor * @param {Object} options Configurations for the client * @param {String} options.token Bot token * * @see https://core.telegram.org/bots/api */ function Bot(options) { this.base_url = 'https://api.telegram.org/'; this.polling = false; this.id = ''; this.first_name = ''; this.username = ''; this.token = options.token; this.offset = options.offset ? options.offset : 0; this.interval = options.interval ? options.interval : 500; this.webhook = options.webhook ? options.webhook : false; this.parseCommand = options.parseCommand ? options.parseCommand : true; this.maxAttempts = options.maxAttempts ? options.maxAttempts : 5; this.polling = false; } util.inherits(Bot, EventEmitter); /** * This callback occur after client request for a certain webservice. * * @callback Bot~requestCallback * @param {Error} Error during request * @param {Object} Response from Telegram service */ Bot.prototype._get = function (options, callback) { var self = this; var url = this.base_url + 'bot' + this.token + '/' + options.method; if (options.params) { url += '?' + qs.stringify(options.params); } var attempt = 1; function retry() { request.get({ url: url, json: true }, function (err, res, body) { if (err) { if (err.code === 'ENOTFOUND' && attempt < self.maxAttempts) { ++attempt; self.emit('retry', attempt); retry(); } else { callback(err); } } else { callback(null, body); } }); } retry(); return this; }; /** * To perform multipart request e.g. file upload * * @callback Bot~requestCallback * @param {Error} Error during request * @param {Object} Response from Telegram service */ Bot.prototype._multipart = function (options, callback) { var self = this; var url = this.base_url + 'bot' + this.token + '/' + options.method; var attempt = 1; function retry() { var req = request.post(url, function (err, res, body) { if (err) { if (err.code === 'ENOTFOUND' && attempt < self.maxAttempts) { ++attempt; self.emit('retry', attempt); retry(); } else { callback(err); } } else { var contentType = res.headers['content-type']; if (contentType.indexOf('application/json') >= 0) { try { body = JSON.parse(body); } catch (e) { callback(e, body); } } callback(null, body); } }); var form = req.form() , filename , type , stream , contentType; var arr = Object.keys(options.files); if (arr.indexOf('stream') > -1) { type = options.files['type']; filename = options.files['filename']; stream = options.files['stream']; contentType = options.files['contentType']; } else { arr.forEach(function (key) { var file = options.files[key]; type = key; filename = path.basename(file); stream = fs.createReadStream(file); contentType = mime.lookup(file); }) } form.append(type, stream, { filename: filename, contentType: contentType }); Object.keys(options.params).forEach(function (key) { if (options.params[key]) { form.append(key, options.params[key]); } }); } retry(); return this; }; /** * Temporary solution to set webhook * * @param {Error} Error during request * @param {Object} Response from Telegram service */ Bot.prototype._setWebhook = function (webhook) { var self = this; var url = this.base_url + 'bot' + this.token + '/setWebhook' + "?" + qs.stringify({url: webhook}); request.get({ url: url, json: true }, function (err, res, body) { if (!err && res.statusCode === 200) { if (body.ok) { debug("Set webhook to " + self.webhook); } else { debug("Body not ok"); debug(body); } } else if(res && res.hasOwnProperty('statusCode') && res.statusCode === 401){ debug(err); debug("Failed to set webhook with code" + res.statusCode); } else { debug(err); debug("Failed to set webhook with code" + res.statusCode); } }); } /** * Start polling for messages * * @return {Bot} Self */ Bot.prototype._poll = function () { var self = this; var url = this.base_url + 'bot' + this.token + '/getUpdates?timeout=60&offset=' + this.offset; request.get({ url: url, json: true }, function (err, res, body) { if (err) { self.emit('error', err); } else if (res.statusCode === 200) { if (body.ok) { body.result.forEach(function (msg) { if (msg.update_id >= self.offset) { self.offset = msg.update_id + 1; if (self.parseCommand) { if (msg.message.text && msg.message.text.charAt(0) === '/') { var command = msg.message.text.split(' ', 2)[0]; command = command.replace(/[^a-zA-Z0-9 ]/g, ""); if (msg.message.text.split(' ')[1]) { var arg = msg.message.text.split(' '); arg.shift(); self.emit(command, msg.message, arguments); } else{ self.emit(command, msg.message); } } } self.emit('message', msg.message); } }); } } else if(res && res.hasOwnProperty('statusCode') && res.statusCode === 401) { self.emit('error', 'Invalid token'); } else { self.emit('error', 'Unknown error, ' + err.stack); } if (self.polling) { self._poll(); } }); return this; }; /** * Bot start receiving activities * * @return {Bot} Self */ Bot.prototype.start = function () { var self = this; if (self.webhook) { self._setWebhook(this.webhook); } else { self._poll(); self.polling = true; } return self; }; /** * End polling for messages * * @return {Bot} Self */ Bot.prototype.stop = function () { var self = this; self._setWebhook(""); self.polling = false; return self; }; /** * Returns basic information about the bot in form of a User object. * * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#getme */ Bot.prototype.getMe = function (callback) { var self = this , deferred = Q.defer(); this._get({ method: 'getMe' }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { self.id = res.result.id; self.first_name = res.result.first_name; self.username = res.result.username; deferred.resolve(res.result); } else { deferred.reject(res); } }); return deferred.promise.nodeify(callback); }; /** * Use this method to get a list of profile pictures for a user. * * @param {Object} options Options * @param {Integer} options.user_id Unique identifier of the target user * @param {String=} options.offset Sequential number of the first photo to be returned. By default, all photos are returned. * @param {Integer=} options.limit Limits the number of photos to be retrieved. Values between 1—100 are accepted. Defaults to 100. * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#getuserprofilephotos */ Bot.prototype.getUserProfilePhotos = function (options, callback) { var self = this , deferred = Q.defer(); this._get({ method: 'getUserProfilePhotos', params: { user_id: options.user_id, offset: options.offset, limit: options.limit } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); return deferred.promise.nodeify(callback); }; /** * Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. * * @param {Object} options Options * @param {String} options.file_id File identifier to get info about * @param {String=} options.dir Directory the file to be stored (if it is not specified, no file willbe downloaded) * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#getfile */ Bot.prototype.getFile = function (options, callback) { var self = this , deferred = Q.defer(); this._get({ method: 'getFile', params: { file_id: options.file_id } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { var filename = path.basename(res.result.file_path); if (options.dir) { var filepath = path.join(options.dir, filename); var url = self.base_url + 'file/bot' + self.token + '/' + res.result.file_path; var destination = fs.createWriteStream(filepath); request(url) .pipe(destination) .on('finish', function () { deferred.resolve({ destination: filepath, url: url }); }) .on('error', function(error){ deferred.reject(error); }); } else { deferred.resolve({ url: url }); } } else { deferred.reject(res); } }); return deferred.promise.nodeify(callback); }; /** * Use this method to send text messages. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {String} options.text Text of the message to be sent * @param {String} options.parse_mode Send Markdown, if you want Telegram apps to show bold, italic and inline URLs in your bot's message. * @param {Boolean=} options.disable_web_page_preview Disables link previews for links in this message * @param {Integer=} options.reply_to_message_id If the message is a reply, ID of the original message * @param {Object=} options.reply_markup Additional interface options. {@link https://core.telegram.org/bots/api/#replykeyboardmarkup| ReplyKeyboardMarkup} * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#sendmessage */ Bot.prototype.sendMessage = function (options, callback) { var self = this , deferred = Q.defer(); this._get({ method: 'sendMessage', params: { chat_id: options.chat_id, text: options.text, parse_mode: options.parse_mode, disable_web_page_preview: options.disable_web_page_preview, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); return deferred.promise.nodeify(callback); }; /** * Use this method to forward messages of any kind. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {Integer} options.from_chat_id Unique identifier for the chat where the original message was sent — User or GroupChat id * @param {Integer} options.message_id Unique message identifier * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#forwardmessage */ Bot.prototype.forwardMessage = function (options, callback) { var self = this , deferred = Q.defer(); this._get({ method: 'forwardMessage', params: { chat_id: options.chat_id, from_chat_id: options.from_chat_id, message_id: options.message_id } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); return deferred.promise.nodeify(callback); }; /** * Use this method to send photos. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {String} options.photo Path to photo file (Library will create a stream if the path exist) * @param {String=} options.file_id If file_id is passed, method will use this instead * @param {String=} options.caption Photo caption (may also be used when resending photos by file_id). * @param {Integer=} options.reply_to_message_id If the message is a reply, ID of the original message * @param {Object=} options.reply_markup Additional interface options. {@link https://core.telegram.org/bots/api/#replykeyboardmarkup| ReplyKeyboardMarkup} * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#sendphoto */ Bot.prototype.sendPhoto = function (options, callback) { var self = this , deferred = Q.defer(); if (options.file_id) { this._get({ method: 'sendPhoto', params: { chat_id: options.chat_id, caption: options.caption, photo: options.file_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } else { var files; if (options.files.stream) { files = { type: 'photo', filename: options.files.filename, contentType: options.files.contentType, stream: options.files.stream } } else { files = { photo: options.files.photo } } this._multipart({ method: 'sendPhoto', params: { chat_id: options.chat_id, caption: options.caption, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) }, files: files }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } return deferred.promise.nodeify(callback); }; /** * Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {String} options.audio Path to audio file (Library will create a stream if the path exist) * @param {String=} options.file_id If file_id is passed, method will use this instead * @param {Integer=} options.reply_to_message_id If the message is a reply, ID of the original message * @param {Object=} options.reply_markup Additional interface options. {@link https://core.telegram.org/bots/api/#replykeyboardmarkup| ReplyKeyboardMarkup} * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#sendaudio */ Bot.prototype.sendAudio = function (options, callback) { var self = this , deferred = Q.defer(); if (options.file_id) { this._get({ method: 'sendAudio', params: { chat_id: options.chat_id, audio: options.file_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } else { var files; if (options.files.stream) { files = { type: 'audio', filename: options.files.filename, contentType: options.files.contentType, stream: options.files.stream } } else if (mime.lookup(options.files.audio) !== 'audio/ogg') { return Q.reject(new Error('Invalid file type')) .nodeify(callback); } else { files = { audio: options.files.audio } } this._multipart({ method: 'sendAudio', params: { chat_id: options.chat_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) }, files: files }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } return deferred.promise.nodeify(callback); }; /** * Use this method to send general files. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {String} options.document Path to document file (Library will create a stream if the path exist) * @param {String=} options.file_id If file_id is passed, method will use this instead * @param {Integer=} options.reply_to_message_id If the message is a reply, ID of the original message * @param {Object=} options.reply_markup Additional interface options. {@link https://core.telegram.org/bots/api/#replykeyboardmarkup| ReplyKeyboardMarkup} * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#senddocument */ Bot.prototype.sendDocument = function (options, callback) { var self = this , deferred = Q.defer(); if (options.file_id) { this._get({ method: 'sendDocument', params: { chat_id: options.chat_id, document: options.file_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } else { var files; if (options.files.stream) { files = { type: 'document', filename: options.files.filename, contentType: options.files.contentType, stream: options.files.stream } } else { files = { document: options.files.document } } this._multipart({ method: 'sendDocument', params: { chat_id: options.chat_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) }, files: files }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } return deferred.promise.nodeify(callback); }; /** * Use this method to send .webp stickers. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {String} options.sticker Path to sticker file (Library will create a stream if the path exist) * @param {String=} options.file_id If file_id is passed, method will use this instead * @param {Integer=} options.reply_to_message_id If the message is a reply, ID of the original message * @param {Object=} options.reply_markup Additional interface options. {@link https://core.telegram.org/bots/api/#replykeyboardmarkup| ReplyKeyboardMarkup} * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#sendsticker */ Bot.prototype.sendSticker = function (options, callback) { var self = this , deferred = Q.defer(); if (options.file_id) { this._get({ method: 'sendSticker', params: { chat_id: options.chat_id, sticker: options.file_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } else { if (mime.lookup(options.files.sticker) !== 'image/webp') { return Q.reject(new Error('Invalid file type')) .nodeify(callback); } this._multipart({ method: 'sendSticker', params: { chat_id: options.chat_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) }, files: { sticker: options.files.sticker } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } return deferred.promise.nodeify(callback); }; /** * Use this method to send video files, Telegram clients support mp4 video. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {String} options.video Path to video file (Library will create a stream if the path exist) * @param {String=} options.file_id If file_id is passed, method will use this instead * @param {Integer=} options.reply_to_message_id If the message is a reply, ID of the original message * @param {Object=} options.reply_markup Additional interface options. {@link https://core.telegram.org/bots/api/#replykeyboardmarkup| ReplyKeyboardMarkup} * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#sendvideo */ Bot.prototype.sendVideo = function (options, callback) { var self = this , deferred = Q.defer(); if (options.file_id) { this._get({ method: 'sendSticker', params: { chat_id: options.chat_id, video: options.file_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } else { var files; if (options.files.stream) { files = { type: 'video', filename: options.files.filename, contentType: options.files.contentType, stream: options.files.stream } } else if (mime.lookup(options.files.video.filename) !== 'video/mp4') { return Q.reject(new Error('Invalid file type')) .nodeify(callback); } else { files = { video: options.files.video } } this._multipart({ method: 'sendVideo', params: { chat_id: options.chat_id, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) }, files: files }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); } return deferred.promise.nodeify(callback); }; /** * Use this method to send point on the map. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {Float} options.latitude Latitude of location * @param {Float} options.longitude Longitude of location * @param {Integer=} options.reply_to_message_id If the message is a reply, ID of the original message * @param {Object=} options.reply_markup Additional interface options. {@link https://core.telegram.org/bots/api/#replykeyboardmarkup| ReplyKeyboardMarkup} * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#sendlocation */ Bot.prototype.sendLocation = function (options, callback) { var self = this , deferred = Q.defer(); this._get({ method: 'sendLocation', params: { chat_id: options.chat_id, latitude: options.latitude, longitude: options.longitude, reply_to_message_id: options.reply_to_message_id, reply_markup: JSON.stringify(options.reply_markup) } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); return deferred.promise.nodeify(callback); }; /** * Use this method when you need to tell the user that something is happening on the bot's side. * * @param {Object} options Options * @param {Integer} options.chat_id Unique identifier for the message recipient — User or GroupChat id * @param {String} options.action Type of action to broadcast. * @param {Bot~requestCallback} callback The callback that handles the response. * @return {Promise} Q Promise * * @see https://core.telegram.org/bots/api#sendchataction */ Bot.prototype.sendChatAction = function (options, callback) { var self = this , deferred = Q.defer(); this._get({ method: 'sendChatAction', params: { chat_id: options.chat_id, action: options.action } }, function (err, res) { if (err) { return deferred.reject(err); } if (res.ok) { deferred.resolve(res.result); } else { deferred.reject(res); } }); return deferred.promise.nodeify(callback); }; module.exports = Bot;