node-telegram-bot
Version: 
Telegram Bot API Wrapper for nodejs
988 lines (888 loc) • 29.6 kB
JavaScript
'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')
  , botanio = require('botanio-node')
  , 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.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;
  this.pollingRequest = null;
  this.analytics = null;
  this.timeout = options.timeout ? options.timeout : 60; //specify in seconds
}
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 && 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 unknown error");
    }
  });
}
/**
 * Start polling for messages
 *
 * @return {Bot} Self
 */
Bot.prototype._poll = function () {
  var self = this;
  var url = self.base_url + 'bot' + self.token + '/getUpdates?timeout=' + self.timeout + '&offset=' + self.offset;
  self.pollingRequest = null;
  if (self.polling) {
    debug("Poll");
    self.pollingRequest = request.get({
      url: url,
      timeout: self.timeout * 1000,
      json: true
    }, function (err, res, body) {
      if (err && err.code !== 'ETIMEDOUT') {
        self.emit('error', err);
      } else if (res && 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) === '/') {
                  /**
                   * Split the message on space and @
                   * Zero part = complete message
                   * First part = command with leading /
                   * Third part = target or empty ""
                   * Fourth part = arguments or empty ""
                   */
                  var messageParts = msg.message.text.match(/([^@ ]*)([^ ]*)[ ]?(.*)/);
                  // Filter everything not alphaNum out of the command
                  var command = messageParts[1].replace(/[^a-zA-Z0-9 ]/g, "");
                  // Target incl @ sign or null
                  var target = (messageParts[2] !== "" ? messageParts[2]: null);
                  // Optional arguments or null
                  var args = (messageParts[3] !== "" ? messageParts[3].split(' '): null);
                  self.emit(command, msg.message, args, target);
                }
              }
              if (self.analytics !== null) {
                self.analytics.track(msg.message);
              }
              self.emit('message', msg.message);
            }
          });
        }
        if (self.polling) {
            self._poll();
        }
      } else if(res && res.hasOwnProperty('statusCode') && res.statusCode === 401) {
        self.emit('error', new Error('Invalid token.'));
      } else if(res && res.hasOwnProperty('statusCode') && res.statusCode === 409) {
        self.emit('error', new Error('Duplicate token.'));
      } else if(res && res.hasOwnProperty('statusCode') && res.statusCode === 502) {
        self.emit('error', new Error('Gateway error.'));
      } else if(self.pollingRequest && !self.pollingRequest._aborted) { //Skip error throwing, this is an abort due to stopping
        self.emit('error', new Error(util.format('Unknown error')));
      }
    });
  }
  return self;
};
/**
 * Bot start receiving activities
 *
 * @return {Bot} Self
 */
Bot.prototype.start = function () {
  var self = this;
  if (self.webhook) {
      self._setWebhook(this.webhook);
  } else if (!self.polling) {
      self.polling = true;
      self._poll();
  }
  return self;
};
/**
 * End polling for messages
 *
 * @return {Bot} Self
 */
Bot.prototype.stop = function () {
  var self = this;
  self.polling = false;
  if (self.pollingRequest) {
      self.pollingRequest.abort();
  }
  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);
};
/**
 * Analytics from http://botan.io/
 * Allows all incoming messages, and you can make tagging, for specific messages
 * bot.analytics.track(message, 'Specific tag');
 *
 * @param  {String} token You can take this token here: https://appmetrica.yandex.com/
 * @return {Bot}          Self
 *
 * @see https://github.com/botanio/sdk
 */
Bot.prototype.enableAnalytics = function(token) {
  this.analytics = botanio(token);
  return this;
};
module.exports = Bot;